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": "^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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
@@ -30,4 +31,4 @@
|
|||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"vite": "^7.1.2"
|
"vite": "^7.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user