This commit is contained in:
2026-09-02 11:56:19 +03:30
parent fdbf02bc07
commit e248943f4c
4 changed files with 340 additions and 92 deletions
+82 -8
View File
@@ -17,7 +17,7 @@ import {
PageHeader,
Badge,
} from "@/components/ui";
import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons";
import { EditIcon, EyeIcon, PlusIcon, TrashIcon } from "@/components/icons";
import { formatMinutes, fileNameToTitle } from "@/lib/utils";
import { MediaPreview } from "@/components/MediaPreview";
import { ImagePicker } from "@/components/ImagePicker";
@@ -42,6 +42,8 @@ interface Media {
duration?: number;
visibility?: string;
is_premium?: boolean;
is_deeplink_only?: boolean;
deep_link?: string | null;
external_url?: string | null;
image_id?: number | null;
image?: unknown;
@@ -86,7 +88,13 @@ function ChipMultiSelect({
export default function MediaPage() {
const [search, setSearch] = useState("");
const { data, loading, error, reload } = useList<Media>("/media", { search });
// Deep-link-only media is hidden from the normal listing, so it needs its own
// view here — otherwise media sent to deep links disappears from this page.
const [showDeepLinks, setShowDeepLinks] = useState(false);
const { data, loading, error, reload } = useList<Media>(
"/media",
showDeepLinks ? { deeplink_only: 1 } : { search },
);
const { data: categories } = useList<Category>("/categories", {
type: "media",
});
@@ -203,6 +211,24 @@ export default function MediaPage() {
}
}
// Move a media into or out of deep-link-only mode, both directions.
async function toggleDeepLink(row: Media) {
try {
await apiFetch(`/media/${row.id}`, {
method: "POST",
body: toFormData({ is_deeplink_only: row.is_deeplink_only ? 0 : 1 }),
});
toast.success(
row.is_deeplink_only
? "به فهرست عادی بازگشت."
: "به لینک مستقیم تبدیل شد.",
);
reload();
} catch (err) {
toast.error(err instanceof ApiError ? err.message : "خطا در ذخیره‌سازی");
}
}
async function confirmDelete() {
if (!deleting) return;
setRemoving(true);
@@ -258,6 +284,25 @@ export default function MediaPage() {
header: "مدت",
render: (r) => formatMinutes(r.duration),
},
{
key: "deep_link",
header: "لینک مستقیم",
className: "w-56",
render: (r) =>
r.is_deeplink_only ? (
<a
href={r.deep_link ?? undefined}
target="_blank"
rel="noreferrer"
dir="ltr"
className="inline-block max-w-[200px] truncate text-xs text-primary underline"
>
{r.deep_link ?? "—"}
</a>
) : (
<span className="text-xs text-muted">—</span>
),
},
{
key: "is_premium",
header: "ویژه",
@@ -282,11 +327,19 @@ export default function MediaPage() {
}
/>
<div className="mb-4 max-w-sm">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="جستجو در رسانه‌ها..."
<div className="mb-4 flex items-center gap-4">
<div className="max-w-sm flex-1">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="جستجو در رسانه‌ها..."
disabled={showDeepLinks}
/>
</div>
<Switch
checked={showDeepLinks}
onChange={setShowDeepLinks}
label="فقط لینک‌های مستقیم"
/>
</div>
@@ -296,7 +349,11 @@ export default function MediaPage() {
loading={loading}
error={error}
onRetry={reload}
emptyMessage="هنوز رسانه‌ای اضافه نکرده‌اید. اولین رسانه را بارگذاری کنید."
emptyMessage={
showDeepLinks
? "هیچ رسانه لینک مستقیمی وجود ندارد."
: "هنوز رسانه‌ای اضافه نکرده‌اید. اولین رسانه را بارگذاری کنید."
}
emptyAction={
<Button icon={<PlusIcon className="h-4 w-4" />} onClick={openCreate}>
رسانه جدید
@@ -307,6 +364,23 @@ export default function MediaPage() {
<Button variant="ghost" onClick={() => openEdit(row)} aria-label="ویرایش">
<EditIcon className="h-4 w-4" />
</Button>
<Button
variant="ghost"
onClick={() => toggleDeepLink(row)}
aria-label={
row.is_deeplink_only
? "بازگرداندن به فهرست"
: "تبدیل به لینک مستقیم"
}
title={
row.is_deeplink_only
? "بازگرداندن به فهرست"
: "تبدیل به لینک مستقیم"
}
className={row.is_deeplink_only ? "text-primary" : undefined}
>
<EyeIcon className="h-4 w-4" />
</Button>
<Button
variant="ghost"
onClick={() => setDeleting(row)}