Add the ability to disable sources and embeds

This commit is contained in:
Pas 2025-10-19 18:33:26 -06:00
parent 40eb846a68
commit f0a35f6408
11 changed files with 243 additions and 13 deletions

View file

@ -21,8 +21,10 @@ export interface SettingsInput {
forceCompactEpisodeView?: boolean;
sourceOrder?: string[];
enableSourceOrder?: boolean;
disabledSources?: string[];
embedOrder?: string[];
enableEmbedOrder?: boolean;
disabledEmbeds?: string[];
proxyTmdb?: boolean;
enableLowPerformanceMode?: boolean;
enableNativeSubtitles?: boolean;
@ -48,8 +50,10 @@ export interface SettingsResponse {
enableCarouselView?: boolean;
sourceOrder?: string[];
enableSourceOrder?: boolean;
disabledSources?: string[];
embedOrder?: string[];
enableEmbedOrder?: boolean;
disabledEmbeds?: string[];
proxyTmdb?: boolean;
enableLowPerformanceMode?: boolean;
enableNativeSubtitles?: boolean;

View file

@ -0,0 +1,139 @@
import {
DndContext,
DragEndEvent,
KeyboardSensor,
MouseSensor,
TouchSensor,
closestCenter,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
restrictToParentElement,
restrictToVerticalAxis,
} from "@dnd-kit/modifiers";
import {
SortableContext,
arrayMove,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import classNames from "classnames";
import { Toggle } from "../buttons/Toggle";
import { Icon, Icons } from "../Icon";
export interface ToggleableItem {
id: string;
name: string;
disabled?: boolean;
enabled?: boolean;
}
function SortableItemWithToggle(props: {
item: ToggleableItem;
onToggle: (id: string) => void;
}) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: props.item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
const handleToggleClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
props.onToggle(props.item.id);
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
className={classNames(
"bg-dropdown-background hover:bg-dropdown-hoverBackground select-none space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg",
props.item.disabled && "opacity-50",
)}
>
<span className="flex-1 text-white font-bold">{props.item.name}</span>
{props.item.disabled && <Icon icon={Icons.UNPLUG} />}
<div
onClick={handleToggleClick}
className="cursor-pointer flex-shrink-0"
onMouseDown={(e) => e.stopPropagation()}
onTouchStart={(e) => e.stopPropagation()}
>
<Toggle enabled={props.item.enabled ?? true} />
</div>
<div
{...listeners}
className={classNames(
"cursor-grab touch-manipulation flex-shrink-0",
transform ? "cursor-grabbing" : "cursor-grab",
)}
>
<Icon icon={Icons.MENU} />
</div>
</div>
);
}
export function SortableListWithToggles(props: {
items: ToggleableItem[];
setItems: (items: ToggleableItem[]) => void;
onToggle: (id: string) => void;
}) {
const sensors = useSensors(
useSensor(TouchSensor, {
activationConstraint: {
delay: 75,
tolerance: 1,
},
}),
useSensor(MouseSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over) return;
if (active.id !== over.id) {
const currentItems = props.items;
const oldIndex = currentItems.findIndex((item) => item.id === active.id);
const newIndex = currentItems.findIndex((item) => item.id === over.id);
const newItems = arrayMove(currentItems, oldIndex, newIndex);
props.setItems(newItems);
}
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
>
<SortableContext
items={props.items}
strategy={verticalListSortingStrategy}
>
<div className="flex flex-col gap-2">
{props.items.map((item) => (
<SortableItemWithToggle
key={item.id}
item={item}
onToggle={props.onToggle}
/>
))}
</div>
</SortableContext>
</DndContext>
);
}

View file

@ -144,12 +144,14 @@ export function SourceSelectionView({
const currentSourceId = usePlayerStore((s) => s.sourceId);
const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder);
const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder);
const disabledSources = usePreferencesStore((s) => s.disabledSources);
const sources = useMemo(() => {
if (!metaType) return [];
const allSources = getCachedMetadata()
.filter((v) => v.type === "source")
.filter((v) => v.mediaTypes?.includes(metaType));
.filter((v) => v.mediaTypes?.includes(metaType))
.filter((v) => !disabledSources.includes(v.id));
if (!enableSourceOrder || preferredSourceOrder.length === 0) {
return allSources;
@ -172,7 +174,7 @@ export function SourceSelectionView({
orderedSources.push(...remainingSources);
return orderedSources;
}, [metaType, preferredSourceOrder, enableSourceOrder]);
}, [metaType, preferredSourceOrder, enableSourceOrder, disabledSources]);
return (
<>

View file

@ -54,6 +54,8 @@ export function useAuthData() {
const setEnableSourceOrder = usePreferencesStore(
(s) => s.setEnableSourceOrder,
);
const setDisabledSources = usePreferencesStore((s) => s.setDisabledSources);
const setProxyTmdb = usePreferencesStore((s) => s.setProxyTmdb);
const setEnableLowPerformanceMode = usePreferencesStore(
@ -179,6 +181,10 @@ export function useAuthData() {
setEnableSourceOrder(settings.enableSourceOrder);
}
if (settings.disabledSources !== undefined) {
setDisabledSources(settings.disabledSources);
}
if (settings.proxyTmdb !== undefined) {
setProxyTmdb(settings.proxyTmdb);
}
@ -224,6 +230,7 @@ export function useAuthData() {
setEnableCarouselView,
setSourceOrder,
setEnableSourceOrder,
setDisabledSources,
setProxyTmdb,
setFebboxKey,
setEnableLowPerformanceMode,

View file

@ -155,11 +155,23 @@ export function useScrape() {
const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder);
const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder);
const disabledSources = usePreferencesStore((s) => s.disabledSources);
const preferredEmbedOrder = usePreferencesStore((s) => s.embedOrder);
const enableEmbedOrder = usePreferencesStore((s) => s.enableEmbedOrder);
const disabledEmbeds = usePreferencesStore((s) => s.disabledEmbeds);
const startScraping = useCallback(
async (media: ScrapeMedia) => {
// Filter out disabled sources from the source order
const filteredSourceOrder = enableSourceOrder
? preferredSourceOrder.filter((id) => !disabledSources.includes(id))
: undefined;
// Filter out disabled embeds from the embed order
const filteredEmbedOrder = enableEmbedOrder
? preferredEmbedOrder.filter((id) => !disabledEmbeds.includes(id))
: undefined;
const providerApiUrl = getLoadbalancedProviderApiUrl();
if (providerApiUrl && !isExtensionActiveCached()) {
startScrape();
@ -167,8 +179,8 @@ export function useScrape() {
const conn = await connectServerSideEvents<RunOutput | "">(
baseUrlMaker.scrapeAll(
media,
enableSourceOrder ? preferredSourceOrder : undefined,
enableEmbedOrder ? preferredEmbedOrder : undefined,
filteredSourceOrder,
filteredEmbedOrder,
),
["completed", "noOutput"],
);
@ -187,10 +199,10 @@ export function useScrape() {
const providers = getProviders();
const output = await providers.runAll({
media,
// Only pass sourceOrder if enableSourceOrder is true
sourceOrder: enableSourceOrder ? preferredSourceOrder : undefined,
// Only pass sourceOrder if enableSourceOrder is true, and filter out disabled sources
sourceOrder: filteredSourceOrder,
// Only pass embedOrder if enableEmbedOrder is true
embedOrder: enableEmbedOrder ? preferredEmbedOrder : undefined,
embedOrder: filteredEmbedOrder,
events: {
init: initEvent,
start: startEvent,
@ -211,8 +223,10 @@ export function useScrape() {
startScrape,
preferredSourceOrder,
enableSourceOrder,
disabledSources,
preferredEmbedOrder,
enableEmbedOrder,
disabledEmbeds,
],
);

View file

@ -59,6 +59,7 @@ export function useSettingsState(
enableDetailsModal: boolean,
sourceOrder: string[],
enableSourceOrder: boolean,
disabledSources: string[],
proxyTmdb: boolean,
enableSkipCredits: boolean,
enableImageLogos: boolean,
@ -158,6 +159,12 @@ export function useSettingsState(
resetEnableSourceOrder,
enableSourceOrderChanged,
] = useDerived(enableSourceOrder);
const [
disabledSourcesState,
setDisabledSourcesState,
resetDisabledSources,
disabledSourcesChanged,
] = useDerived(disabledSources);
const [proxyTmdbState, setProxyTmdbState, resetProxyTmdb, proxyTmdbChanged] =
useDerived(proxyTmdb);
const [
@ -223,6 +230,7 @@ export function useSettingsState(
resetEnableImageLogos();
resetSourceOrder();
resetEnableSourceOrder();
resetDisabledSources();
resetProxyTmdb();
resetEnableCarouselView();
resetForceCompactEpisodeView();
@ -252,6 +260,7 @@ export function useSettingsState(
enableImageLogosChanged ||
sourceOrderChanged ||
enableSourceOrderChanged ||
disabledSourcesChanged ||
proxyTmdbChanged ||
enableCarouselViewChanged ||
forceCompactEpisodeViewChanged ||
@ -359,6 +368,11 @@ export function useSettingsState(
set: setProxyTmdbState,
changed: proxyTmdbChanged,
},
disabledSources: {
state: disabledSourcesState,
set: setDisabledSourcesState,
changed: disabledSourcesChanged,
},
enableCarouselView: {
state: enableCarouselViewState,
set: setEnableCarouselViewState,

View file

@ -156,6 +156,9 @@ export function SettingsPage() {
(s) => s.setEnableSourceOrder,
);
const disabledSources = usePreferencesStore((s) => s.disabledSources);
const setDisabledSources = usePreferencesStore((s) => s.setDisabledSources);
const enableDiscover = usePreferencesStore((s) => s.enableDiscover);
const setEnableDiscover = usePreferencesStore((s) => s.setEnableDiscover);
@ -259,6 +262,7 @@ export function SettingsPage() {
enableDetailsModal,
sourceOrder,
enableSourceOrder,
disabledSources,
proxyTmdb,
enableSkipCredits,
enableImageLogos,
@ -323,6 +327,7 @@ export function SettingsPage() {
state.enableImageLogos.changed ||
state.sourceOrder.changed ||
state.enableSourceOrder.changed ||
state.disabledSources.changed ||
state.proxyTmdb.changed ||
state.enableCarouselView.changed ||
state.forceCompactEpisodeView.changed ||
@ -346,6 +351,7 @@ export function SettingsPage() {
enableImageLogos: state.enableImageLogos.state,
sourceOrder: state.sourceOrder.state,
enableSourceOrder: state.enableSourceOrder.state,
disabledSources: state.disabledSources.state,
proxyTmdb: state.proxyTmdb.state,
enableCarouselView: state.enableCarouselView.state,
forceCompactEpisodeView: state.forceCompactEpisodeView.state,
@ -381,6 +387,7 @@ export function SettingsPage() {
setEnableImageLogos(state.enableImageLogos.state);
setSourceOrder(state.sourceOrder.state);
setEnableSourceOrder(state.enableSourceOrder.state);
setDisabledSources(state.disabledSources.state);
setAppLanguage(state.appLanguage.state);
setTheme(state.theme.state);
setSubStyling(state.subtitleStyling.state);
@ -427,6 +434,7 @@ export function SettingsPage() {
setEnableImageLogos,
setSourceOrder,
setEnableSourceOrder,
setDisabledSources,
setAppLanguage,
setTheme,
setSubStyling,
@ -491,6 +499,8 @@ export function SettingsPage() {
setSourceOrder={state.sourceOrder.set}
enableSourceOrder={state.enableSourceOrder.state}
setenableSourceOrder={state.enableSourceOrder.set}
disabledSources={state.disabledSources.state}
setDisabledSources={state.disabledSources.set}
enableLowPerformanceMode={state.enableLowPerformanceMode.state}
setEnableLowPerformanceMode={state.enableLowPerformanceMode.set}
enableHoldToBoost={state.enableHoldToBoost.state}

View file

@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
import { getAllProviders, getProviders } from "@/backend/providers/providers";
import { Button } from "@/components/buttons/Button";
import { Toggle } from "@/components/buttons/Toggle";
import { SortableList } from "@/components/form/SortableList";
import { SortableListWithToggles } from "@/components/form/SortableListWithToggles";
import { Heading2 } from "@/components/utils/Text";
import { usePreferencesStore } from "@/stores/preferences";
@ -17,6 +17,8 @@ export function EmbedOrderPart() {
const setEmbedOrder = usePreferencesStore((s) => s.setEmbedOrder);
const enableEmbedOrder = usePreferencesStore((s) => s.enableEmbedOrder);
const setEnableEmbedOrder = usePreferencesStore((s) => s.setEnableEmbedOrder);
const disabledEmbeds = usePreferencesStore((s) => s.disabledEmbeds);
const setDisabledEmbeds = usePreferencesStore((s) => s.setDisabledEmbeds);
const allEmbeds = getAllProviders().listEmbeds();
@ -29,6 +31,7 @@ export function EmbedOrderPart() {
id: e.id,
name: e.name || e.id,
disabled: !currentDeviceEmbeds.find((embed) => embed.id === e.id),
enabled: !disabledEmbeds.includes(e.id),
}));
}
@ -37,8 +40,16 @@ export function EmbedOrderPart() {
id,
name: allEmbeds.find((e) => e.id === id)?.name || id,
disabled: !currentDeviceEmbeds.find((e) => e.id === id),
enabled: !disabledEmbeds.includes(id),
}));
}, [embedOrder, allEmbeds]);
}, [embedOrder, allEmbeds, disabledEmbeds]);
const handleEmbedToggle = (embedId: string) => {
const newDisabledEmbeds = disabledEmbeds.includes(embedId)
? disabledEmbeds.filter((id) => id !== embedId)
: [...disabledEmbeds, embedId];
setDisabledEmbeds(newDisabledEmbeds);
};
return (
<div className="space-y-6">
@ -72,9 +83,10 @@ export function EmbedOrderPart() {
{enableEmbedOrder && (
<div className="w-full flex flex-col gap-4">
<SortableList
<SortableListWithToggles
items={embedItems}
setItems={(items) => setEmbedOrder(items.map((item) => item.id))}
onToggle={handleEmbedToggle}
/>
<Button
className="max-w-[25rem]"

View file

@ -52,6 +52,7 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
enableCarouselView: store.enableCarouselView,
sourceOrder: store.sourceOrder,
enableSourceOrder: store.enableSourceOrder,
disabledSources: store.disabledSources,
proxyTmdb: store.proxyTmdb,
febboxKey: store.febboxKey,
enableLowPerformanceMode: store.enableLowPerformanceMode,

View file

@ -8,7 +8,7 @@ import { Button } from "@/components/buttons/Button";
import { Toggle } from "@/components/buttons/Toggle";
import { FlagIcon } from "@/components/FlagIcon";
import { Dropdown } from "@/components/form/Dropdown";
import { SortableList } from "@/components/form/SortableList";
import { SortableListWithToggles } from "@/components/form/SortableListWithToggles";
import { Heading1 } from "@/components/utils/Text";
import { appLanguageOptions } from "@/setup/i18n";
import { isAutoplayAllowed } from "@/utils/autoplay";
@ -27,6 +27,8 @@ export function PreferencesPart(props: {
setSourceOrder: (v: string[]) => void;
enableSourceOrder: boolean;
setenableSourceOrder: (v: boolean) => void;
disabledSources: string[];
setDisabledSources: (v: string[]) => void;
enableLowPerformanceMode: boolean;
setEnableLowPerformanceMode: (v: boolean) => void;
enableHoldToBoost: boolean;
@ -61,8 +63,9 @@ export function PreferencesPart(props: {
id,
name: allSources.find((s) => s.id === id)?.name || id,
disabled: !currentDeviceSources.find((s) => s.id === id),
enabled: !props.disabledSources.includes(id),
}));
}, [props.sourceOrder, allSources]);
}, [props.sourceOrder, props.disabledSources, allSources]);
const navigate = useNavigate();
@ -77,6 +80,13 @@ export function PreferencesPart(props: {
}
};
const handleSourceToggle = (sourceId: string) => {
const newDisabledSources = props.disabledSources.includes(sourceId)
? props.disabledSources.filter((id) => id !== sourceId)
: [...props.disabledSources, sourceId];
props.setDisabledSources(newDisabledSources);
};
return (
<div className="space-y-12">
<Heading1 border>{t("settings.preferences.title")}</Heading1>
@ -294,11 +304,12 @@ export function PreferencesPart(props: {
{props.enableSourceOrder && (
<div className="w-full flex flex-col gap-4">
<SortableList
<SortableListWithToggles
items={sourceItems}
setItems={(items) =>
props.setSourceOrder(items.map((item) => item.id))
}
onToggle={handleSourceToggle}
/>
<Button
className="max-w-[25rem]"

View file

@ -14,8 +14,10 @@ export interface PreferencesStore {
forceCompactEpisodeView: boolean;
sourceOrder: string[];
enableSourceOrder: boolean;
disabledSources: string[];
embedOrder: string[];
enableEmbedOrder: boolean;
disabledEmbeds: string[];
proxyTmdb: boolean;
febboxKey: string | null;
realDebridKey: string | null;
@ -37,8 +39,10 @@ export interface PreferencesStore {
setForceCompactEpisodeView(v: boolean): void;
setSourceOrder(v: string[]): void;
setEnableSourceOrder(v: boolean): void;
setDisabledSources(v: string[]): void;
setEmbedOrder(v: string[]): void;
setEnableEmbedOrder(v: boolean): void;
setDisabledEmbeds(v: string[]): void;
setProxyTmdb(v: boolean): void;
setFebboxKey(v: string | null): void;
setRealDebridKey(v: string | null): void;
@ -64,8 +68,10 @@ export const usePreferencesStore = create(
forceCompactEpisodeView: false,
sourceOrder: [],
enableSourceOrder: false,
disabledSources: [],
embedOrder: [],
enableEmbedOrder: false,
disabledEmbeds: [],
proxyTmdb: false,
febboxKey: null,
realDebridKey: null,
@ -130,6 +136,11 @@ export const usePreferencesStore = create(
s.enableSourceOrder = v;
});
},
setDisabledSources(v) {
set((s) => {
s.disabledSources = v;
});
},
setEmbedOrder(v) {
set((s) => {
s.embedOrder = v;
@ -140,6 +151,11 @@ export const usePreferencesStore = create(
s.enableEmbedOrder = v;
});
},
setDisabledEmbeds(v) {
set((s) => {
s.disabledEmbeds = v;
});
},
setProxyTmdb(v) {
set((s) => {
s.proxyTmdb = v;