Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5bab651
Add game metadata support with SteamGridDB integration
dammitjeff Dec 30, 2025
d384eee
Merge pull request #367 from dammitjeff/add-game-metadata-support
ShaneIsrael Dec 31, 2025
8d11853
Initial plan
Copilot Jan 2, 2026
6c041e3
Fix video player overflow on /w/ page and modal view
Copilot Jan 2, 2026
ee1891a
Merge pull request #368 from ShaneIsrael/copilot/fix-video-player-lay…
ShaneIsrael Jan 2, 2026
58a6608
fixed undefined views and steam api key popup
dammitjeff Jan 3, 2026
ff8cad7
fixed saving files locally
dammitjeff Jan 3, 2026
972a40c
disabled user input when selecting game
dammitjeff Jan 3, 2026
193e92b
simplify color hex
dammitjeff Jan 3, 2026
fe8fa47
clip visibility now matches with game tab visibility
dammitjeff Jan 3, 2026
541c767
Merge pull request #369 from dammitjeff/fix-games-bugs
ShaneIsrael Jan 3, 2026
0e8d23c
if game assets are missing, can now check and re-download assets
dammitjeff Jan 3, 2026
34e8c38
Added edit button to games tab
dammitjeff Jan 3, 2026
079864f
fix alignment
dammitjeff Jan 4, 2026
7f42483
Made all edit buttons equal in padding
dammitjeff Jan 4, 2026
0275153
Add edit mode functionality for video clips and game linking in "My v…
dammitjeff Jan 4, 2026
d8fc2d0
Merge pull request #371 from dammitjeff/games-page-edit-button
ShaneIsrael Jan 4, 2026
012c286
fix thumbnail overflow issue
ShaneIsrael Jan 4, 2026
e2f5b53
Merge branch 'develop' into fix-thumbnail-overflow-issue
ShaneIsrael Jan 4, 2026
a1c976d
Merge pull request #372 from ShaneIsrael/fix-thumbnail-overflow-issue
ShaneIsrael Jan 4, 2026
ea3a786
Bump version from 1.3.2 to 1.3.3
ShaneIsrael Jan 4, 2026
508da44
Update docker-compose.yml with additional comments
ShaneIsrael Jan 4, 2026
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/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fireshare",
"version": "1.3.2",
"version": "1.3.3",
"private": true,
"dependencies": {
"@emotion/react": "^11.9.0",
Expand Down
22 changes: 22 additions & 0 deletions app/client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Dashboard from './views/Dashboard'
import NotFound from './views/NotFound'
import Settings from './views/Settings'
import Feed from './views/Feed'
import Games from './views/Games'
import GameVideos from './views/GameVideos'
import darkTheme from './common/darkTheme'
import { ConfigService } from './services'
import { getSetting, setSetting } from './common/utils'
Expand Down Expand Up @@ -73,6 +75,26 @@ export default function App() {
</AuthWrapper>
}
/>
<Route
path="/games"
element={
<AuthWrapper>
<Navbar20 page="/games" collapsed={!drawerOpen} searchable>
<Games />
</Navbar20>
</AuthWrapper>
}
/>
<Route
path="/games/:gameId"
element={
<AuthWrapper>
<Navbar20 page="/games" collapsed={!drawerOpen} styleToggle cardSlider searchable>
<GameVideos />
</Navbar20>
</AuthWrapper>
}
/>
<Route
path="/w/:id"
element={
Expand Down
8 changes: 4 additions & 4 deletions app/client/src/common/darkTheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,16 +174,16 @@ const theme = {
},
styleOverrides: {
root: {
color: '#66B2FF',
color: '#a6d2feff',
fontWeight: 700,
display: 'inline-flex',
alignItems: 'center',
'&:hover': {
color: '#99CCF3',
color: '#b6dbf7ff',
},
'&.MuiTypography-body1 > svg': {
marginTop: 2,
},
},
'& svg:last-child': {
marginLeft: 2,
},
Expand Down Expand Up @@ -473,7 +473,7 @@ const theme = {
main: '#DEA500',
light: '#FFDC48',
dark: '#AB6800',
contrastText: 'rgba(0, 0, 0, 0.87)',
contrastText: 'rgba(255, 255, 255, 0.87)',
},
secondary: {
main: '#ce93d8',
Expand Down
44 changes: 40 additions & 4 deletions app/client/src/components/admin/CompactVideoCard.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { Button, ButtonGroup, Grid, IconButton, InputBase, Typography, Box } from '@mui/material'
import { Button, ButtonGroup, Grid, IconButton, InputBase, Typography, Box, Checkbox } from '@mui/material'
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'
import VisibilityIcon from '@mui/icons-material/Visibility'
import EditIcon from '@mui/icons-material/Edit'
Expand All @@ -15,7 +15,17 @@ const URL = getUrl()
const PURL = getPublicWatchUrl()
const SERVED_BY = getServedBy()

const CompactVideoCard = ({ video, openVideoHandler, alertHandler, cardWidth, authenticated, deleted }) => {
const CompactVideoCard = ({
video,
openVideoHandler,
alertHandler,
cardWidth,
authenticated,
deleted,
editMode = false,
isSelected = false,
onSelect = () => {},
}) => {
const [intVideo, setIntVideo] = React.useState(video)
const [videoId, setVideoId] = React.useState(video.video_id)
const [title, setTitle] = React.useState(video.info?.title)
Expand Down Expand Up @@ -144,6 +154,10 @@ const CompactVideoCard = ({ video, openVideoHandler, alertHandler, cardWidth, au
width: '100%',
bgcolor: 'rgba(0, 0, 0, 0)',
lineHeight: 0,
border: isSelected ? '3px solid' : '3px solid transparent',
borderColor: isSelected ? 'primary.main' : 'transparent',
borderRadius: '6px',
transition: 'border 0.3s ease',
}}
>
<ButtonGroup
Expand Down Expand Up @@ -232,12 +246,34 @@ const CompactVideoCard = ({ video, openVideoHandler, alertHandler, cardWidth, au
}}
>
<div
style={{ position: 'relative', cursor: 'pointer' }}
onClick={() => openVideoHandler(video.video_id)}
style={{ position: 'relative', cursor: 'pointer', display: 'flex' }}
onClick={() => (editMode ? onSelect(video.video_id) : openVideoHandler(video.video_id))}
onMouseEnter={debouncedMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseDown={handleMouseDown}
>
{/* Checkbox for edit mode */}
{editMode && (
<Checkbox
checked={isSelected}
onChange={(e) => {
e.stopPropagation()
onSelect(video.video_id)
}}
sx={{
position: 'absolute',
top: 8,
left: 8,
zIndex: 2,
color: 'white',
bgcolor: 'rgba(0, 0, 0, 0.5)',
borderRadius: '4px',
'&.Mui-checked': {
color: 'primary.main',
},
}}
/>
)}
{!video.available && (
<Box
sx={{ position: 'absolute', top: 0, left: 0, background: '#FF000060', width: '100%', height: '100%' }}
Expand Down
6 changes: 6 additions & 0 deletions app/client/src/components/admin/VideoCards.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const VideoCards = ({
fetchVideos,
authenticated,
size,
editMode = false,
selectedVideos = new Set(),
onVideoSelect = () => {},
}) => {
const [vids, setVideos] = React.useState(videos)
const [alert, setAlert] = React.useState({ open: false })
Expand Down Expand Up @@ -165,6 +168,9 @@ const VideoCards = ({
cardWidth={size}
authenticated={authenticated}
deleted={handleDelete}
editMode={editMode}
isSelected={selectedVideos.has(v.video_id)}
onSelect={onVideoSelect}
/>
))}
</Grid>
Expand Down
16 changes: 15 additions & 1 deletion app/client/src/components/admin/VisibilityCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@ import { useIsVisible } from 'react-is-visible'
import { Grid } from '@mui/material'
import CompactVideoCard from './CompactVideoCard'

const VisibilityCard = ({ video, openVideo, handleAlert, cardWidth, authenticated, openDetailsModal, deleted }) => {
const VisibilityCard = ({
video,
openVideo,
handleAlert,
cardWidth,
authenticated,
openDetailsModal,
deleted,
editMode = false,
isSelected = false,
onSelect = () => {},
}) => {
const nodeRef = useRef()
const isVisible = useIsVisible(nodeRef)

Expand All @@ -22,6 +33,9 @@ const VisibilityCard = ({ video, openVideo, handleAlert, cardWidth, authenticate
authenticated={authenticated}
openDetailsModal={openDetailsModal}
deleted={deleted}
editMode={editMode}
isSelected={isSelected}
onSelect={onSelect}
/>
) : (
<div
Expand Down
128 changes: 128 additions & 0 deletions app/client/src/components/game/GameSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React from 'react'
import { Autocomplete, TextField, InputAdornment, Box, CircularProgress } from '@mui/material'
import SportsEsportsIcon from '@mui/icons-material/SportsEsports'
import { GameService } from '../../services'

/**
* Reusable game search autocomplete component
* Searches SteamGridDB, creates game if needed, and calls onGameLinked callback
*/
const GameSearch = ({ onGameLinked, onError, disabled = false, placeholder = 'Search for a game...', sx = {} }) => {
const [selectedGame, setSelectedGame] = React.useState(null)
const [gameOptions, setGameOptions] = React.useState([])
const [gameSearchLoading, setGameSearchLoading] = React.useState(false)
const [gameLinkLoading, setGameLinkLoading] = React.useState(false)

const searchGames = async (query) => {
if (!query || query.length < 2) {
setGameOptions([])
return
}
setGameSearchLoading(true)
try {
const results = (await GameService.searchSteamGrid(query)).data || []
setGameOptions(results)
} catch (err) {
setGameOptions([])
}
setGameSearchLoading(false)
}

const handleGameChange = async (_, newValue) => {
if (!newValue) {
setSelectedGame(null)
return
}

setSelectedGame(newValue)
setGameLinkLoading(true)

try {
// Check if game already exists
const allGames = (await GameService.getGames()).data
let game = allGames.find((g) => g.steamgriddb_id === newValue.id)

if (!game) {
// Create the game
const assets = (await GameService.getGameAssets(newValue.id)).data
const gameData = {
steamgriddb_id: newValue.id,
name: newValue.name,
release_date: newValue.release_date
? new Date(newValue.release_date * 1000).toISOString().split('T')[0]
: null,
hero_url: assets.hero_url,
logo_url: assets.logo_url,
icon_url: assets.icon_url,
}
game = (await GameService.createGame(gameData)).data
}

// Call the callback with the game
if (onGameLinked) {
onGameLinked(game)
}

// Reset the autocomplete
setSelectedGame(null)
setGameOptions([])
} catch (err) {
console.error('Error creating/linking game:', err)
if (onError) {
onError(err)
}
setSelectedGame(null)
} finally {
setGameLinkLoading(false)
}
}

return (
<Autocomplete
value={selectedGame}
onChange={handleGameChange}
onInputChange={(_, val) => searchGames(val)}
options={gameOptions}
getOptionLabel={(option) => option.name || ''}
loading={gameSearchLoading}
disabled={disabled || gameLinkLoading}
renderInput={(params) => (
<TextField
{...params}
placeholder={placeholder}
size="small"
sx={{
'& .MuiOutlinedInput-notchedOutline': { border: 'none' },
...sx,
}}
InputProps={{
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<SportsEsportsIcon sx={{ color: 'rgba(255, 255, 255, 0.7)' }} />
</InputAdornment>
),
endAdornment: (
<>
{gameLinkLoading && (
<InputAdornment position="end">
<CircularProgress size={20} sx={{ mr: 1 }} />
</InputAdornment>
)}
{params.InputProps.endAdornment}
</>
),
}}
/>
)}
renderOption={(props, option) => (
<Box component="li" {...props} key={option.id}>
{option.name}
{option.release_date && ` (${new Date(option.release_date * 1000).getFullYear()})`}
</Box>
)}
/>
)
}

export default GameSearch
Loading