mirror of
https://github.com/wisplite/raster.git
synced 2026-05-01 06:32:44 -05:00
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:
Generated
+11
@@ -15,6 +15,7 @@
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.9.6",
|
||||
"react-spinners": "^0.17.0",
|
||||
"tailwindcss": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3662,6 +3663,16 @@
|
||||
"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": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.9.6",
|
||||
"react-spinners": "^0.17.0",
|
||||
"tailwindcss": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(false)
|
||||
@@ -8,6 +8,7 @@ export default function AuthImage({ src, token, alt, className, ...props }) {
|
||||
useEffect(() => {
|
||||
let objectUrl = null
|
||||
let active = true
|
||||
const controller = new AbortController()
|
||||
|
||||
const fetchImage = async () => {
|
||||
setLoading(true)
|
||||
@@ -16,7 +17,8 @@ export default function AuthImage({ src, token, alt, className, ...props }) {
|
||||
const response = await fetch(src, {
|
||||
headers: {
|
||||
'Authorization': token
|
||||
}
|
||||
},
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -28,6 +30,11 @@ export default function AuthImage({ src, token, alt, className, ...props }) {
|
||||
objectUrl = URL.createObjectURL(blob)
|
||||
setImageSrc(objectUrl)
|
||||
setLoading(false)
|
||||
if (onLoad) {
|
||||
setTimeout(() => {
|
||||
onLoad()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (active) {
|
||||
@@ -46,6 +53,7 @@ export default function AuthImage({ src, token, alt, className, ...props }) {
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
controller.abort()
|
||||
if (objectUrl) {
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
}
|
||||
|
||||
@@ -30,40 +30,68 @@ export default function MediaUploadModal({ open, onOpenChange, trigger, albumNam
|
||||
}
|
||||
|
||||
const handleUpload = async () => {
|
||||
// Placeholder for server-side logic hook
|
||||
const performUpload = async (fileWrapper) => {
|
||||
// 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 pendingFiles = files.filter(f => f.status === 'pending')
|
||||
if (pendingFiles.length === 0) return
|
||||
|
||||
setFiles(prev => prev.map(f => f.status === 'pending' ? { ...f, status: 'uploading' } : f))
|
||||
|
||||
const uploadFile = (fileWrapper) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
const formData = new FormData()
|
||||
formData.append('file', fileWrapper.file)
|
||||
formData.append('albumId', albumId)
|
||||
const response = await fetch(`${getServerUrl()}/api/media/uploadMedia`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Authorization': getAccessToken(),
|
||||
},
|
||||
})
|
||||
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')
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
// Start uploads
|
||||
files.forEach(fileWrapper => {
|
||||
if (fileWrapper.status === 'pending') {
|
||||
performUpload(fileWrapper)
|
||||
const results = await Promise.allSettled(pendingFiles.map(f => uploadFile(f)))
|
||||
|
||||
const failedCount = results.filter(r => r.status === 'rejected').length
|
||||
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) => {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import AuthImage from '../../components/AuthImage'
|
||||
import { getServerUrl } from '../../hooks/getConstants'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
export default function ImageViewer({ albumId, mediaId, token, title }) {
|
||||
const src = `${getServerUrl()}/api/media/${albumId}/${mediaId}`
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
return (
|
||||
<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}
|
||||
token={token}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user