From 3f1d09f813c3e0744e42c7558120a502ee6691da Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 14 Aug 2026 21:57:31 -0500 Subject: [PATCH 1/4] feat: implement Steam catalog page with virtualized grid and infinite scroll support --- apps/desktop/bun.lock | 13 +- apps/desktop/package.json | 1 - .../SteamCatalogVirtualizedGrid.tsx | 124 +++++------------- .../hooks/useCatalogScrollRestoration.ts | 42 ------ .../steam-catalog/pages/SteamCatalogPage.tsx | 32 ++--- 5 files changed, 53 insertions(+), 159 deletions(-) delete mode 100644 apps/desktop/src/features/steam-catalog/hooks/useCatalogScrollRestoration.ts diff --git a/apps/desktop/bun.lock b/apps/desktop/bun.lock index 198caa7f..690bb312 100644 --- a/apps/desktop/bun.lock +++ b/apps/desktop/bun.lock @@ -8,7 +8,6 @@ "@heroui/react": "^2.8.9", "@tailwindcss/vite": "^4.2.1", "@tanstack/react-query": "^5", - "@tanstack/react-virtual": "^3.14.9", "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-autostart": "^2.5.1", "@tauri-apps/plugin-dialog": "~2", @@ -698,9 +697,9 @@ "@tanstack/react-query": ["@tanstack/react-query@5.99.0", "", { "dependencies": { "@tanstack/query-core": "5.99.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw=="], - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.9", "", { "dependencies": { "@tanstack/virtual-core": "3.17.7" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qZyr0FZDP8rDC4WBhsryIZmAd9bveJvFGUJJtskWaew6/0dTRS6wZxnR6VQ5bY2KwL3LjerrHqQLk3a0GKcPXQ=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.11.3", "", { "dependencies": { "@tanstack/virtual-core": "3.11.3" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-vCU+OTylXN3hdC8RKg68tPlBPjjxtzon7Ys46MgrSLE+JhSjSTPvoQifV6DQJeJmA8Q3KT6CphJbejupx85vFw=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.7", "", {}, "sha512-bp+v10y65sp2H7WpWfIMyxTNfl8ZVfxFTLRjPIFRryi6FV/J33z4IS53WO4pTk36KlvJ4iLiQz+oaydDC1xbcA=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.11.3", "", {}, "sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w=="], "@tauri-apps/api": ["@tauri-apps/api@2.11.0", "", {}, "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA=="], @@ -1428,10 +1427,6 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@heroui/listbox/@tanstack/react-virtual": ["@tanstack/react-virtual@3.11.3", "", { "dependencies": { "@tanstack/virtual-core": "3.11.3" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-vCU+OTylXN3hdC8RKg68tPlBPjjxtzon7Ys46MgrSLE+JhSjSTPvoQifV6DQJeJmA8Q3KT6CphJbejupx85vFw=="], - - "@heroui/table/@tanstack/react-virtual": ["@tanstack/react-virtual@3.11.3", "", { "dependencies": { "@tanstack/virtual-core": "3.11.3" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-vCU+OTylXN3hdC8RKg68tPlBPjjxtzon7Ys46MgrSLE+JhSjSTPvoQifV6DQJeJmA8Q3KT6CphJbejupx85vFw=="], - "@heroui/use-aria-multiselect/@react-aria/label": ["@react-aria/label@3.7.25", "", { "dependencies": { "@react-aria/utils": "^3.33.1", "@react-types/shared": "^3.33.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-oNK3Pqj4LDPwEbQaoM/uCip4QvQmmwGOh08VeW+vzSi6TAwf+KoWTyH/tiAeB0CHWNDK0k3e1iTygTAt4wzBmg=="], "@heroui/use-is-mobile/@react-aria/ssr": ["@react-aria/ssr@3.9.10", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ=="], @@ -1528,10 +1523,6 @@ "react-stately/@react-types/shared": ["@react-types/shared@3.34.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-gp6xo/s2lX54AlTjOiqwDnxA7UW79BNvI9dB9pr3LZTzRKCd1ZA+ZbgKw/ReIiWuvvVw/8QFJpnqeeFyLocMcQ=="], - "@heroui/listbox/@tanstack/react-virtual/@tanstack/virtual-core": ["@tanstack/virtual-core@3.11.3", "", {}, "sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w=="], - - "@heroui/table/@tanstack/react-virtual/@tanstack/virtual-core": ["@tanstack/virtual-core@3.11.3", "", {}, "sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w=="], - "@react-types/slider/@react-aria/slider/@react-types/shared": ["@react-types/shared@3.34.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-gp6xo/s2lX54AlTjOiqwDnxA7UW79BNvI9dB9pr3LZTzRKCd1ZA+ZbgKw/ReIiWuvvVw/8QFJpnqeeFyLocMcQ=="], "@react-types/tabs/@react-aria/tabs/@react-types/shared": ["@react-types/shared@3.34.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-gp6xo/s2lX54AlTjOiqwDnxA7UW79BNvI9dB9pr3LZTzRKCd1ZA+ZbgKw/ReIiWuvvVw/8QFJpnqeeFyLocMcQ=="], diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1c45f56e..1999f904 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -18,7 +18,6 @@ "@heroui/react": "^2.8.9", "@tailwindcss/vite": "^4.2.1", "@tanstack/react-query": "^5", - "@tanstack/react-virtual": "^3.14.9", "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-autostart": "^2.5.1", "@tauri-apps/plugin-dialog": "~2", diff --git a/apps/desktop/src/features/steam-catalog/components/SteamCatalogVirtualizedGrid.tsx b/apps/desktop/src/features/steam-catalog/components/SteamCatalogVirtualizedGrid.tsx index b429b769..bf366c73 100644 --- a/apps/desktop/src/features/steam-catalog/components/SteamCatalogVirtualizedGrid.tsx +++ b/apps/desktop/src/features/steam-catalog/components/SteamCatalogVirtualizedGrid.tsx @@ -1,5 +1,4 @@ -import { memo, useEffect, useLayoutEffect, useRef, useState } from "react"; -import { useWindowVirtualizer } from "@tanstack/react-virtual"; +import { memo } from "react"; import { cn } from "@heroui/react"; import type { CatalogListItem, SteamAppdetailsMediaResult, SourceBestMatch } from "@services/tauri"; import type { ConfiguredGame } from "@app-types/config"; @@ -28,96 +27,43 @@ export const SteamCatalogVirtualizedGrid = memo(function SteamCatalogVirtualized onInstall, consoleMode = false, }: SteamCatalogVirtualizedGridProps) { - const containerRef = useRef(null); - const [columnCount, setColumnCount] = useState(4); - - useEffect(() => { - if (!containerRef.current) return; - const minWidth = consoleMode ? 320 : 280; - const gap = 20; - - const updateColumns = () => { - if (!containerRef.current) return; - const width = containerRef.current.clientWidth; - const cols = Math.max(1, Math.floor((width + gap) / (minWidth + gap))); - setColumnCount(cols); - }; - - updateColumns(); - const observer = new ResizeObserver(updateColumns); - observer.observe(containerRef.current); - return () => observer.disconnect(); - }, [consoleMode]); - - const rowCount = Math.ceil(items.length / columnCount); - - const virtualizer = useWindowVirtualizer({ - count: rowCount, - estimateSize: () => (consoleMode ? 320 : 266), - getItemKey: (index) => index, - overscan: 6, - scrollMargin: containerRef.current?.offsetTop ?? 0, - }); - - useLayoutEffect(() => { - virtualizer.measure(); - }, [columnCount, consoleMode]); - return ( -
-
- {virtualizer.getVirtualItems().map((virtualRow) => { - const rowStartIndex = virtualRow.index * columnCount; - const rowItems = items.slice(rowStartIndex, rowStartIndex + columnCount); - - return ( -
-
- {rowItems.map((item, colIdx) => { - const idx = rowStartIndex + colIdx; - const libraryGame = - (item.steamAppId ? libraryGamesMap.get(String(item.steamAppId)) : undefined) ?? - libraryGamesMap.get(item.name.toLowerCase()); +
+ {items.map((item, idx) => { + const libraryGame = + (item.steamAppId ? libraryGamesMap.get(String(item.steamAppId)) : undefined) ?? + libraryGamesMap.get(item.name.toLowerCase()); - const uniqueKey = item.steamAppId ? `steam-${item.steamAppId}` : `${item.name}-${idx}`; + const uniqueKey = item.steamAppId ? `steam-${item.steamAppId}` : `${item.name}-${idx}`; - return ( - - ); - })} -
-
- ); - })} -
+ return ( +
+ +
+ ); + })}
); }); diff --git a/apps/desktop/src/features/steam-catalog/hooks/useCatalogScrollRestoration.ts b/apps/desktop/src/features/steam-catalog/hooks/useCatalogScrollRestoration.ts deleted file mode 100644 index df54dbde..00000000 --- a/apps/desktop/src/features/steam-catalog/hooks/useCatalogScrollRestoration.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useEffect, useLayoutEffect, useRef } from "react"; - -const SCROLL_STORAGE_KEY = "savecloud_catalog_scroll_y"; - -export function useCatalogScrollRestoration(itemsLength: number, isReady: boolean) { - const restoredRef = useRef(false); - - useEffect(() => { - const handleScroll = () => { - if (window.scrollY > 0) { - sessionStorage.setItem(SCROLL_STORAGE_KEY, String(window.scrollY)); - } - }; - - window.addEventListener("scroll", handleScroll, { passive: true }); - return () => window.removeEventListener("scroll", handleScroll); - }, []); - - useLayoutEffect(() => { - if (restoredRef.current || !isReady || itemsLength === 0) return; - - const savedY = sessionStorage.getItem(SCROLL_STORAGE_KEY); - if (savedY) { - const scrollY = parseInt(savedY, 10); - if (!isNaN(scrollY) && scrollY > 0) { - window.scrollTo({ top: scrollY, behavior: "instant" }); - - requestAnimationFrame(() => { - window.scrollTo({ top: scrollY, behavior: "instant" }); - }); - } - } - restoredRef.current = true; - }, [itemsLength, isReady]); - - const clearSavedScroll = () => { - sessionStorage.removeItem(SCROLL_STORAGE_KEY); - restoredRef.current = true; - }; - - return { clearSavedScroll }; -} diff --git a/apps/desktop/src/features/steam-catalog/pages/SteamCatalogPage.tsx b/apps/desktop/src/features/steam-catalog/pages/SteamCatalogPage.tsx index e330b8e7..df742e45 100644 --- a/apps/desktop/src/features/steam-catalog/pages/SteamCatalogPage.tsx +++ b/apps/desktop/src/features/steam-catalog/pages/SteamCatalogPage.tsx @@ -28,7 +28,9 @@ export function SteamCatalogPage() { const catalogScrollPosition = useShellUiStore((state) => state.catalogScrollPosition); const setCatalogScrollPosition = useShellUiStore((state) => state.setCatalogScrollPosition); + const initialScrollTargetRef = useRef(catalogScrollPosition); const hasRestored = useRef(false); + const isRestoringRef = useRef(false); const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false); const bigPictureConsole = useMemo( @@ -162,25 +164,26 @@ export function SteamCatalogPage() { error: heroError, } = useSteamCatalogTrendingHero(showTrendingHero); - const isRestoringRef = useRef(false); const prevFiltersRef = useRef({ debouncedSearch, filterSignature, sortOption }); useLayoutEffect(() => { - if (catalogScrollPosition > 0 && isReady && !hasRestored.current) { - void document.body.offsetHeight; + if (hasRestored.current || !isReady) return; + const targetY = initialScrollTargetRef.current; + if (targetY > 0) { isRestoringRef.current = true; - window.scrollTo({ top: catalogScrollPosition, behavior: "instant" }); + window.scrollTo({ top: targetY, behavior: "instant" }); const timer = setTimeout(() => { - void document.body.offsetHeight; - window.scrollTo({ top: catalogScrollPosition, behavior: "instant" }); + window.scrollTo({ top: targetY, behavior: "instant" }); hasRestored.current = true; isRestoringRef.current = false; }, 100); return () => clearTimeout(timer); + } else { + hasRestored.current = true; } - }, [isReady, catalogScrollPosition, activeItems.length]); + }, [isReady]); useEffect(() => { const prev = prevFiltersRef.current; @@ -191,8 +194,10 @@ export function SteamCatalogPage() { prevFiltersRef.current = { debouncedSearch, filterSignature, sortOption }; - if (hasChanged && hasRestored.current) { + if (hasChanged) { setCatalogScrollPosition(0); + initialScrollTargetRef.current = 0; + window.scrollTo({ top: 0, behavior: "instant" }); } }, [debouncedSearch, filterSignature, sortOption, setCatalogScrollPosition]); @@ -200,13 +205,11 @@ export function SteamCatalogPage() { let scrollTimeout: ReturnType; const handleScroll = () => { - if (isRestoringRef.current) return; + if (!hasRestored.current || isRestoringRef.current) return; clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { const currentY = window.scrollY || document.documentElement.scrollTop; - if (currentY > 0) { - setCatalogScrollPosition(currentY); - } + setCatalogScrollPosition(currentY); }, 200); }; @@ -215,7 +218,7 @@ export function SteamCatalogPage() { window.removeEventListener("scroll", handleScroll); clearTimeout(scrollTimeout); const currentY = window.scrollY || document.documentElement.scrollTop; - if (currentY > 0 && !isRestoringRef.current) { + if (hasRestored.current && !isRestoringRef.current) { setCatalogScrollPosition(currentY); } }; @@ -364,9 +367,6 @@ export function SteamCatalogPage() {
) : ( <> - {activeIsMatchingPending ? ( -

{t("steamCatalog.validatingSources")}

- ) : null}
{isListRefetching ? (
Date: Fri, 14 Aug 2026 22:19:58 -0500 Subject: [PATCH 2/4] feat: implement native View Transition API with custom animations and update GameCard for navigation support --- apps/desktop/src/features/games/GameCard.tsx | 2 +- .../components/SteamCatalogGridItem.tsx | 10 +++++--- .../components/SteamCatalogTrendingHero.tsx | 7 +++++- .../steam-catalog/pages/SteamCatalogPage.tsx | 23 +++++++++++-------- apps/desktop/src/styles/App.css | 22 +++++++----------- 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/apps/desktop/src/features/games/GameCard.tsx b/apps/desktop/src/features/games/GameCard.tsx index 123ac97c..54905dea 100644 --- a/apps/desktop/src/features/games/GameCard.tsx +++ b/apps/desktop/src/features/games/GameCard.tsx @@ -201,7 +201,7 @@ export const GameCard = memo(function GameCard(props: GameCardProps) { {!isCatalog && syncProgress && } - +
{(isEffectivelyLoading || (displayImageUrl && !imgLoaded && !imgError)) && ( diff --git a/apps/desktop/src/features/steam-catalog/components/SteamCatalogGridItem.tsx b/apps/desktop/src/features/steam-catalog/components/SteamCatalogGridItem.tsx index 4c36b6db..0df59edf 100644 --- a/apps/desktop/src/features/steam-catalog/components/SteamCatalogGridItem.tsx +++ b/apps/desktop/src/features/steam-catalog/components/SteamCatalogGridItem.tsx @@ -1,5 +1,4 @@ import { memo, startTransition } from "react"; -import { motion } from "framer-motion"; import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; import { addTransitionType } from "react"; @@ -10,6 +9,7 @@ import { GameCard } from "@features/games/GameCard"; import { catalogListItemToConfiguredGame } from "@features/steam-catalog/model/catalogConfiguredGame"; import { useLowPerformanceMode } from "@hooks/useLowPerformanceMode"; import { sourceCandidateKey } from "@utils/sourceMatch"; +import { useShellUiStore } from "@store/ShellUiStore"; export type CatalogGridItemProps = { item: CatalogListItem; @@ -56,7 +56,7 @@ export const CatalogGridItem = memo(function CatalogGridItem({ const best = candidates.length > 0 ? candidates[0] : undefined; return ( - +
{ + const currentY = window.scrollY || document.documentElement.scrollTop; + if (currentY > 0) { + useShellUiStore.getState().setCatalogScrollPosition(currentY); + } if (isLowPerf) { navigate(`/games/${toRouteGameId(item)}`, { state: { @@ -140,7 +145,7 @@ export function SteamCatalogTrendingHero({ pauseOnMouseEnter: true, }} speed={520} - className="sg-trending-swiper min-h-[380px] overflow-hidden rounded-2xl border border-default-200/70 shadow-sm dark:border-default-100/15"> + className="sg-trending-swiper min-h-95 overflow-hidden rounded-2xl border border-default-200/70 shadow-sm dark:border-default-100/15"> {slides.map((featured, slideIndex) => ( state.setCatalogScrollPosition); const initialScrollTargetRef = useRef(catalogScrollPosition); + const currentScrollYRef = useRef(catalogScrollPosition); const hasRestored = useRef(false); const isRestoringRef = useRef(false); const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false); @@ -173,12 +174,11 @@ export function SteamCatalogPage() { if (targetY > 0) { isRestoringRef.current = true; window.scrollTo({ top: targetY, behavior: "instant" }); + hasRestored.current = true; const timer = setTimeout(() => { - window.scrollTo({ top: targetY, behavior: "instant" }); - hasRestored.current = true; isRestoringRef.current = false; - }, 100); + }, 200); return () => clearTimeout(timer); } else { hasRestored.current = true; @@ -197,6 +197,7 @@ export function SteamCatalogPage() { if (hasChanged) { setCatalogScrollPosition(0); initialScrollTargetRef.current = 0; + currentScrollYRef.current = 0; window.scrollTo({ top: 0, behavior: "instant" }); } }, [debouncedSearch, filterSignature, sortOption, setCatalogScrollPosition]); @@ -206,20 +207,24 @@ export function SteamCatalogPage() { const handleScroll = () => { if (!hasRestored.current || isRestoringRef.current) return; + const currentY = window.scrollY || document.documentElement.scrollTop; + if (currentY > 0) { + currentScrollYRef.current = currentY; + } clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { - const currentY = window.scrollY || document.documentElement.scrollTop; - setCatalogScrollPosition(currentY); - }, 200); + if (currentScrollYRef.current > 0) { + setCatalogScrollPosition(currentScrollYRef.current); + } + }, 150); }; window.addEventListener("scroll", handleScroll, { passive: true }); return () => { window.removeEventListener("scroll", handleScroll); clearTimeout(scrollTimeout); - const currentY = window.scrollY || document.documentElement.scrollTop; - if (hasRestored.current && !isRestoringRef.current) { - setCatalogScrollPosition(currentY); + if (hasRestored.current && !isRestoringRef.current && currentScrollYRef.current > 0) { + setCatalogScrollPosition(currentScrollYRef.current); } }; }, [setCatalogScrollPosition]); diff --git a/apps/desktop/src/styles/App.css b/apps/desktop/src/styles/App.css index c5ce2c33..0fc5b240 100644 --- a/apps/desktop/src/styles/App.css +++ b/apps/desktop/src/styles/App.css @@ -4,9 +4,10 @@ ::view-transition-old(.page-scale-lift), ::view-transition-new(.page-scale-lift) { - animation-duration: 240ms; + animation-duration: 220ms; animation-fill-mode: both; - will-change: transform, opacity, filter; + will-change: transform, opacity; + transform: translateZ(0); } ::view-transition-old(.page-scale-lift) { @@ -57,22 +58,18 @@ @keyframes page-scale-out { from { transform: scale(1) translateY(0); - filter: blur(0px); } to { transform: scale(0.97) translateY(-6px); - filter: blur(4px); } } @keyframes page-scale-in { from { transform: scale(0.97) translateY(10px); - filter: blur(4px); } to { transform: scale(1) translateY(0); - filter: blur(0px); } } @@ -153,9 +150,10 @@ /* Opción 1: game-detail-ps5 (PS5/Xbox Console Unfold) */ ::view-transition-old(.game-detail-ps5), ::view-transition-new(.game-detail-ps5) { - animation-duration: 280ms; + animation-duration: 240ms; animation-fill-mode: both; - will-change: transform, opacity, filter; + will-change: transform, opacity; + transform: translateZ(0); } ::view-transition-old(.game-detail-ps5) { @@ -171,22 +169,18 @@ @keyframes detail-ps5-out { from { transform: scale(1) translateY(0); - filter: blur(0px); } to { - transform: scale(0.96) translateY(-12px); - filter: blur(6px); + transform: scale(0.96) translateY(-10px); } } @keyframes detail-ps5-in { from { - transform: translateY(28px) scale(0.98); - filter: blur(8px); + transform: translateY(20px) scale(0.98); } to { transform: translateY(0) scale(1); - filter: blur(0px); } } From 149ccbe4e20df7902fc196646d9609f900457a79 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 14 Aug 2026 22:24:25 -0500 Subject: [PATCH 3/4] feat: add GitHub Actions workflows for CI and release pipeline automation --- .github/workflows/ci.yml | 97 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 7 ++- 2 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..2f7debf9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: CI & Cache + +on: + push: + branches: + - main + paths: + - "apps/desktop/**" + - ".github/workflows/ci.yml" + - ".github/workflows/release.yml" + - "package.json" + - "bun.lock" + pull_request: + branches: + - main + paths: + - "apps/desktop/**" + - ".github/workflows/ci.yml" + - ".github/workflows/release.yml" + - "package.json" + - "bun.lock" + workflow_dispatch: + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + typecheck-frontend: + name: Frontend Typecheck + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + working-directory: apps/desktop + + - name: Typecheck + run: bun run tsc + working-directory: apps/desktop + + rust-check-cache: + name: Rust Check & Cache (${{ matrix.platform }}) + strategy: + fail-fast: false + matrix: + include: + - platform: windows-latest + - platform: ubuntu-22.04 + - platform: macos-latest + rustTargets: "aarch64-apple-darwin x86_64-apple-darwin" + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rustTargets || '' }} + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: "apps/desktop/src-tauri -> apps/desktop/src-tauri/target" + cache-all-crates: "true" + prefix-key: "v1-rust-${{ matrix.platform }}" + shared-key: "desktop-cache-${{ matrix.platform }}" + + - name: Install dependencies (macOS) + if: matrix.platform == 'macos-latest' + run: brew install opus pkg-config + + - name: Install dependencies (Ubuntu) + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y pkg-config libasound2-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libudev-dev build-essential curl wget file libxdo-dev libssl-dev + + - name: Cargo Check + run: cargo check --manifest-path apps/desktop/src-tauri/Cargo.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84ff82e7..4d302727 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: tags: - "v*" + workflow_dispatch: permissions: contents: write @@ -134,8 +135,10 @@ jobs: - name: Rust cache uses: Swatinem/rust-cache@v2 with: - workspaces: "apps/desktop/src-tauri" - shared-key: "desktop-release-cache" + workspaces: "apps/desktop/src-tauri -> apps/desktop/src-tauri/target" + cache-all-crates: "true" + prefix-key: "v1-rust-${{ matrix.platform }}" + shared-key: "desktop-cache-${{ matrix.platform }}" - name: Setup Python uses: actions/setup-python@v5 From 1dd73c0eb891128c95b3a94096303925dfb725f4 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 14 Aug 2026 22:38:14 -0500 Subject: [PATCH 4/4] ci: add GitHub Actions workflow for frontend typechecking and Rust caching --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f7debf9..2ed28c2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,11 @@ jobs: - platform: macos-latest rustTargets: "aarch64-apple-darwin x86_64-apple-darwin" runs-on: ${{ matrix.platform }} + + env: + CMAKE_POLICY_VERSION_MINIMUM: "3.5" + CMAKEFLAGS: "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" + steps: - name: Checkout uses: actions/checkout@v4