fix authimage loading on larger images, fix media upload modal so it displays progress, and show thumbnails before loading on imageviewer

This commit is contained in:
wisplite
2025-11-23 23:51:43 -06:00
parent 751a724a4b
commit 02369d7107
5 changed files with 100 additions and 34 deletions
+11
View File
@@ -15,6 +15,7 @@
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.9.6", "react-router-dom": "^7.9.6",
"react-spinners": "^0.17.0",
"tailwindcss": "^4.1.12" "tailwindcss": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
@@ -3662,6 +3663,16 @@
"react-dom": ">=18" "react-dom": ">=18"
} }
}, },
"node_modules/react-spinners": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.17.0.tgz",
"integrity": "sha512-L/8HTylaBmIWwQzIjMq+0vyaRXuoAevzWoD35wKpNTxxtYXWZp+xtgkfD7Y4WItuX0YvdxMPU79+7VhhmbmuTQ==",
"license": "MIT",
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-style-singleton": { "node_modules/react-style-singleton": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+1
View File
@@ -17,6 +17,7 @@
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.9.6", "react-router-dom": "^7.9.6",
"react-spinners": "^0.17.0",
"tailwindcss": "^4.1.12" "tailwindcss": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
+10 -2
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
export default function AuthImage({ src, token, alt, className, ...props }) { export default function AuthImage({ src, token, alt, className, onLoad, ...props }) {
const [imageSrc, setImageSrc] = useState(null) const [imageSrc, setImageSrc] = useState(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState(false) const [error, setError] = useState(false)
@@ -8,6 +8,7 @@ export default function AuthImage({ src, token, alt, className, ...props }) {
useEffect(() => { useEffect(() => {
let objectUrl = null let objectUrl = null
let active = true let active = true
const controller = new AbortController()
const fetchImage = async () => { const fetchImage = async () => {
setLoading(true) setLoading(true)
@@ -16,7 +17,8 @@ export default function AuthImage({ src, token, alt, className, ...props }) {
const response = await fetch(src, { const response = await fetch(src, {
headers: { headers: {
'Authorization': token 'Authorization': token
} },
signal: controller.signal
}) })
if (!response.ok) { if (!response.ok) {
@@ -28,6 +30,11 @@ export default function AuthImage({ src, token, alt, className, ...props }) {
objectUrl = URL.createObjectURL(blob) objectUrl = URL.createObjectURL(blob)
setImageSrc(objectUrl) setImageSrc(objectUrl)
setLoading(false) setLoading(false)
if (onLoad) {
setTimeout(() => {
onLoad()
}, 500)
}
} }
} catch (err) { } catch (err) {
if (active) { if (active) {
@@ -46,6 +53,7 @@ export default function AuthImage({ src, token, alt, className, ...props }) {
return () => { return () => {
active = false active = false
controller.abort()
if (objectUrl) { if (objectUrl) {
URL.revokeObjectURL(objectUrl) URL.revokeObjectURL(objectUrl)
} }
@@ -30,40 +30,68 @@ export default function MediaUploadModal({ open, onOpenChange, trigger, albumNam
} }
const handleUpload = async () => { const handleUpload = async () => {
// Placeholder for server-side logic hook const pendingFiles = files.filter(f => f.status === 'pending')
const performUpload = async (fileWrapper) => { if (pendingFiles.length === 0) return
// TODO: Implement actual server upload here
// Example:
// const formData = new FormData()
// formData.append('file', fileWrapper.file)
// formData.append('albumId', albumId)
// await fetch('/api/upload', { method: 'POST', body: formData, ... })
const formData = new FormData() setFiles(prev => prev.map(f => f.status === 'pending' ? { ...f, status: 'uploading' } : f))
formData.append('file', fileWrapper.file)
formData.append('albumId', albumId) const uploadFile = (fileWrapper) => {
const response = await fetch(`${getServerUrl()}/api/media/uploadMedia`, { return new Promise((resolve, reject) => {
method: 'POST', const xhr = new XMLHttpRequest()
body: formData, const formData = new FormData()
headers: { formData.append('file', fileWrapper.file)
'Authorization': getAccessToken(), formData.append('albumId', albumId)
},
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100)
setFiles(prev => prev.map(f => f.id === fileWrapper.id ? { ...f, progress } : f))
}
})
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const data = JSON.parse(xhr.responseText)
if (data.error) {
setFiles(prev => prev.map(f => f.id === fileWrapper.id ? { ...f, status: 'error' } : f))
reject(data.error)
} else {
setFiles(prev => prev.map(f => f.id === fileWrapper.id ? { ...f, status: 'completed', progress: 100 } : f))
resolve(data)
}
} catch (e) {
setFiles(prev => prev.map(f => f.id === fileWrapper.id ? { ...f, status: 'error' } : f))
reject(e)
}
} else {
setFiles(prev => prev.map(f => f.id === fileWrapper.id ? { ...f, status: 'error' } : f))
reject(new Error(xhr.statusText))
}
})
xhr.addEventListener('error', () => {
setFiles(prev => prev.map(f => f.id === fileWrapper.id ? { ...f, status: 'error' } : f))
reject(new Error('Network Error'))
})
xhr.open('POST', `${getServerUrl()}/api/media/uploadMedia`)
xhr.setRequestHeader('Authorization', getAccessToken())
xhr.send(formData)
}) })
const data = await response.json()
if (data.error) {
showError(data.error)
} else {
setFiles(prev => prev.map(f => f.id === fileWrapper.id ? { ...f, status: 'completed' } : f))
showSuccess('Media uploaded successfully')
}
} }
// Start uploads const results = await Promise.allSettled(pendingFiles.map(f => uploadFile(f)))
files.forEach(fileWrapper => {
if (fileWrapper.status === 'pending') { const failedCount = results.filter(r => r.status === 'rejected').length
performUpload(fileWrapper) const successCount = results.filter(r => r.status === 'fulfilled').length
}
}) if (failedCount > 0) {
showError(`${failedCount} file(s) failed to upload`)
}
if (successCount > 0) {
showSuccess(`${successCount} file(s) uploaded successfully`)
}
} }
const formatFileSize = (bytes) => { const formatFileSize = (bytes) => {
+19 -1
View File
@@ -1,8 +1,11 @@
import AuthImage from '../../components/AuthImage' import AuthImage from '../../components/AuthImage'
import { getServerUrl } from '../../hooks/getConstants' import { getServerUrl } from '../../hooks/getConstants'
import { useState, useEffect } from 'react'
import { Loader2 } from 'lucide-react'
export default function ImageViewer({ albumId, mediaId, token, title }) { export default function ImageViewer({ albumId, mediaId, token, title }) {
const src = `${getServerUrl()}/api/media/${albumId}/${mediaId}` const src = `${getServerUrl()}/api/media/${albumId}/${mediaId}`
const [loaded, setLoaded] = useState(false)
return ( return (
<div className="flex-1 flex items-center justify-center bg-black relative overflow-hidden h-full"> <div className="flex-1 flex items-center justify-center bg-black relative overflow-hidden h-full">
@@ -10,8 +13,23 @@ export default function ImageViewer({ albumId, mediaId, token, title }) {
src={src} src={src}
token={token} token={token}
alt={title} alt={title}
className="max-w-full max-h-full object-contain" className={`max-w-full max-h-full object-contain absolute ${loaded ? 'block' : 'hidden'}`}
onLoad={() => {
setLoaded(true)
}}
/> />
{/* loading image */}
{!loaded && (
<div className="absolute top-0 left-0 w-full h-full bg-black/50 flex items-center justify-center">
<AuthImage
src={`${getServerUrl()}/api/media/thumb/${albumId}/${mediaId}`}
token={token}
alt={title}
className="max-w-full max-h-full object-contain w-full h-full"
/>
<Loader2 className="w-10 h-10 text-white animate-spin absolute bottom-5 right-5" />
</div>
)}
</div> </div>
) )
} }