Add video thumbnails and playback in Assets
Video assets now show their first frame as a thumbnail in the grid instead of a generic Film icon, and the detail modal includes a video player with controls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,8 @@ const formatFileSize = (bytes) => {
|
|||||||
export default function AssetCard({ asset, onClick }) {
|
export default function AssetCard({ asset, onClick }) {
|
||||||
const TypeIcon = typeIcons[asset.type] || File
|
const TypeIcon = typeIcons[asset.type] || File
|
||||||
const isImage = asset.type === 'image'
|
const isImage = asset.type === 'image'
|
||||||
|
const isVideo = asset.type === 'video'
|
||||||
|
const hasPreview = (isImage || isVideo) && asset.url
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -36,10 +38,19 @@ export default function AssetCard({ asset, onClick }) {
|
|||||||
e.target.nextSibling.style.display = 'flex'
|
e.target.nextSibling.style.display = 'flex'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : isVideo && asset.url ? (
|
||||||
|
<video
|
||||||
|
src={asset.url}
|
||||||
|
preload="metadata"
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
className="w-full h-full object-cover pointer-events-none"
|
||||||
|
onLoadedData={(e) => { e.target.currentTime = 0.1 }}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col items-center justify-center gap-2 ${isImage && asset.url ? 'hidden' : ''}`}
|
className={`flex flex-col items-center justify-center gap-2 ${hasPreview ? 'hidden' : ''}`}
|
||||||
style={{ display: isImage && asset.url ? 'none' : 'flex' }}
|
style={{ display: hasPreview ? 'none' : 'flex' }}
|
||||||
>
|
>
|
||||||
<TypeIcon className="w-10 h-10 text-text-tertiary" />
|
<TypeIcon className="w-10 h-10 text-text-tertiary" />
|
||||||
<span className="text-xs text-text-tertiary uppercase font-medium">
|
<span className="text-xs text-text-tertiary uppercase font-medium">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Plus, Upload, Search, FolderOpen, ChevronRight, Grid3X3, X } from 'luci
|
|||||||
import { api } from '../utils/api'
|
import { api } from '../utils/api'
|
||||||
import AssetCard from '../components/AssetCard'
|
import AssetCard from '../components/AssetCard'
|
||||||
import Modal from '../components/Modal'
|
import Modal from '../components/Modal'
|
||||||
|
import CommentsSection from '../components/CommentsSection'
|
||||||
|
|
||||||
export default function Assets() {
|
export default function Assets() {
|
||||||
const [assets, setAssets] = useState([])
|
const [assets, setAssets] = useState([])
|
||||||
@@ -284,6 +285,11 @@ export default function Assets() {
|
|||||||
<img src={selectedAsset.url} alt={selectedAsset.name} className="w-full max-h-[400px] object-contain" />
|
<img src={selectedAsset.url} alt={selectedAsset.name} className="w-full max-h-[400px] object-contain" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{selectedAsset.type === 'video' && selectedAsset.url && (
|
||||||
|
<div className="rounded-lg overflow-hidden bg-surface-tertiary">
|
||||||
|
<video src={selectedAsset.url} controls preload="metadata" className="w-full max-h-[400px]" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-text-tertiary">Type</p>
|
<p className="text-text-tertiary">Type</p>
|
||||||
@@ -318,6 +324,7 @@ export default function Assets() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<CommentsSection entityType="asset" entityId={selectedAsset.id || selectedAsset._id} />
|
||||||
<div className="flex items-center gap-3 pt-4 border-t border-border">
|
<div className="flex items-center gap-3 pt-4 border-t border-border">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteAsset(selectedAsset)}
|
onClick={() => handleDeleteAsset(selectedAsset)}
|
||||||
|
|||||||
Reference in New Issue
Block a user