mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 03:55:38 -04:00
feat: reorganize API around operation domains
Restructure the oRPC contract from 14 resource-oriented routers to 8 domain-oriented routers for a cleaner public API surface. - Consolidate 7 watch procedures into unified tracking.watch/unwatch with scope + ids input (movie, episode, season, series) - Split dashboard across tracking (stats, history), library (continueWatching, upcoming), and discover (recommendations) - Merge explore + search + discover into single discover router - Absorb integrations into account.integrations - Merge system.authConfig into system.publicInfo - Collapse 6 admin setting endpoints into admin.settings.get/update - Deduplicate platforms.list + explore.watchProviders into discover.platforms - Add symmetric unwatchMovie/unwatchSeries core functions - Rename titles.detail→get, titles.recommendations→similar, people.detail→get BREAKING CHANGE: All client API paths have changed. REST paths now mirror router structure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ export default function LoginScreen() {
|
||||
const [errorFields, setErrorFields] = useState<Set<string>>(new Set());
|
||||
const [isSignedIn, setIsSignedIn] = useState(false);
|
||||
|
||||
const authConfig = useQuery(orpc.system.authConfig.queryOptions());
|
||||
const publicInfo = useQuery(orpc.system.publicInfo.queryOptions());
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: { email: "", password: "" },
|
||||
@@ -76,9 +76,9 @@ export default function LoginScreen() {
|
||||
const statusCompletedColor = useCSSVariable("--color-status-completed") as string;
|
||||
const serverHost = splitUrl(getServerUrl()).host;
|
||||
|
||||
const showPasswordLogin = !authConfig.data?.passwordLoginDisabled;
|
||||
const showOidc = authConfig.data?.oidcEnabled;
|
||||
const showRegister = authConfig.data?.registrationOpen;
|
||||
const showPasswordLogin = !publicInfo.data?.passwordLoginDisabled;
|
||||
const showOidc = publicInfo.data?.oidcEnabled;
|
||||
const showRegister = publicInfo.data?.registrationOpen;
|
||||
|
||||
const clearFieldError = (name: string) => {
|
||||
if (errorFields.has(name)) {
|
||||
@@ -94,7 +94,7 @@ export default function LoginScreen() {
|
||||
<AuthScreen title="Sofa" subtitle={t`Sign in to continue`}>
|
||||
{showOidc &&
|
||||
(() => {
|
||||
const providerName = authConfig.data?.oidcProviderName ?? "SSO";
|
||||
const providerName = publicInfo.data?.oidcProviderName ?? "SSO";
|
||||
return (
|
||||
<Animated.View entering={FadeInDown.duration(300).delay(100)} className="mb-4">
|
||||
<Button
|
||||
|
||||
@@ -18,7 +18,7 @@ const exploreContentContainerStyle = {
|
||||
export default function ExploreScreen() {
|
||||
const { t } = useLingui();
|
||||
const trending = useInfiniteQuery(
|
||||
orpc.explore.trending.infiniteOptions({
|
||||
orpc.discover.trending.infiniteOptions({
|
||||
input: (pageParam: number) => ({ type: "all" as const, page: pageParam }),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) =>
|
||||
@@ -26,16 +26,15 @@ export default function ExploreScreen() {
|
||||
maxPages: 10,
|
||||
}),
|
||||
);
|
||||
const popularMovies = useQuery(orpc.explore.popular.queryOptions({ input: { type: "movie" } }));
|
||||
const popularTv = useQuery(orpc.explore.popular.queryOptions({ input: { type: "tv" } }));
|
||||
const movieGenres = useQuery(orpc.explore.genres.queryOptions({ input: { type: "movie" } }));
|
||||
const tvGenres = useQuery(orpc.explore.genres.queryOptions({ input: { type: "tv" } }));
|
||||
const popularMovies = useQuery(orpc.discover.popular.queryOptions({ input: { type: "movie" } }));
|
||||
const popularTv = useQuery(orpc.discover.popular.queryOptions({ input: { type: "tv" } }));
|
||||
const movieGenres = useQuery(orpc.discover.genres.queryOptions({ input: { type: "movie" } }));
|
||||
const tvGenres = useQuery(orpc.discover.genres.queryOptions({ input: { type: "tv" } }));
|
||||
|
||||
const isRefreshing =
|
||||
trending.isRefetching || popularMovies.isRefetching || popularTv.isRefetching;
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: orpc.explore.key() });
|
||||
queryClient.invalidateQueries({ queryKey: orpc.discover.key() });
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -51,20 +51,20 @@ export default function DashboardScreen() {
|
||||
"--color-status-completed",
|
||||
]) as [string, string, string, string];
|
||||
|
||||
const stats = useQuery(orpc.dashboard.stats.queryOptions());
|
||||
const stats = useQuery(orpc.tracking.stats.queryOptions());
|
||||
const movieHistory = useQuery(
|
||||
orpc.dashboard.watchHistory.queryOptions({
|
||||
orpc.tracking.history.queryOptions({
|
||||
input: { type: "movie", period: moviePeriod },
|
||||
}),
|
||||
);
|
||||
const episodeHistory = useQuery(
|
||||
orpc.dashboard.watchHistory.queryOptions({
|
||||
orpc.tracking.history.queryOptions({
|
||||
input: { type: "episode", period: episodePeriod },
|
||||
}),
|
||||
);
|
||||
const continueWatching = useQuery(orpc.dashboard.continueWatching.queryOptions());
|
||||
const continueWatching = useQuery(orpc.library.continueWatching.queryOptions());
|
||||
const library = useQuery(orpc.library.list.queryOptions({ input: { page: 1, limit: 10 } }));
|
||||
const recommendations = useQuery(orpc.dashboard.recommendations.queryOptions());
|
||||
const recommendations = useQuery(orpc.discover.recommendations.queryOptions());
|
||||
|
||||
const isRefreshing =
|
||||
stats.isRefetching ||
|
||||
@@ -74,7 +74,7 @@ export default function DashboardScreen() {
|
||||
episodeHistory.isRefetching;
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: orpc.dashboard.key() });
|
||||
queryClient.invalidateQueries({ queryKey: orpc.tracking.key() });
|
||||
queryClient.invalidateQueries({ queryKey: orpc.library.key() });
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function UpcomingScreen() {
|
||||
|
||||
const { data, isPending, fetchNextPage, hasNextPage, isFetchingNextPage, isRefetching } =
|
||||
useInfiniteQuery(
|
||||
orpc.dashboard.upcoming.infiniteOptions({
|
||||
orpc.library.upcoming.infiniteOptions({
|
||||
input: (pageParam: string | undefined) => ({
|
||||
days: 90,
|
||||
limit: 20,
|
||||
@@ -83,7 +83,7 @@ export default function UpcomingScreen() {
|
||||
);
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: orpc.dashboard.upcoming.key() });
|
||||
queryClient.invalidateQueries({ queryKey: orpc.library.upcoming.key() });
|
||||
}, []);
|
||||
|
||||
const onEndReached = useCallback(() => {
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function SearchScreen() {
|
||||
const debouncedQuery = useDebounce(query.trim(), 300);
|
||||
|
||||
const searchResults = useInfiniteQuery({
|
||||
...orpc.search.infiniteOptions({
|
||||
...orpc.discover.search.infiniteOptions({
|
||||
input:
|
||||
debouncedQuery.length > 0
|
||||
? (pageParam: number) => ({ query: debouncedQuery, page: pageParam })
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function SettingsScreen() {
|
||||
const isAdmin = session?.user?.role === "admin";
|
||||
const serverUrl = getServerUrl();
|
||||
|
||||
const authConfig = useQuery(orpc.system.authConfig.queryOptions());
|
||||
const publicInfo = useQuery(orpc.system.publicInfo.queryOptions());
|
||||
const { data: accounts } = useQuery({
|
||||
queryKey: ["auth", "listAccounts"],
|
||||
queryFn: async () => {
|
||||
@@ -100,7 +100,7 @@ export default function SettingsScreen() {
|
||||
});
|
||||
const hasPassword =
|
||||
accounts?.some((a: { providerId: string }) => a.providerId === "credential") ?? false;
|
||||
const showPasswordOption = hasPassword && !(authConfig.data?.passwordLoginDisabled ?? true);
|
||||
const showPasswordOption = hasPassword && !(publicInfo.data?.passwordLoginDisabled ?? true);
|
||||
|
||||
const systemHealth = useQuery({
|
||||
...orpc.admin.systemHealth.queryOptions(),
|
||||
@@ -162,40 +162,48 @@ export default function SettingsScreen() {
|
||||
|
||||
const hasAvatarImage = !!session?.user?.image;
|
||||
|
||||
const registration = useQuery({
|
||||
...orpc.admin.registration.queryOptions(),
|
||||
const adminSettings = useQuery({
|
||||
...orpc.admin.settings.get.queryOptions(),
|
||||
enabled: isAdmin,
|
||||
});
|
||||
|
||||
const toggleRegistration = useMutation(
|
||||
orpc.admin.toggleRegistration.mutationOptions({
|
||||
onSuccess: (_data, { open }) => {
|
||||
toast.success(open ? t`Registration opened` : t`Registration closed`);
|
||||
const updateAdminSettings = useMutation(
|
||||
orpc.admin.settings.update.mutationOptions({
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.admin.registration.key(),
|
||||
});
|
||||
},
|
||||
onError: () => toast.error(t`Failed to update registration setting`),
|
||||
}),
|
||||
);
|
||||
|
||||
const updateCheck = useQuery({
|
||||
...orpc.admin.updateCheck.queryOptions(),
|
||||
enabled: isAdmin,
|
||||
});
|
||||
|
||||
const toggleUpdateCheck = useMutation(
|
||||
orpc.admin.toggleUpdateCheck.mutationOptions({
|
||||
onSuccess: (_data, { enabled }) => {
|
||||
toast.success(enabled ? t`Update checks enabled` : t`Update checks disabled`);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.admin.updateCheck.key(),
|
||||
queryKey: orpc.admin.settings.key(),
|
||||
});
|
||||
},
|
||||
onError: () => toast.error(t`Failed to update setting`),
|
||||
}),
|
||||
);
|
||||
|
||||
const toggleRegistration = {
|
||||
mutate: ({ open }: { open: boolean }) => {
|
||||
updateAdminSettings.mutate(
|
||||
{ registration: { open } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(open ? t`Registration opened` : t`Registration closed`);
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const toggleUpdateCheck = {
|
||||
mutate: ({ enabled }: { enabled: boolean }) => {
|
||||
updateAdminSettings.mutate(
|
||||
{ updateCheck: { enabled } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(enabled ? t`Update checks enabled` : t`Update checks disabled`);
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const primaryFgColor = useCSSVariable("--color-primary-foreground") as string;
|
||||
const mutedFgColor = useCSSVariable("--color-muted-foreground") as string;
|
||||
|
||||
@@ -484,7 +492,7 @@ export default function SettingsScreen() {
|
||||
icon={IconUserPlus}
|
||||
right={
|
||||
<Switch
|
||||
value={registration.data?.open ?? false}
|
||||
value={adminSettings.data?.registration?.open ?? false}
|
||||
accessibilityLabel={t`Open registration`}
|
||||
onValueChange={(open) => toggleRegistration.mutate({ open })}
|
||||
/>
|
||||
@@ -495,15 +503,15 @@ export default function SettingsScreen() {
|
||||
icon={IconCloud}
|
||||
right={
|
||||
<Switch
|
||||
value={updateCheck.data?.enabled ?? false}
|
||||
value={adminSettings.data?.updateCheck?.enabled ?? false}
|
||||
accessibilityLabel={t`Check for updates`}
|
||||
onValueChange={(enabled) => toggleUpdateCheck.mutate({ enabled })}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{(() => {
|
||||
const latestVersion = updateCheck.data?.updateCheck?.latestVersion;
|
||||
return updateCheck.data?.updateCheck?.updateAvailable ? (
|
||||
const latestVersion = adminSettings.data?.updateCheck?.latestVersion;
|
||||
return adminSettings.data?.updateCheck?.updateAvailable ? (
|
||||
<View className="py-3.5">
|
||||
<Text className="text-status-completed font-sans text-sm font-medium">
|
||||
<Trans>Update available: {latestVersion}</Trans>
|
||||
@@ -567,8 +575,8 @@ export default function SettingsScreen() {
|
||||
Native
|
||||
{Application.nativeApplicationVersion ? ` v${Application.nativeApplicationVersion}` : ""}
|
||||
{Application.nativeBuildVersion ? ` (${Application.nativeBuildVersion})` : ""}
|
||||
{updateCheck.data?.updateCheck?.currentVersion
|
||||
? ` · Server v${updateCheck.data.updateCheck.currentVersion}`
|
||||
{adminSettings.data?.updateCheck?.currentVersion
|
||||
? ` · Server v${adminSettings.data.updateCheck.currentVersion}`
|
||||
: ""}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function PersonDetailScreen() {
|
||||
|
||||
const { data, isPending, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useInfiniteQuery(
|
||||
orpc.people.detail.infiniteOptions({
|
||||
orpc.people.get.infiniteOptions({
|
||||
input: (pageParam: number) => ({ id, page: pageParam, limit: 20 }),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) =>
|
||||
|
||||
@@ -104,9 +104,9 @@ export default function TitleDetailScreen() {
|
||||
"--color-title-accent-foreground",
|
||||
]) as [string, string, string];
|
||||
|
||||
const detail = useQuery(orpc.titles.detail.queryOptions({ input: { id } }));
|
||||
const userInfo = useQuery(orpc.titles.userInfo.queryOptions({ input: { id } }));
|
||||
const recommendations = useQuery(orpc.titles.recommendations.queryOptions({ input: { id } }));
|
||||
const detail = useQuery(orpc.titles.get.queryOptions({ input: { id } }));
|
||||
const userInfo = useQuery(orpc.tracking.userInfo.queryOptions({ input: { id } }));
|
||||
const recommendations = useQuery(orpc.titles.similar.queryOptions({ input: { id } }));
|
||||
|
||||
const {
|
||||
updateStatus,
|
||||
@@ -425,7 +425,7 @@ export default function TitleDetailScreen() {
|
||||
|
||||
{title.type === "movie" && (
|
||||
<Pressable
|
||||
onPress={() => watchMovie.mutate({ id })}
|
||||
onPress={() => watchMovie.mutate({ scope: "movie", ids: [id] })}
|
||||
disabled={watchMovie.isPending}
|
||||
className="bg-title-accent flex-row items-center gap-1.5 rounded-lg px-4 py-2"
|
||||
>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function UpcomingSection() {
|
||||
const { t } = useLingui();
|
||||
const { push } = useRouter();
|
||||
const { data, isPending } = useQuery(
|
||||
orpc.dashboard.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
|
||||
orpc.library.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
|
||||
);
|
||||
|
||||
if (isPending) return null;
|
||||
|
||||
@@ -48,7 +48,7 @@ export function FilterableTitleRow({
|
||||
const [selectedGenre, setSelectedGenre] = useState<number | null>(null);
|
||||
|
||||
const discover = useInfiniteQuery({
|
||||
...orpc.discover.infiniteOptions({
|
||||
...orpc.discover.browse.infiniteOptions({
|
||||
input:
|
||||
selectedGenre != null
|
||||
? (pageParam: number) => ({
|
||||
|
||||
@@ -81,30 +81,30 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
|
||||
const toggleSetup = useCallback(() => setSetupOpen((v) => !v), []);
|
||||
|
||||
const connectMutation = useMutation(
|
||||
orpc.integrations.create.mutationOptions({
|
||||
orpc.account.integrations.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
toast.success(t`${label} connected`);
|
||||
queryClient.invalidateQueries({ queryKey: orpc.integrations.key() });
|
||||
queryClient.invalidateQueries({ queryKey: orpc.account.integrations.key() });
|
||||
},
|
||||
onError: () => toast.error(t`Failed to connect ${label}`),
|
||||
}),
|
||||
);
|
||||
|
||||
const { mutate: deleteIntegration } = useMutation(
|
||||
orpc.integrations.delete.mutationOptions({
|
||||
orpc.account.integrations.delete.mutationOptions({
|
||||
onSuccess: () => {
|
||||
toast.success(t`${label} disconnected`);
|
||||
queryClient.invalidateQueries({ queryKey: orpc.integrations.key() });
|
||||
queryClient.invalidateQueries({ queryKey: orpc.account.integrations.key() });
|
||||
},
|
||||
onError: () => toast.error(t`Failed to disconnect ${label}`),
|
||||
}),
|
||||
);
|
||||
|
||||
const { mutate: regenerateToken, isPending: isRegenerating } = useMutation(
|
||||
orpc.integrations.regenerateToken.mutationOptions({
|
||||
orpc.account.integrations.regenerateToken.mutationOptions({
|
||||
onSuccess: () => {
|
||||
toast.success(t`${label} URL regenerated`);
|
||||
queryClient.invalidateQueries({ queryKey: orpc.integrations.key() });
|
||||
queryClient.invalidateQueries({ queryKey: orpc.account.integrations.key() });
|
||||
},
|
||||
onError: () => toast.error(t`Failed to regenerate ${label} URL`),
|
||||
}),
|
||||
|
||||
@@ -14,7 +14,7 @@ import { orpc } from "@/lib/orpc";
|
||||
export function IntegrationsSection() {
|
||||
const { t, i18n } = useLingui();
|
||||
const configs = getIntegrationConfigs(i18n);
|
||||
const integrations = useQuery(orpc.integrations.list.queryOptions());
|
||||
const integrations = useQuery(orpc.account.integrations.list.queryOptions());
|
||||
|
||||
return (
|
||||
<View className="mb-6">
|
||||
|
||||
@@ -76,13 +76,15 @@ export function SeasonAccordion({
|
||||
|
||||
const { watchEpisode, unwatchEpisode, watchSeason } = useTitleActions({
|
||||
toasts: {
|
||||
watchEpisode: ({ id: epId }) => {
|
||||
watchEpisode: ({ ids }) => {
|
||||
const epId = ids[0];
|
||||
const ep = episodes.find((e) => e.id === epId);
|
||||
const sNum = season.seasonNumber;
|
||||
const eNum = ep?.episodeNumber;
|
||||
return ep ? t`Watched S${sNum} E${eNum}` : t`Episode watched`;
|
||||
},
|
||||
unwatchEpisode: ({ id: epId }) => {
|
||||
unwatchEpisode: ({ ids }) => {
|
||||
const epId = ids[0];
|
||||
const ep = episodes.find((e) => e.id === epId);
|
||||
const sNum = season.seasonNumber;
|
||||
const eNum = ep?.episodeNumber;
|
||||
@@ -99,9 +101,9 @@ export function SeasonAccordion({
|
||||
const handleEpisodeToggle = useCallback(
|
||||
(episodeId: string) => {
|
||||
if (watchedEpisodeIds.has(episodeId)) {
|
||||
unwatchEpisode.mutate({ id: episodeId });
|
||||
unwatchEpisode.mutate({ scope: "episode", ids: [episodeId] });
|
||||
} else {
|
||||
watchEpisode.mutate({ id: episodeId });
|
||||
watchEpisode.mutate({ scope: "episode", ids: [episodeId] });
|
||||
}
|
||||
},
|
||||
[watchedEpisodeIds, unwatchEpisode, watchEpisode],
|
||||
@@ -156,7 +158,7 @@ export function SeasonAccordion({
|
||||
<Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)}>
|
||||
{watchedCount < episodes.length && (
|
||||
<Pressable
|
||||
onPress={() => watchSeason.mutate({ id: season.id })}
|
||||
onPress={() => watchSeason.mutate({ scope: "season", ids: [season.id] })}
|
||||
className="bg-secondary mx-4 mb-2 flex-row items-center justify-center rounded-lg py-2"
|
||||
>
|
||||
<Text className="text-title-accent font-sans text-xs font-medium">
|
||||
|
||||
@@ -18,15 +18,20 @@ function resolveToast<TInput>(
|
||||
return typeof override === "function" ? override(input) : override;
|
||||
}
|
||||
|
||||
interface WatchInput {
|
||||
scope: string;
|
||||
ids: string[];
|
||||
}
|
||||
|
||||
interface UseTitleActionsOptions {
|
||||
toasts?: {
|
||||
quickAdd?: ToastOverride<{ id: string }>;
|
||||
updateStatus?: ToastOverride<{ id: string; status: string | null }>;
|
||||
watchMovie?: ToastOverride<{ id: string }>;
|
||||
watchMovie?: ToastOverride<WatchInput>;
|
||||
updateRating?: ToastOverride<{ id: string; stars: number }>;
|
||||
watchEpisode?: ToastOverride<{ id: string }>;
|
||||
unwatchEpisode?: ToastOverride<{ id: string }>;
|
||||
watchSeason?: ToastOverride<{ id: string }>;
|
||||
watchEpisode?: ToastOverride<WatchInput>;
|
||||
unwatchEpisode?: ToastOverride<WatchInput>;
|
||||
watchSeason?: ToastOverride<WatchInput>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +44,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
const toastOverrides = options?.toasts;
|
||||
|
||||
const quickAdd = useMutation(
|
||||
orpc.titles.quickAdd.mutationOptions({
|
||||
orpc.tracking.quickAdd.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
toast.success(resolveToast(toastOverrides?.quickAdd, t`Added to watchlist`, input));
|
||||
invalidateTitleQueries();
|
||||
@@ -53,7 +58,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
);
|
||||
|
||||
const updateStatus = useMutation(
|
||||
orpc.titles.updateStatus.mutationOptions({
|
||||
orpc.tracking.updateStatus.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
const statusMessages: Record<string, string> = {
|
||||
watchlist: t`Added to watchlist`,
|
||||
@@ -69,7 +74,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
);
|
||||
|
||||
const watchMovie = useMutation(
|
||||
orpc.titles.watchMovie.mutationOptions({
|
||||
orpc.tracking.watch.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
toast.success(resolveToast(toastOverrides?.watchMovie, t`Marked as watched`, input));
|
||||
invalidateTitleQueries();
|
||||
@@ -79,7 +84,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
);
|
||||
|
||||
const updateRating = useMutation(
|
||||
orpc.titles.updateRating.mutationOptions({
|
||||
orpc.tracking.rate.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
const stars = input.stars;
|
||||
const defaultMsg =
|
||||
@@ -87,7 +92,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
? t`Rated ${plural(stars, { one: "# star", other: "# stars" })}`
|
||||
: t`Rating removed`;
|
||||
toast.success(resolveToast(toastOverrides?.updateRating, defaultMsg, input));
|
||||
// Rating only invalidates title queries, not dashboard
|
||||
// Rating only invalidates title queries, not tracking
|
||||
queryClient.invalidateQueries({ queryKey: orpc.titles.key() });
|
||||
},
|
||||
onError: () => toast.error(t`Failed to update rating`),
|
||||
@@ -95,7 +100,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
);
|
||||
|
||||
const watchEpisode = useMutation(
|
||||
orpc.episodes.watch.mutationOptions({
|
||||
orpc.tracking.watch.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
toast.success(resolveToast(toastOverrides?.watchEpisode, t`Episode watched`, input));
|
||||
invalidateTitleQueries();
|
||||
@@ -105,7 +110,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
);
|
||||
|
||||
const unwatchEpisode = useMutation(
|
||||
orpc.episodes.unwatch.mutationOptions({
|
||||
orpc.tracking.unwatch.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
toast.success(resolveToast(toastOverrides?.unwatchEpisode, t`Episode unwatched`, input));
|
||||
invalidateTitleQueries();
|
||||
@@ -115,7 +120,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
);
|
||||
|
||||
const watchSeason = useMutation(
|
||||
orpc.seasons.watch.mutationOptions({
|
||||
orpc.tracking.watch.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
toast.success(resolveToast(toastOverrides?.watchSeason, t`Season watched`, input));
|
||||
invalidateTitleQueries();
|
||||
|
||||
@@ -8,10 +8,11 @@ import { i18n } from "@sofa/i18n";
|
||||
|
||||
let widgetRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/** Invalidate title + dashboard queries. Used by most title mutations. */
|
||||
/** Invalidate title + tracking + library queries. Used by most title mutations. */
|
||||
export function invalidateTitleQueries() {
|
||||
queryClient.invalidateQueries({ queryKey: orpc.titles.key() });
|
||||
queryClient.invalidateQueries({ queryKey: orpc.dashboard.key() });
|
||||
queryClient.invalidateQueries({ queryKey: orpc.tracking.key() });
|
||||
queryClient.invalidateQueries({ queryKey: orpc.library.key() });
|
||||
|
||||
// Debounce widget refresh to batch rapid mutations (e.g. watching multiple episodes)
|
||||
if (widgetRefreshTimer) clearTimeout(widgetRefreshTimer);
|
||||
@@ -28,7 +29,7 @@ export function invalidateTitleQueries() {
|
||||
export const titleActions = {
|
||||
async quickAdd(id: string, titleName?: string) {
|
||||
try {
|
||||
await client.titles.quickAdd({ id });
|
||||
await client.tracking.quickAdd({ id });
|
||||
toast.success(
|
||||
titleName
|
||||
? i18n._(msg`Added "${titleName}" to watchlist`)
|
||||
@@ -44,7 +45,7 @@ export const titleActions = {
|
||||
|
||||
async markMovieWatched(id: string, titleName?: string) {
|
||||
try {
|
||||
await client.titles.watchMovie({ id });
|
||||
await client.tracking.watch({ scope: "movie", ids: [id] });
|
||||
toast.success(
|
||||
titleName ? i18n._(msg`Marked "${titleName}" as watched`) : i18n._(msg`Marked as watched`),
|
||||
);
|
||||
@@ -56,7 +57,7 @@ export const titleActions = {
|
||||
|
||||
async removeFromLibrary(id: string) {
|
||||
try {
|
||||
await client.titles.updateStatus({ id, status: null });
|
||||
await client.tracking.updateStatus({ id, status: null });
|
||||
toast.success(i18n._(msg`Removed from library`));
|
||||
invalidateTitleQueries();
|
||||
} catch {
|
||||
@@ -66,7 +67,7 @@ export const titleActions = {
|
||||
|
||||
async rate(id: string, stars: number) {
|
||||
try {
|
||||
await client.titles.updateRating({ id, stars });
|
||||
await client.tracking.rate({ id, stars });
|
||||
toast.success(
|
||||
stars > 0
|
||||
? i18n._(msg`Rated ${plural(stars, { one: "# star", other: "# stars" })}`)
|
||||
@@ -80,7 +81,7 @@ export const titleActions = {
|
||||
|
||||
async watchEpisode(id: string) {
|
||||
try {
|
||||
await client.episodes.watch({ id });
|
||||
await client.tracking.watch({ scope: "episode", ids: [id] });
|
||||
toast.success(i18n._(msg`Episode watched`));
|
||||
invalidateTitleQueries();
|
||||
} catch {
|
||||
@@ -90,7 +91,7 @@ export const titleActions = {
|
||||
|
||||
async unwatchEpisode(id: string) {
|
||||
try {
|
||||
await client.episodes.unwatch({ id });
|
||||
await client.tracking.unwatch({ scope: "episode", ids: [id] });
|
||||
toast.success(i18n._(msg`Episode unwatched`));
|
||||
invalidateTitleQueries();
|
||||
} catch {
|
||||
@@ -100,7 +101,7 @@ export const titleActions = {
|
||||
|
||||
async markAllWatched(id: string, titleName?: string) {
|
||||
try {
|
||||
await client.titles.watchAll({ id });
|
||||
await client.tracking.watch({ scope: "series", ids: [id] });
|
||||
toast.success(
|
||||
titleName
|
||||
? i18n._(msg`Marked all episodes of "${titleName}" as watched`)
|
||||
@@ -114,7 +115,7 @@ export const titleActions = {
|
||||
|
||||
async watchSeason(id: string, seasonLabel?: string) {
|
||||
try {
|
||||
await client.seasons.watch({ id });
|
||||
await client.tracking.watch({ scope: "season", ids: [id] });
|
||||
toast.success(
|
||||
seasonLabel ? i18n._(msg`Watched all of ${seasonLabel}`) : i18n._(msg`Season watched`),
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ vi.mock("@/lib/widget-assets", () => ({
|
||||
|
||||
vi.mock("@/lib/orpc", () => ({
|
||||
client: {
|
||||
dashboard: {
|
||||
library: {
|
||||
continueWatching,
|
||||
upcoming,
|
||||
},
|
||||
|
||||
@@ -140,7 +140,7 @@ async function refreshContinueWatching(
|
||||
iconFilePath: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { items } = await client.dashboard.continueWatching();
|
||||
const { items } = await client.library.continueWatching();
|
||||
|
||||
if (items.length === 0) {
|
||||
widget.updateSnapshot(
|
||||
@@ -200,7 +200,7 @@ async function refreshUpcoming(
|
||||
iconFilePath: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { items } = await client.dashboard.upcoming({
|
||||
const { items } = await client.library.upcoming({
|
||||
days: 30,
|
||||
limit: 5,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user