11
2- import { useEffect , useCallback , useMemo , useRef , useState } from "react"
2+ import { useEffect , useCallback , useMemo , useRef , useState , type MouseEvent as ReactMouseEvent } from "react"
33import { createPortal } from "react-dom"
44import { useParams } from "react-router-dom"
55import { 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"
5253import { DownloadCheckModal } from "@/components/DownloadCheckModal"
5354import { DesktopShortcutModal } from "@/components/DesktopShortcutModal"
5455import { EditGameMetadataModal } from "@/components/EditGameMetadataModal"
56+ import { GameActionContextMenu , GameActionMenuPanel } from "@/components/GameActionMenu"
5557import { UpdateBackupWarningModal } from "@/components/VersionConflictModal"
5658import { GameLinuxConfigModal } from "@/components/GameLinuxConfigModal"
5759import { 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