add: more commits

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-03 17:28:36 +08:00
parent 7e964f2432
commit ad48f8add1
7 changed files with 165 additions and 118 deletions
@@ -1,12 +1,13 @@
// modules/admin/pages/assets/ViewAudioAsset.jsx
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ArrowLeft, Lock, Globe, Music2 } from "lucide-react";
import api from "@/utils/api.util";
import { ArrowLeft, Lock, Globe, Music2, Download } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useAssets } from "@/contexts/AdminAssetsContext";
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
import { downloadAsset } from "@/utils/media.util";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
@@ -33,39 +34,12 @@ export default function ViewAudioAsset() {
const { fmtDateTime } = useDateFormat();
const { selectedAsset, loading, fetchAsset } = useAssets();
const [streamUrl, setStreamUrl] = useState(null);
const [thumbnailUrl, setThumbnailUrl] = useState(null);
const { src: streamUrl, thumbnailUrl } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
useEffect(() => {
if (assetId) fetchAsset(assetId);
}, [assetId]);
useEffect(() => {
if (!selectedAsset) return;
setStreamUrl(null);
setThumbnailUrl(null);
if (selectedAsset.storage_provider !== "s3") {
setStreamUrl(selectedAsset.file_url ?? null);
return;
}
const API_BASE = (import.meta.env.VITE_API_URL ?? "http://localhost:3024").replace(/\/$/, "");
// fetchAsset already embeds stream_token/thumbnail_url for S3 assets —
// only fall back to a token request if it's somehow missing (expired
// cache edge case).
if (selectedAsset.stream_token) {
setStreamUrl(`${API_BASE}/client/media/stream/${selectedAsset.stream_token}`);
if (selectedAsset.thumbnail_url) setThumbnailUrl(selectedAsset.thumbnail_url);
return;
}
api.post("/admin/media/token", { asset_id: selectedAsset.asset_id })
.then(({ data }) => {
const { token, thumbnail_url } = data?.data ?? {};
if (token) setStreamUrl(`${API_BASE}/client/media/stream/${token}`);
if (thumbnail_url) setThumbnailUrl(thumbnail_url);
})
.catch(() => setStreamUrl(null));
}, [selectedAsset]);
if (loading) {
return <MediaFallback className="h-96 rounded-lg" />;
}
@@ -101,6 +75,9 @@ export default function ViewAudioAsset() {
<h1 className="text-xl font-semibold truncate">{a.display_name ?? a.original_name}</h1>
<p className="text-sm text-muted-foreground">{a.mime_type}</p>
</div>
<Button variant="outline" size="sm" onClick={() => downloadAsset(a, { scope: "admin" })}>
<Download className="h-4 w-4" /> Download
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
@@ -1,12 +1,13 @@
// modules/admin/pages/assets/ViewDocumentAsset.jsx
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ArrowLeft, Lock, Globe, FileText } from "lucide-react";
import api from "@/utils/api.util";
import { ArrowLeft, Lock, Globe, FileText, Download } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useAssets } from "@/contexts/AdminAssetsContext";
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
import { downloadAsset } from "@/utils/media.util";
import { Spinner } from "@/components/ui/spinner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -30,31 +31,12 @@ export default function ViewDocumentAsset() {
const { fmtDateTime } = useDateFormat();
const { selectedAsset, loading, fetchAsset } = useAssets();
const [streamUrl, setStreamUrl] = useState(null);
const { src: streamUrl } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
useEffect(() => {
if (assetId) fetchAsset(assetId);
}, [assetId]);
useEffect(() => {
if (!selectedAsset) return;
setStreamUrl(null);
if (selectedAsset.storage_provider !== "s3") {
setStreamUrl(selectedAsset.file_url ?? null);
return;
}
const API_BASE = (import.meta.env.VITE_API_URL ?? "http://localhost:3024").replace(/\/$/, "");
// fetchAsset already embeds stream_token for S3 assets — only fall back
// to a token request if it's somehow missing (expired cache edge case).
if (selectedAsset.stream_token) {
setStreamUrl(`${API_BASE}/client/media/stream/${selectedAsset.stream_token}`);
return;
}
api.post("/admin/media/token", { asset_id: selectedAsset.asset_id })
.then(({ data }) => setStreamUrl(`${API_BASE}/client/media/stream/${data?.data?.token}`))
.catch(() => setStreamUrl(null));
}, [selectedAsset]);
if (loading) {
return (
<div className="flex items-center justify-center h-96">
@@ -87,6 +69,9 @@ export default function ViewDocumentAsset() {
<h1 className="text-xl font-semibold truncate">{a.display_name ?? a.original_name}</h1>
<p className="text-sm text-muted-foreground">{a.mime_type}</p>
</div>
<Button variant="outline" size="sm" onClick={() => downloadAsset(a, { scope: "admin" })}>
<Download className="h-4 w-4" /> Download
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
@@ -1,12 +1,13 @@
// modules/admin/pages/assets/ViewImageAsset.jsx
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import api from "@/utils/api.util";
import { ArrowLeft, Lock, Globe } from "lucide-react";
import { ArrowLeft, Lock, Globe, Download } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useAssets } from "@/contexts/AdminAssetsContext";
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
import { downloadAsset } from "@/utils/media.util";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
@@ -28,31 +29,12 @@ export default function ViewImageAsset() {
const { fmtDateTime } = useDateFormat();
const { selectedAsset, loading, fetchAsset } = useAssets();
const [streamUrl, setStreamUrl] = useState(null);
const { src: streamUrl } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
useEffect(() => {
if (assetId) fetchAsset(assetId);
}, [assetId]);
useEffect(() => {
if (!selectedAsset) return;
setStreamUrl(null);
if (selectedAsset.storage_provider !== "s3") {
setStreamUrl(selectedAsset.file_url ?? null);
return;
}
const API_BASE = (import.meta.env.VITE_API_URL ?? "http://localhost:3024").replace(/\/$/, "");
// fetchAsset already embeds stream_token for S3 assets — only fall back
// to a token request if it's somehow missing (expired cache edge case).
if (selectedAsset.stream_token) {
setStreamUrl(`${API_BASE}/client/media/stream/${selectedAsset.stream_token}`);
return;
}
api.post("/admin/media/token", { asset_id: selectedAsset.asset_id })
.then(({ data }) => setStreamUrl(`${API_BASE}/client/media/stream/${data?.data?.token}`))
.catch(() => setStreamUrl(null));
}, [selectedAsset]);
if (loading) {
return <MediaFallback className="h-96 rounded-lg" />;
}
@@ -80,6 +62,9 @@ export default function ViewImageAsset() {
<h1 className="text-xl font-semibold truncate">{a.display_name ?? a.original_name}</h1>
<p className="text-sm text-muted-foreground">{a.mime_type}</p>
</div>
<Button variant="outline" size="sm" onClick={() => downloadAsset(a, { scope: "admin" })}>
<Download className="h-4 w-4" /> Download
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
@@ -1,12 +1,13 @@
// modules/admin/pages/assets/ViewVideoAsset.jsx
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ArrowLeft, Lock, Globe } from "lucide-react";
import api from "@/utils/api.util";
import { ArrowLeft, Lock, Globe, Download } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useAssets } from "@/contexts/AdminAssetsContext";
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
import { downloadAsset } from "@/utils/media.util";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
@@ -38,31 +39,12 @@ export default function ViewVideoAsset() {
const { fmtDateTime } = useDateFormat();
const { selectedAsset, loading, fetchAsset } = useAssets();
const [streamUrl, setStreamUrl] = useState(null);
const { src: streamUrl } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
useEffect(() => {
if (assetId) fetchAsset(assetId);
}, [assetId]);
useEffect(() => {
if (!selectedAsset) return;
setStreamUrl(null);
if (selectedAsset.storage_provider !== "s3") {
setStreamUrl(selectedAsset.file_url ?? null);
return;
}
const API_BASE = (import.meta.env.VITE_API_URL ?? "http://localhost:3024").replace(/\/$/, "");
// fetchAsset already embeds stream_token for S3 assets — only fall back
// to a token request if it's somehow missing (expired cache edge case).
if (selectedAsset.stream_token) {
setStreamUrl(`${API_BASE}/client/media/stream/${selectedAsset.stream_token}`);
return;
}
api.post("/admin/media/token", { asset_id: selectedAsset.asset_id })
.then(({ data }) => setStreamUrl(`${API_BASE}/client/media/stream/${data?.data?.token}`))
.catch(() => setStreamUrl(null));
}, [selectedAsset]);
if (loading) {
return <MediaFallback className="h-96 rounded-lg" />;
}
@@ -90,6 +72,9 @@ export default function ViewVideoAsset() {
<h1 className="text-xl font-semibold truncate">{a.display_name ?? a.original_name}</h1>
<p className="text-sm text-muted-foreground">{a.mime_type}</p>
</div>
<Button variant="outline" size="sm" onClick={() => downloadAsset(a, { scope: "admin" })}>
<Download className="h-4 w-4" /> Download
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
+2 -13
View File
@@ -36,6 +36,7 @@ import {
import { formatDate } from '@/utils/table.util';
import { formatBytes } from './blocks/FileUpload';
import api from '@/utils/api.util';
import { saveBlob } from '@/utils/media.util';
import FileZoomViewer from './FileZoomViewer';
// ─── Resolve preview kind from mime type ──────────────────────────────────────
@@ -190,18 +191,6 @@ const PreviewBody = ({ file, kind, blobUrl, loading, error, downloadUrl, onDownl
}
};
// ─── Trigger a browser download from a blob ────────────────────────────────────
const downloadBlob = (blob, fileName) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// ─── Main dialog ────────────────────────────────────────────────────────────────
const FilePreview = ({ file, open, onOpenChange, streamUrl, downloadUrl }) => {
const kind = file ? resolveKind(file.mime_type, file.file_name) : 'other';
@@ -223,7 +212,7 @@ const FilePreview = ({ file, open, onOpenChange, streamUrl, downloadUrl }) => {
setDownloading(true);
try {
const res = await api.get(url, { responseType: 'blob' });
downloadBlob(res.data, file.file_name);
saveBlob(res.data, file.file_name);
} catch {
// fall back to direct link if proxy fails (e.g. public file_url)
if (file.file_url) window.open(file.file_url, '_blank');