-
-
-
-
-
- {LANGUAGES.map((language) => (
-
- {language.label}
-
- ))}
-
-
+ {version && (
+
+ )}
-
- {searching ? (
-
- ) : (
-
- )}
- Search
-
+
+
Search online
+
+
+
+
+
+
+ {LANGUAGES.map((language) => (
+
+ {language.label}
+
+ ))}
+
+
+
+
+ {searching ? (
+
+ ) : (
+
+ )}
+ Search
+
+
{searchError && (
diff --git a/web/src/player/components/SubtitleSearchModal.tsx b/web/src/player/components/SubtitleSearchModal.tsx
index a18a5f40..0a87cc5b 100644
--- a/web/src/player/components/SubtitleSearchModal.tsx
+++ b/web/src/player/components/SubtitleSearchModal.tsx
@@ -2,7 +2,12 @@ import { useState, useEffect, useCallback, useRef } from "react";
import { createPortal } from "react-dom";
import type { PlayerConfig } from "../context/PlayerConfigContext";
import { playerFetch } from "../player-fetch";
-import type { SubtitleSearchResponse, SubtitleResult } from "@/api/types";
+import type {
+ SubtitleLanguageDetection,
+ SubtitleSearchResponse,
+ SubtitleResult,
+} from "@/api/types";
+import { SubtitleUploadForm } from "@/components/subtitles/SubtitleUploadForm";
import { LANGUAGES } from "../utils/languageNames";
interface SubtitleSearchModalProps {
@@ -22,6 +27,7 @@ const providerInfo: Record
= {
opensubtitles: { abbr: "OS", color: "#eab308" },
subdl: { abbr: "SDL", color: "#3b82f6" },
subsource: { abbr: "SS", color: "#ef4444" },
+ upload: { abbr: "UP", color: "#a855f7" },
};
function scoreColor(score: number): string {
@@ -132,6 +138,56 @@ export function SubtitleSearchModal({
}
}, [playerConfig, mediaFileId, selectedLang]);
+ const handleUpload = useCallback(
+ async (input: {
+ mediaFileId: number;
+ file: File;
+ language?: string;
+ languageOverride?: boolean;
+ hearingImpaired: boolean;
+ }) => {
+ const form = new FormData();
+ form.set("media_file_id", String(input.mediaFileId));
+ if (input.language) {
+ form.set("language", input.language);
+ }
+ if (input.languageOverride) {
+ form.set("language_override", "true");
+ }
+ form.set("file", input.file);
+ if (input.hearingImpaired) {
+ form.set("hearing_impaired", "true");
+ }
+
+ await playerFetch(playerConfig, "/subtitles/upload", {
+ method: "POST",
+ body: form,
+ });
+ },
+ [playerConfig],
+ );
+
+ const handleDetectLanguage = useCallback(
+ async (file: File, fallbackLanguage?: string): Promise => {
+ const form = new FormData();
+ form.set("file", file);
+ if (fallbackLanguage) {
+ form.set("language", fallbackLanguage);
+ }
+
+ return playerFetch(playerConfig, "/subtitles/detect-language", {
+ method: "POST",
+ body: form,
+ });
+ },
+ [playerConfig],
+ );
+
+ const handleUploadSuccess = useCallback(() => {
+ onSubtitleDownloaded();
+ handleClose();
+ }, [onSubtitleDownloaded, handleClose]);
+
const handleDownload = useCallback(
async (result: SubtitleResult) => {
const key = `${result.provider}:${result.id}`;
@@ -171,7 +227,7 @@ export function SubtitleSearchModal({
onClick={handleClose}
role="dialog"
aria-modal="true"
- aria-label="Subtitle Search"
+ aria-label="Add Subtitles"
onKeyDown={handleFocusTrap}
>
{/* Header */}
-
Search Subtitles
+ Add Subtitles
+
+
+
+
{/* Search controls */}
(
options: RequestInit = {},
): Promise {
const headers: Record = {
- "Content-Type": "application/json",
...(options.headers as Record),
};
+ if (!(options.body instanceof FormData)) {
+ headers["Content-Type"] = "application/json";
+ }
const token = config.getAccessToken();
if (token) {