Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export default function App() {
path="/games/:gameId"
element={
<AuthWrapper>
<Navbar20 page="/games" collapsed={!drawerOpen} styleToggle cardSlider searchable>
<Navbar20 page="/games" collapsed={!drawerOpen} styleToggle cardSlider searchable mainPadding={0}>
<GameVideos />
</Navbar20>
</AuthWrapper>
Expand Down
16 changes: 8 additions & 8 deletions app/client/src/common/constants.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
export const SORT_OPTIONS = [
{
value: 'updated_at desc',
label: 'Created Desc',
label: 'Newest',
},
{
value: 'updated_at asc',
label: 'Created Asc',
label: 'Oldest',
},
{
value: 'video_info.title desc',
label: 'Title Desc',
value: 'video_info.title asc',
label: 'A-Z',
},
{
value: 'video_info.title asc',
label: 'Title Asc',
value: 'video_info.title desc',
label: 'Z-A',
},
{
value: 'views desc',
label: 'Views Desc',
label: 'Most Views',
},
{
value: 'views asc',
label: 'Views Asc',
label: 'Least Views',
},
]

Expand Down
5 changes: 5 additions & 0 deletions app/client/src/common/reactSelectSortTheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const selectTheme = {
...styles,
backgroundColor: '#001E3C',
borderColor: '#2684FF',
borderRadius: 10,
'&:hover': {
borderColor: '#2684FF',
},
Expand All @@ -17,6 +18,10 @@ const selectTheme = {
borderColor: '#2684FF',
},
}),
menuPortal: (styles) => ({
...styles,
zIndex: 2000,
}),
menuList: (styles) => ({
...styles,
backgroundColor: '#001E3C',
Expand Down
53 changes: 53 additions & 0 deletions app/client/src/components/game/GameVideosHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react'
import { Box } from '@mui/material'

const GameVideosHeader = ({ game, height = 200 }) => (
<Box
sx={{
position: 'relative',
width: '100%',
height,
overflow: 'hidden',
mb: 3,
}}
>
{game?.steamgriddb_id && (
<Box
sx={{
position: 'absolute',
inset: 0,
backgroundImage: `url(/api/game/assets/${game.steamgriddb_id}/hero_2.png?fallback=hero_1)`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
opacity: 0.7,
pointerEvents: 'none',
}}
/>
)}
<Box
sx={{
position: 'relative',
height: '100%',
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
px: 3,
}}
>
{game?.logo_url && (
<Box
component="img"
src={game.logo_url}
sx={{
maxHeight: 80,
maxWidth: 300,
objectFit: 'contain',
}}
/>
)}
</Box>
</Box>
)

export default GameVideosHeader
8 changes: 5 additions & 3 deletions app/client/src/components/nav/Navbar20.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ function Navbar20({
styleToggle = false,
cardSlider = false,
toolbar = true,
mainPadding = 3,
children,
}) {

Expand Down Expand Up @@ -641,7 +642,7 @@ function Navbar20({
>
<IconButton onClick={handleDrawerCollapse}>{open ? <ChevronLeftIcon /> : <ChevronRightIcon />}</IconButton>
</DrawerControl>
<Toolbar sx={{ backgroundColor: 'rgba(0,0,0,0)' }}>
<Toolbar sx={{ backgroundColor: 'rgba(0,0,0,0)', gap: 2 }}>
<IconButton
color="inherit"
aria-label="open drawer"
Expand All @@ -655,9 +656,10 @@ function Navbar20({
<Search
placeholder={`Search videos...`}
searchHandler={(value) => setSearchText(value)}
sx={{ width: '100%', ml: { xs: 0, sm: 2 } }}
sx={{ flexGrow: 1, minWidth: 0, ml: { xs: 0, sm: 2 } }}
/>
)}
<Box id="navbar-toolbar-extra" sx={{ display: 'flex', alignItems: 'center' }} />
</Toolbar>
</AppBar>
)}
Expand Down Expand Up @@ -696,7 +698,7 @@ function Navbar20({
component="main"
sx={{
flexGrow: 1,
p: page !== '/w' ? 3 : 0,
p: page !== '/w' ? mainPadding : 0,
width: { sm: `calc(100% - ${open ? drawerWidth : minimizedDrawerWidth}px)` },
}}
>
Expand Down
70 changes: 53 additions & 17 deletions app/client/src/views/GameVideos.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import React from 'react'
import { Box, Divider } from '@mui/material'
import ReactDOM from 'react-dom'
import { Box } from '@mui/material'
import { useParams } from 'react-router-dom'
import Select from 'react-select'
import { GameService } from '../services'
import VideoCards from '../components/admin/VideoCards'
import VideoList from '../components/admin/VideoList'
import GameVideosHeader from '../components/game/GameVideosHeader'
import LoadingSpinner from '../components/misc/LoadingSpinner'
import SnackbarAlert from '../components/alert/SnackbarAlert'
import { SORT_OPTIONS } from '../common/constants'
import selectSortTheme from '../common/reactSelectSortTheme'

const GameVideos = ({ cardSize, listStyle, authenticated, searchText }) => {
const { gameId } = useParams()
Expand All @@ -15,6 +20,8 @@ const GameVideos = ({ cardSize, listStyle, authenticated, searchText }) => {
const [game, setGame] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [alert, setAlert] = React.useState({ open: false })
const [sortOrder, setSortOrder] = React.useState(SORT_OPTIONS?.[0] || { value: 'newest', label: 'Newest' })
const [toolbarTarget, setToolbarTarget] = React.useState(null)

// Filter videos when searchText changes
if (searchText !== search) {
Expand All @@ -40,41 +47,70 @@ const GameVideos = ({ cardSize, listStyle, authenticated, searchText }) => {
})
}, [gameId])

React.useEffect(() => {
setToolbarTarget(document.getElementById('navbar-toolbar-extra'))
}, [])

function fetchVideos() {
GameService.getGameVideos(gameId)
.then((res) => setVideos(res.data))
.catch((err) => console.error(err))
}

const sortedVideos = React.useMemo(() => {
if (!filteredVideos || !Array.isArray(filteredVideos)) return []
const [field, direction] = (sortOrder?.value || 'updated_at desc').split(' ')
return [...filteredVideos].sort((a, b) => {
let aVal, bVal
if (field === 'video_info.title') {
aVal = a.info?.title?.toLowerCase() || ''
bVal = b.info?.title?.toLowerCase() || ''
} else if (field === 'views') {
aVal = a.view_count || 0
bVal = b.view_count || 0
} else {
aVal = new Date(a[field] || a.created_at || 0)
bVal = new Date(b[field] || b.created_at || 0)
}
if (aVal < bVal) return direction === 'asc' ? -1 : 1
if (aVal > bVal) return direction === 'asc' ? 1 : -1
return 0
})
}, [filteredVideos, sortOrder])

if (loading) return <LoadingSpinner />

return (
<Box>
<SnackbarAlert alert={alert} setAlert={setAlert} />
{toolbarTarget && ReactDOM.createPortal(
<Box sx={{ minWidth: 200 }}>
<Select
value={sortOrder}
options={SORT_OPTIONS}
onChange={setSortOrder}
styles={selectSortTheme}
menuPortalTarget={document.body}
menuPosition="fixed"
blurInputOnSelect
isSearchable={false}
/>
</Box>,
toolbarTarget,
)}
<GameVideosHeader
game={game}
/>
<Box sx={{ p: 3 }}>
{game?.logo_url && (
<Box sx={{ mb: 3 }}>
<Box
component="img"
src={game.logo_url}
sx={{
maxHeight: 80,
maxWidth: 300,
objectFit: 'contain',
}}
/>
<Divider sx={{ mt: 2 }} />
</Box>
)}
{listStyle === 'list' ? (
<VideoList
videos={filteredVideos}
videos={sortedVideos}
authenticated={authenticated}
feedView={false}
/>
) : (
<VideoCards
videos={filteredVideos}
videos={sortedVideos}
authenticated={authenticated}
size={cardSize}
feedView={false}
Expand Down
67 changes: 48 additions & 19 deletions app/server/fireshare/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -984,9 +984,16 @@ def get_steamgrid_assets(game_id):
def get_games():
from flask_login import current_user

# If user is authenticated, show all games
# If user is authenticated, show games that have at least one linked video
if current_user.is_authenticated:
games = GameMetadata.query.order_by(GameMetadata.name).all()
games = (
db.session.query(GameMetadata)
.join(VideoGameLink)
.join(Video)
.distinct()
.order_by(GameMetadata.name)
.all()
)
else:
# For public users, only show games that have at least one public (available) video
games = (
Expand Down Expand Up @@ -1125,26 +1132,46 @@ def unlink_video_from_game(video_id):

return Response(status=204)

def find_asset_with_extensions(asset_dir, base_name):
"""Try to find an asset file with any supported extension."""
for ext in ['.png', '.jpg', '.jpeg', '.webp']:
path = asset_dir / f'{base_name}{ext}'
if path.exists():
return path
return None

@api.route('/api/game/assets/<int:steamgriddb_id>/<filename>')
def get_game_asset(steamgriddb_id, filename):
# Validate filename to prevent path traversal
if not re.match(r'^(hero_[12]|logo_1|icon_1)\.(png|jpg|jpeg|webp)$', filename):
return Response(status=400, response='Invalid filename.')

paths = current_app.config['PATHS']
asset_dir = paths['data'] / 'game_assets' / str(steamgriddb_id)
base_name = filename.rsplit('.', 1)[0]

# Optional fallback parameter (e.g., ?fallback=hero_1)
fallback = request.args.get('fallback')
if fallback and not re.match(r'^(hero_[12]|logo_1|icon_1)$', fallback):
fallback = None # Invalid fallback, ignore it

asset_path = paths['data'] / 'game_assets' / str(steamgriddb_id) / filename

# Try exact filename first
if not asset_path.exists():
# Try other extensions if the requested one doesn't exist
base_name = filename.rsplit('.', 1)[0]
asset_dir = paths['data'] / 'game_assets' / str(steamgriddb_id)
# Try other extensions for the requested asset
if asset_dir.exists():
found = find_asset_with_extensions(asset_dir, base_name)
if found:
asset_path = found

# If still not found and fallback is specified, try the fallback asset
if not asset_path.exists() and fallback:
logger.info(f"{base_name} not found for game {steamgriddb_id}, trying fallback: {fallback}")
if asset_dir.exists():
for ext in ['.png', '.jpg', '.jpeg', '.webp']:
alternative_path = asset_dir / f'{base_name}{ext}'
if alternative_path.exists():
asset_path = alternative_path
break
found = find_asset_with_extensions(asset_dir, fallback)
if found:
asset_path = found

# If asset still doesn't exist, try to re-download from SteamGridDB
if not asset_path.exists():
Expand All @@ -1160,15 +1187,17 @@ def get_game_asset(steamgriddb_id, filename):

if result.get('success'):
logger.info(f"Assets downloaded for game {steamgriddb_id}: {result.get('assets')}")
# Try to find the file again after re-download
base_name = filename.rsplit('.', 1)[0]
asset_dir = paths['data'] / 'game_assets' / str(steamgriddb_id)
for ext in ['.png', '.jpg', '.jpeg', '.webp']:
alternative_path = asset_dir / f'{base_name}{ext}'
if alternative_path.exists():
asset_path = alternative_path
logger.info(f"Found {alternative_path.name}")
break
# Try to find the requested file after re-download
found = find_asset_with_extensions(asset_dir, base_name)
if found:
asset_path = found
logger.info(f"Found {asset_path.name}")
# If still not found, try fallback after re-download
elif fallback:
found = find_asset_with_extensions(asset_dir, fallback)
if found:
asset_path = found
logger.info(f"Found fallback {asset_path.name}")
else:
logger.error(f"Download failed for game {steamgriddb_id}: {result.get('error')}")
else:
Expand Down