* 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
157 lines
3.6 KiB
TypeScript
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;
|