diff --git a/src/pages/parts/player/SourceSelectPart.tsx b/src/pages/parts/player/SourceSelectPart.tsx
new file mode 100644
index 00000000..3ce05a65
--- /dev/null
+++ b/src/pages/parts/player/SourceSelectPart.tsx
@@ -0,0 +1,173 @@
+import { ScrapeMedia } from "@p-stream/providers";
+import React, { ReactNode, useEffect, useMemo, useRef } from "react";
+import { useTranslation } from "react-i18next";
+
+import { getCachedMetadata } from "@/backend/helpers/providerApi";
+import { Loading } from "@/components/layout/Loading";
+import {
+ useEmbedScraping,
+ useSourceScraping,
+} from "@/components/player/hooks/useSourceSelection";
+import { Menu } from "@/components/player/internals/ContextMenu";
+import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
+
+// Embed option component
+function EmbedOption(props: {
+ embedId: string;
+ url: string;
+ sourceId: string;
+ routerId: string;
+}) {
+ const { t } = useTranslation();
+ const unknownEmbedName = t("player.menus.sources.unknownOption");
+
+ const embedName = useMemo(() => {
+ if (!props.embedId) return unknownEmbedName;
+ const sourceMeta = getCachedMetadata().find((s) => s.id === props.embedId);
+ return sourceMeta?.name ?? unknownEmbedName;
+ }, [props.embedId, unknownEmbedName]);
+
+ const { run, errored, loading } = useEmbedScraping(
+ props.routerId,
+ props.sourceId,
+ props.url,
+ props.embedId,
+ );
+
+ return (
+
+
+ {embedName}
+
+
+ );
+}
+
+// Embed selection view (when a source is selected)
+function EmbedSelectionView(props: {
+ sourceId: string;
+ routerId: string;
+ onBack: () => void;
+}) {
+ const { t } = useTranslation();
+ const { run, notfound, loading, items, errored } = useSourceScraping(
+ props.sourceId,
+ props.routerId,
+ );
+
+ const sourceName = useMemo(() => {
+ if (!props.sourceId) return "...";
+ const sourceMeta = getCachedMetadata().find((s) => s.id === props.sourceId);
+ return sourceMeta?.name ?? "...";
+ }, [props.sourceId]);
+
+ const lastSourceId = useRef
(null);
+ useEffect(() => {
+ if (lastSourceId.current === props.sourceId) return;
+ lastSourceId.current = props.sourceId;
+ if (!props.sourceId) return;
+ run();
+ }, [run, props.sourceId]);
+
+ let content: ReactNode = null;
+ if (loading)
+ content = (
+
+
+
+ );
+ else if (notfound)
+ content = (
+
+ {t("player.menus.sources.noStream.text")}
+
+ );
+ else if (items?.length === 0)
+ content = (
+
+ {t("player.menus.sources.noEmbeds.text")}
+
+ );
+ else if (errored)
+ content = (
+
+ {t("player.menus.sources.failed.text")}
+
+ );
+ else if (items && props.sourceId)
+ content = items.map((v) => (
+
+ ));
+
+ return (
+ <>
+ {sourceName}
+ {content}
+ >
+ );
+}
+
+// Main source selection view
+export function SourceSelectPart(props: { media: ScrapeMedia }) {
+ const { t } = useTranslation();
+ const [selectedSourceId, setSelectedSourceId] = React.useState(
+ null,
+ );
+ const routerId = "manualSourceSelect";
+
+ const sources = useMemo(() => {
+ const metaType = props.media.type;
+ if (!metaType) return [];
+ return getCachedMetadata()
+ .filter((v) => v.type === "source")
+ .filter((v) => v.mediaTypes?.includes(metaType));
+ }, [props.media.type]);
+
+ if (selectedSourceId) {
+ return (
+
+
+
+ setSelectedSourceId(null)}
+ />
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {t("player.menus.sources.title")}
+
+ {sources.map((v) => (
+ setSelectedSourceId(v.id)}
+ >
+ {v.name}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/pages/parts/settings/PreferencesPart.tsx b/src/pages/parts/settings/PreferencesPart.tsx
index 7edc2896..00a873e4 100644
--- a/src/pages/parts/settings/PreferencesPart.tsx
+++ b/src/pages/parts/settings/PreferencesPart.tsx
@@ -31,6 +31,8 @@ export function PreferencesPart(props: {
setEnableLowPerformanceMode: (v: boolean) => void;
enableHoldToBoost: boolean;
setEnableHoldToBoost: (v: boolean) => void;
+ manualSourceSelection: boolean;
+ setManualSourceSelection: (v: boolean) => void;
}) {
const { t } = useTranslation();
const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code));
@@ -219,6 +221,26 @@ export function PreferencesPart(props: {
{/* Column */}
+ {/* Manual Source Selection */}
+
+
+ {t("settings.preferences.manualSource")}
+
+
+ {t("settings.preferences.manualSourceDescription")}
+
+
+ props.setManualSourceSelection(!props.manualSourceSelection)
+ }
+ className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg"
+ >
+
+
+ {t("settings.preferences.manualSourceLabel")}
+
+
+
{t("settings.preferences.sourceOrder")}
diff --git a/src/stores/preferences/index.tsx b/src/stores/preferences/index.tsx
index 065fcc8e..6354bfb3 100644
--- a/src/stores/preferences/index.tsx
+++ b/src/stores/preferences/index.tsx
@@ -23,6 +23,7 @@ export interface PreferencesStore {
enableNativeSubtitles: boolean;
enableHoldToBoost: boolean;
homeSectionOrder: string[];
+ manualSourceSelection: boolean;
setEnableThumbnails(v: boolean): void;
setEnableAutoplay(v: boolean): void;
@@ -44,6 +45,7 @@ export interface PreferencesStore {
setEnableNativeSubtitles(v: boolean): void;
setEnableHoldToBoost(v: boolean): void;
setHomeSectionOrder(v: string[]): void;
+ setManualSourceSelection(v: boolean): void;
}
export const usePreferencesStore = create(
@@ -69,6 +71,7 @@ export const usePreferencesStore = create(
enableNativeSubtitles: false,
enableHoldToBoost: true,
homeSectionOrder: ["watching", "bookmarks"],
+ manualSourceSelection: false,
setEnableThumbnails(v) {
set((s) => {
s.enableThumbnails = v;
@@ -169,6 +172,11 @@ export const usePreferencesStore = create(
s.homeSectionOrder = v;
});
},
+ setManualSourceSelection(v) {
+ set((s) => {
+ s.manualSourceSelection = v;
+ });
+ },
})),
{
name: "__MW::preferences",