pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

unify source and external subtitle views

Pas f03fbdfc f0736c60

+227 -366
+2
src/assets/locales/en.json
··· 691 691 "subtitles": { 692 692 "customChoice": "Drop or upload file", 693 693 "customizeLabel": "Customize", 694 + "previewLabel": "Subtitle preview:", 694 695 "offChoice": "Off", 695 696 "onChoice": "On", 696 697 "SourceChoice": "Source Subtitles", 697 698 "OpenSubtitlesChoice": "External Subtitles", 699 + "loadingExternal": "Loading external subtitles...", 698 700 "settings": { 699 701 "backlink": "Custom subtitles", 700 702 "delay": "Subtitle delay",
+1 -40
src/components/player/atoms/Settings.tsx
··· 18 18 import { CaptionSettingsView } from "./settings/CaptionSettingsView"; 19 19 import { CaptionsView } from "./settings/CaptionsView"; 20 20 import { DownloadRoutes } from "./settings/Downloads"; 21 - import { OpenSubtitlesCaptionView } from "./settings/OpensubtitlesCaptionsView"; 22 21 import { PlaybackSettingsView } from "./settings/PlaybackSettingsView"; 23 22 import { QualityView } from "./settings/QualityView"; 24 23 import { SettingsMenu } from "./settings/SettingsMenu"; 25 - import SourceCaptionsView from "./settings/SourceCaptionsView"; 26 24 import { WatchPartyView } from "./settings/WatchPartyView"; 27 25 28 26 function SettingsOverlay({ id }: { id: string }) { ··· 55 53 <AudioView id={id} /> 56 54 </Menu.Card> 57 55 </OverlayPage> 58 - <OverlayPage id={id} path="/captions" width={343} height={320}> 56 + <OverlayPage id={id} path="/captions" width={343} height={452}> 59 57 <Menu.CardWithScrollable> 60 58 <CaptionsView id={id} backLink /> 61 59 </Menu.CardWithScrollable> ··· 65 63 <Menu.CardWithScrollable> 66 64 <CaptionsView id={id} /> 67 65 </Menu.CardWithScrollable> 68 - </OverlayPage> 69 - <OverlayPage 70 - id={id} 71 - path="/captions/opensubtitles" 72 - width={343} 73 - height={452} 74 - > 75 - <Menu.Card> 76 - <OpenSubtitlesCaptionView id={id} /> 77 - </Menu.Card> 78 - </OverlayPage> 79 - {/* This is used by the captions shortcut in bottomControls of player */} 80 - <OverlayPage 81 - id={id} 82 - path="/captions/opensubtitlesOverlay" 83 - width={343} 84 - height={452} 85 - > 86 - <Menu.Card> 87 - <OpenSubtitlesCaptionView id={id} overlayBackLink /> 88 - </Menu.Card> 89 - </OverlayPage> 90 - <OverlayPage id={id} path="/captions/source" width={343} height={452}> 91 - <Menu.Card> 92 - <SourceCaptionsView id={id} /> 93 - </Menu.Card> 94 - </OverlayPage> 95 - {/* This is used by the captions shortcut in bottomControls of player */} 96 - <OverlayPage 97 - id={id} 98 - path="/captions/sourceOverlay" 99 - width={343} 100 - height={452} 101 - > 102 - <Menu.Card> 103 - <SourceCaptionsView id={id} overlayBackLink /> 104 - </Menu.Card> 105 66 </OverlayPage> 106 67 <OverlayPage id={id} path="/captions/settings" width={343} height={452}> 107 68 <Menu.Card>
+214 -46
src/components/player/atoms/settings/CaptionsView.tsx
··· 1 1 import classNames from "classnames"; 2 + import Fuse from "fuse.js"; 2 3 import { type DragEvent, useEffect, useMemo, useRef, useState } from "react"; 3 4 import { useTranslation } from "react-i18next"; 5 + import { useAsyncFn } from "react-use"; 4 6 import { convert } from "subsrt-ts"; 5 7 6 8 import { subtitleTypeList } from "@/backend/helpers/subs"; ··· 9 11 import { Icon, Icons } from "@/components/Icon"; 10 12 import { useCaptions } from "@/components/player/hooks/useCaptions"; 11 13 import { Menu } from "@/components/player/internals/ContextMenu"; 14 + import { Input } from "@/components/player/internals/ContextMenu/Input"; 12 15 import { SelectableLink } from "@/components/player/internals/ContextMenu/Links"; 13 - import { fixUTF8Encoding } from "@/components/player/utils/captions"; 16 + import { 17 + captionIsVisible, 18 + fixUTF8Encoding, 19 + parseSubtitles, 20 + } from "@/components/player/utils/captions"; 14 21 import { useOverlayRouter } from "@/hooks/useOverlayRouter"; 22 + import { CaptionListItem } from "@/stores/player/slices/source"; 15 23 import { usePlayerStore } from "@/stores/player/store"; 16 24 import { useSubtitleStore } from "@/stores/subtitles"; 17 - import { getPrettyLanguageNameFromLocale } from "@/utils/language"; 25 + import { 26 + getPrettyLanguageNameFromLocale, 27 + sortLangCodes, 28 + } from "@/utils/language"; 18 29 19 30 export function CaptionOption(props: { 20 31 countryCode?: string; ··· 141 152 ); 142 153 } 143 154 155 + // Hook to filter and sort subtitle list with search 156 + export function useSubtitleList(subs: CaptionListItem[], searchQuery: string) { 157 + const { t: translate } = useTranslation(); 158 + const unknownChoice = translate("player.menus.subtitles.unknownLanguage"); 159 + return useMemo(() => { 160 + const input = subs.map((t) => ({ 161 + ...t, 162 + languageName: 163 + getPrettyLanguageNameFromLocale(t.language) ?? unknownChoice, 164 + })); 165 + const sorted = sortLangCodes(input.map((t) => t.language)); 166 + let results = input.sort((a, b) => { 167 + return sorted.indexOf(a.language) - sorted.indexOf(b.language); 168 + }); 169 + 170 + if (searchQuery.trim().length > 0) { 171 + const fuse = new Fuse(input, { 172 + includeScore: true, 173 + threshold: 0.3, // Lower threshold = stricter matching (0 = exact, 1 = match anything) 174 + keys: ["languageName"], 175 + }); 176 + 177 + results = fuse.search(searchQuery).map((res) => res.item); 178 + } 179 + 180 + return results; 181 + }, [subs, searchQuery, unknownChoice]); 182 + } 183 + 144 184 export function CustomCaptionOption() { 145 185 const { t } = useTranslation(); 146 186 const lang = usePlayerStore((s) => s.caption.selected?.language); ··· 198 238 const { t } = useTranslation(); 199 239 const router = useOverlayRouter(id); 200 240 const selectedCaptionId = usePlayerStore((s) => s.caption.selected?.id); 201 - const { disable, toggleLastUsed } = useCaptions(); 241 + const { disable, selectCaptionById } = useCaptions(); 202 242 const [dragging, setDragging] = useState(false); 203 243 const setCaption = usePlayerStore((s) => s.setCaption); 204 - const selectedCaptionLanguage = usePlayerStore( 205 - (s) => s.caption.selected?.language, 244 + const [searchQuery, setSearchQuery] = useState(""); 245 + const [currentlyDownloading, setCurrentlyDownloading] = useState< 246 + string | null 247 + >(null); 248 + const videoTime = usePlayerStore((s) => s.progress.time); 249 + const srtData = usePlayerStore((s) => s.caption.selected?.srtData); 250 + const language = usePlayerStore((s) => s.caption.selected?.language); 251 + const captionList = usePlayerStore((s) => s.captionList); 252 + const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList); 253 + const isLoadingExternalSubtitles = usePlayerStore( 254 + (s) => s.isLoadingExternalSubtitles, 255 + ); 256 + const delay = useSubtitleStore((s) => s.delay); 257 + 258 + // Get combined caption list 259 + const captions = useMemo( 260 + () => 261 + captionList.length !== 0 ? captionList : (getHlsCaptionList?.() ?? []), 262 + [captionList, getHlsCaptionList], 263 + ); 264 + 265 + // Split captions into source and external (opensubtitles) 266 + const sourceCaptions = useMemo( 267 + () => captions.filter((x) => !x.opensubtitles), 268 + [captions], 269 + ); 270 + const externalCaptions = useMemo( 271 + () => captions.filter((x) => x.opensubtitles), 272 + [captions], 273 + ); 274 + 275 + // Filter lists based on search query 276 + const sourceList = useSubtitleList(sourceCaptions, searchQuery); 277 + const externalList = useSubtitleList(externalCaptions, searchQuery); 278 + 279 + // Get current subtitle text preview 280 + const currentSubtitleText = useMemo(() => { 281 + if (!srtData || !selectedCaptionId) return null; 282 + const parsedCaptions = parseSubtitles(srtData, language); 283 + const visibleCaption = parsedCaptions.find(({ start, end }) => 284 + captionIsVisible(start, end, delay, videoTime), 285 + ); 286 + return visibleCaption?.content; 287 + }, [srtData, language, delay, videoTime, selectedCaptionId]); 288 + 289 + // Download handler 290 + const [downloadReq, startDownload] = useAsyncFn( 291 + async (captionId: string) => { 292 + setCurrentlyDownloading(captionId); 293 + return selectCaptionById(captionId); 294 + }, 295 + [selectCaptionById, setCurrentlyDownloading], 206 296 ); 207 297 208 298 function onDrop(event: DragEvent<HTMLDivElement>) { ··· 219 309 reader.addEventListener("load", (e) => { 220 310 if (!e.target || typeof e.target.result !== "string") return; 221 311 222 - // Ensure the data is in UTF-8 and fix any encoding issues 223 312 const encoder = new TextEncoder(); 224 313 const decoder = new TextDecoder("utf-8"); 225 314 const utf8Bytes = encoder.encode(e.target.result); ··· 238 327 reader.readAsText(firstFile, "utf-8"); 239 328 } 240 329 241 - const selectedLanguagePretty = selectedCaptionLanguage 242 - ? (getPrettyLanguageNameFromLocale(selectedCaptionLanguage) ?? 243 - t("player.menus.subtitles.unknownLanguage")) 244 - : undefined; 330 + // Render subtitle option 331 + const renderSubtitleOption = ( 332 + v: CaptionListItem & { languageName: string }, 333 + ) => ( 334 + <CaptionOption 335 + key={v.id} 336 + countryCode={v.language} 337 + selected={v.id === selectedCaptionId} 338 + loading={v.id === currentlyDownloading && downloadReq.loading} 339 + error={ 340 + v.id === currentlyDownloading && downloadReq.error 341 + ? downloadReq.error.toString() 342 + : undefined 343 + } 344 + onClick={() => startDownload(v.id)} 345 + flag 346 + subtitleUrl={v.url} 347 + subtitleType={v.type} 348 + subtitleSource={v.source} 349 + subtitleEncoding={v.encoding} 350 + isHearingImpaired={v.isHearingImpaired} 351 + > 352 + {v.languageName} 353 + </CaptionOption> 354 + ); 245 355 246 356 return ( 247 357 <> ··· 298 408 }} 299 409 onDrop={(event) => onDrop(event)} 300 410 > 411 + {/* Current subtitle preview */} 412 + {selectedCaptionId && ( 413 + <div className="mt-3 p-2 rounded-xl bg-video-context-light bg-opacity-10 text-center sm:hidden"> 414 + <div className="text-sm text-video-context-type-secondary mb-1"> 415 + {t("player.menus.subtitles.previewLabel")} 416 + </div> 417 + <div 418 + className="text-base font-medium min-h-[3rem] flex items-center justify-center" 419 + style={{ minHeight: "3rem" }} 420 + > 421 + {currentSubtitleText ? ( 422 + <div 423 + // eslint-disable-next-line react/no-danger 424 + dangerouslySetInnerHTML={{ 425 + __html: currentSubtitleText.replaceAll(/\r?\n/g, "<br />"), 426 + }} 427 + /> 428 + ) : ( 429 + <span className="text-video-context-type-secondary italic"> 430 + ...{" "} 431 + </span> 432 + )} 433 + </div> 434 + </div> 435 + )} 436 + 437 + {/* Search input */} 438 + <div className="mt-3"> 439 + <Input value={searchQuery} onInput={setSearchQuery} /> 440 + </div> 441 + 301 442 <Menu.ScrollToActiveSection className="!pt-1 mt-2 pb-3"> 443 + {/* Off button */} 302 444 <CaptionOption 303 445 onClick={() => disable()} 304 446 selected={!selectedCaptionId} 305 447 > 306 448 {t("player.menus.subtitles.offChoice")} 307 449 </CaptionOption> 308 - <CaptionOption 309 - onClick={() => toggleLastUsed().catch(() => {})} 310 - selected={!!selectedCaptionId} 311 - > 312 - {t("player.menus.subtitles.onChoice")} 313 - </CaptionOption> 450 + 451 + {/* Custom upload option */} 314 452 <CustomCaptionOption /> 315 - <Menu.ChevronLink 316 - onClick={() => 317 - router.navigate( 318 - backLink ? "/captions/source" : "/captions/sourceOverlay", 319 - ) 320 - } 321 - rightText={ 322 - useSubtitleStore((s) => s.isOpenSubtitles) 323 - ? "" 324 - : selectedLanguagePretty 325 - } 326 - > 327 - {t("player.menus.subtitles.SourceChoice")} 328 - </Menu.ChevronLink> 329 - <Menu.ChevronLink 330 - onClick={() => 331 - router.navigate( 332 - backLink 333 - ? "/captions/opensubtitles" 334 - : "/captions/opensubtitlesOverlay", 335 - ) 336 - } 337 - rightText={ 338 - useSubtitleStore((s) => s.isOpenSubtitles) 339 - ? selectedLanguagePretty 340 - : "" 341 - } 342 - > 343 - {t("player.menus.subtitles.OpenSubtitlesChoice")} 344 - </Menu.ChevronLink> 453 + 454 + {/* No subtitles available message */} 455 + {!isLoadingExternalSubtitles && 456 + sourceCaptions.length === 0 && 457 + externalCaptions.length === 0 && ( 458 + <div className="p-4 mt-6 rounded-xl bg-video-context-light bg-opacity-10 text-center"> 459 + <div className="text-video-context-type-secondary"> 460 + {t("player.menus.subtitles.empty")} 461 + </div> 462 + </div> 463 + )} 464 + 465 + {/* Loading external subtitles */} 466 + {isLoadingExternalSubtitles && externalCaptions.length === 0 && ( 467 + <div className="p-4 mt-6 rounded-xl bg-video-context-light bg-opacity-10 text-center"> 468 + <div className="text-video-context-type-secondary"> 469 + {t("player.menus.subtitles.loadingExternal")} 470 + </div> 471 + </div> 472 + )} 473 + 474 + {/* Source Subtitles Section */} 475 + {sourceCaptions.length > 0 && ( 476 + <> 477 + <div className="text-sm font-semibold text-video-context-type-secondary pt-2 mb-2"> 478 + {t("player.menus.subtitles.SourceChoice")} 479 + </div> 480 + {sourceList.length > 0 ? ( 481 + sourceList.map(renderSubtitleOption) 482 + ) : ( 483 + <div className="text-center text-video-context-type-secondary py-2"> 484 + {t("player.menus.subtitles.notFound")} 485 + </div> 486 + )} 487 + </> 488 + )} 489 + 490 + {/* External Subtitles Section */} 491 + {externalCaptions.length > 0 && ( 492 + <> 493 + <div className="text-sm font-semibold text-video-context-type-secondary pt-2 mb-2"> 494 + {t("player.menus.subtitles.OpenSubtitlesChoice")} 495 + </div> 496 + {externalList.length > 0 ? ( 497 + externalList.map(renderSubtitleOption) 498 + ) : ( 499 + <div className="text-center text-video-context-type-secondary py-2"> 500 + {t("player.menus.subtitles.notFound")} 501 + </div> 502 + )} 503 + </> 504 + )} 505 + 506 + {/* Loading indicator for external subtitles while source exists */} 507 + {isLoadingExternalSubtitles && sourceCaptions.length > 0 && ( 508 + <div className="text-center text-video-context-type-secondary py-4 mt-2"> 509 + {t("player.menus.subtitles.loadingExternal") || 510 + "Loading external subtitles..."} 511 + </div> 512 + )} 345 513 </Menu.ScrollToActiveSection> 346 514 </FileDropHandler> 347 515 </>
-124
src/components/player/atoms/settings/OpensubtitlesCaptionsView.tsx
··· 1 - import { useMemo, useState } from "react"; 2 - import { useTranslation } from "react-i18next"; 3 - import { useAsyncFn } from "react-use"; 4 - 5 - import { useCaptions } from "@/components/player/hooks/useCaptions"; 6 - import { Menu } from "@/components/player/internals/ContextMenu"; 7 - import { Input } from "@/components/player/internals/ContextMenu/Input"; 8 - import { useOverlayRouter } from "@/hooks/useOverlayRouter"; 9 - import { usePlayerStore } from "@/stores/player/store"; 10 - 11 - import { CaptionOption } from "./CaptionsView"; 12 - import { useSubtitleList } from "./SourceCaptionsView"; 13 - 14 - export function OpenSubtitlesCaptionView({ 15 - id, 16 - overlayBackLink, 17 - }: { 18 - id: string; 19 - overlayBackLink?: true; 20 - }) { 21 - const { t } = useTranslation(); 22 - const router = useOverlayRouter(id); 23 - const selectedCaptionId = usePlayerStore((s) => s.caption.selected?.id); 24 - const [currentlyDownloading, setCurrentlyDownloading] = useState< 25 - string | null 26 - >(null); 27 - const { selectCaptionById } = useCaptions(); 28 - const captionList = usePlayerStore((s) => s.captionList); 29 - const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList); 30 - const addExternalSubtitles = usePlayerStore((s) => s.addExternalSubtitles); 31 - 32 - const captions = useMemo( 33 - () => 34 - captionList.length !== 0 ? captionList : (getHlsCaptionList?.() ?? []), 35 - [captionList, getHlsCaptionList], 36 - ); 37 - 38 - const [searchQuery, setSearchQuery] = useState(""); 39 - const subtitleList = useSubtitleList( 40 - captions.filter((x) => x.opensubtitles), 41 - searchQuery, 42 - ); 43 - 44 - const [downloadReq, startDownload] = useAsyncFn( 45 - async (captionId: string) => { 46 - setCurrentlyDownloading(captionId); 47 - return selectCaptionById(captionId); 48 - }, 49 - [selectCaptionById, setCurrentlyDownloading], 50 - ); 51 - 52 - const [refreshReq, startRefresh] = useAsyncFn(async () => { 53 - return addExternalSubtitles(); 54 - }, [addExternalSubtitles]); 55 - 56 - const content = subtitleList.length 57 - ? subtitleList.map((v) => { 58 - return ( 59 - <CaptionOption 60 - // key must use index to prevent url collisions 61 - key={v.id} 62 - countryCode={v.language} 63 - selected={v.id === selectedCaptionId} 64 - loading={v.id === currentlyDownloading && downloadReq.loading} 65 - error={ 66 - v.id === currentlyDownloading && downloadReq.error 67 - ? downloadReq.error.toString() 68 - : undefined 69 - } 70 - onClick={() => startDownload(v.id)} 71 - flag 72 - subtitleUrl={v.url} 73 - subtitleType={v.type} 74 - // subtitle details from wyzie 75 - subtitleSource={v.source} 76 - subtitleEncoding={v.encoding} 77 - isHearingImpaired={v.isHearingImpaired} 78 - > 79 - {v.languageName} 80 - </CaptionOption> 81 - ); 82 - }) 83 - : t("player.menus.subtitles.notFound"); 84 - 85 - return ( 86 - <> 87 - <div> 88 - <Menu.BackLink 89 - onClick={() => 90 - router.navigate(overlayBackLink ? "/captionsOverlay" : "/captions") 91 - } 92 - > 93 - {t("player.menus.subtitles.OpenSubtitlesChoice")} 94 - </Menu.BackLink> 95 - </div> 96 - {captionList.filter((x) => x.opensubtitles).length ? ( 97 - <div className="mt-3"> 98 - <Input value={searchQuery} onInput={setSearchQuery} /> 99 - </div> 100 - ) : null} 101 - <Menu.ScrollToActiveSection className="!pt-1 mt-2 pb-3"> 102 - {!captionList.filter((x) => x.opensubtitles).length ? ( 103 - <div className="p-4 rounded-xl bg-video-context-light bg-opacity-10 font-medium text-center"> 104 - <div className="flex flex-col items-center justify-center gap-3"> 105 - {t("player.menus.subtitles.empty")} 106 - <button 107 - type="button" 108 - onClick={() => startRefresh()} 109 - disabled={refreshReq.loading} 110 - className="p-1 w-3/4 rounded tabbable duration-200 bg-opacity-10 bg-video-context-light hover:bg-opacity-20" 111 - > 112 - {t("player.menus.subtitles.scrapeButton")} 113 - </button> 114 - </div> 115 - </div> 116 - ) : ( 117 - <div className="text-center">{content}</div> 118 - )} 119 - </Menu.ScrollToActiveSection> 120 - </> 121 - ); 122 - } 123 - 124 - export default OpenSubtitlesCaptionView;
-156
src/components/player/atoms/settings/SourceCaptionsView.tsx
··· 1 - import Fuse from "fuse.js"; 2 - import { useMemo, useState } from "react"; 3 - import { useTranslation } from "react-i18next"; 4 - import { useAsyncFn } from "react-use"; 5 - 6 - import { useCaptions } from "@/components/player/hooks/useCaptions"; 7 - import { Menu } from "@/components/player/internals/ContextMenu"; 8 - import { Input } from "@/components/player/internals/ContextMenu/Input"; 9 - import { useOverlayRouter } from "@/hooks/useOverlayRouter"; 10 - import { CaptionListItem } from "@/stores/player/slices/source"; 11 - import { usePlayerStore } from "@/stores/player/store"; 12 - import { 13 - getPrettyLanguageNameFromLocale, 14 - sortLangCodes, 15 - } from "@/utils/language"; 16 - 17 - import { CaptionOption } from "./CaptionsView"; 18 - 19 - export function useSubtitleList(subs: CaptionListItem[], searchQuery: string) { 20 - const { t: translate } = useTranslation(); 21 - const unknownChoice = translate("player.menus.subtitles.unknownLanguage"); 22 - return useMemo(() => { 23 - const input = subs.map((t) => ({ 24 - ...t, 25 - languageName: 26 - getPrettyLanguageNameFromLocale(t.language) ?? unknownChoice, 27 - })); 28 - const sorted = sortLangCodes(input.map((t) => t.language)); 29 - let results = input.sort((a, b) => { 30 - return sorted.indexOf(a.language) - sorted.indexOf(b.language); 31 - }); 32 - 33 - if (searchQuery.trim().length > 0) { 34 - const fuse = new Fuse(input, { 35 - includeScore: true, 36 - keys: ["languageName"], 37 - }); 38 - 39 - results = fuse.search(searchQuery).map((res) => res.item); 40 - } 41 - 42 - return results; 43 - }, [subs, searchQuery, unknownChoice]); 44 - } 45 - 46 - export function SourceCaptionsView({ 47 - id, 48 - overlayBackLink, 49 - }: { 50 - id: string; 51 - overlayBackLink?: true; 52 - }) { 53 - const { t } = useTranslation(); 54 - const router = useOverlayRouter(id); 55 - const selectedCaptionId = usePlayerStore((s) => s.caption.selected?.id); 56 - const [currentlyDownloading, setCurrentlyDownloading] = useState< 57 - string | null 58 - >(null); 59 - const { selectCaptionById } = useCaptions(); 60 - const captionList = usePlayerStore((s) => s.captionList); 61 - const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList); 62 - 63 - const captions = useMemo( 64 - () => 65 - captionList.length !== 0 ? captionList : (getHlsCaptionList?.() ?? []), 66 - [captionList, getHlsCaptionList], 67 - ); 68 - 69 - const [searchQuery, setSearchQuery] = useState(""); 70 - const subtitleList = useSubtitleList( 71 - captions.filter((x) => !x.opensubtitles), 72 - searchQuery, 73 - ); 74 - 75 - const [downloadReq, startDownload] = useAsyncFn( 76 - async (captionId: string) => { 77 - setCurrentlyDownloading(captionId); 78 - return selectCaptionById(captionId); 79 - }, 80 - [selectCaptionById, setCurrentlyDownloading], 81 - ); 82 - 83 - const content = subtitleList.length 84 - ? subtitleList.map((v) => { 85 - return ( 86 - <CaptionOption 87 - // key must use index to prevent url collisions 88 - key={v.id} 89 - countryCode={v.language} 90 - selected={v.id === selectedCaptionId} 91 - loading={v.id === currentlyDownloading && downloadReq.loading} 92 - error={ 93 - v.id === currentlyDownloading && downloadReq.error 94 - ? downloadReq.error.toString() 95 - : undefined 96 - } 97 - onClick={() => startDownload(v.id)} 98 - flag 99 - subtitleUrl={v.url} 100 - subtitleType={v.type} 101 - // subtitle details from wyzie 102 - subtitleSource={v.source} 103 - subtitleEncoding={v.encoding} 104 - isHearingImpaired={v.isHearingImpaired} 105 - > 106 - {v.languageName} 107 - </CaptionOption> 108 - ); 109 - }) 110 - : t("player.menus.subtitles.notFound"); 111 - 112 - return ( 113 - <> 114 - <div> 115 - <Menu.BackLink 116 - onClick={() => 117 - router.navigate(overlayBackLink ? "/captionsOverlay" : "/captions") 118 - } 119 - > 120 - {t("player.menus.subtitles.SourceChoice")} 121 - </Menu.BackLink> 122 - </div> 123 - {captionList.filter((x) => !x.opensubtitles).length ? ( 124 - <div className="mt-3"> 125 - <Input value={searchQuery} onInput={setSearchQuery} /> 126 - </div> 127 - ) : null} 128 - <Menu.ScrollToActiveSection className="!pt-1 mt-2 pb-3"> 129 - {!captionList.filter((x) => !x.opensubtitles).length ? ( 130 - <div className="p-4 rounded-xl bg-video-context-light bg-opacity-10 font-medium text-center"> 131 - <div className="flex flex-col items-center justify-center gap-3"> 132 - {t("player.menus.subtitles.empty")} 133 - <button 134 - type="button" 135 - onClick={() => 136 - router.navigate( 137 - overlayBackLink 138 - ? "/captions/opensubtitlesOverlay" 139 - : "/captions/opensubtitles", 140 - ) 141 - } 142 - className="p-1 w-3/4 rounded tabbable duration-200 bg-opacity-10 bg-video-context-light hover:bg-opacity-20" 143 - > 144 - {t("player.menus.subtitles.scrapeButton")} 145 - </button> 146 - </div> 147 - </div> 148 - ) : ( 149 - <div className="text-center">{content}</div> 150 - )} 151 - </Menu.ScrollToActiveSection> 152 - </> 153 - ); 154 - } 155 - 156 - export default SourceCaptionsView;
+10
src/stores/player/slices/source.ts
··· 83 83 currentQuality: SourceQuality | null; 84 84 currentAudioTrack: AudioTrack | null; 85 85 captionList: CaptionListItem[]; 86 + isLoadingExternalSubtitles: boolean; 86 87 caption: { 87 88 selected: Caption | null; 88 89 asTrack: boolean; ··· 135 136 qualities: [], 136 137 audioTracks: [], 137 138 captionList: [], 139 + isLoadingExternalSubtitles: false, 138 140 currentQuality: null, 139 141 currentAudioTrack: null, 140 142 status: playerStatus.IDLE, ··· 258 260 const store = get(); 259 261 if (!store.meta) return; 260 262 263 + set((s) => { 264 + s.isLoadingExternalSubtitles = true; 265 + }); 266 + 261 267 try { 262 268 const { scrapeExternalSubtitles } = await import( 263 269 "@/utils/externalSubtitles" ··· 277 283 } 278 284 } catch (error) { 279 285 console.error("Failed to scrape external subtitles:", error); 286 + } finally { 287 + set((s) => { 288 + s.isLoadingExternalSubtitles = false; 289 + }); 280 290 } 281 291 }, 282 292 });