Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
49b29e6
feat: reject uploads of videos already in the library
ShaneIsrael Sep 22, 2026
76a6c04
feat: upload tokens for scripts and other tools
ShaneIsrael Sep 22, 2026
fbbc646
feat: chunked token uploads, and a folders/games listing for tools
ShaneIsrael Sep 22, 2026
3d25e89
Merge pull request #740 from fireshare-app/feat/reject-duplicate-uploads
ShaneIsrael Sep 22, 2026
a966f8c
Merge pull request #741 from fireshare-app/feat/upload-tokens
ShaneIsrael Sep 22, 2026
db6e151
Merge pull request #742 from fireshare-app/feat/reject-duplicate-uploads
ShaneIsrael Sep 22, 2026
4c425bc
fix: make chunked token uploads survive a restart
ShaneIsrael Sep 22, 2026
797e7bd
Merge pull request #743 from fireshare-app/feat/chunked-upload-durabi…
ShaneIsrael Sep 22, 2026
b6218e1
Tell tools which folder each game's media belongs in
ShaneIsrael Sep 23, 2026
875f093
Merge pull request #744 from fireshare-app/feat/folder-rules-for-tools
ShaneIsrael Sep 23, 2026
55e670f
fix: give Videos a path of its own so its sidebar link stops redirecting
ShaneIsrael Sep 23, 2026
77e7d1e
Merge pull request #745 from fireshare-app/fix/videos-sidebar-link
ShaneIsrael Sep 23, 2026
ebd8bea
Let a tool ask about an image before uploading it, too
ShaneIsrael Sep 23, 2026
f57f8b8
Merge pull request #746 from fireshare-app/feat/exists-images
ShaneIsrael Sep 23, 2026
8742cb2
feat: point people at Firesync from the Upload Tokens pane
ShaneIsrael Sep 23, 2026
f4a5d6f
Merge pull request #747 from fireshare-app/feat/advertise-firesync
ShaneIsrael Sep 23, 2026
b03f5d0
fix: capture the video id for derived assets so nginx stops warning
ShaneIsrael Sep 23, 2026
99357e4
chore: bump version to 1.8.3
ShaneIsrael Sep 23, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ If Fireshare is useful to you, [GitHub Sponsors](https://github.com/sponsors/Sha
- [Shareable user profiles for uploads](./docs/Users.md#profiles)
- [Two-factor authentication (TOTP authenticator apps)](./docs/Security.md#two-factor-authentication-mfa)
- [Login IP whitelisting](./docs/Security.md#login-ip-whitelist)
- [Upload tokens for scripts and other tools](./docs/UploadTokens.md)

## Supported Video Formats

Expand Down
4 changes: 2 additions & 2 deletions 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.8.2",
"version": "1.8.3",
"private": true,
"dependencies": {
"@emotion/react": "^11.9.0",
Expand Down
31 changes: 17 additions & 14 deletions app/client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,25 @@ export default function App() {
<CssBaseline />
<GlobalDragDropOverlay>
<Routes>
{/* "/" is not a page of its own: it forwards to whichever page the
administrator put at the top of the sidebar. The Videos page it
used to hold now lives at "/videos" so that the sidebar can link
to it without passing back through that decision. */}
<Route path="/" element={<LandingRoute />} />
<Route
path="/"
path="/videos"
element={
<LandingRoute>
<AuthWrapper>
<MainNavbar
page="/"
collapsed={!drawerOpen}
searchable
styleToggle
searchPlaceholder="Search title, game, or #tag..."
>
<Dashboard />
</MainNavbar>
</AuthWrapper>
</LandingRoute>
<AuthWrapper>
<MainNavbar
page="/videos"
collapsed={!drawerOpen}
searchable
styleToggle
searchPlaceholder="Search title, game, or #tag..."
>
<Dashboard />
</MainNavbar>
</AuthWrapper>
}
/>
<Route
Expand Down
9 changes: 5 additions & 4 deletions app/client/src/common/sidebarPages.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// because saving writes both.
export const SIDEBAR_PAGES = [
{ key: 'home', href: '/home', title: 'Home', legacyFlag: 'show_home' },
{ key: 'videos', href: '/', title: 'Videos', legacyFlag: 'show_videos' },
{ key: 'videos', href: '/videos', title: 'Videos', legacyFlag: 'show_videos' },
{ key: 'images', href: '/images', title: 'Images', legacyFlag: 'show_images' },
{ key: 'games', href: '/games', title: 'Games', legacyFlag: 'show_games' },
{ key: 'tags', href: '/tags', title: 'Tags', legacyFlag: 'show_tags' },
Expand Down Expand Up @@ -63,10 +63,11 @@ export function sidebarPagesPatch(pages) {
}

// Where "/" should take a visitor: the first page still shown in the sidebar.
// Undefined until the config has been loaded at least once, "/" (the Videos
// page, which lives there) when nothing is left to prefer.
// Undefined until the config has been loaded at least once, and the Videos page
// when every page has been hidden -- never "/" itself, which would loop back
// here.
export function landingHref(uiConfig) {
if (!uiConfig) return undefined
const first = resolveSidebarPages(uiConfig).find((p) => p.enabled)
return first ? KNOWN.get(first.key).href : '/'
return first ? KNOWN.get(first.key).href : '/videos'
}
99 changes: 82 additions & 17 deletions app/client/src/components/cards/UploadCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,63 @@ import {
InputAdornment,
Checkbox,
FormControlLabel,
Link,
} from '@mui/material'
import CloudUploadIcon from '@mui/icons-material/CloudUpload'
import styled from '@emotion/styled'
import { keyframes } from '@emotion/react'
import { VideoService, GameService, TagService } from '../../services'
import { getSetting } from '../../common/utils'

const FINISHED_STATUSES = new Set(['done', 'error', 'duplicate'])
const isFinished = (item) => FINISHED_STATUSES.has(item.status)

function summarizeBatch(succeeded, duplicates, failed, total) {
if (duplicates.length === 0 && failed === 0) {
return succeeded === 1
? 'Your upload will be available in a few seconds.'
: `${succeeded} uploads will be available in a few seconds.`
}
if (succeeded === 0 && failed === 0) {
return duplicates.length === 1
? `${duplicates[0].file.name} is already in the library.`
: `All ${duplicates.length} of those videos are already in the library.`
}
const parts = []
if (duplicates.length > 0) parts.push(`${duplicates.length} already in the library`)
if (failed > 0) parts.push(`${failed} failed`)
return `${succeeded} of ${total} uploads succeeded — ${parts.join(', ')}.`
}

// Links to the existing copies of duplicate uploads. They open in a new tab so
// the page the uploader is sitting on is left alone.
function renderDuplicateLinks(items) {
const link = (item, label) => (
<Link
href={item.duplicate.url}
target="_blank"
rel="noopener"
color="inherit"
underline="always"
sx={{ fontWeight: 700 }}
>
{label}
</Link>
)
if (items.length === 1) return link(items[0], 'Open the existing video')
return (
<>
Open the existing videos:{' '}
{items.map((item, idx) => (
<React.Fragment key={item.id}>
{idx > 0 && ', '}
{link(item, item.duplicate.title || item.file.name)}
</React.Fragment>
))}
</>
)
}

function checkUploadLimit(file, handleAlert) {
const limitMb = getSetting('upload_limit_mb') || 0
if (limitMb > 0 && file.size > limitMb * 1024 * 1024) {
Expand Down Expand Up @@ -473,6 +523,13 @@ const UploadCard = React.forwardRef(function UploadCard(
updateQueueItem(item.id, { status: 'done', progress: 1 })
if (onUploadComplete) onUploadComplete()
} catch (err) {
// The server hashes every upload and answers 409 when that content is
// already in the library. It has removed its copy; nothing more to do.
const duplicate = err?.response?.status === 409 ? err.response.data : null
if (duplicate?.video_id) {
updateQueueItem(item.id, { status: 'duplicate', progress: 1, duplicate })
return
}
updateQueueItem(item.id, { status: 'error' })
handleAlert({
type: 'error',
Expand Down Expand Up @@ -500,19 +557,21 @@ const UploadCard = React.forwardRef(function UploadCard(
}

// Once every upload has finished, show a single summary alert and reset the card
if (uploadQueue.every((i) => i.status === 'done' || i.status === 'error')) {
if (uploadQueue.every(isFinished)) {
const succeeded = uploadQueue.filter((i) => i.status === 'done').length
const failed = uploadQueue.length - succeeded
if (succeeded > 0) {
const duplicates = uploadQueue.filter((i) => i.status === 'duplicate')
const failed = uploadQueue.length - succeeded - duplicates.length
if (succeeded > 0 || duplicates.length > 0) {
handleAlert({
type: failed > 0 ? 'warning' : 'success',
message:
failed > 0
? `${succeeded} of ${uploadQueue.length} uploads succeeded — ${failed} failed.`
: succeeded === 1
? 'Your upload will be available in a few seconds.'
: `${succeeded} uploads will be available in a few seconds.`,
autohideDuration: 3500,
type: failed > 0 ? 'warning' : succeeded > 0 ? 'success' : 'info',
message: (
<>
{summarizeBatch(succeeded, duplicates, failed, uploadQueue.length)}
{duplicates.length > 0 && <> {renderDuplicateLinks(duplicates)}</>}
</>
),
// A message with a link in it needs to stay up long enough to click.
autoHideDuration: duplicates.length > 0 ? 10000 : 3500,
open: true,
})
}
Expand All @@ -539,7 +598,7 @@ const UploadCard = React.forwardRef(function UploadCard(
0,
)
const aggregateProgress = totalQueueBytes > 0 ? Math.min(loadedQueueBytes / totalQueueBytes, 1) : 0
const finishedCount = uploadQueue.filter((i) => i.status === 'done' || i.status === 'error').length
const finishedCount = uploadQueue.filter(isFinished).length
const allSent = isUploading && uploadQueue.every((i) => i.status !== 'queued' && i.status !== 'uploading')
const singleUpload = uploadQueue.length === 1 ? uploadQueue[0] : null

Expand Down Expand Up @@ -1179,9 +1238,11 @@ const UploadCard = React.forwardRef(function UploadCard(
color:
item.status === 'error'
? '#FF6B6B'
: item.status === 'done'
? '#6BFF95'
: '#FFFFFFCC',
: item.status === 'duplicate'
? '#FFD166'
: item.status === 'done'
? '#6BFF95'
: '#FFFFFFCC',
}}
>
{item.status === 'queued'
Expand All @@ -1192,7 +1253,9 @@ const UploadCard = React.forwardRef(function UploadCard(
? 'Done'
: item.status === 'error'
? 'Failed'
: `${(100 * item.progress).toFixed(0)}%`}
: item.status === 'duplicate'
? 'Already exists'
: `${(100 * item.progress).toFixed(0)}%`}
</Typography>
</Box>
<Box
Expand All @@ -1212,7 +1275,9 @@ const UploadCard = React.forwardRef(function UploadCard(
background:
item.status === 'error'
? '#FF6B6B'
: 'linear-gradient(90deg, #BC00E6, #FF2E80, #FF6B00)',
: item.status === 'duplicate'
? '#FFD166'
: 'linear-gradient(90deg, #BC00E6, #FF2E80, #FF6B00)',
transition: 'width 0.6s cubic-bezier(0.25, 0.1, 0.25, 1)',
}}
/>
Expand Down
9 changes: 7 additions & 2 deletions app/client/src/components/nav/MainNavbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const PAGES_WITHOUT_TOP_BAR = ['/files', '/settings', '/image', '/profile']
// group that always follows them.
const allPages = [
{ key: 'home', title: 'Home', icon: <HomeIcon />, href: '/home', private: false },
{ key: 'videos', title: 'Videos', icon: <VideoLibraryIcon />, href: '/', private: false },
{ key: 'videos', title: 'Videos', icon: <VideoLibraryIcon />, href: '/videos', private: false },
{ key: 'images', title: 'Images', icon: <PhotoLibraryIcon />, href: '/images', private: false },
{ key: 'games', title: 'Games', icon: <SportsEsportsIcon />, href: '/games', private: false },
{ key: 'tags', title: 'Tags', icon: <LocalOfferIcon />, href: '/tags', private: false },
Expand Down Expand Up @@ -841,7 +841,12 @@ function MainNavbar({
>
{showDemoBanner && <Box sx={{ height: DEMO_BANNER_HEIGHT, flexShrink: 0 }} />}
{toolbar && page !== '/watch' && showTopBar && <Toolbar />}
<SnackbarAlert severity={alert.type} open={alert.open} setOpen={(open) => setAlert({ ...alert, open })}>
<SnackbarAlert
severity={alert.type}
open={alert.open}
autoHideDuration={alert.autoHideDuration}
setOpen={(open) => setAlert({ ...alert, open })}
>
{alert.message}
</SnackbarAlert>
{React.cloneElement(children, {
Expand Down
Loading
Loading