diff --git a/app/client/src/components/admin/CompactVideoCard.js b/app/client/src/components/admin/CompactVideoCard.js
index 5cf4c0b3..9a5fa07d 100644
--- a/app/client/src/components/admin/CompactVideoCard.js
+++ b/app/client/src/components/admin/CompactVideoCard.js
@@ -10,6 +10,7 @@ import VideoService from '../../services/VideoService'
import _ from 'lodash'
import UpdateDetailsModal from '../modal/UpdateDetailsModal'
import LightTooltip from '../misc/LightTooltip'
+import GameDetectionCard from '../game/GameDetectionCard'
const URL = getUrl()
const PURL = getPublicWatchUrl()
@@ -36,6 +37,8 @@ const CompactVideoCard = ({
const [privateView, setPrivateView] = React.useState(video.info?.private)
const [detailsModalOpen, setDetailsModalOpen] = React.useState(false)
+ const [gameSuggestion, setGameSuggestion] = React.useState(null)
+ const [showSuggestion, setShowSuggestion] = React.useState(true)
const previousVideoRef = React.useRef()
const previousVideo = previousVideoRef.current
@@ -51,6 +54,25 @@ const CompactVideoCard = ({
previousVideoRef.current = video
})
+ React.useEffect(() => {
+ // Fetch game suggestion when component mounts
+ VideoService.getGameSuggestion(video.video_id)
+ .then((response) => {
+ if (response.data) {
+ setGameSuggestion(response.data)
+ setShowSuggestion(true)
+ }
+ })
+ .catch(() => {
+ // No suggestion or error - that's fine
+ })
+ }, [video.video_id])
+
+ const handleSuggestionComplete = () => {
+ setShowSuggestion(false)
+ setGameSuggestion(null)
+ }
+
const debouncedMouseEnter = React.useRef(
_.debounce(() => {
setHover(true)
@@ -304,8 +326,8 @@ const CompactVideoCard = ({
width: cardWidth,
minHeight: previewVideoHeight,
border: '1px solid #3399FFAE',
- borderBottomRightRadius: '6px',
- borderBottomLeftRadius: '6px',
+ borderBottomRightRadius: (authenticated && gameSuggestion && showSuggestion && !editMode) ? 0 : '6px',
+ borderBottomLeftRadius: (authenticated && gameSuggestion && showSuggestion && !editMode) ? 0 : '6px',
borderTop: 'none',
background: 'repeating-linear-gradient(45deg,#606dbc,#606dbc 10px,#465298 10px,#465298 20px)',
overflow: 'hidden'
@@ -326,8 +348,8 @@ const CompactVideoCard = ({
WebkitAnimationDuration: '1.5s',
WebkitAnimationFillMode: 'both',
border: '1px solid #3399FFAE',
- borderBottomRightRadius: '6px',
- borderBottomLeftRadius: '6px',
+ borderBottomRightRadius: (authenticated && gameSuggestion && showSuggestion && !editMode) ? 0 : '6px',
+ borderBottomLeftRadius: (authenticated && gameSuggestion && showSuggestion && !editMode) ? 0 : '6px',
borderTop: 'none',
overflow: 'hidden'
}}
@@ -398,6 +420,16 @@ const CompactVideoCard = ({
+
+ {/* Game Detection Suggestion Card */}
+ {authenticated && gameSuggestion && showSuggestion && !editMode && (
+
+ )}
>
)
diff --git a/app/client/src/components/game/GameDetectionCard.js b/app/client/src/components/game/GameDetectionCard.js
new file mode 100644
index 00000000..2c8bc0b5
--- /dev/null
+++ b/app/client/src/components/game/GameDetectionCard.js
@@ -0,0 +1,181 @@
+import React, { useState } from 'react'
+import { Box, IconButton, Typography, Fade } from '@mui/material'
+import CheckIcon from '@mui/icons-material/Check'
+import CloseIcon from '@mui/icons-material/Close'
+import SportsEsportsIcon from '@mui/icons-material/SportsEsports'
+import VideoService from '../../services/VideoService'
+import GameService from '../../services/GameService'
+
+/**
+ * GameDetectionCard - Shows automatic game detection suggestions
+ * Appears below video thumbnails when a game is detected from the filename
+ */
+export default function GameDetectionCard({ videoId, suggestion, onComplete, cardWidth }) {
+ const [loading, setLoading] = useState(false)
+ const [status, setStatus] = useState('pending') // 'pending', 'accepted', 'rejected'
+
+ const handleAccept = async (e) => {
+ e.stopPropagation() // Prevent triggering video card click
+ setLoading(true)
+
+ try {
+ let gameId = suggestion.game_id
+
+ // If game doesn't exist in our DB (came from SteamGridDB), create it first
+ if (!gameId && suggestion.steamgriddb_id) {
+ // Reuse the same logic as GameSearch.js
+ const assets = (await GameService.getGameAssets(suggestion.steamgriddb_id)).data
+ const gameData = {
+ steamgriddb_id: suggestion.steamgriddb_id,
+ name: suggestion.game_name,
+ hero_url: assets.hero_url,
+ logo_url: assets.logo_url,
+ icon_url: assets.icon_url,
+ }
+ const createdGame = (await GameService.createGame(gameData)).data
+ gameId = createdGame.id
+ }
+
+ // Link video to game using existing service
+ await GameService.linkVideoToGame(videoId, gameId)
+
+ // Remove the suggestion from cache
+ await VideoService.rejectGameSuggestion(videoId)
+
+ setStatus('accepted')
+ // Auto-hide after showing success
+ setTimeout(() => {
+ onComplete?.()
+ }, 2000)
+ } catch (err) {
+ console.error('Failed to accept game suggestion:', err)
+ setLoading(false)
+ }
+ }
+
+ const handleReject = async (e) => {
+ e.stopPropagation() // Prevent triggering video card click
+ setLoading(true)
+
+ try {
+ await VideoService.rejectGameSuggestion(videoId)
+ setStatus('rejected')
+ // Hide immediately
+ setTimeout(() => {
+ onComplete?.()
+ }, 300)
+ } catch (err) {
+ console.error('Failed to reject game suggestion:', err)
+ setLoading(false)
+ }
+ }
+
+ if (status === 'accepted') {
+ return (
+
+
+
+
+ Linked to {suggestion.game_name}
+
+
+
+ )
+ }
+
+ if (status === 'rejected') {
+ return null
+ }
+
+ return (
+
+ e.stopPropagation()} // Prevent triggering video card click
+ sx={{
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ p: 1,
+ width: cardWidth,
+ background: 'rgba(50, 50, 50, 0.95)',
+ borderLeft: '1px solid #3399FFAE',
+ borderRight: '1px solid #3399FFAE',
+ borderBottom: '1px solid #3399FFAE',
+ borderBottomLeftRadius: '6px',
+ borderBottomRightRadius: '6px',
+ lineHeight: 0,
+ }}
+ >
+
+
+
+ Detected: {suggestion.game_name}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/client/src/components/nav/Navbar20.js b/app/client/src/components/nav/Navbar20.js
index 10a4264e..ea4fc69d 100644
--- a/app/client/src/components/nav/Navbar20.js
+++ b/app/client/src/components/nav/Navbar20.js
@@ -49,7 +49,7 @@ const minimizedDrawerWidth = 57
const CARD_SIZE_DEFAULT = 375
const CARD_SIZE_MULTIPLIER = 2
-const pages = [
+const allPages = [
{ title: 'My Videos', icon: , href: '/', private: true },
{ title: 'Public Videos', icon: , href: '/feed', private: false },
{ title: 'Games', icon: , href: '/games', private: false },
@@ -149,6 +149,14 @@ function Navbar20({
const [alert, setAlert] = React.useState({ open: false })
const navigate = useNavigate()
+ const uiConfig = getSetting('ui_config') || {}
+ const pages = allPages.filter((p) => {
+ if (p.href === '/' && uiConfig.show_my_videos === false) return false
+ if (p.href === '/feed' && uiConfig.show_public_videos === false) return false
+ if (p.href === '/games' && uiConfig.show_games === false) return false
+ return true
+ })
+
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen)
}
diff --git a/app/client/src/services/VideoService.js b/app/client/src/services/VideoService.js
index d11b89ad..e639e7e6 100644
--- a/app/client/src/services/VideoService.js
+++ b/app/client/src/services/VideoService.js
@@ -116,6 +116,15 @@ const service = {
scan() {
return Api().get('/api/manual/scan')
},
+ scanGames() {
+ return Api().get('/api/manual/scan-games')
+ },
+ getGameSuggestion(videoId) {
+ return Api().get(`/api/videos/${videoId}/game/suggestion`)
+ },
+ rejectGameSuggestion(videoId) {
+ return Api().delete(`/api/videos/${videoId}/game/suggestion`)
+ },
}
export default service
diff --git a/app/client/src/views/Dashboard.js b/app/client/src/views/Dashboard.js
index 6bb15c5b..053395ae 100644
--- a/app/client/src/views/Dashboard.js
+++ b/app/client/src/views/Dashboard.js
@@ -396,13 +396,10 @@ const Dashboard = ({ authenticated, searchText, cardSize, listStyle }) => {
{/* Link to Game Dialog */}