Add delete button for series/movies/collections

This commit is contained in:
Zoe Roux 2026-03-22 00:19:18 +01:00
parent 35e8ca9d69
commit 4fe79f9bc3
No known key found for this signature in database
8 changed files with 258 additions and 48 deletions

View File

@ -1,8 +1,11 @@
import Elysia from "elysia";
import { and, eq } from "drizzle-orm";
import Elysia, { t } from "elysia";
import { db } from "~/db";
import { shows } from "~/db/schema";
import { KError } from "~/models/error";
import { SeedMovie } from "~/models/movie";
import { SeedSerie } from "~/models/serie";
import { Resource } from "~/models/utils";
import { isUuid, Resource } from "~/models/utils";
import { comment } from "~/utils";
import { SeedMovieResponse, seedMovie } from "./movies";
import { SeedSerieResponse, seedSerie } from "./series";
@ -77,4 +80,121 @@ export const seed = new Elysia()
422: KError,
},
},
)
.delete(
"/movies/:id",
async ({ params: { id }, status }) => {
const [deleted] = await db
.delete(shows)
.where(
and(
eq(shows.kind, "movie"),
isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id),
),
)
.returning({ id: shows.id, slug: shows.slug });
if (!deleted) {
return status(404, {
status: 404,
message: "No movie found with the given id or slug.",
});
}
return deleted;
},
{
detail: {
tags: ["movies"],
description: "Delete a movie by id or slug.",
},
params: t.Object({
id: t.String({
description: "The id or slug of the movie to delete.",
}),
}),
response: {
200: Resource(),
404: {
...KError,
description: "No movie found with the given id or slug.",
},
},
},
)
.delete(
"/series/:id",
async ({ params: { id }, status }) => {
const [deleted] = await db
.delete(shows)
.where(
and(
eq(shows.kind, "serie"),
isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id),
),
)
.returning({ id: shows.id, slug: shows.slug });
if (!deleted) {
return status(404, {
status: 404,
message: "No serie found with the given id or slug.",
});
}
return deleted;
},
{
detail: {
tags: ["series"],
description: "Delete a serie by id or slug.",
},
params: t.Object({
id: t.String({
description: "The id or slug of the serie to delete.",
}),
}),
response: {
200: Resource(),
404: {
...KError,
description: "No serie found with the given id or slug.",
},
},
},
)
.delete(
"/collections/:id",
async ({ params: { id }, status }) => {
const [deleted] = await db
.delete(shows)
.where(
and(
eq(shows.kind, "collection"),
isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id),
),
)
.returning({ id: shows.id, slug: shows.slug });
if (!deleted) {
return status(404, {
status: 404,
message: "No collection found with the given id or slug.",
});
}
return deleted;
},
{
detail: {
tags: ["collections"],
description: "Delete a collection by id or slug.",
},
params: t.Object({
id: t.String({
description: "The id or slug of the collection to delete.",
}),
}),
response: {
200: Resource(),
404: {
...KError,
description: "No collection found with the given id or slug.",
},
},
},
);

View File

@ -57,7 +57,6 @@
"react-native-worklets": "0.7.2",
"react-tooltip": "^5.30.0",
"react-use-websocket": "^4.13.0",
"sweetalert2": "^11.26.22",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"tsx": "^4.21.0",
@ -1476,8 +1475,6 @@
"svgo": ["svgo@3.3.3", "", { "dependencies": { "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0", "sax": "^1.5.0" }, "bin": "./bin/svgo" }, "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng=="],
"sweetalert2": ["sweetalert2@11.26.24", "", {}, "sha512-SLgukW4wicewpW5VOukSXY5Z6DL/z7HCOK2ODSjmQPiSphCN8gJAmh9npoceXOtBRNoDN0xIz+zHYthtfiHmjg=="],
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],

View File

@ -68,7 +68,7 @@
"react-native-worklets": "0.7.2",
"react-tooltip": "^5.30.0",
"react-use-websocket": "^4.13.0",
"sweetalert2": "^11.26.22",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"tsx": "^4.21.0",

View File

@ -117,6 +117,7 @@
"prev-page": "Previous page",
"next-page": "Next page",
"delete": "Delete",
"delete-name": "Delete {{name}}",
"cancel": "Cancel",
"more": "More",
"expand": "Expand",

View File

@ -5,14 +5,13 @@ import {
type AlertOptions,
Alert as RNAlert,
} from "react-native";
import type { SweetAlertIcon } from "sweetalert2";
export interface ExtendedAlertStatic {
alert: (
title: string,
message?: string,
buttons?: AlertButton[],
options?: AlertOptions & { icon?: SweetAlertIcon },
options?: AlertOptions,
) => void;
}

View File

