Skip to content

Commit bc0f24d

Browse files
committed
fix playtime tracking + collection cloud sync
1 parent ff17ee7 commit bc0f24d

7 files changed

Lines changed: 128 additions & 187 deletions

File tree

electron/main.cjs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11071,8 +11071,16 @@ ipcMain.handle('uc:game-exe-running', async (_event, appid) => {
1107111071
if (!running) return { ok: true, running: false }
1107211072
const alive = await isProcessRunning(running.pid)
1107311073
if (!alive) {
11074-
if (running.appid) runningGames.delete(running.appid)
11075-
if (running.exePath) runningGames.delete(running.exePath)
11074+
// Route dead processes through the tracked-exit finalizer so playtime is
11075+
// recorded before the bookkeeping entry is removed.
11076+
if (typeof running.handleDead === 'function') {
11077+
try { running.handleDead(running.pid) } catch (err) {
11078+
ucLog(`[Game] running check finalizer failed for ${running.appid || running.pid}: ${err?.message || err}`, 'warn')
11079+
}
11080+
} else {
11081+
if (running.appid) runningGames.delete(running.appid)
11082+
if (running.exePath) runningGames.delete(running.exePath)
11083+
}
1107611084
return { ok: true, running: false }
1107711085
}
1107811086
return { ok: true, running: true, pid: running.pid, exePath: running.exePath }
@@ -11092,9 +11100,17 @@ ipcMain.handle('uc:game-exe-quit', async (_event, appid) => {
1109211100
if (!alive) stopped = true
1109311101
}
1109411102
if (stopped) {
11095-
if (running.appid) runningGames.delete(running.appid)
11096-
if (running.exePath) runningGames.delete(running.exePath)
11097-
if (runningGames.size === 0) clearGameRpcActivity()
11103+
// Let the normal exit finalizer record the completed playtime session.
11104+
// Direct deletion here drops the only in-memory start time.
11105+
if (typeof running.handleDead === 'function') {
11106+
try { running.handleDead(running.pid) } catch (err) {
11107+
ucLog(`[Game] quit finalizer failed for ${running.appid || running.pid}: ${err?.message || err}`, 'warn')
11108+
}
11109+
} else {
11110+
if (running.appid) runningGames.delete(running.appid)
11111+
if (running.exePath) runningGames.delete(running.exePath)
11112+
if (runningGames.size === 0) clearGameRpcActivity()
11113+
}
1109811114
}
1109911115
return { ok: true, stopped }
1110011116
} catch (err) {

renderer/src/app/App.tsx

Lines changed: 53 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
1-
import { useEffect, useMemo, useState } from "react"
1+
import { lazy, Suspense, useEffect, useMemo, useState } from "react"
22
import { HashRouter, Route, Routes, Navigate } from "react-router-dom"
33
import { AppLayout } from "@/app/Layout"
4-
import { LauncherPage } from "@/app/pages/LauncherPage"
5-
import { SearchPage } from "@/app/pages/SearchPage"
6-
import { GameDetailPage } from "@/app/pages/GameDetailPage"
7-
import { LibraryPage } from "@/app/pages/LibraryPage"
8-
import { CollectionsPage } from "@/app/pages/CollectionsPage"
9-
import { DownloadsPage } from "@/app/pages/DownloadsPage"
10-
import { SettingsPage } from "@/app/pages/SettingsPage"
11-
import { WishlistPage } from "@/app/pages/WishlistPage"
12-
import { LikedPage } from "@/app/pages/LikedPage"
13-
import { AccountOverviewPage } from "@/app/pages/AccountOverviewPage"
14-
import { ViewHistoryPage } from "@/app/pages/ViewHistoryPage"
15-
import { SearchHistoryPage } from "@/app/pages/SearchHistoryPage"
16-
import { ScreenshotsPage } from "@/app/pages/ScreenshotsPage"
17-
import { LoginPage } from "@/app/pages/LoginPage"
18-
import { VerifyEmailPage } from "@/app/pages/VerifyEmailPage"
19-
import { ForgotPasswordPage } from "@/app/pages/ForgotPasswordPage"
20-
import { ResetPasswordPage } from "@/app/pages/ResetPasswordPage"
214
import { DownloadsProvider, useDownloads } from "@/context/downloads-context"
225
import { ToastProvider } from "@/context/toast-context"
236
import { AuthProvider } from "@/context/auth-context"
24-
import { InGameOverlay } from "@/components/InGameOverlay"
257
import { Toaster } from "@/components/Toaster"
268
import { Button } from "@/components/ui/button"
279
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
2810
import { AlertTriangle } from "lucide-react"
2911

12+
const LauncherPage = lazy(() => import("@/app/pages/LauncherPage").then((m) => ({ default: m.LauncherPage })))
13+
const SearchPage = lazy(() => import("@/app/pages/SearchPage").then((m) => ({ default: m.SearchPage })))
14+
const GameDetailPage = lazy(() => import("@/app/pages/GameDetailPage").then((m) => ({ default: m.GameDetailPage })))
15+
const LibraryPage = lazy(() => import("@/app/pages/LibraryPage").then((m) => ({ default: m.LibraryPage })))
16+
const CollectionsPage = lazy(() => import("@/app/pages/CollectionsPage").then((m) => ({ default: m.CollectionsPage })))
17+
const DownloadsPage = lazy(() => import("@/app/pages/DownloadsPage").then((m) => ({ default: m.DownloadsPage })))
18+
const SettingsPage = lazy(() => import("@/app/pages/SettingsPage").then((m) => ({ default: m.SettingsPage })))
19+
const WishlistPage = lazy(() => import("@/app/pages/WishlistPage").then((m) => ({ default: m.WishlistPage })))
20+
const LikedPage = lazy(() => import("@/app/pages/LikedPage").then((m) => ({ default: m.LikedPage })))
21+
const AccountOverviewPage = lazy(() => import("@/app/pages/AccountOverviewPage").then((m) => ({ default: m.AccountOverviewPage })))
22+
const ViewHistoryPage = lazy(() => import("@/app/pages/ViewHistoryPage").then((m) => ({ default: m.ViewHistoryPage })))
23+
const SearchHistoryPage = lazy(() => import("@/app/pages/SearchHistoryPage").then((m) => ({ default: m.SearchHistoryPage })))
24+
const ScreenshotsPage = lazy(() => import("@/app/pages/ScreenshotsPage").then((m) => ({ default: m.ScreenshotsPage })))
25+
const LoginPage = lazy(() => import("@/app/pages/LoginPage").then((m) => ({ default: m.LoginPage })))
26+
const VerifyEmailPage = lazy(() => import("@/app/pages/VerifyEmailPage").then((m) => ({ default: m.VerifyEmailPage })))
27+
const ForgotPasswordPage = lazy(() => import("@/app/pages/ForgotPasswordPage").then((m) => ({ default: m.ForgotPasswordPage })))
28+
const ResetPasswordPage = lazy(() => import("@/app/pages/ResetPasswordPage").then((m) => ({ default: m.ResetPasswordPage })))
29+
const InGameOverlay = lazy(() => import("@/components/InGameOverlay").then((m) => ({ default: m.InGameOverlay })))
30+
31+
function RouteFallback() {
32+
return <div className="min-h-screen bg-[#09090b]" />
33+
}
34+
3035
function ExtractionCloseGuard() {
3136
const { downloads } = useDownloads()
3237
const [request, setRequest] = useState<{ mode: "quit" | "hide"; extractionCount?: number; downloadCount?: number; appids?: string[] } | null>(null)
@@ -104,36 +109,38 @@ export default function App() {
104109
<ToastProvider>
105110
<AuthProvider>
106111
<DownloadsProvider>
107-
<Routes>
108-
<Route path="/overlay" element={<InGameOverlay />} />
112+
<Suspense fallback={<RouteFallback />}>
113+
<Routes>
114+
<Route path="/overlay" element={<InGameOverlay />} />
109115

110-
{/* Auth pages (inside app layout) */}
111-
<Route path="/login" element={<LoginPage />} />
112-
<Route path="/verify-email" element={<VerifyEmailPage />} />
113-
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
114-
<Route path="/reset-password" element={<ResetPasswordPage />} />
116+
{/* Auth pages (inside app layout) */}
117+
<Route path="/login" element={<LoginPage />} />
118+
<Route path="/verify-email" element={<VerifyEmailPage />} />
119+
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
120+
<Route path="/reset-password" element={<ResetPasswordPage />} />
115121

116-
{/* App routes - no login required */}
117-
<Route element={<AppWithDownloads />}>
118-
<Route path="/" element={<LauncherPage />} />
119-
<Route path="/launcher" element={<LauncherPage />} />
120-
<Route path="/search" element={<SearchPage />} />
121-
<Route path="/game/:id" element={<GameDetailPage />} />
122-
<Route path="/library" element={<LibraryPage />} />
123-
<Route path="/collections" element={<CollectionsPage />} />
124-
<Route path="/downloads" element={<DownloadsPage />} />
125-
<Route path="/settings" element={<SettingsPage />} />
126-
<Route path="/wishlist" element={<WishlistPage />} />
127-
<Route path="/liked" element={<LikedPage />} />
128-
<Route path="/account" element={<AccountOverviewPage />} />
129-
<Route path="/view-history" element={<ViewHistoryPage />} />
130-
<Route path="/search-history" element={<SearchHistoryPage />} />
131-
<Route path="/screenshots" element={<ScreenshotsPage />} />
132-
</Route>
122+
{/* App routes - no login required */}
123+
<Route element={<AppWithDownloads />}>
124+
<Route path="/" element={<LauncherPage />} />
125+
<Route path="/launcher" element={<LauncherPage />} />
126+
<Route path="/search" element={<SearchPage />} />
127+
<Route path="/game/:id" element={<GameDetailPage />} />
128+
<Route path="/library" element={<LibraryPage />} />
129+
<Route path="/collections" element={<CollectionsPage />} />
130+
<Route path="/downloads" element={<DownloadsPage />} />
131+
<Route path="/settings" element={<SettingsPage />} />
132+
<Route path="/wishlist" element={<WishlistPage />} />
133+
<Route path="/liked" element={<LikedPage />} />
134+
<Route path="/account" element={<AccountOverviewPage />} />
135+
<Route path="/view-history" element={<ViewHistoryPage />} />
136+
<Route path="/search-history" element={<SearchHistoryPage />} />
137+
<Route path="/screenshots" element={<ScreenshotsPage />} />
138+
</Route>
133139

134-
{/* Fallback */}
135-
<Route path="*" element={<Navigate to="/" replace />} />
136-
</Routes>
140+
{/* Fallback */}
141+
<Route path="*" element={<Navigate to="/" replace />} />
142+
</Routes>
143+
</Suspense>
137144
</DownloadsProvider>
138145
</AuthProvider>
139146
<Toaster />

renderer/src/app/pages/GameDetailPage.tsx

Lines changed: 0 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { createPortal } from "react-dom"
44
import { useNavigate, useParams, useSearchParams } from "react-router-dom"
55
import { Badge } from "@/components/ui/badge"
66
import { Button } from "@/components/ui/button"
7-
import { Input } from "@/components/ui/input"
87
import { GameCard } from "@/components/GameCard"
98
import { GameComments } from "@/components/GameComments"
109
import { CommentMarkdown } from "@/components/CommentMarkdown"
@@ -39,13 +38,11 @@ import {
3938
X,
4039
FolderOpen,
4140
Info,
42-
Layers3,
4341
Loader2,
4442
Minus,
4543
MoreHorizontal,
4644
Plus,
4745
Play,
48-
Tags,
4946
Terminal,
5047
} from "lucide-react"
5148
import { ExePickerModal } from "@/components/ExePickerModal"
@@ -162,11 +159,6 @@ export function GameDetailPage() {
162159
const [gameStartFailedOpen, setGameStartFailedOpen] = useState(false)
163160
const [linuxConfigOpen, setLinuxConfigOpen] = useState(false)
164161

165-
// Collection/tag editing state
166-
const [gameMeta, setGameMeta] = useState<{ collections?: string[]; tags?: string[] }>({})
167-
const [collectionInput, setCollectionInput] = useState("")
168-
const [tagInput, setTagInput] = useState("")
169-
170162
// Ref to track whether a game was just launched (cleared on manual quit)
171163
// Stores the expiry timestamp of the quick-exit detection window (0 = not watching)
172164
const gameJustLaunchedRef = useRef<number>(0)
@@ -199,56 +191,6 @@ export function GameDetailPage() {
199191
setLogoLoaded(false)
200192
}, [appid])
201193

202-
// ── Load library meta (collections/tags) for this game ──
203-
useEffect(() => {
204-
if (!appid) return
205-
let cancelled = false
206-
;(async () => {
207-
try {
208-
const allMeta = (await window.ucSettings?.get?.("libraryGameMeta")) || {}
209-
if (!cancelled) setGameMeta(allMeta[appid] || {})
210-
} catch {}
211-
})()
212-
return () => { cancelled = true }
213-
}, [appid])
214-
215-
const saveGameMeta = useCallback(async (updated: { collections?: string[]; tags?: string[] }) => {
216-
setGameMeta(updated)
217-
try {
218-
const allMeta = (await window.ucSettings?.get?.("libraryGameMeta")) || {}
219-
allMeta[appid] = updated
220-
await window.ucSettings?.set?.("libraryGameMeta", allMeta)
221-
} catch {}
222-
}, [appid])
223-
224-
const addCollection = useCallback(async () => {
225-
const val = collectionInput.trim()
226-
if (!val) return
227-
const existing = gameMeta.collections || []
228-
if (existing.includes(val)) { setCollectionInput(""); return }
229-
await saveGameMeta({ ...gameMeta, collections: [...existing, val] })
230-
setCollectionInput("")
231-
}, [collectionInput, gameMeta, saveGameMeta])
232-
233-
const removeCollection = useCallback(async (name: string) => {
234-
const existing = gameMeta.collections || []
235-
await saveGameMeta({ ...gameMeta, collections: existing.filter((c) => c !== name) })
236-
}, [gameMeta, saveGameMeta])
237-
238-
const addTag = useCallback(async () => {
239-
const val = tagInput.trim()
240-
if (!val) return
241-
const existing = gameMeta.tags || []
242-
if (existing.includes(val)) { setTagInput(""); return }
243-
await saveGameMeta({ ...gameMeta, tags: [...existing, val] })
244-
setTagInput("")
245-
}, [tagInput, gameMeta, saveGameMeta])
246-
247-
const removeTag = useCallback(async (name: string) => {
248-
const existing = gameMeta.tags || []
249-
await saveGameMeta({ ...gameMeta, tags: existing.filter((t) => t !== name) })
250-
}, [gameMeta, saveGameMeta])
251-
252194
// Fetch ProtonDB summary for this game (proxied through the web API)
253195
useEffect(() => {
254196
if (!game?.appid) return
@@ -1893,76 +1835,6 @@ export function GameDetailPage() {
18931835
/>
18941836
)}
18951837

1896-
{/* ── Collections & Tags ── */}
1897-
<div className="p-8 rounded-3xl bg-zinc-900/60 border border-white/[.07] backdrop-blur-md space-y-5 shadow-xl">
1898-
<div className="space-y-3">
1899-
<h3 className="section-label flex items-center gap-2">
1900-
<Layers3 className="h-4 w-4 text-zinc-400" />
1901-
Collections
1902-
</h3>
1903-
<div className="flex flex-wrap gap-1.5">
1904-
{(gameMeta.collections || []).map((c) => (
1905-
<Badge
1906-
key={c}
1907-
className="rounded-full border-zinc-700/50 bg-zinc-800/50 text-zinc-300 pl-2.5 pr-1 gap-1 cursor-pointer hover:bg-zinc-700/50"
1908-
onClick={() => void removeCollection(c)}
1909-
>
1910-
{c}
1911-
<X className="h-3 w-3 ml-0.5" />
1912-
</Badge>
1913-
))}
1914-
{!(gameMeta.collections?.length) && (
1915-
<span className="text-xs text-zinc-500 italic">No collections</span>
1916-
)}
1917-
</div>
1918-
<div className="flex gap-2">
1919-
<Input
1920-
value={collectionInput}
1921-
onChange={(e) => setCollectionInput(e.target.value)}
1922-
onKeyDown={(e) => { if (e.key === "Enter") void addCollection() }}
1923-
placeholder="Add collection..."
1924-
className="h-8 text-xs flex-1"
1925-
/>
1926-
<Button size="sm" className="h-8 px-3" onClick={() => void addCollection()} disabled={!collectionInput.trim()}>
1927-
Add
1928-
</Button>
1929-
</div>
1930-
</div>
1931-
<div className="h-px bg-white/10" />
1932-
<div className="space-y-3">
1933-
<h3 className="section-label flex items-center gap-2">
1934-
<Tags className="h-4 w-4 text-zinc-400" />
1935-
Tags
1936-
</h3>
1937-
<div className="flex flex-wrap gap-1.5">
1938-
{(gameMeta.tags || []).map((t) => (
1939-
<Badge
1940-
key={t}
1941-
className="rounded-full border-zinc-700/50 bg-zinc-800/50 text-zinc-300 pl-2.5 pr-1 gap-1 cursor-pointer hover:bg-zinc-700/50"
1942-
onClick={() => void removeTag(t)}
1943-
>
1944-
#{t}
1945-
<X className="h-3 w-3 ml-0.5" />
1946-
</Badge>
1947-
))}
1948-
{!(gameMeta.tags?.length) && (
1949-
<span className="text-xs text-zinc-500 italic">No tags</span>
1950-
)}
1951-
</div>
1952-
<div className="flex gap-2">
1953-
<Input
1954-
value={tagInput}
1955-
onChange={(e) => setTagInput(e.target.value)}
1956-
onKeyDown={(e) => { if (e.key === "Enter") void addTag() }}
1957-
placeholder="Add tag..."
1958-
className="h-8 text-xs flex-1"
1959-
/>
1960-
<Button size="sm" className="h-8 px-3" onClick={() => void addTag()} disabled={!tagInput.trim()}>
1961-
Add
1962-
</Button>
1963-
</div>
1964-
</div>
1965-
</div>
19661838
</div>
19671839
</div>
19681840
</div>

renderer/src/app/pages/WishlistPage.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ export function WishlistPage() {
127127
)}
128128

129129
{accountUser && (
130-
<UpgradeSuggesterSection baseUrl={baseUrl} />
130+
<div className="mb-6">
131+
<UpgradeSuggesterSection baseUrl={baseUrl} />
132+
</div>
131133
)}
132134

133135
{!accountUser && !accountLoading ? null : loading || accountLoading ? (

renderer/src/components/Sidebar.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { primaryNavItems, secondaryNavItems, bottomNavItems } from "@/lib/naviga
55
import { cn } from "@/lib/utils"
66
import { useState } from "react"
77
import { ScrollArea } from "@/components/ui/scroll-area"
8-
import { useLibraryCollections } from "@/hooks/use-library-collections"
8+
import { useUserCollections } from "@/hooks/use-user-collections"
99
import { useFollowedCollections } from "@/hooks/use-followed-collections"
1010

1111
interface SidebarProps {
@@ -20,7 +20,7 @@ export function Sidebar({ mobileOpen, onClose, collapsed, onToggleCollapse }: Si
2020
const [collectionsOpen, setCollectionsOpen] = useState(true)
2121
const location = useLocation()
2222
const navigate = useNavigate()
23-
const { collections } = useLibraryCollections()
23+
const { collections } = useUserCollections()
2424
const followed = useFollowedCollections()
2525
const followedUpdateCount = followed.items?.filter((c) => c.hasUpdates).length || 0
2626

@@ -201,10 +201,10 @@ export function Sidebar({ mobileOpen, onClose, collapsed, onToggleCollapse }: Si
201201
const isActive = activeCollection?.toLowerCase() === collection.name.toLowerCase()
202202
return (
203203
<NavLink
204-
key={collection.name}
204+
key={collection.id}
205205
to={`/library?collection=${encodeURIComponent(collection.name)}`}
206206
onClick={onClose}
207-
title={`${collection.name} (${collection.count})`}
207+
title={`${collection.name} (${collection.appids.length})`}
208208
className={cn(
209209
"flex justify-center items-center rounded-lg p-2.5 transition-colors duration-150",
210210
isActive
@@ -277,7 +277,7 @@ export function Sidebar({ mobileOpen, onClose, collapsed, onToggleCollapse }: Si
277277
const isActive = activeCollection?.toLowerCase() === collection.name.toLowerCase()
278278
return (
279279
<NavLink
280-
key={collection.name}
280+
key={collection.id}
281281
to={`/library?collection=${encodeURIComponent(collection.name)}`}
282282
onClick={onClose}
283283
className={cn(
@@ -293,7 +293,7 @@ export function Sidebar({ mobileOpen, onClose, collapsed, onToggleCollapse }: Si
293293
"text-[10px] font-medium tabular-nums",
294294
isActive ? "text-zinc-400" : "text-zinc-700 group-hover:text-zinc-500"
295295
)}>
296-
{collection.count}
296+
{collection.appids.length}
297297
</span>
298298
</NavLink>
299299
)

0 commit comments

Comments
 (0)