Skip to content

Commit 7a418f8

Browse files
committed
1.8.2-BETA
1 parent 832dc78 commit 7a418f8

6 files changed

Lines changed: 650 additions & 292 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Fixes & Improvements
66

7+
- Reworked installed-game actions so Library covers now support right-click context menus, visible hover tools, and a shared action surface across Library and game Details instead of hiding everything behind a tiny settings gear.
8+
- Polished the Library command header with clearer hierarchy, better search guidance, and direct affordance hints so advanced actions are easier to discover.
79
- Simplified UC.Files download behavior to use Electron's standard downloader path (same flow as other hosts) now that storage is Backblaze-backed, removing reliance on the custom parallel UC.Files path.
810
- Fixed UC.Files share-link resolution for `/download/{token}` URLs by treating them as share tokens (not file IDs), preventing false "link could not be resolved" failures during game downloads.
911
- Fixed UC.Files-backed artwork and animated hero media on mirror domains by routing them through the active website domain instead of loading `files.union-crax.xyz` directly. This also covers remaining overlay and avatar media paths that were still bypassing the shared proxy helper.

electron/main.cjs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2201,8 +2201,32 @@ async function getDownloadsStateStore() {
22012201
}
22022202
if (!downloadsStateStorePromise) {
22032203
downloadsStateStorePromise = (async () => {
2204-
const db = new ClassicLevel(downloadsStateDbPath, { valueEncoding: 'json' })
2205-
await db.open()
2204+
async function openDbWithRecovery() {
2205+
let db = new ClassicLevel(downloadsStateDbPath, { valueEncoding: 'json' })
2206+
try {
2207+
await db.open()
2208+
return db
2209+
} catch {
2210+
// Step 1: stale LOCK file from a previous crash — delete and retry
2211+
const lockPath = path.join(downloadsStateDbPath, 'LOCK')
2212+
if (fs.existsSync(lockPath)) {
2213+
try { fs.unlinkSync(lockPath) } catch { }
2214+
try {
2215+
await db.open()
2216+
ucLog('[State] Recovered LevelDB after stale LOCK removal', 'warn')
2217+
return db
2218+
} catch { }
2219+
}
2220+
// Step 2: DB is corrupted / unrecoverable — wipe directory and start fresh
2221+
ucLog('[State] LevelDB unrecoverable, wiping state-db and starting fresh', 'warn')
2222+
try { await db.close() } catch { }
2223+
try { fs.rmSync(downloadsStateDbPath, { recursive: true, force: true }) } catch { }
2224+
db = new ClassicLevel(downloadsStateDbPath, { valueEncoding: 'json' })
2225+
await db.open()
2226+
return db
2227+
}
2228+
}
2229+
const db = await openDbWithRecovery()
22062230
const store = db.sublevel('downloads', { valueEncoding: 'json' })
22072231
await store.open()
22082232
return store
@@ -6490,11 +6514,17 @@ function createWindow(existingSplash) {
64906514
const url = item.getURL()
64916515
const normalizedUrl = normalizeDownloadUrl(url)
64926516
const itemFilename = item.getFilename()
6517+
// Collect all URLs in the redirect chain (original + any intermediate + final CDN URL)
6518+
const itemUrlChain = (item.getURLChain ? item.getURLChain() : [url]).filter(Boolean)
6519+
const itemNormalizedChain = new Set(itemUrlChain.map(normalizeDownloadUrl))
64936520
const matchIndex = pendingDownloads.findIndex((entry) =>
64946521
entry.url === url ||
64956522
(entry.normalizedUrl && entry.normalizedUrl === normalizedUrl) ||
64966523
(entry.filename && entry.filename === itemFilename) ||
6497-
(Array.isArray(entry.urlChain) && entry.urlChain.includes(url))
6524+
(Array.isArray(entry.urlChain) && entry.urlChain.some((u) => itemUrlChain.includes(u))) ||
6525+
// Match original pending URL against any URL in the redirect chain (handles CDN redirects)
6526+
(entry.url && itemNormalizedChain.has(normalizeDownloadUrl(entry.url))) ||
6527+
(entry.normalizedUrl && itemNormalizedChain.has(entry.normalizedUrl))
64986528
)
64996529
const match = matchIndex >= 0 ? pendingDownloads.splice(matchIndex, 1)[0] : null
65006530
const downloadId = match?.downloadId || `${Date.now()}-${Math.random().toString(16).slice(2)}`
@@ -6506,8 +6536,11 @@ function createWindow(existingSplash) {
65066536
return
65076537
}
65086538

6509-
// Always use match.filename if available to ensure consistency with parser logic, even if server sends different Content-Disposition
6510-
const filename = match?.filename ? match.filename : itemFilename
6539+
// Prefer the CDN-served filename (has a real extension from Content-Disposition) over match.filename,
6540+
// which may be a token hash with no extension (e.g. redirect via files.union-crax.xyz/download/<token>).
6541+
const filename = (itemFilename && path.extname(itemFilename))
6542+
? itemFilename
6543+
: (match?.filename || itemFilename || 'download')
65116544
uc_log(`will-download - url=${item.getURL()}`)
65126545
uc_log(`will-download - match.filename=${match?.filename}, item.getFilename()=${item.getFilename()}, final filename=${filename}`)
65136546
const partIndex = match?.partIndex

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "unioncrax-direct",
3-
"version": "1.8.1",
3+
"version": "1.8.2-BETA",
44
"description": "Standalone Electron app for managing and launching games from UnionCrax",
55
"type": "module",
66
"main": "electron/main.cjs",

renderer/src/app/pages/GameDetailPage.tsx

Lines changed: 77 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
import { useEffect, useCallback, useMemo, useRef, useState } from "react"
2+
import { useEffect, useCallback, useMemo, useRef, useState, type MouseEvent as ReactMouseEvent } from "react"
33
import { createPortal } from "react-dom"
44
import { useParams } from "react-router-dom"
55
import { Badge } from "@/components/ui/badge"
@@ -40,6 +40,7 @@ import {
4040
Layers3,
4141
Loader2,
4242
Minus,
43+
MoreHorizontal,
4344
Plus,
4445
Play,
4546
Tags,
@@ -52,6 +53,7 @@ import { LinuxExperiences } from "@/components/LinuxExperiences"
5253
import { DownloadCheckModal } from "@/components/DownloadCheckModal"
5354
import { DesktopShortcutModal } from "@/components/DesktopShortcutModal"
5455
import { EditGameMetadataModal } from "@/components/EditGameMetadataModal"
56+
import { GameActionContextMenu, GameActionMenuPanel } from "@/components/GameActionMenu"
5557
import { UpdateBackupWarningModal } from "@/components/VersionConflictModal"
5658
import { GameLinuxConfigModal } from "@/components/GameLinuxConfigModal"
5759
import { gameLogger } from "@/lib/logger"
@@ -126,6 +128,7 @@ export function GameDetailPage() {
126128
const [exePickerFolder, setExePickerFolder] = useState<string | null>(null)
127129
const [exePickerMode, setExePickerMode] = useState<"launch" | "set">("launch")
128130
const [actionMenuOpen, setActionMenuOpen] = useState(false)
131+
const [actionMenuContextPosition, setActionMenuContextPosition] = useState<{ x: number; y: number } | null>(null)
129132
const [shortcutFeedback, setShortcutFeedback] = useState<{ type: 'success' | 'error'; message: string } | null>(null)
130133
const [pendingDeleteAction, setPendingDeleteAction] = useState<"installed" | "installing" | null>(null)
131134
const [editMetadataOpen, setEditMetadataOpen] = useState(false)
@@ -1065,6 +1068,13 @@ export function GameDetailPage() {
10651068
setPendingDeleteAction("installed")
10661069
}
10671070

1071+
const handleActionCardContextMenu = (event: ReactMouseEvent<HTMLDivElement>) => {
1072+
if (!showActionMenu) return
1073+
event.preventDefault()
1074+
setActionMenuOpen(false)
1075+
setActionMenuContextPosition({ x: event.clientX, y: event.clientY })
1076+
}
1077+
10681078
const runDeleteGame = async (action: "installed" | "installing") => {
10691079
if (!game) return
10701080
try {
@@ -1431,7 +1441,10 @@ export function GameDetailPage() {
14311441

14321442
</div>
14331443
<div className="space-y-6">
1434-
<div className="p-8 rounded-3xl bg-zinc-950/60 border border-white/[.07] backdrop-blur-md shadow-xl">
1444+
<div
1445+
className={`p-8 rounded-3xl bg-zinc-950/60 border border-white/[.07] backdrop-blur-md shadow-xl ${showActionMenu ? "cursor-context-menu" : ""}`}
1446+
onContextMenu={handleActionCardContextMenu}
1447+
>
14351448
<div className="flex items-center gap-3">
14361449
<Button
14371450
size="lg"
@@ -1473,93 +1486,46 @@ export function GameDetailPage() {
14731486
<PopoverTrigger asChild>
14741487
<Button
14751488
variant="outline"
1476-
size="icon"
1477-
className="h-[52px] w-[52px] rounded-full border-white/[.07] bg-zinc-900/60 text-zinc-300 hover:bg-zinc-800 hover:text-white backdrop-blur-md active:scale-95"
1489+
size="lg"
1490+
onClick={() => setActionMenuContextPosition(null)}
1491+
className="h-[52px] rounded-full border-white/[.07] bg-zinc-900/60 px-4 text-zinc-300 hover:bg-zinc-800 hover:text-white backdrop-blur-md active:scale-95"
14781492
aria-label="Game actions"
14791493
>
1480-
<Settings className="h-5 w-5" />
1494+
<MoreHorizontal className="h-4.5 w-4.5" />
1495+
<span className="text-sm font-medium">Actions</span>
14811496
</Button>
14821497
</PopoverTrigger>
1483-
<PopoverContent align="end" className="w-56 rounded-2xl p-2 bg-zinc-950/95 border border-white/[.07] text-white shadow-2xl backdrop-blur-xl">
1484-
<button
1485-
type="button"
1486-
onClick={() => {
1498+
<PopoverContent align="end" className="w-auto border-none bg-transparent p-0 shadow-none">
1499+
<GameActionMenuPanel
1500+
gameName={game?.name || "Game"}
1501+
gameSource={game?.source}
1502+
isExternal={Boolean(installedManifest?.isExternal)}
1503+
isLinux={isLinux}
1504+
shortcutFeedback={shortcutFeedback}
1505+
onSetExecutable={() => {
1506+
setActionMenuOpen(false)
14871507
void openExecutablePicker()
14881508
}}
1489-
className="flex w-full items-center rounded-lg px-3 py-2 text-left text-sm text-zinc-400 transition-colors hover:text-white hover:bg-white/10"
1490-
>
1491-
<Settings className="mr-2 h-4 w-4" />
1492-
Set Executable
1493-
</button>
1494-
<button
1495-
type="button"
1496-
onClick={() => {
1509+
onOpenFiles={() => {
14971510
setActionMenuOpen(false)
1511+
void openGameFiles()
1512+
}}
1513+
onCreateShortcut={() => {
14981514
void handleCreateShortcut()
14991515
}}
1500-
className="flex w-full items-center rounded-lg px-3 py-2 text-left text-sm text-zinc-400 transition-colors hover:text-white hover:bg-white/10"
1501-
>
1502-
<ExternalLink className="mr-2 h-4 w-4" />
1503-
Create Desktop Shortcut
1504-
</button>
1505-
<button
1506-
type="button"
1507-
onClick={() => {
1516+
onEditDetails={isExternalGame ? () => {
15081517
setActionMenuOpen(false)
1509-
void openGameFiles()
1510-
}}
1511-
className="flex w-full items-center rounded-lg px-3 py-2 text-left text-sm text-zinc-400 transition-colors hover:text-white hover:bg-white/10"
1512-
>
1513-
<FolderOpen className="mr-2 h-4 w-4" />
1514-
Open Game Files
1515-
</button>
1516-
{isExternalGame && (
1517-
<button
1518-
type="button"
1519-
onClick={() => {
1520-
setActionMenuOpen(false)
1521-
setEditMetadataOpen(true)
1522-
}}
1523-
className="flex w-full items-center rounded-lg px-3 py-2 text-left text-sm text-zinc-400 transition-colors hover:text-white hover:bg-white/10"
1524-
>
1525-
<Settings className="mr-2 h-4 w-4" />
1526-
Edit Details
1527-
</button>
1528-
)}
1529-
{isLinux && (
1530-
<button
1531-
type="button"
1532-
onClick={() => {
1533-
setActionMenuOpen(false)
1534-
setLinuxConfigOpen(true)
1535-
}}
1536-
className="flex w-full items-center rounded-lg px-3 py-2 text-left text-sm text-zinc-400 transition-colors hover:text-white hover:bg-white/10"
1537-
>
1538-
<Terminal className="mr-2 h-4 w-4" />
1539-
Linux / VR Config
1540-
</button>
1541-
)}
1542-
<div className="my-1 h-px bg-white/10" />
1543-
<button
1544-
type="button"
1545-
onClick={() => {
1518+
setEditMetadataOpen(true)
1519+
} : undefined}
1520+
onLinuxConfig={isLinux ? () => {
1521+
setActionMenuOpen(false)
1522+
setLinuxConfigOpen(true)
1523+
} : undefined}
1524+
onDelete={() => {
15461525
setActionMenuOpen(false)
15471526
void handleDeleteGame()
15481527
}}
1549-
className="flex w-full items-center rounded-lg px-3 py-2 text-left text-sm text-destructive transition-colors hover:bg-destructive/10"
1550-
>
1551-
{installedManifest?.isExternal ? (
1552-
<>
1553-
<Unlink2 className="mr-2 h-4 w-4" />
1554-
Unlink Game
1555-
</>
1556-
) : (
1557-
<>
1558-
<Trash2 className="mr-2 h-4 w-4" />
1559-
Delete Game
1560-
</>
1561-
)}
1562-
</button>
1528+
/>
15631529
</PopoverContent>
15641530
</Popover>
15651531
) : null}
@@ -1599,6 +1565,41 @@ export function GameDetailPage() {
15991565
)}
16001566
</div>
16011567

1568+
<GameActionContextMenu
1569+
open={Boolean(actionMenuContextPosition && showActionMenu)}
1570+
position={actionMenuContextPosition}
1571+
onClose={() => setActionMenuContextPosition(null)}
1572+
gameName={game?.name || "Game"}
1573+
gameSource={game?.source}
1574+
isExternal={Boolean(installedManifest?.isExternal)}
1575+
isLinux={isLinux}
1576+
shortcutFeedback={null}
1577+
onSetExecutable={() => {
1578+
setActionMenuContextPosition(null)
1579+
void openExecutablePicker()
1580+
}}
1581+
onOpenFiles={() => {
1582+
setActionMenuContextPosition(null)
1583+
void openGameFiles()
1584+
}}
1585+
onCreateShortcut={() => {
1586+
setActionMenuContextPosition(null)
1587+
void handleCreateShortcut()
1588+
}}
1589+
onEditDetails={isExternalGame ? () => {
1590+
setActionMenuContextPosition(null)
1591+
setEditMetadataOpen(true)
1592+
} : undefined}
1593+
onLinuxConfig={isLinux ? () => {
1594+
setActionMenuContextPosition(null)
1595+
setLinuxConfigOpen(true)
1596+
} : undefined}
1597+
onDelete={() => {
1598+
setActionMenuContextPosition(null)
1599+
void handleDeleteGame()
1600+
}}
1601+
/>
1602+
16021603
<div className={`grid grid-cols-2 gap-4${isUCMatched ? ' opacity-40 blur-[2px] pointer-events-none select-none' : ''}`}>
16031604
<div className="p-5 rounded-3xl bg-zinc-900/60 border border-white/[.07] backdrop-blur-md text-center shadow-xl">
16041605
<Download className="h-6 w-6 text-white mx-auto mb-3 drop-shadow-md" />

0 commit comments

Comments
 (0)