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 Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ RUN ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log
RUN mkdir /data && mkdir /processed
COPY entrypoint.sh /
COPY app/nginx/prod.conf /etc/nginx/nginx.conf
COPY app/nginx/prod.conf /etc/nginx/nginx.conf.template
COPY app/nginx/error.html /etc/nginx/error.html
COPY app/nginx/api_unavailable.html /etc/nginx/api_unavailable.html
COPY app/server/ /app/server
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.lite
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ RUN adduser --disabled-password --gecos '' nginx
RUN mkdir /data && mkdir /processed
COPY entrypoint.sh /
COPY entrypoint-lite.sh /
COPY app/nginx/prod.conf /etc/nginx/nginx.conf
COPY app/nginx/prod.conf /etc/nginx/nginx.conf.template
COPY app/nginx/error.html /etc/nginx/error.html
COPY app/nginx/api_unavailable.html /etc/nginx/api_unavailable.html
COPY app/server/ /app/server
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ services:
- ADMIN_USERNAME=your-admin-username
- ADMIN_PASSWORD=your-admin-password
- SECRET_KEY=replace_with_random_string_can_be_anything
# The domain your instance is hosted at. e.x: v.fireshare.net
# The domain your instance is hosted at. e.x: demo.fireshare.net
# this is required for opengraph to work correctly for shared links.
- DOMAIN=
# PUID/PGID: the user/group ID the container runs as. Files written to your
Expand All @@ -138,6 +138,10 @@ services:
# Run `id` on your host to find your UID and GID.
- PUID=1000
- PGID=1000
# The port the web server (nginx) listens on inside the container (default: 80).
# Leave this alone and change the host-side "ports" mapping above unless you run
# with network_mode: host, where "ports" is ignored and this becomes the host port.
# - FIRESHARE_PORT=80
```

Update the volume paths and credentials, then run:
Expand Down
2 changes: 1 addition & 1 deletion app/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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.7.1",
"version": "1.7.2",
"private": true,
"dependencies": {
"@emotion/react": "^11.9.0",
Expand Down
9 changes: 9 additions & 0 deletions app/client/src/components/cards/MasonryImageCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,15 @@ const MasonryImageCard = ({
{image.game.name}
</Typography>
)}
{image.created_at && (
<Typography sx={{ fontSize: 12, color: '#FFFFFF80', mt: 0.25 }}>
{new Date(image.created_at).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</Typography>
)}
</Box>
</Box>
{localTags.length > 0 && (
Expand Down
84 changes: 84 additions & 0 deletions app/client/src/components/modal/DateField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import * as React from 'react'
import { Box, Typography, Button, IconButton, Popover } from '@mui/material'
import CloseIcon from '@mui/icons-material/Close'
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'
import { DayPicker } from 'react-day-picker'
import './datepicker-dark.css'
import { rowBoxSx, timeInputStyle } from '../../common/modalStyles'

const DateField = ({ selectedDate, selectedTime, onDateChange, onTimeChange }) => {
const [anchor, setAnchor] = React.useState(null)

const display = selectedDate
? selectedDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +
(selectedTime ? ` at ${selectedTime}` : '')
: null

return (
<>
<Box
onClick={(e) => setAnchor(e.currentTarget)}
sx={{ ...rowBoxSx, cursor: 'pointer', py: 1.1, '&:hover': { borderColor: '#FFFFFF55' } }}
>
<CalendarMonthIcon sx={{ color: '#FFFFFF66', fontSize: 20 }} />
<Typography sx={{ color: display ? 'white' : '#FFFFFF4D', fontSize: 14, flex: 1 }}>
{display || 'Pick a date…'}
</Typography>
{selectedDate && (
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation()
onDateChange(null)
onTimeChange('')
}}
sx={{ color: '#FFFFFF66', '&:hover': { color: 'white' }, p: 0.25 }}
>
<CloseIcon sx={{ fontSize: 16 }} />
</IconButton>
)}
</Box>

<Popover
open={Boolean(anchor)}
anchorEl={anchor}
onClose={() => setAnchor(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
slotProps={{ paper: { sx: { bgcolor: 'transparent', boxShadow: 'none', mt: 0.5 } } }}
>
<div className="fireshare-rdp">
<DayPicker
animate
mode="single"
selected={selectedDate}
onSelect={(d) => onDateChange(d || null)}
defaultMonth={selectedDate || new Date()}
captionLayout="dropdown"
startMonth={new Date(1970, 0)}
endMonth={new Date(new Date().getFullYear() + 1, 11)}
/>
<Box sx={{ px: 1, pb: 1, display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Typography sx={{ color: '#FFFFFFB3', fontSize: 13 }}>Time</Typography>
<input
type="time"
value={selectedTime}
onChange={(e) => onTimeChange(e.target.value)}
style={timeInputStyle}
/>
<Button
size="small"
variant="contained"
onClick={() => setAnchor(null)}
sx={{ bgcolor: '#3399FF', '&:hover': { bgcolor: '#1976D2' }, minWidth: 60 }}
>
Done
</Button>
</Box>
</div>
</Popover>
</>
)
}

export default DateField
73 changes: 72 additions & 1 deletion app/client/src/components/modal/EditImageModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ImageService } from '../../services'
import { getPublicImageUrl, getImageUrl } from '../../common/utils'
import { labelSx, inputSx, dialogPaperSx } from '../../common/modalStyles'
import GameSearch from '../game/GameSearch'
import DateField from './DateField'

const EditImageModal = ({ open, onClose, image, alertHandler, authenticated, onNext, onPrev }) => {
const theme = useTheme()
Expand All @@ -35,9 +36,12 @@ const EditImageModal = ({ open, onClose, image, alertHandler, authenticated, onN
const [imgLoaded, setImgLoaded] = React.useState(false)
const [showSwipeHint, setShowSwipeHint] = React.useState(false)
const [panningDisabled, setPanningDisabled] = React.useState(true)
const [selectedDate, setSelectedDate] = React.useState(null)
const [selectedTime, setSelectedTime] = React.useState('')
const wasOpenRef = React.useRef(false)
const saveTimerRef = React.useRef(null)
const latestTitleRef = React.useRef('')
const latestCreatedAtRef = React.useRef(undefined)
const transformRef = React.useRef(null)
const prevZoomedRef = React.useRef(false)

Expand All @@ -54,7 +58,10 @@ const EditImageModal = ({ open, onClose, image, alertHandler, authenticated, onN
setImgLoaded(false)
setShowSwipeHint(false)
setPanningDisabled(true)
setSelectedDate(null)
setSelectedTime('')
latestTitleRef.current = ''
latestCreatedAtRef.current = undefined
return
}
const t =
Expand All @@ -67,7 +74,17 @@ const EditImageModal = ({ open, onClose, image, alertHandler, authenticated, onN
: 'Untitled')
setTitle(t)
latestTitleRef.current = t
latestCreatedAtRef.current = undefined
setPrivateView(image.info?.private || false)
if (image.created_at) {
const d = new Date(image.created_at)
const pad = (n) => n.toString().padStart(2, '0')
setSelectedDate(d)
setSelectedTime(`${pad(d.getHours())}:${pad(d.getMinutes())}`)
} else {
setSelectedDate(null)
setSelectedTime('')
}
ImageService.addView(image.image_id).catch(() => {})
ImageService.getGame(image.image_id)
.then((res) => setSelectedGame(res.data?.game || null))
Expand Down Expand Up @@ -174,7 +191,12 @@ const EditImageModal = ({ open, onClose, image, alertHandler, authenticated, onN
saveTimerRef.current = null
ImageService.updateDetails(imageId, { title: latestTitleRef.current }).catch(() => {})
}
onClose({ title: latestTitleRef.current, private: privateView, game: selectedGame })
onClose({
title: latestTitleRef.current,
private: privateView,
game: selectedGame,
...(latestCreatedAtRef.current !== undefined && { created_at: latestCreatedAtRef.current }),
})
}

const handleGameLinked = async (game, warning) => {
Expand Down Expand Up @@ -215,6 +237,42 @@ const EditImageModal = ({ open, onClose, image, alertHandler, authenticated, onN
}
}

// Build a naive local ISO string (no Z/offset) so the server stores the time
// exactly as entered, without any timezone conversion.
const getCreatedAtISO = (dateVal, timeVal) => {
if (!dateVal) return null
const d = new Date(dateVal)
if (timeVal) {
const [h, m] = timeVal.split(':')
d.setHours(+h, +m, 0, 0)
}
const pad = (n) => n.toString().padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(
d.getMinutes(),
)}:00`
}

