mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-03-22 09:27:49 -04:00
Add delete button for series/movies/collections (#1386)
This commit is contained in:
commit
59a20a627c
@ -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.",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
23
auth/main.go
23
auth/main.go
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@ -38,6 +39,7 @@ func ErrorHandler(c *echo.Context, err error) {
|
||||
|
||||
code := http.StatusInternalServerError
|
||||
var message string
|
||||
var sc echo.HTTPStatusCoder
|
||||
|
||||
if he, ok := err.(*echo.HTTPError); ok {
|
||||
code = he.Code
|
||||
@ -46,8 +48,12 @@ func ErrorHandler(c *echo.Context, err error) {
|
||||
if message == "missing or malformed jwt" {
|
||||
code = http.StatusUnauthorized
|
||||
}
|
||||
} else if errors.As(err, &sc) {
|
||||
if tmp := sc.StatusCode(); tmp != 0 {
|
||||
code = tmp
|
||||
}
|
||||
} else {
|
||||
c.Logger().Error(err.Error())
|
||||
c.Logger().Error("Unhandled error", slog.Any("err", err))
|
||||
}
|
||||
|
||||
c.JSON(code, KError{
|
||||
@ -171,14 +177,25 @@ func (h *Handler) TokenToJwt(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
jwt = &token
|
||||
} else {
|
||||
auth := c.Request().Header.Get("Authorization")
|
||||
var token string
|
||||
|
||||
if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
|
||||
if auth == "" {
|
||||
cookie, _ := c.Request().Cookie("X-Bearer")
|
||||
if cookie != nil {
|
||||
token = cookie.Value
|
||||
}
|
||||
} else if strings.HasPrefix(auth, "Bearer ") {
|
||||
token = auth[len("Bearer "):]
|
||||
} else if auth != "" {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid bearer format.")
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
jwt = h.createGuestJwt()
|
||||
if jwt == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Guests not allowed.")
|
||||
}
|
||||
} else {
|
||||
token := auth[len("Bearer "):]
|
||||
// this is only used to check if it is a session token or a jwt
|
||||
_, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
|
||||
@ -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=="],
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -117,6 +117,7 @@
|
||||
"prev-page": "Previous page",
|
||||
"next-page": "Next page",
|
||||
"delete": "Delete",
|
||||
"delete-name": "Delete {{name}}",
|
||||
"cancel": "Cancel",
|
||||
"more": "More",
|
||||
"expand": "Expand",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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?.();
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { createMMKV, useMMKVString } from "react-native-mmkv";
|
||||
import type { ZodType, z } from "zod/v4";
|
||||
import { getServerData } from "~/utils";
|
||||
|
||||
export const storage = createMMKV();
|
||||
export const storage = createMMKV({ id: "kyoo-v5" });
|
||||
|
||||
function toBase64(utf8: string) {
|
||||
if (typeof window !== "undefined") return window.btoa(utf8);
|
||||
@ -35,7 +35,6 @@ export const setCookie = (
|
||||
|
||||
export const readCookie = <T extends ZodType>(key: string, parser: T) => {
|
||||
const cookies = getServerData("cookies");
|
||||
console.log("cookies", cookies);
|
||||
const decodedCookie = decodeURIComponent(cookies);
|
||||
const ca = decodedCookie.split(";");
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { type Entry, FullVideo, type Page } from "~/models";
|
||||
import { type Entry, type Episode, FullVideo, type Page } from "~/models";
|
||||
import { Modal, P } from "~/primitives";
|
||||
import {
|
||||
InfiniteFetch,
|
||||
@ -32,17 +32,17 @@ export const useEditLinks = (
|
||||
body: [
|
||||
{
|
||||
id: video,
|
||||
for: entries.map((x) =>
|
||||
x.kind === "episode" && !x.slug
|
||||
? {
|
||||
serie: slug,
|
||||
// @ts-expect-error: idk why it couldn't match x as an episode
|
||||
season: x.seasonNumber,
|
||||
// @ts-expect-error: idk why it couldn't match x as an episode
|
||||
episode: x.episodeNumber,
|
||||
}
|
||||
: { slug: x.slug },
|
||||
),
|
||||
for: entries.map((x) => {
|
||||
if (x.slug) return { slug: x.slug };
|
||||
const ep = x as Episode;
|
||||
if (ep.seasonNumber === 0)
|
||||
return { serie: slug, special: ep.episodeNumber };
|
||||
return {
|
||||
serie: slug,
|
||||
season: ep.seasonNumber,
|
||||
episoed: ep.episodeNumber,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
@ -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} */}
|
||||
|
||||
@ -91,7 +91,6 @@ export const AccountSettings = () => {
|
||||
{
|
||||
cancelable: true,
|
||||
userInterfaceStyle: theme as "light" | "dark",
|
||||
icon: "warning",
|
||||
},
|
||||
);
|
||||
}}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user