Files
channels-seerr/src/hooks/useDiscover.ts
Jam 185167a0a7 feat(blacklist): hide blacklisted items from discover pages for admins (#1601)
* feat(blacklist): add 'Hide Blacklisted Items' setting to general settings and UI

* feat(migration): add HideBlacklistedItems migration for PostgreSQL and SQLite

* feat(settings): add hideBlacklisted option to application settings

* feat(settings): add tooltips for hideAvailable and hideBlacklisted options in settings

* chore(migration): remove HideBlacklistedItems migration files for PostgreSQL and SQLite

* docs(settings): clarify description of 'Hide Blacklisted Items' setting to specify it affects all users

* docs(settings): update tooltip and description for 'Hide Blacklisted Items' to clarify it applies to all users, including administrators

* docs(settings): clarify 'Hide Blacklisted Items' functionality to specify it applies only to administrators with the "Manage Blacklist" permission

* fix(hooks): update permission check for 'Hide Blacklisted Items' to include 'Manage Blacklist'

* fix(settings): update tooltip for 'Hide Blacklisted Items' to clarify it applies to all users with the "Manage Blacklist" permission

* feat(settings): add experimental badge to settings tooltip for 'Hide Available' option
2025-05-07 02:01:23 +08:00

157 lines
3.6 KiB
TypeScript

import { MediaStatus } from '@server/constants/media';
import useSWRInfinite from 'swr/infinite';
import useSettings from './useSettings';
import { Permission, useUser } from './useUser';
export interface BaseSearchResult<T> {
page: number;
totalResults: number;
totalPages: number;
results: T[];
}
interface BaseMedia {
id: number;
mediaType: string;
mediaInfo?: {
status: MediaStatus;
};
}
interface DiscoverResult<T, S> {
isLoadingInitialData: boolean;
isLoadingMore: boolean;
fetchMore: () => void;
isEmpty: boolean;
isReachingEnd: boolean;
error: unknown;
titles: T[];
firstResultData?: BaseSearchResult<T> & S;
mutate?: () => void;
}
const extraEncodes: [RegExp, string][] = [
[/\(/g, '%28'],
[/\)/g, '%29'],
[/!/g, '%21'],
[/\*/g, '%2A'],
];
export const encodeURIExtraParams = (string: string): string => {
let finalString = encodeURIComponent(string);
extraEncodes.forEach((encode) => {
finalString = finalString.replace(encode[0], encode[1]);
});
return finalString;
};
const useDiscover = <
T extends BaseMedia,
S = Record<string, never>,
O = Record<string, unknown>
>(
endpoint: string,
options?: O,
{ hideAvailable = true, hideBlacklisted = true } = {}
): DiscoverResult<T, S> => {
const settings = useSettings();
const { hasPermission } = useUser();
const { data, error, size, setSize, isValidating, mutate } = useSWRInfinite<
BaseSearchResult<T> & S
>(
(pageIndex: number, previousPageData) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null;
}
const params: Record<string, unknown> = {
page: pageIndex + 1,
...options,
};
const finalQueryString = Object.keys(params)
.map(
(paramKey) =>
`${paramKey}=${encodeURIExtraParams(params[paramKey] as string)}`
)
.join('&');
return `${endpoint}?${finalQueryString}`;
},
{
initialSize: 3,
revalidateFirstPage: false,
}
);
const resultIds: Set<number> = new Set<number>();
const isLoadingInitialData = !data && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 &&
!!data &&
typeof data[size - 1] === 'undefined' &&
isValidating);
const fetchMore = () => {
setSize(size + 1);
};
let titles = (data ?? []).reduce((a, v) => {
const results: T[] = [];
for (const result of v.results) {
if (!resultIds.has(result.id)) {
resultIds.add(result.id);
results.push(result);
}
}
return [...a, ...results];
}, [] as T[]);
if (settings.currentSettings.hideAvailable && hideAvailable) {
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
if (
settings.currentSettings.hideBlacklisted &&
hideBlacklisted &&
hasPermission(Permission.MANAGE_BLACKLIST)
) {
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
i.mediaInfo?.status !== MediaStatus.BLACKLISTED
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd =
isEmpty ||
(!!data && (data[data?.length - 1]?.results.length ?? 0) < 20) ||
(!!data && (data[data?.length - 1]?.totalResults ?? 0) < 41);
return {
isLoadingInitialData,
isLoadingMore,
fetchMore,
isEmpty,
isReachingEnd,
error,
titles,
firstResultData: data?.[0],
mutate,
};
};
export default useDiscover;