diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 60c08bfe..eeda3629 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -317,7 +317,7 @@ "unknownOption": "Unknown" }, "subtitles": { - "customChoice": "Select subtitle from file", + "customChoice": "Drop or upload file", "customizeLabel": "Customize", "offChoice": "Off", "settings": { @@ -326,7 +326,8 @@ "fixCapitals": "Fix capitalization" }, "title": "Subtitles", - "unknownLanguage": "Unknown" + "unknownLanguage": "Unknown", + "dropSubtitleFile": "Drop subtitle file here" } }, "metadata": { diff --git a/src/components/DropFile.tsx b/src/components/DropFile.tsx new file mode 100644 index 00000000..8b0ab84e --- /dev/null +++ b/src/components/DropFile.tsx @@ -0,0 +1,51 @@ +import { useEffect, useState } from "react"; +import type { DragEvent, ReactNode } from "react"; + +interface FileDropHandlerProps { + children: ReactNode; + className: string; + onDrop: (event: DragEvent) => void; + onDraggingChange: (isDragging: boolean) => void; +} + +export function FileDropHandler(props: FileDropHandlerProps) { + const [dragging, setDragging] = useState(false); + + const handleDragEnter = (event: DragEvent) => { + event.preventDefault(); + setDragging(true); + }; + + const handleDragLeave = (event: DragEvent) => { + if (!event.currentTarget.contains(event.relatedTarget as Node)) { + setDragging(false); + } + }; + + const handleDragOver = (event: DragEvent) => { + event.preventDefault(); + }; + + const handleDrop = (event: DragEvent) => { + event.preventDefault(); + setDragging(false); + + props.onDrop(event); + }; + + useEffect(() => { + props.onDraggingChange(dragging); + }, [dragging, props]); + + return ( +
+ {props.children} +
+ ); +} diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index ec5e26cb..500b408a 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -64,6 +64,7 @@ export enum Icons { DONATION = "donation", CIRCLE_QUESTION = "circle_question", BRUSH = "brush", + UPLOAD = "upload", } export interface IconProps { @@ -134,6 +135,7 @@ const iconList: Record = { donation: ``, circle_question: ``, brush: ``, + upload: ``, }; function ChromeCastButton() { diff --git a/src/components/player/atoms/settings/CaptionsView.tsx b/src/components/player/atoms/settings/CaptionsView.tsx index 8524ecc8..035567e2 100644 --- a/src/components/player/atoms/settings/CaptionsView.tsx +++ b/src/components/player/atoms/settings/CaptionsView.tsx @@ -1,11 +1,14 @@ +import classNames from "classnames"; import Fuse from "fuse.js"; -import { useMemo, useRef, useState } from "react"; +import { type DragEvent, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useAsyncFn } from "react-use"; import { convert } from "subsrt-ts"; import { subtitleTypeList } from "@/backend/helpers/subs"; +import { FileDropHandler } from "@/components/DropFile"; import { FlagIcon } from "@/components/FlagIcon"; +import { Icon, Icons } from "@/components/Icon"; import { useCaptions } from "@/components/player/hooks/useCaptions"; import { Menu } from "@/components/player/internals/ContextMenu"; import { Input } from "@/components/player/internals/ContextMenu/Input"; @@ -123,6 +126,34 @@ export function CaptionsView({ id }: { id: string }) { const { selectCaptionById, disable } = useCaptions(); const captionList = usePlayerStore((s) => s.captionList); const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList); + const [dragging, setDragging] = useState(false); + const setCaption = usePlayerStore((s) => s.setCaption); + + function onDrop(event: DragEvent) { + const files = event.dataTransfer.files; + const firstFile = files[0]; + if (!files || !firstFile) return; + + const fileExtension = `.${firstFile.name.split(".").pop()}`; + if (!fileExtension || !subtitleTypeList.includes(fileExtension)) { + return; + } + + const reader = new FileReader(); + reader.addEventListener("load", (e) => { + if (!e.target || typeof e.target.result !== "string") return; + + const converted = convert(e.target.result, "srt"); + + setCaption({ + language: "custom", + srtData: converted, + id: "custom-caption", + }); + }); + + reader.readAsText(firstFile); + } const captions = useMemo( () => @@ -164,6 +195,20 @@ export function CaptionsView({ id }: { id: string }) { return ( <>
+
+
+ + + {t("player.menus.subtitles.dropSubtitleFile")} + +
+
+ router.navigate("/")} rightSide={ @@ -178,17 +223,28 @@ export function CaptionsView({ id }: { id: string }) { > {t("player.menus.subtitles.title")} +
+ { + setDragging(isDragging); + }} + onDrop={(event) => onDrop(event)} + >
- - - disable()} selected={!selectedCaptionId}> - {t("player.menus.subtitles.offChoice")} - - - {content} - + + disable()} + selected={!selectedCaptionId} + > + {t("player.menus.subtitles.offChoice")} + + + {content} + +
); }