/*
* Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import { Fragment, useMemo, useRef, useState } from "react";
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from "@headlessui/react";
import {
ArrowsRightLeftIcon,
DocumentTextIcon,
EllipsisHorizontalIcon,
ForwardIcon,
PencilSquareIcon,
TrashIcon
} from "@heroicons/react/24/outline";
import { APIClient } from "@api/APIClient";
import { FeedsQueryOptions } from "@api/queries";
import { FeedKeys } from "@api/query_keys";
import { useToggle } from "@hooks/hooks";
import { baseUrl, classNames, IsEmptyDate, simplifyDate } from "@utils";
import { toast } from "@components/hot-toast";
import Toast from "@components/notifications/Toast";
import { DeleteModal, ForceRunModal } from "@components/modals";
import { FeedUpdateForm } from "@forms/settings/FeedForms";
import { EmptySimple } from "@components/emptystates";
import { ImplementationBadges } from "./Indexer";
import { ArrowPathIcon } from "@heroicons/react/24/solid";
import { ExternalLink } from "@components/ExternalLink";
import { Section } from "./_components";
import { Checkbox } from "@components/Checkbox";
interface SortConfig {
key: keyof ListItemProps["feed"] | "enabled";
direction: "ascending" | "descending";
}
const isErrorWithMessage = (error: unknown): error is { message: string } => {
return typeof error === 'object' && error !== null && 'message' in error;
};
function useSort(items: ListItemProps["feed"][], config?: SortConfig) {
const [sortConfig, setSortConfig] = useState(config);
const sortedItems = useMemo(() => {
if (!sortConfig) {
return items;
}
const sortableItems = [...items];
sortableItems.sort((a, b) => {
const aValue = sortConfig.key === "enabled" ? (a[sortConfig.key] ?? false) as number | boolean | string : a[sortConfig.key] as number | boolean | string;
const bValue = sortConfig.key === "enabled" ? (b[sortConfig.key] ?? false) as number | boolean | string : b[sortConfig.key] as number | boolean | string;
if (aValue < bValue) {
return sortConfig.direction === "ascending" ? -1 : 1;
}
if (aValue > bValue) {
return sortConfig.direction === "ascending" ? 1 : -1;
}
return 0;
});
return sortableItems;
}, [items, sortConfig]);
const requestSort = (key: keyof ListItemProps["feed"] | "enabled") => {
let direction: "ascending" | "descending" = "ascending";
if (
sortConfig &&
sortConfig.key === key &&
sortConfig.direction === "ascending"
) {
direction = "descending";
}
setSortConfig({ key, direction });
};
const getSortIndicator = (key: keyof ListItemProps["feed"]) => {
if (!sortConfig || sortConfig.key !== key) {
return "";
}
return sortConfig.direction === "ascending" ? "↑" : "↓";
};
return { items: sortedItems, requestSort, sortConfig, getSortIndicator };
}
function FeedSettings() {
const feedsQuery = useSuspenseQuery(FeedsQueryOptions())
const sortedFeeds = useSort(feedsQuery.data || []);
return (
{feedsQuery.data && feedsQuery.data.length > 0 ? (
-
sortedFeeds.requestSort("enabled")}>
Enabled {sortedFeeds.getSortIndicator("enabled")}
sortedFeeds.requestSort("name")}>
Name {sortedFeeds.getSortIndicator("name")}
sortedFeeds.requestSort("type")}>
Type {sortedFeeds.getSortIndicator("type")}
sortedFeeds.requestSort("last_run")}>
Last run {sortedFeeds.getSortIndicator("last_run")}
sortedFeeds.requestSort("next_run")}>
Next run {sortedFeeds.getSortIndicator("next_run")}
{sortedFeeds.items.map((feed) => (
))}
) : (
)}
);
}
interface ListItemProps {
feed: Feed;
}
function ListItem({ feed }: ListItemProps) {
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
const [enabled, setEnabled] = useState(feed.enabled);
const queryClient = useQueryClient();
const updateMutation = useMutation({
mutationFn: (status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: FeedKeys.lists() });
queryClient.invalidateQueries({ queryKey: FeedKeys.detail(feed.id) });
toast.custom((t) => );
}
});
const toggleActive = (status: boolean) => {
setEnabled(status);
updateMutation.mutate(status);
};
return (
{feed.name}
{feed.indexer.identifier}
{ImplementationBadges[feed.type.toLowerCase()]}
{IsEmptyDate(feed.last_run)}
{IsEmptyDate(feed.next_run)}
);
}
interface FeedItemDropdownProps {
feed: Feed;
onToggle: (newState: boolean) => void;
toggleUpdate: () => void;
}
const FeedItemDropdown = ({
feed,
onToggle,
toggleUpdate
}: FeedItemDropdownProps) => {
const cancelModalButtonRef = useRef(null);
const cancelCacheModalButtonRef = useRef(null);
const queryClient = useQueryClient();
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const [deleteCacheModalIsOpen, toggleDeleteCacheModal] = useToggle(false);
const [forceRunModalIsOpen, toggleForceRunModal] = useToggle(false);
const deleteMutation = useMutation({
mutationFn: (id: number) => APIClient.feeds.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: FeedKeys.lists() });
queryClient.invalidateQueries({ queryKey: FeedKeys.detail(feed.id) });
toast.custom((t) => );
}
});
const deleteCacheMutation = useMutation({
mutationFn: (id: number) => APIClient.feeds.deleteCache(id),
onSuccess: () => {
toast.custom((t) => );
}
});
const forceRunMutation = useMutation({
mutationFn: (id: number) => APIClient.feeds.forceRun(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: FeedKeys.lists() });
toast.custom((t) => );
toggleForceRunModal();
},
onError: (error: unknown) => {
let errorMessage = 'An unknown error occurred';
if (isErrorWithMessage(error)) {
errorMessage = error.message;
}
toast.custom((t) => , {
duration: 10000
});
toggleForceRunModal();
}
});
return (
);
};
export default FeedSettings;