const persistCreatedAt = async (dateVal, timeVal) => {
const iso = getCreatedAtISO(dateVal, timeVal)
latestCreatedAtRef.current = iso
try {
await ImageService.updateDetails(imageId, { created_at: iso })
alertHandler?.({ open: true, type: 'success', message: 'Created date updated.' })
} catch (err) {
alertHandler?.({ open: true, type: 'error', message: 'Failed to save created date.' })
}
}

const handleDateChange = (d) => {
setSelectedDate(d)
persistCreatedAt(d, selectedTime)
}

const handleTimeChange = (t) => {
setSelectedTime(t)
persistCreatedAt(selectedDate, t)
}

const handleDownload = () => {
const a = document.createElement('a')
a.href = `/api/image/original?id=${imageId}`
Expand Down Expand Up @@ -428,6 +486,19 @@ const EditImageModal = ({ open, onClose, image, alertHandler, authenticated, onN
)}
</Box>

{/* Created Date */}
{authenticated && (
<Box>
<Typography sx={labelSx}>Created Date</Typography>
<DateField
selectedDate={selectedDate}
selectedTime={selectedTime}
onDateChange={handleDateChange}
onTimeChange={handleTimeChange}
/>
</Box>
)}

{/* Privacy */}
{authenticated && (
<Box>
Expand Down
82 changes: 2 additions & 80 deletions app/client/src/components/modal/UpdateDetailsModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
InputAdornment,
IconButton,
Divider,
Popover,
Chip,
Autocomplete,
CircularProgress,
Expand All @@ -18,14 +17,12 @@ import TagChip from '../ui/TagChip'
import CloseIcon from '@mui/icons-material/Close'
import CheckIcon from '@mui/icons-material/Check'
import RefreshIcon from '@mui/icons-material/Refresh'
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'
import LockIcon from '@mui/icons-material/Lock'
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
import { DayPicker } from 'react-day-picker'
import { VideoService, GameService, TagService } from '../../services'
import GameSearch from '../game/GameSearch'
import './datepicker-dark.css'
import { labelSx, inputSx, rowBoxSx, dialogPaperSx, timeInputStyle } from '../../common/modalStyles'
import DateField from './DateField'
import { labelSx, inputSx, rowBoxSx, dialogPaperSx } from '../../common/modalStyles'

const modalSx = {
position: 'absolute',
Expand All @@ -49,81 +46,6 @@ const LabeledField = ({ label, children }) => (
</Box>
)

const DateField = ({ selectedDate, selectedTime, onDateChange, onTimeChange }) => {
const [anchor, setAnchor] = React.useState(null)

const display = selectedDate
? selectedDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +
(selectedTime ? ` at ${selectedTime}` : '')
: null

return (
<>
<Box
onClick={(e) => setAnchor(e.currentTarget)}
sx={{ ...rowBoxSx, cursor: 'pointer', py: 1.1, '&:hover': { borderColor: '#FFFFFF55' } }}
>
<CalendarMonthIcon sx={{ color: '#FFFFFF66', fontSize: 20 }} />
<Typography sx={{ color: display ? 'white' : '#FFFFFF4D', fontSize: 14, flex: 1 }}>
{display || 'Pick a date…'}
</Typography>
{selectedDate && (
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation()
onDateChange(null)
onTimeChange('')
}}
sx={{ color: '#FFFFFF66', '&:hover': { color: 'white' }, p: 0.25 }}
>
<CloseIcon sx={{ fontSize: 16 }} />
</IconButton>
)}
</Box>

<Popover
open={Boolean(anchor)}
anchorEl={anchor}
onClose={() => setAnchor(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
slotProps={{ paper: { sx: { bgcolor: 'transparent', boxShadow: 'none', mt: 0.5 } } }}
>
<div className="fireshare-rdp">
<DayPicker
animate
mode="single"
selected={selectedDate}
onSelect={(d) => onDateChange(d || null)}
defaultMonth={selectedDate || new Date()}
captionLayout="dropdown"
startMonth={new Date(1970, 0)}
endMonth={new Date(new Date().getFullYear() + 1, 11)}
/>
<Box sx={{ px: 1, pb: 1, display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Typography sx={{ color: '#FFFFFFB3', fontSize: 13 }}>Time</Typography>
<input
type="time"
value={selectedTime}
onChange={(e) => onTimeChange(e.target.value)}
style={timeInputStyle}
/>
<Button
size="small"
variant="contained"
onClick={() => setAnchor(null)}
sx={{ bgcolor: '#3399FF', '&:hover': { bgcolor: '#1976D2' }, minWidth: 60 }}
>
Done
</Button>
</Box>
</div>
</Popover>
</>
)
}

const LinkedGameField = ({ game, onLink, onUnlink, alertHandler }) => {
if (game) {
return (
Expand Down
1 change: 1 addition & 0 deletions app/client/src/views/FolderView.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const FolderView = ({ authenticated, cardSize, searchText }) => {
...(update.private !== undefined && { private: update.private }),
},
...(update.game !== undefined && { game: update.game }),
...(update.created_at !== undefined && { created_at: update.created_at }),
}
}
setMedia((prev) => prev.map(updateImage))
Expand Down
Loading
Loading