@ -1,7 +1,72 @@
// Stolen from https://github.com/necolas/react-native-web/issues/1026#issuecomment-1458279681
import { createRoot } from "react-dom/client";
import type { AlertButton, AlertOptions } from "react-native";
import Swal, { type SweetAlertIcon } from "sweetalert2";
import { Pressable, View } from "react-native";
import { cn } from "~/utils";
import { Heading, P } from "./text";
const AlertDialog = ({
title,
message,
buttons,
onDismiss,
}: {
title: string;
message?: string;
buttons: AlertButton[];
onDismiss: () => void;
}) => {
return (
<Pressable
className="absolute inset-0 cursor-default! items-center justify-center bg-black/60"
onPress={onDismiss}
>
<Pressable
className={cn(
"w-full max-w-md rounded-md bg-background",
"cursor-default! overflow-hidden",
)}
onPress={(e) => e.preventDefault()}
>
<View className="items-center gap-2 p-6">
<Heading>{title}</Heading>
{message && <P className="text-center">{message}</P>}
</View>
<View className="flex-row justify-end gap-2 p-4">
{buttons.map((button, i) => (
<Pressable
key={i}
onPress={() => {
onDismiss();
button.onPress?.();
}}
className={cn(
"rounded px-4 py-2",
button.style === "destructive"
? "bg-red-700 hover:bg-red-500 focus:bg-red-500"
: button.style === "cancel"
? "hover:bg-popover focus:bg-popover"
: "bg-accent hover:bg-accent/50 focus:bg-accent/50",
)}
>
<P
className={cn(
button.style === "destructive" &&
"text-slate-800 dark:text-slate-800",
button.style === "cancel" &&
"text-slate-600 dark:text-slate-400",
button.style === "default" &&
"text-slate-200 dark:text-slate-200",
)}
>
{button.text ?? "OK"}
</P>
</Pressable>
))}
</View>
</Pressable>
</Pressable>
);
};
// biome-ignore lint/complexity/noStaticOnlyClass: Compatibility with rn
export class Alert {
@ -9,42 +74,39 @@ export class Alert {
title: string,
message?: string,
buttons?: AlertButton[],
options?: AlertOptions & { icon?: SweetAlertIcon },
options?: AlertOptions,
): void {
const confirmButton = buttons
? buttons.find((button) => button.style === "default")
: undefined;
const denyButton = buttons
? buttons.find((button) => button.style === "destructive")
: undefined;
const cancelButton = buttons
? buttons.find((button) => button.style === "cancel")
: undefined;
const resolvedButtons = buttons ?? [
{ text: "OK", style: "default" as const },
];
Swal.fire({
title: title,
text: message,
showConfirmButton: !!confirmButton,
showDenyButton: !!denyButton,
showCancelButton: !!cancelButton,
confirmButtonText: confirmButton?.text,
denyButtonText: denyButton?.text,
cancelButtonText: cancelButton?.text,
icon: options?.icon,
}).then((result) => {
if (result.isConfirmed) {
if (confirmButton?.onPress !== undefined) {
confirmButton.onPress();
}
} else if (result.isDenied) {
if (denyButton?.onPress !== undefined) {
denyButton.onPress();
}
} else if (result.isDismissed) {
if (cancelButton?.onPress !== undefined) {
cancelButton.onPress();
}
}
});
const container = document.createElement("div");
container.style.position = "fixed";
container.style.inset = "0";
container.style.zIndex = "9999";
document.body.appendChild(container);
const root = createRoot(container);
const dismiss = () => {
root.unmount();
container.remove();
};
const cancelButton = resolvedButtons.find((b) => b.style === "cancel");
root.render(
<AlertDialog
title={title}
message={message}
buttons={resolvedButtons}
onDismiss={() => {
if (options?.cancelable !== false) {
dismiss();
cancelButton?.onPress?.();
}
}}
/>,
);
}
}

View File

@ -1,9 +1,11 @@
import BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.svg";
import Delete from "@material-symbols/svg-400/rounded/delete.svg";
import MoreHoriz from "@material-symbols/svg-400/rounded/more_horiz.svg";
import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg";
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg";
import VideoLibrary from "@material-symbols/svg-400/rounded/video_library-fill.svg";
import { useRouter } from "expo-router";
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
@ -22,6 +24,7 @@ import {
import type { Metadata } from "~/models/utils/metadata";
import {
A,
Alert,
Chip,
Container,
capitalize,
@ -45,7 +48,7 @@ import {
usePopup,
} from "~/primitives";
import { useAccount } from "~/providers/account-context";
import { Fetch, type QueryIdentifier } from "~/query";
import { Fetch, type QueryIdentifier, useMutation } from "~/query";
import { cn, displayRuntime, getDisplayDate } from "~/utils";
import { PartOf } from "./part-of";
@ -72,7 +75,13 @@ const ButtonList = ({
}) => {
const account = useAccount();
const { t } = useTranslation();
const router = useRouter();
const [setPopup, closePopup] = usePopup();
const deleteMutation = useMutation({
method: "DELETE",
path: ["api", `${kind}s`, slug],
invalidate: ["api", "shows"],
});
// const metadataRefreshMutation = useMutation({
// method: "POST",
@ -154,6 +163,29 @@ const ButtonList = ({
icon={VideoLibrary}
href={`/${kind === "movie" ? "movies" : "series"}/${slug}/videos`}
/>
{kind !== "collection" && <HR />}
<Menu.Item
label={t("misc.delete")}
icon={Delete}
onSelect={() => {
Alert.alert(
t("misc.delete-name", { name }),
t("login.delete-confirmation"),
[
{ text: t("misc.cancel"), style: "cancel" },
{
text: t("misc.delete"),
style: "destructive",
onPress: async () => {
await deleteMutation.mutateAsync();
router.back();
},
},
],
{ cancelable: true },
);
}}
/>
{/* <Menu.Item */}
{/* label={t("home.refreshMetadata")} */}
{/* icon={Refresh} */}

View File

@ -91,7 +91,6 @@ export const AccountSettings = () => {
{
cancelable: true,
userInterfaceStyle: theme as "light" | "dark",
icon: "warning",
},
);
}}