mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd8ae4ccf9 | ||
|
|
553fe98013 | ||
|
|
0c747a9275 | ||
|
|
bb3b7891d4
|
||
|
|
dce333e128 | ||
|
|
3906f6021d
|
||
|
|
213977d7f8 | ||
|
|
b0172a2b25
|
@@ -11,8 +11,8 @@
|
||||
<a href="https://crowdin.com/project/sofa"><img alt="Crowdin" src="https://badges.crowdin.net/sofa/localized.svg" /></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://apps.apple.com/us/app/sofa-tv-movie-tracker/id6760432427"><img alt="App Store" src="https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg" /></a><br />
|
||||
<sub>Play Store soon™</sub>
|
||||
<a href="https://apps.apple.com/us/app/sofa-tv-movie-tracker/id6760432427"><img alt="App Store" src="https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg" height="40" width="120" /></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.jakejarvis.sofa"><img alt="GetItOnGooglePlay_Badge_Web_color_English" src="https://github.com/user-attachments/assets/f04efab4-a129-4392-be95-90c97372231f" height="40" width="135" /></a>
|
||||
</p>
|
||||
|
||||
[Sofa](https://sofa.watch) is a self-hosted movie and TV tracker for nerds. Track what you've watched, discover what's next, and plug your data into your existing home media stack. All without anything leaving your homelab. 🍿
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sofa/native",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
@@ -33,7 +33,7 @@
|
||||
"@react-native-menu/menu": "2.0.0",
|
||||
"@react-navigation/elements": "2.9.11",
|
||||
"@react-navigation/native": "7.1.34",
|
||||
"@sentry/react-native": "8.5.0",
|
||||
"@sentry/react-native": "8.6.0",
|
||||
"@shopify/flash-list": "2.0.2",
|
||||
"@sofa/api": "workspace:*",
|
||||
"@sofa/i18n": "workspace:*",
|
||||
@@ -85,7 +85,7 @@
|
||||
"react-native-worklets": "0.7.2",
|
||||
"tailwind-merge": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"uniwind": "1.6.0",
|
||||
"uniwind": "1.6.1",
|
||||
"zeego": "3.0.6",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
|
||||
@@ -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,30 +51,30 @@ export default function DashboardScreen() {
|
||||
"--color-status-completed",
|
||||
]) as [string, string, string, string];
|
||||
|
||||
const stats = useQuery(orpc.dashboard.stats.queryOptions());
|
||||
const libraryStats = useQuery(orpc.library.stats.queryOptions());
|
||||
const movieHistory = useQuery(
|
||||
orpc.dashboard.watchHistory.queryOptions({
|
||||
orpc.tracking.stats.queryOptions({
|
||||
input: { type: "movie", period: moviePeriod },
|
||||
}),
|
||||
);
|
||||
const episodeHistory = useQuery(
|
||||
orpc.dashboard.watchHistory.queryOptions({
|
||||
orpc.tracking.stats.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 ||
|
||||
libraryStats.isRefetching ||
|
||||
continueWatching.isRefetching ||
|
||||
library.isRefetching ||
|
||||
movieHistory.isRefetching ||
|
||||
episodeHistory.isRefetching;
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: orpc.dashboard.key() });
|
||||
queryClient.invalidateQueries({ queryKey: orpc.tracking.key() });
|
||||
queryClient.invalidateQueries({ queryKey: orpc.library.key() });
|
||||
}, []);
|
||||
|
||||
@@ -82,8 +82,8 @@ export default function DashboardScreen() {
|
||||
const hasContinueWatching = (continueWatching.data?.items?.length ?? 0) > 0;
|
||||
const hasRecommendations = (recommendations.data?.items?.length ?? 0) > 0;
|
||||
|
||||
const movieCount = movieHistory.data?.count ?? stats.data?.moviesThisMonth;
|
||||
const episodeCount = episodeHistory.data?.count ?? stats.data?.episodesThisWeek;
|
||||
const movieCount = movieHistory.data?.count;
|
||||
const episodeCount = episodeHistory.data?.count;
|
||||
|
||||
const periodLabels: Record<TimePeriod, string> = {
|
||||
today: t`today`,
|
||||
@@ -144,7 +144,7 @@ export default function DashboardScreen() {
|
||||
<View className="flex-row gap-3">
|
||||
<StatsCard
|
||||
label={t`In Library`}
|
||||
value={stats.data?.librarySize}
|
||||
value={libraryStats.data?.size}
|
||||
icon={IconBooks}
|
||||
color="text-status-watchlist"
|
||||
tintColor={watchlistColor}
|
||||
@@ -152,7 +152,7 @@ export default function DashboardScreen() {
|
||||
/>
|
||||
<StatsCard
|
||||
label={t`Completed`}
|
||||
value={stats.data?.completed}
|
||||
value={libraryStats.data?.completed}
|
||||
icon={IconCheck}
|
||||
color="text-status-completed"
|
||||
tintColor={completedColor}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -246,9 +246,12 @@ export default function LibraryScreen() {
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const { quickAdd } = useTitleActions();
|
||||
const handleQuickAdd = useCallback((id: string) => quickAdd.mutate({ id }), [quickAdd]);
|
||||
const addingId = quickAdd.isPending ? (quickAdd.variables?.id ?? null) : null;
|
||||
const { updateStatus } = useTitleActions();
|
||||
const handleQuickAdd = useCallback(
|
||||
(id: string) => updateStatus.mutate({ id, status: "watchlist" }),
|
||||
[updateStatus],
|
||||
);
|
||||
const addingId = updateStatus.isPending ? (updateStatus.variables?.id ?? null) : null;
|
||||
|
||||
const allItems = useMemo(
|
||||
() => libraryQuery.data?.pages.flatMap((page) => page.items) ?? [],
|
||||
|
||||
@@ -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 })
|
||||
@@ -32,13 +32,13 @@ export default function SearchScreen() {
|
||||
}),
|
||||
});
|
||||
|
||||
const { quickAdd: quickAddMutation } = useTitleActions();
|
||||
const { updateStatus } = useTitleActions();
|
||||
|
||||
const handleQuickAdd = useCallback(
|
||||
(id: string) => {
|
||||
quickAddMutation.mutate({ id });
|
||||
updateStatus.mutate({ id, status: "watchlist" });
|
||||
},
|
||||
[quickAddMutation],
|
||||
[updateStatus],
|
||||
);
|
||||
|
||||
// Memoize mapped results to maintain stable references
|
||||
@@ -57,7 +57,7 @@ export default function SearchScreen() {
|
||||
[searchResults.data?.pages],
|
||||
);
|
||||
|
||||
const addingId = quickAddMutation.isPending ? (quickAddMutation.variables?.id ?? null) : null;
|
||||
const addingId = updateStatus.isPending ? (updateStatus.variables?.id ?? null) : null;
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: SearchResultItem }) => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -62,16 +62,16 @@ export default function PersonDetailScreen() {
|
||||
const mutedForeground = useCSSVariable("--color-muted-foreground") as string;
|
||||
const primaryColor = useCSSVariable("--color-primary") as string;
|
||||
|
||||
const { quickAdd } = useTitleActions();
|
||||
const { updateStatus } = useTitleActions();
|
||||
const handleQuickAdd = useCallback(
|
||||
(titleId: string) => quickAdd.mutate({ id: titleId }),
|
||||
[quickAdd],
|
||||
(titleId: string) => updateStatus.mutate({ id: titleId, status: "watchlist" }),
|
||||
[updateStatus],
|
||||
);
|
||||
const addingKey = quickAdd.isPending ? (quickAdd.variables?.id ?? null) : null;
|
||||
const addingKey = updateStatus.isPending ? (updateStatus.variables?.id ?? null) : null;
|
||||
|
||||
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) =>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
IconStarFilled,
|
||||
IconThumbUp,
|
||||
IconUsers,
|
||||
IconX,
|
||||
} from "@tabler/icons-react-native";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
@@ -98,27 +99,27 @@ export default function TitleDetailScreen() {
|
||||
const { back } = useRouter();
|
||||
const useAutomaticInsets = process.env.EXPO_OS === "ios";
|
||||
|
||||
const [titleAccent, mutedForeground, titleAccentForeground] = useCSSVariable([
|
||||
const [titleAccent, mutedForeground, titleAccentForeground, destructiveColor] = useCSSVariable([
|
||||
"--color-title-accent",
|
||||
"--color-muted-foreground",
|
||||
"--color-title-accent-foreground",
|
||||
]) as [string, string, string];
|
||||
"--color-destructive",
|
||||
]) as [string, 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,
|
||||
updateRating,
|
||||
watchMovie,
|
||||
quickAdd: quickAddMutation,
|
||||
} = useTitleActions({
|
||||
const { updateStatus, updateRating, watchMovie, unwatchMovie } = useTitleActions({
|
||||
toasts: {
|
||||
watchMovie: () => {
|
||||
const name = title?.title;
|
||||
return name ? t`Marked "${name}" as watched` : t`Marked as watched`;
|
||||
},
|
||||
unwatchMovie: () => {
|
||||
const name = title?.title;
|
||||
return name ? t`Marked "${name}" as unwatched` : t`Marked as unwatched`;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -152,6 +153,7 @@ export default function TitleDetailScreen() {
|
||||
() => new Set(userInfo.data?.episodeWatches ?? []),
|
||||
[userInfo.data?.episodeWatches],
|
||||
);
|
||||
const isMovieWatched = userInfo.data?.status === "completed";
|
||||
|
||||
const recItems = useMemo<PosterRowItem[]>(
|
||||
() =>
|
||||
@@ -413,24 +415,39 @@ export default function TitleDetailScreen() {
|
||||
currentStatus={userInfo.data?.status ?? null}
|
||||
onStatusChange={(status) => {
|
||||
if (status === "in_watchlist") {
|
||||
quickAddMutation.mutate({ id });
|
||||
updateStatus.mutate({ id, status: "watchlist" });
|
||||
} else {
|
||||
updateStatus.mutate({ id, status: null });
|
||||
}
|
||||
}}
|
||||
isPending={
|
||||
updateStatus.isPending || quickAddMutation.isPending || watchMovie.isPending
|
||||
}
|
||||
isPending={updateStatus.isPending || watchMovie.isPending || unwatchMovie.isPending}
|
||||
/>
|
||||
|
||||
{title.type === "movie" && (
|
||||
<Pressable
|
||||
onPress={() => watchMovie.mutate({ id })}
|
||||
disabled={watchMovie.isPending}
|
||||
className="bg-title-accent flex-row items-center gap-1.5 rounded-lg px-4 py-2"
|
||||
onPress={() => {
|
||||
if (isMovieWatched) {
|
||||
unwatchMovie.mutate({ scope: "movie", ids: [id] });
|
||||
} else {
|
||||
watchMovie.mutate({ scope: "movie", ids: [id] });
|
||||
}
|
||||
}}
|
||||
disabled={watchMovie.isPending || unwatchMovie.isPending}
|
||||
className={
|
||||
isMovieWatched
|
||||
? "border-destructive/20 bg-destructive/10 flex-row items-center gap-1.5 rounded-lg border px-4 py-2"
|
||||
: "bg-title-accent flex-row items-center gap-1.5 rounded-lg px-4 py-2"
|
||||
}
|
||||
>
|
||||
{watchMovie.isPending ? (
|
||||
{watchMovie.isPending || unwatchMovie.isPending ? (
|
||||
<Spinner size="sm" />
|
||||
) : isMovieWatched ? (
|
||||
<>
|
||||
<ScaledIcon icon={IconX} size={16} color={destructiveColor} />
|
||||
<Text className="text-destructive font-sans text-sm font-medium">
|
||||
<Trans>Mark Unwatched</Trans>
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ScaledIcon icon={IconCheck} size={16} color={titleAccentForeground} />
|
||||
@@ -471,30 +488,76 @@ export default function TitleDetailScreen() {
|
||||
iconColor={titleAccent}
|
||||
/>
|
||||
</View>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={titleAvailabilityContentStyle}
|
||||
>
|
||||
{availability.map((offer) => (
|
||||
<View key={`${offer.platformId}-${offer.offerType}`} className="items-center">
|
||||
{offer.logoPath && (
|
||||
<Image
|
||||
source={{ uri: offer.logoPath }}
|
||||
style={titleDetailStyles.providerLogo}
|
||||
contentFit="cover"
|
||||
/>
|
||||
{(() => {
|
||||
const stream = availability.filter((o) => o.offerType === "stream");
|
||||
const purchase = availability.filter((o) => o.offerType === "purchase");
|
||||
return (
|
||||
<View className="gap-3">
|
||||
{stream.length > 0 && (
|
||||
<View>
|
||||
<Text className="text-muted-foreground/60 mb-1.5 px-4 text-[10px] font-medium tracking-wider uppercase">
|
||||
{t`Stream`}
|
||||
</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={titleAvailabilityContentStyle}
|
||||
>
|
||||
{stream.map((offer) => (
|
||||
<View key={offer.platformId} className="items-center">
|
||||
{offer.logoPath && (
|
||||
<Image
|
||||
source={{ uri: offer.logoPath }}
|
||||
style={titleDetailStyles.providerLogo}
|
||||
contentFit="cover"
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
maxFontSizeMultiplier={1.0}
|
||||
className="text-muted-foreground mt-1 max-w-[60px] text-center text-xs"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{offer.providerName}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
{purchase.length > 0 && (
|
||||
<View>
|
||||
<Text className="text-muted-foreground/60 mb-1.5 px-4 text-[10px] font-medium tracking-wider uppercase">
|
||||
{t`Buy or Rent`}
|
||||
</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={titleAvailabilityContentStyle}
|
||||
>
|
||||
{purchase.map((offer) => (
|
||||
<View key={offer.platformId} className="items-center">
|
||||
{offer.logoPath && (
|
||||
<Image
|
||||
source={{ uri: offer.logoPath }}
|
||||
style={titleDetailStyles.providerLogo}
|
||||
contentFit="cover"
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
maxFontSizeMultiplier={1.0}
|
||||
className="text-muted-foreground mt-1 max-w-[60px] text-center text-xs"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{offer.providerName}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
<Text
|
||||
maxFontSizeMultiplier={1.0}
|
||||
className="text-muted-foreground mt-1 max-w-[60px] text-center text-xs"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{offer.providerName}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
})()}
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
|
||||
@@ -29,9 +29,12 @@ export function HorizontalPosterRow({
|
||||
items: PosterRowItem[];
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const { quickAdd } = useTitleActions();
|
||||
const handleQuickAdd = useCallback((id: string) => quickAdd.mutate({ id }), [quickAdd]);
|
||||
const addingKey = quickAdd.isPending ? (quickAdd.variables?.id ?? null) : null;
|
||||
const { updateStatus } = useTitleActions();
|
||||
const handleQuickAdd = useCallback(
|
||||
(id: string) => updateStatus.mutate({ id, status: "watchlist" }),
|
||||
[updateStatus],
|
||||
);
|
||||
const addingKey = updateStatus.isPending ? (updateStatus.variables?.id ?? null) : null;
|
||||
const keyExtractor = useCallback((item: PosterRowItem) => item.id, []);
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: PosterRowItem }) => (
|
||||
|
||||
@@ -126,7 +126,14 @@ export function UpcomingRow({ item }: { item: UpcomingItem }) {
|
||||
<Link.Trigger withAppleZoom>{rowContent}</Link.Trigger>
|
||||
<Link.Preview />
|
||||
<Link.Menu>
|
||||
{item.titleType === "movie" && (
|
||||
{item.titleType === "movie" && item.userStatus === "completed" && (
|
||||
<Link.MenuAction
|
||||
title={t`Mark Unwatched`}
|
||||
icon="xmark.circle"
|
||||
onPress={() => titleActions.unwatchMovie(item.titleId, item.titleName)}
|
||||
/>
|
||||
)}
|
||||
{item.titleType === "movie" && item.userStatus !== "completed" && (
|
||||
<Link.MenuAction
|
||||
title={t`Mark Watched`}
|
||||
icon="checkmark.circle"
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -131,7 +131,7 @@ export function HeroBanner({ item }: { item: HeroBannerItem }) {
|
||||
<Link.MenuAction
|
||||
title={t`Add to Watchlist`}
|
||||
icon="bookmark"
|
||||
onPress={() => titleActions.quickAdd(item.id, item.title)}
|
||||
onPress={() => titleActions.addToWatchlist(item.id, item.title)}
|
||||
/>
|
||||
</Link.Menu>
|
||||
</Link>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -217,7 +217,14 @@ export const PosterCard = memo(function PosterCard({
|
||||
onPress={handleQuickAddPress}
|
||||
/>
|
||||
)}
|
||||
{type === "movie" && (
|
||||
{type === "movie" && userStatus === "completed" && (
|
||||
<Link.MenuAction
|
||||
title={t`Mark Unwatched`}
|
||||
icon="xmark.circle"
|
||||
onPress={() => titleActions.unwatchMovie(id, title)}
|
||||
/>
|
||||
)}
|
||||
{type === "movie" && userStatus !== "completed" && (
|
||||
<Link.MenuAction
|
||||
title={t`Mark Watched`}
|
||||
icon="checkmark.circle"
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useLingui } from "@lingui/react/macro";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { orpc } from "@/lib/orpc";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { invalidateTitleQueries } from "@/lib/title-actions";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
@@ -18,15 +17,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>;
|
||||
unwatchMovie?: 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>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,22 +42,8 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
const { t } = useLingui();
|
||||
const toastOverrides = options?.toasts;
|
||||
|
||||
const quickAdd = useMutation(
|
||||
orpc.titles.quickAdd.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
toast.success(resolveToast(toastOverrides?.quickAdd, t`Added to watchlist`, input));
|
||||
invalidateTitleQueries();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t`Failed to add to watchlist`);
|
||||
// Refetch so optimistic local status reverts on failure
|
||||
queryClient.invalidateQueries({ queryKey: orpc.titles.key() });
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
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 +59,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();
|
||||
@@ -78,8 +68,18 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
}),
|
||||
);
|
||||
|
||||
const unwatchMovie = useMutation(
|
||||
orpc.tracking.unwatch.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
toast.success(resolveToast(toastOverrides?.unwatchMovie, t`Marked as unwatched`, input));
|
||||
invalidateTitleQueries();
|
||||
},
|
||||
onError: () => toast.error(t`Failed to mark as unwatched`),
|
||||
}),
|
||||
);
|
||||
|
||||
const updateRating = useMutation(
|
||||
orpc.titles.updateRating.mutationOptions({
|
||||
orpc.tracking.rate.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
const stars = input.stars;
|
||||
const defaultMsg =
|
||||
@@ -87,15 +87,14 @@ 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
|
||||
queryClient.invalidateQueries({ queryKey: orpc.titles.key() });
|
||||
invalidateTitleQueries();
|
||||
},
|
||||
onError: () => toast.error(t`Failed to update rating`),
|
||||
}),
|
||||
);
|
||||
|
||||
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 +104,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 +114,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();
|
||||
@@ -125,9 +124,9 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
);
|
||||
|
||||
return {
|
||||
quickAdd,
|
||||
updateStatus,
|
||||
watchMovie,
|
||||
unwatchMovie,
|
||||
updateRating,
|
||||
watchEpisode,
|
||||
unwatchEpisode,
|
||||
|
||||
@@ -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);
|
||||
@@ -26,9 +27,9 @@ export function invalidateTitleQueries() {
|
||||
* Each method calls the RPC, shows a toast, and invalidates the relevant queries.
|
||||
*/
|
||||
export const titleActions = {
|
||||
async quickAdd(id: string, titleName?: string) {
|
||||
async addToWatchlist(id: string, titleName?: string) {
|
||||
try {
|
||||
await client.titles.quickAdd({ id });
|
||||
await client.tracking.updateStatus({ id, status: "watchlist" });
|
||||
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`),
|
||||
);
|
||||
@@ -54,9 +55,23 @@ export const titleActions = {
|
||||
}
|
||||
},
|
||||
|
||||
async unwatchMovie(id: string, titleName?: string) {
|
||||
try {
|
||||
await client.tracking.unwatch({ scope: "movie", ids: [id] });
|
||||
toast.success(
|
||||
titleName
|
||||
? i18n._(msg`Marked "${titleName}" as unwatched`)
|
||||
: i18n._(msg`Marked as unwatched`),
|
||||
);
|
||||
invalidateTitleQueries();
|
||||
} catch {
|
||||
toast.error(i18n._(msg`Failed to mark as unwatched`));
|
||||
}
|
||||
},
|
||||
|
||||
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,13 +81,13 @@ 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" })}`)
|
||||
: i18n._(msg`Rating removed`),
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: orpc.titles.key() });
|
||||
invalidateTitleQueries();
|
||||
} catch {
|
||||
toast.error(i18n._(msg`Failed to update rating`));
|
||||
}
|
||||
@@ -80,7 +95,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 +105,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 +115,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 +129,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,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sofa/public-api",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sofa/server",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -58,14 +58,14 @@ function schedule(name: string, cron: string, handler: () => Promise<void>) {
|
||||
jobs.set(
|
||||
name,
|
||||
new Cron(cron, { name, protect: true }, async () => {
|
||||
log.info(`Running job: ${name}`);
|
||||
log.debug(`Running job: ${name}`);
|
||||
const startMs = performance.now();
|
||||
const run = startCronRun(name);
|
||||
try {
|
||||
await handler();
|
||||
const durationMs = Math.round(performance.now() - startMs);
|
||||
completeCronRun(run.id, durationMs);
|
||||
log.info(`Completed job: ${name} (${durationMs}ms)`);
|
||||
log.debug(`Completed job: ${name} (${durationMs}ms)`);
|
||||
} catch (err) {
|
||||
const durationMs = Math.round(performance.now() - startMs);
|
||||
failCronRun(run.id, durationMs, err);
|
||||
@@ -285,7 +285,7 @@ export function rescheduleBackup() {
|
||||
jobs.delete("scheduledBackup");
|
||||
}
|
||||
const cron = getBackupCronFromSettings();
|
||||
log.info(`Rescheduling backup job with cron: ${cron}`);
|
||||
log.debug(`Rescheduling backup job with cron: ${cron}`);
|
||||
schedule("scheduledBackup", cron, scheduledBackupJob);
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ export function startJobs() {
|
||||
optimizeDatabase();
|
||||
});
|
||||
|
||||
log.info(`Started ${jobs.size} jobs`);
|
||||
log.debug(`Registered ${jobs.size} scheduled jobs`);
|
||||
}
|
||||
|
||||
export function pauseJobs() {
|
||||
|
||||
@@ -43,7 +43,7 @@ seedPlatforms();
|
||||
// Recover import jobs left in running/pending state from a previous crash
|
||||
const recoveredJobs = recoverStaleImportJobs();
|
||||
if (recoveredJobs > 0) {
|
||||
log.info(`Recovered ${recoveredJobs} stale import job(s) from previous shutdown`);
|
||||
log.warn(`Recovered ${recoveredJobs} stale import job(s) from previous shutdown`);
|
||||
}
|
||||
|
||||
// Wire up job schedule provider for system health
|
||||
@@ -157,9 +157,17 @@ process.on("SIGTERM", shutdown);
|
||||
process.on("SIGINT", shutdown);
|
||||
|
||||
const port = Number(process.env.PORT || process.env.API_PORT || 3001);
|
||||
log.info(`API server listening on port ${port}`);
|
||||
|
||||
export default {
|
||||
const server = Bun.serve({
|
||||
port,
|
||||
fetch: app.fetch,
|
||||
};
|
||||
});
|
||||
|
||||
log.info(
|
||||
`🍿 Sofa${process.env.APP_VERSION ? ` v${process.env.APP_VERSION}` : ""}: Now playing at ${server.url.origin}`,
|
||||
{
|
||||
address: server.hostname,
|
||||
port: server.port,
|
||||
},
|
||||
);
|
||||
|
||||
export default server;
|
||||
|
||||
@@ -21,21 +21,15 @@ import { implementedRouter } from "./router";
|
||||
|
||||
export const schemaConverters = [new ZodToJsonSchemaConverter()];
|
||||
export const openApiTags = [
|
||||
{ name: "Titles", description: "Movie and TV show management" },
|
||||
{ name: "Episodes", description: "Episode watch tracking" },
|
||||
{ name: "Seasons", description: "Season watch tracking" },
|
||||
{ name: "Titles", description: "Movie and TV show metadata" },
|
||||
{ name: "Tracking", description: "Watch tracking, ratings, and status management" },
|
||||
{ name: "Library", description: "User library browsing and feeds" },
|
||||
{ name: "Discover", description: "Search, trending, and content discovery" },
|
||||
{ name: "People", description: "Cast and crew information" },
|
||||
{ name: "Dashboard", description: "User dashboard data" },
|
||||
{ name: "Explore", description: "Discover trending and popular content" },
|
||||
{ name: "Search", description: "Search for movies and TV shows" },
|
||||
{
|
||||
name: "Discover",
|
||||
description: "Advanced content discovery with filters",
|
||||
},
|
||||
{ name: "Account", description: "User account and integrations" },
|
||||
{ name: "System", description: "Server status and configuration" },
|
||||
{ name: "Integrations", description: "Media server integrations" },
|
||||
{ name: "Admin", description: "Server administration" },
|
||||
{ name: "Account", description: "User account management" },
|
||||
{ name: "Imports", description: "Data import from external services" },
|
||||
] as const;
|
||||
|
||||
const generator = new OpenAPIGenerator({
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { mkdir, rename } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { ORPCError } from "@orpc/server";
|
||||
|
||||
import { AppErrorCode } from "@sofa/api/errors";
|
||||
import { auth } from "@sofa/auth/server";
|
||||
import { AVATAR_DIR } from "@sofa/config";
|
||||
import {
|
||||
createOrUpdateIntegration,
|
||||
deleteIntegration as coreDeleteIntegration,
|
||||
listUserIntegrations,
|
||||
regenerateToken as coreRegenerateToken,
|
||||
serializeIntegration,
|
||||
} from "@sofa/core/integrations";
|
||||
import { getUserPlatformIdList, updateUserPlatforms } from "@sofa/core/platforms";
|
||||
|
||||
import { os } from "../context";
|
||||
@@ -27,7 +37,6 @@ export const uploadAvatar = os.account.uploadAvatar
|
||||
.handler(async ({ input: file, context }) => {
|
||||
await mkdir(AVATAR_DIR, { recursive: true });
|
||||
|
||||
// Write new avatar first (atomic: temp file + rename)
|
||||
const ext = MIME_TO_EXT[file.type] || "jpg";
|
||||
const filename = `${context.user.id}.${ext}`;
|
||||
const filePath = path.join(AVATAR_DIR, filename);
|
||||
@@ -35,7 +44,6 @@ export const uploadAvatar = os.account.uploadAvatar
|
||||
await Bun.write(tmpPath, file);
|
||||
await rename(tmpPath, filePath);
|
||||
|
||||
// Remove any previous avatar with a different extension
|
||||
const glob = new Bun.Glob(`${context.user.id}.*`);
|
||||
const existing = await Array.fromAsync(glob.scan(AVATAR_DIR));
|
||||
for (const match of existing) {
|
||||
@@ -44,7 +52,6 @@ export const uploadAvatar = os.account.uploadAvatar
|
||||
}
|
||||
}
|
||||
|
||||
// Update user via Better Auth
|
||||
const imageUrl = `/api/avatars/${context.user.id}?v=${Date.now()}`;
|
||||
await auth.api.updateUser({
|
||||
body: { image: imageUrl },
|
||||
@@ -75,3 +82,36 @@ export const updatePlatformsHandler = os.account.updatePlatforms
|
||||
.handler(async ({ input, context }) => {
|
||||
updateUserPlatforms(context.user.id, input.platformIds);
|
||||
});
|
||||
|
||||
// ─── Integrations ─────────────────────────────────────────────
|
||||
|
||||
export const integrationsList = os.account.integrations.list.use(authed).handler(({ context }) => {
|
||||
return listUserIntegrations(context.user.id);
|
||||
});
|
||||
|
||||
export const integrationsCreate = os.account.integrations.create
|
||||
.use(authed)
|
||||
.handler(({ input, context }) => {
|
||||
return createOrUpdateIntegration(context.user.id, input.provider, input.enabled);
|
||||
});
|
||||
|
||||
export const integrationsDelete = os.account.integrations.delete
|
||||
.use(authed)
|
||||
.handler(({ input, context }) => {
|
||||
coreDeleteIntegration(context.user.id, input.provider);
|
||||
});
|
||||
|
||||
export const integrationsRegenerateToken = os.account.integrations.regenerateToken
|
||||
.use(authed)
|
||||
.handler(({ input, context }) => {
|
||||
const row = coreRegenerateToken(context.user.id, input.provider);
|
||||
|
||||
if (!row) {
|
||||
throw new ORPCError("NOT_FOUND", {
|
||||
message: "Integration not found",
|
||||
data: { code: AppErrorCode.INTEGRATION_NOT_FOUND },
|
||||
});
|
||||
}
|
||||
|
||||
return serializeIntegration(row);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,44 @@ import { pauseJobs, rescheduleBackup, resumeJobs, triggerJob as triggerCronJob }
|
||||
import { os } from "../context";
|
||||
import { admin } from "../middleware";
|
||||
|
||||
// ─── Backups ───────────────────────────────────────────────────
|
||||
// ─── Settings (consolidated) ──────────────────────────────────
|
||||
|
||||
export const settingsGet = os.admin.settings.get.use(admin).handler(() => {
|
||||
const updateCheckEnabled = isUpdateCheckEnabled();
|
||||
const check = updateCheckEnabled ? getCachedUpdateCheck() : null;
|
||||
|
||||
return {
|
||||
registration: {
|
||||
open: getSetting("registrationOpen") === "true",
|
||||
},
|
||||
updateCheck: {
|
||||
enabled: updateCheckEnabled,
|
||||
updateAvailable: check?.updateAvailable ?? null,
|
||||
currentVersion: check?.currentVersion ?? null,
|
||||
latestVersion: check?.latestVersion ?? null,
|
||||
releaseUrl: check?.releaseUrl ?? null,
|
||||
lastCheckedAt: check?.lastCheckedAt ?? null,
|
||||
},
|
||||
telemetry: {
|
||||
enabled: isTelemetryEnabled(),
|
||||
lastReportedAt: getSetting("telemetryLastReportedAt") ?? null,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const settingsUpdate = os.admin.settings.update.use(admin).handler(({ input }) => {
|
||||
if (input.registration) {
|
||||
setSetting("registrationOpen", String(input.registration.open));
|
||||
}
|
||||
if (input.updateCheck) {
|
||||
setSetting("updateCheckEnabled", String(input.updateCheck.enabled));
|
||||
}
|
||||
if (input.telemetry) {
|
||||
setSetting("telemetryEnabled", String(input.telemetry.enabled));
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Backups ──────────────────────────────────────────────────
|
||||
|
||||
export const backupsList = os.admin.backups.list.use(admin).handler(async () => {
|
||||
const backups = await listBackups();
|
||||
@@ -42,7 +79,6 @@ export const backupsDelete = os.admin.backups.delete.use(admin).handler(async ({
|
||||
export const backupsRestore = os.admin.backups.restore
|
||||
.use(admin)
|
||||
.handler(async ({ input: file }) => {
|
||||
// Stream upload to disk to avoid buffering the entire file in memory
|
||||
await ensureBackupDir();
|
||||
const tmpPath = path.join(BACKUP_DIR, `.upload-${Date.now()}-${crypto.randomUUID()}.db`);
|
||||
pauseJobs();
|
||||
@@ -50,7 +86,6 @@ export const backupsRestore = os.admin.backups.restore
|
||||
await Bun.write(tmpPath, file);
|
||||
await restoreFromBackup(tmpPath);
|
||||
} catch (err) {
|
||||
// Clean up the upload file if restoreFromBackup didn't consume it
|
||||
const f = Bun.file(tmpPath);
|
||||
if (await f.exists()) await f.delete();
|
||||
if (err instanceof ORPCError) throw err;
|
||||
@@ -89,42 +124,7 @@ export const backupsUpdateSchedule = os.admin.backups.updateSchedule
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Registration ──────────────────────────────────────────────
|
||||
|
||||
export const registration = os.admin.registration.use(admin).handler(() => {
|
||||
return { open: getSetting("registrationOpen") === "true" };
|
||||
});
|
||||
|
||||
export const toggleRegistration = os.admin.toggleRegistration.use(admin).handler(({ input }) => {
|
||||
setSetting("registrationOpen", String(input.open));
|
||||
});
|
||||
|
||||
// ─── Update Check ──────────────────────────────────────────────
|
||||
|
||||
export const updateCheck = os.admin.updateCheck.use(admin).handler(() => {
|
||||
const enabled = isUpdateCheckEnabled();
|
||||
const check = enabled ? getCachedUpdateCheck() : null;
|
||||
return { enabled, updateCheck: check };
|
||||
});
|
||||
|
||||
export const toggleUpdateCheck = os.admin.toggleUpdateCheck.use(admin).handler(({ input }) => {
|
||||
setSetting("updateCheckEnabled", String(input.enabled));
|
||||
});
|
||||
|
||||
// ─── Telemetry ────────────────────────────────────────────────
|
||||
|
||||
export const telemetry = os.admin.telemetry.use(admin).handler(() => {
|
||||
return {
|
||||
enabled: isTelemetryEnabled(),
|
||||
lastReportedAt: getSetting("telemetryLastReportedAt"),
|
||||
};
|
||||
});
|
||||
|
||||
export const toggleTelemetry = os.admin.toggleTelemetry.use(admin).handler(({ input }) => {
|
||||
setSetting("telemetryEnabled", String(input.enabled));
|
||||
});
|
||||
|
||||
// ─── Jobs ──────────────────────────────────────────────────────
|
||||
// ─── Jobs ─────────────────────────────────────────────────────
|
||||
|
||||
export const triggerJob = os.admin.triggerJob.use(admin).handler(async ({ input }) => {
|
||||
const triggered = await triggerCronJob(input.name);
|
||||
@@ -147,7 +147,7 @@ export const purgeImageCache = os.admin.purgeImageCache
|
||||
.use(admin)
|
||||
.handler(async () => purgeImagesFn());
|
||||
|
||||
// ─── System Health ───────────────────────────────────────────────
|
||||
// ─── System Health ────────────────────────────────────────────
|
||||
|
||||
export const systemHealth = os.admin.systemHealth.use(admin).handler(async () => {
|
||||
return await getSystemHealth();
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import {
|
||||
getContinueWatchingFeed,
|
||||
getRecommendationsFeed,
|
||||
getUpcomingFeed,
|
||||
getUserStats,
|
||||
getWatchCount,
|
||||
getWatchHistory,
|
||||
} from "@sofa/core/discovery";
|
||||
import { tmdbImageUrl } from "@sofa/tmdb/image";
|
||||
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
const watchHistoryTypeMap = { movie: "movies", episode: "episodes" } as const;
|
||||
|
||||
export const stats = os.dashboard.stats.use(authed).handler(({ context }) => {
|
||||
return getUserStats(context.user.id);
|
||||
});
|
||||
|
||||
export const continueWatching = os.dashboard.continueWatching.use(authed).handler(({ context }) => {
|
||||
const feed = getContinueWatchingFeed(context.user.id);
|
||||
const items = feed.map((item) => ({
|
||||
title: {
|
||||
id: item.title.id,
|
||||
title: item.title.title,
|
||||
backdropPath: tmdbImageUrl(item.title.backdropPath, "backdrops"),
|
||||
backdropThumbHash: item.title.backdropThumbHash,
|
||||
},
|
||||
nextEpisode: item.nextEpisode
|
||||
? {
|
||||
seasonNumber: item.nextEpisode.seasonNumber,
|
||||
episodeNumber: item.nextEpisode.episodeNumber,
|
||||
name: item.nextEpisode.name,
|
||||
stillPath: tmdbImageUrl(item.nextEpisode.stillPath, "stills"),
|
||||
stillThumbHash: item.nextEpisode.stillThumbHash,
|
||||
}
|
||||
: null,
|
||||
totalEpisodes: item.totalEpisodes,
|
||||
watchedEpisodes: item.watchedEpisodes,
|
||||
}));
|
||||
return { items };
|
||||
});
|
||||
|
||||
export const recommendations = os.dashboard.recommendations.use(authed).handler(({ context }) => {
|
||||
const feed = getRecommendationsFeed(context.user.id);
|
||||
const items = feed
|
||||
.filter((t): t is NonNullable<typeof t> => t != null)
|
||||
.slice(0, 10)
|
||||
.map((t) => ({
|
||||
id: t.id,
|
||||
tmdbId: t.tmdbId,
|
||||
type: t.type,
|
||||
title: t.title,
|
||||
posterPath: tmdbImageUrl(t.posterPath, "posters"),
|
||||
posterThumbHash: t.posterThumbHash ?? null,
|
||||
releaseDate: t.releaseDate ?? null,
|
||||
firstAirDate: t.firstAirDate ?? null,
|
||||
voteAverage: t.voteAverage,
|
||||
}));
|
||||
return { items };
|
||||
});
|
||||
|
||||
export const upcoming = os.dashboard.upcoming.use(authed).handler(({ input, context }) => {
|
||||
const result = getUpcomingFeed(context.user.id, {
|
||||
days: input.days,
|
||||
limit: input.limit,
|
||||
cursor: input.cursor,
|
||||
mediaType: input.mediaType,
|
||||
statusFilter: input.statusFilter,
|
||||
});
|
||||
return {
|
||||
items: result.items.map((item) => ({
|
||||
...item,
|
||||
posterPath: tmdbImageUrl(item.posterPath, "posters"),
|
||||
backdropPath: tmdbImageUrl(item.backdropPath, "backdrops"),
|
||||
streamingProvider: item.streamingProvider
|
||||
? {
|
||||
...item.streamingProvider,
|
||||
logoPath: tmdbImageUrl(item.streamingProvider.logoPath, "logos"),
|
||||
}
|
||||
: null,
|
||||
})),
|
||||
nextCursor: result.nextCursor,
|
||||
};
|
||||
});
|
||||
|
||||
export const watchHistory = os.dashboard.watchHistory.use(authed).handler(({ input, context }) => {
|
||||
const coreType = watchHistoryTypeMap[input.type];
|
||||
const count = getWatchCount(context.user.id, coreType, input.period);
|
||||
const history = getWatchHistory(context.user.id, coreType, input.period);
|
||||
return { count, history };
|
||||
});
|
||||
@@ -2,22 +2,320 @@ import { ORPCError } from "@orpc/server";
|
||||
|
||||
import { AppErrorCode } from "@sofa/api/errors";
|
||||
import { WATCH_REGION } from "@sofa/config";
|
||||
import { getRecommendationsFeed } from "@sofa/core/discovery";
|
||||
import { ensureBrowseTitlesExist } from "@sofa/core/metadata";
|
||||
import { getEpisodeProgressByTitleIds, getDisplayStatusesByTitleIds } from "@sofa/core/tracking";
|
||||
import { discover as discoverTmdb } from "@sofa/tmdb/client";
|
||||
import { ensureBrowsePersonsExist } from "@sofa/core/person";
|
||||
import { getPlatformTmdbIdMap, getPlatformTmdbIds, listPlatforms } from "@sofa/core/platforms";
|
||||
import { getDisplayStatusesByTitleIds, getEpisodeProgressByTitleIds } from "@sofa/core/tracking";
|
||||
import {
|
||||
discover as discoverTmdb,
|
||||
getGenres,
|
||||
getPopular,
|
||||
getTrending,
|
||||
searchMovies,
|
||||
searchMulti,
|
||||
searchPerson,
|
||||
searchTv,
|
||||
} from "@sofa/tmdb/client";
|
||||
import { isTmdbConfigured } from "@sofa/tmdb/config";
|
||||
import { tmdbImageUrl } from "@sofa/tmdb/image";
|
||||
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
export const discover = os.discover.use(authed).handler(async ({ input, context }) => {
|
||||
function requireTmdb() {
|
||||
if (!isTmdbConfigured()) {
|
||||
throw new ORPCError("PRECONDITION_FAILED", {
|
||||
message: "TMDB API key is not configured",
|
||||
data: { code: AppErrorCode.TMDB_NOT_CONFIGURED },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Trending ─────────────────────────────────────────────────
|
||||
|
||||
export const trending = os.discover.trending.use(authed).handler(async ({ input, context }) => {
|
||||
requireTmdb();
|
||||
|
||||
const data = await getTrending(input.type, "day", input.page);
|
||||
const results = (data.results ?? []) as Record<string, unknown>[];
|
||||
|
||||
const baseItems = results
|
||||
.filter((r) => r.poster_path)
|
||||
.map((r) => {
|
||||
const mediaType = r.media_type === "movie" || r.media_type === "tv" ? r.media_type : "movie";
|
||||
return {
|
||||
tmdbId: r.id as number,
|
||||
type: mediaType as "movie" | "tv",
|
||||
title: ((r.title ?? r.name) as string) || "",
|
||||
posterPath: (r.poster_path as string) ?? null,
|
||||
releaseDate: (r.release_date as string | undefined) ?? null,
|
||||
firstAirDate: (r.first_air_date as string | undefined) ?? null,
|
||||
voteAverage: (r.vote_average as number | undefined) ?? null,
|
||||
};
|
||||
});
|
||||
const heroResult = results.find(
|
||||
(r) => r.backdrop_path && (r.media_type === "movie" || r.media_type === "tv"),
|
||||
);
|
||||
|
||||
const allBrowseItems = [
|
||||
...baseItems,
|
||||
...(heroResult
|
||||
? [
|
||||
{
|
||||
tmdbId: heroResult.id as number,
|
||||
type: heroResult.media_type as "movie" | "tv",
|
||||
title: ((heroResult.title ?? heroResult.name) as string | undefined) ?? "",
|
||||
posterPath: (heroResult.poster_path as string) ?? null,
|
||||
releaseDate: (heroResult.release_date as string | undefined) ?? null,
|
||||
firstAirDate: (heroResult.first_air_date as string | undefined) ?? null,
|
||||
voteAverage: (heroResult.vote_average as number | undefined) ?? null,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
const titleMap = ensureBrowseTitlesExist(allBrowseItems);
|
||||
|
||||
const items = baseItems.map((item) => {
|
||||
const entry = titleMap.get(`${item.tmdbId}-${item.type}`);
|
||||
return Object.assign(item, {
|
||||
id: entry?.id ?? "",
|
||||
posterPath: tmdbImageUrl(item.posterPath, "posters"),
|
||||
posterThumbHash: entry?.posterThumbHash ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
const heroEntry = heroResult
|
||||
? titleMap.get(`${heroResult.id as number}-${heroResult.media_type as string}`)
|
||||
: undefined;
|
||||
const hero = heroResult
|
||||
? {
|
||||
id: heroEntry?.id ?? "",
|
||||
tmdbId: heroResult.id as number,
|
||||
type: heroResult.media_type as "movie" | "tv",
|
||||
title: ((heroResult.title ?? heroResult.name) as string | undefined) ?? "",
|
||||
overview: (heroResult.overview as string | undefined) ?? "",
|
||||
backdropPath: tmdbImageUrl((heroResult.backdrop_path as string) ?? null, "backdrops"),
|
||||
voteAverage: heroResult.vote_average as number,
|
||||
}
|
||||
: null;
|
||||
|
||||
const titleIds = items.map((r) => r.id);
|
||||
const [userStatuses, episodeProgress] =
|
||||
titleIds.length > 0
|
||||
? [
|
||||
getDisplayStatusesByTitleIds(context.user.id, titleIds),
|
||||
getEpisodeProgressByTitleIds(context.user.id, titleIds),
|
||||
]
|
||||
: [{}, {}];
|
||||
|
||||
return {
|
||||
items,
|
||||
hero,
|
||||
userStatuses,
|
||||
episodeProgress,
|
||||
page: (data as { page?: number }).page ?? input.page,
|
||||
totalPages: (data as { total_pages?: number }).total_pages ?? 1,
|
||||
totalResults: (data as { total_results?: number }).total_results ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
// ─── Popular ──────────────────────────────────────────────────
|
||||
|
||||
export const popular = os.discover.popular.use(authed).handler(async ({ input, context }) => {
|
||||
requireTmdb();
|
||||
|
||||
const data = await getPopular(input.type, input.page);
|
||||
const baseItems = ((data.results ?? []) as Record<string, unknown>[])
|
||||
.filter((r) => r.poster_path)
|
||||
.map((r) => ({
|
||||
tmdbId: r.id as number,
|
||||
type: input.type,
|
||||
title: ((r.title ?? r.name) as string) || "",
|
||||
posterPath: (r.poster_path as string) ?? null,
|
||||
releaseDate: (r.release_date as string | undefined) ?? null,
|
||||
firstAirDate: (r.first_air_date as string | undefined) ?? null,
|
||||
voteAverage: (r.vote_average as number | undefined) ?? null,
|
||||
}));
|
||||
|
||||
const titleMap = ensureBrowseTitlesExist(baseItems);
|
||||
const items = baseItems.map((item) => {
|
||||
const entry = titleMap.get(`${item.tmdbId}-${item.type}`);
|
||||
return Object.assign(item, {
|
||||
id: entry?.id ?? "",
|
||||
posterPath: tmdbImageUrl(item.posterPath, "posters"),
|
||||
posterThumbHash: entry?.posterThumbHash ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
const titleIds = items.map((r) => r.id);
|
||||
const [userStatuses, episodeProgress] =
|
||||
titleIds.length > 0
|
||||
? [
|
||||
getDisplayStatusesByTitleIds(context.user.id, titleIds),
|
||||
getEpisodeProgressByTitleIds(context.user.id, titleIds),
|
||||
]
|
||||
: [{}, {}];
|
||||
|
||||
return {
|
||||
items,
|
||||
userStatuses,
|
||||
episodeProgress,
|
||||
page: data.page ?? input.page,
|
||||
totalPages: data.total_pages ?? 1,
|
||||
totalResults: data.total_results ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
// ─── Search ───────────────────────────────────────────────────
|
||||
|
||||
export const search = os.discover.search.use(authed).handler(async ({ input }) => {
|
||||
requireTmdb();
|
||||
|
||||
const query = input.query.trim();
|
||||
if (!query) {
|
||||
return { results: [], page: 1, totalPages: 0, totalResults: 0 };
|
||||
}
|
||||
const type = input.type ?? null;
|
||||
|
||||
if (type === "person") {
|
||||
const personResults = await searchPerson(query, input.page);
|
||||
const personItems = (personResults.results ?? []).map((r) => ({
|
||||
tmdbId: r.id,
|
||||
type: "person" as const,
|
||||
title: r.name ?? "",
|
||||
posterPath: null,
|
||||
profilePath: r.profile_path ?? null,
|
||||
overview: null,
|
||||
releaseDate: null,
|
||||
popularity: r.popularity ?? null,
|
||||
voteAverage: null,
|
||||
knownForDepartment: r.known_for_department ?? null,
|
||||
knownFor:
|
||||
(r.known_for
|
||||
?.slice(0, 3)
|
||||
.map((k) => k.title ?? (k as { name?: string }).name)
|
||||
.filter((s): s is string => !!s) as string[]) ?? null,
|
||||
}));
|
||||
const personMap = ensureBrowsePersonsExist(
|
||||
personItems.map((r) => ({
|
||||
tmdbId: r.tmdbId,
|
||||
name: r.title,
|
||||
profilePath: r.profilePath,
|
||||
knownForDepartment: r.knownForDepartment,
|
||||
popularity: r.popularity,
|
||||
})),
|
||||
);
|
||||
return {
|
||||
results: personItems.map((r) =>
|
||||
Object.assign(r, {
|
||||
id: personMap.get(r.tmdbId),
|
||||
profilePath: tmdbImageUrl(r.profilePath, "profiles"),
|
||||
}),
|
||||
),
|
||||
page: personResults.page ?? input.page,
|
||||
totalPages: personResults.total_pages ?? 1,
|
||||
totalResults: personResults.total_results ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
const raw =
|
||||
type === "movie"
|
||||
? await searchMovies(query, input.page)
|
||||
: type === "tv"
|
||||
? await searchTv(query, input.page)
|
||||
: await searchMulti(query, input.page);
|
||||
|
||||
type SearchResult = {
|
||||
id: number;
|
||||
media_type?: string;
|
||||
title?: string;
|
||||
name?: string;
|
||||
overview?: string;
|
||||
poster_path?: string | null;
|
||||
profile_path?: string | null;
|
||||
release_date?: string;
|
||||
first_air_date?: string;
|
||||
popularity?: number;
|
||||
vote_average?: number;
|
||||
};
|
||||
|
||||
const mapped = ((raw.results ?? []) as SearchResult[])
|
||||
.map((r) => {
|
||||
if (r.media_type === "person") {
|
||||
return {
|
||||
tmdbId: r.id,
|
||||
type: "person" as const,
|
||||
title: r.name ?? "Unknown",
|
||||
posterPath: null,
|
||||
profilePath: r.profile_path ?? null,
|
||||
overview: null,
|
||||
releaseDate: null,
|
||||
popularity: r.popularity ?? null,
|
||||
voteAverage: null,
|
||||
knownForDepartment: null,
|
||||
knownFor: null,
|
||||
};
|
||||
}
|
||||
|
||||
const mediaType = r.media_type === "movie" || r.media_type === "tv" ? r.media_type : type;
|
||||
if (!mediaType) return null;
|
||||
|
||||
return {
|
||||
tmdbId: r.id,
|
||||
type: mediaType,
|
||||
title: r.title ?? r.name ?? "",
|
||||
overview: r.overview ?? null,
|
||||
releaseDate: r.release_date ?? r.first_air_date ?? null,
|
||||
posterPath: r.poster_path ?? null,
|
||||
profilePath: null,
|
||||
popularity: r.popularity ?? null,
|
||||
voteAverage: r.vote_average ?? null,
|
||||
knownForDepartment: null,
|
||||
knownFor: null,
|
||||
};
|
||||
})
|
||||
.filter((r): r is NonNullable<typeof r> => r !== null);
|
||||
|
||||
const titleResults = mapped.filter(
|
||||
(r): r is typeof r & { type: "movie" | "tv" } => r.type !== "person",
|
||||
);
|
||||
const titleMap = ensureBrowseTitlesExist(titleResults);
|
||||
|
||||
const personResults = mapped.filter((r) => r.type === "person");
|
||||
const personMap = ensureBrowsePersonsExist(
|
||||
personResults.map((r) => ({
|
||||
tmdbId: r.tmdbId,
|
||||
name: r.title,
|
||||
profilePath: r.profilePath,
|
||||
knownForDepartment: r.knownForDepartment,
|
||||
popularity: r.popularity,
|
||||
})),
|
||||
);
|
||||
|
||||
const results = mapped.map((r) => {
|
||||
if (r.type === "person") {
|
||||
return Object.assign(r, {
|
||||
id: personMap.get(r.tmdbId),
|
||||
profilePath: tmdbImageUrl(r.profilePath, "profiles"),
|
||||
});
|
||||
}
|
||||
const entry = titleMap.get(`${r.tmdbId}-${r.type}`);
|
||||
return Object.assign(r, { id: entry?.id, posterPath: tmdbImageUrl(r.posterPath, "posters") });
|
||||
});
|
||||
|
||||
return {
|
||||
results,
|
||||
page: raw.page ?? input.page,
|
||||
totalPages: raw.total_pages ?? 1,
|
||||
totalResults: raw.total_results ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
// ─── Browse (filtered discovery) ──────────────────────────────
|
||||
|
||||
export const browse = os.discover.browse.use(authed).handler(async ({ input, context }) => {
|
||||
requireTmdb();
|
||||
|
||||
const params: Record<string, string> = {
|
||||
sort_by: input.sortBy ?? "popularity.desc",
|
||||
@@ -34,9 +332,12 @@ export const discover = os.discover.use(authed).handler(async ({ input, context
|
||||
}
|
||||
if (input.ratingMin != null) params["vote_average.gte"] = String(input.ratingMin);
|
||||
if (input.language) params.with_original_language = input.language;
|
||||
if (input.providerId) {
|
||||
params.with_watch_providers = String(input.providerId);
|
||||
params.watch_region = WATCH_REGION;
|
||||
if (input.platformId) {
|
||||
const tmdbIds = getPlatformTmdbIds(input.platformId);
|
||||
if (tmdbIds.length > 0) {
|
||||
params.with_watch_providers = tmdbIds.join("|");
|
||||
params.watch_region = WATCH_REGION;
|
||||
}
|
||||
}
|
||||
|
||||
const results = await discoverTmdb(input.type, params, input.page);
|
||||
@@ -88,3 +389,53 @@ export const discover = os.discover.use(authed).handler(async ({ input, context
|
||||
totalResults: results.total_results ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
// ─── Genres ───────────────────────────────────────────────────
|
||||
|
||||
export const genres = os.discover.genres.use(authed).handler(async ({ input }) => {
|
||||
requireTmdb();
|
||||
const data = await getGenres(input.type);
|
||||
return {
|
||||
genres: (data.genres ?? []).map((g) => ({
|
||||
id: g.id,
|
||||
name: g.name ?? "",
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// ─── Platforms ────────────────────────────────────────────────
|
||||
|
||||
export const platforms = os.discover.platforms.use(authed).handler(async () => {
|
||||
const allPlatforms = listPlatforms();
|
||||
const tmdbIdsMap = getPlatformTmdbIdMap(allPlatforms.map((p) => p.id));
|
||||
return {
|
||||
platforms: allPlatforms.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
tmdbProviderIds: tmdbIdsMap.get(p.id) ?? [],
|
||||
logoPath: tmdbImageUrl(p.logoPath, "logos"),
|
||||
isSubscription: p.isSubscription,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// ─── Recommendations ──────────────────────────────────────────
|
||||
|
||||
export const recommendations = os.discover.recommendations.use(authed).handler(({ context }) => {
|
||||
const feed = getRecommendationsFeed(context.user.id);
|
||||
const items = feed
|
||||
.filter((t): t is NonNullable<typeof t> => t != null)
|
||||
.slice(0, 10)
|
||||
.map((t) => ({
|
||||
id: t.id,
|
||||
tmdbId: t.tmdbId,
|
||||
type: t.type,
|
||||
title: t.title,
|
||||
posterPath: tmdbImageUrl(t.posterPath, "posters"),
|
||||
posterThumbHash: t.posterThumbHash ?? null,
|
||||
releaseDate: t.releaseDate ?? null,
|
||||
firstAirDate: t.firstAirDate ?? null,
|
||||
voteAverage: t.voteAverage,
|
||||
}));
|
||||
return { items };
|
||||
});
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { logEpisodeWatch, logEpisodeWatchBatch, unwatchEpisode } from "@sofa/core/tracking";
|
||||
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
export const watch = os.episodes.watch.use(authed).handler(({ input, context }) => {
|
||||
logEpisodeWatch(context.user.id, input.id);
|
||||
});
|
||||
|
||||
export const unwatch = os.episodes.unwatch.use(authed).handler(({ input, context }) => {
|
||||
unwatchEpisode(context.user.id, input.id);
|
||||
});
|
||||
|
||||
export const batchWatch = os.episodes.batchWatch.use(authed).handler(({ input, context }) => {
|
||||
logEpisodeWatchBatch(context.user.id, input.episodeIds);
|
||||
});
|
||||
@@ -1,176 +0,0 @@
|
||||
import { ORPCError } from "@orpc/server";
|
||||
|
||||
import { AppErrorCode } from "@sofa/api/errors";
|
||||
import { ensureBrowseTitlesExist } from "@sofa/core/metadata";
|
||||
import { listPlatforms } from "@sofa/core/platforms";
|
||||
import { getEpisodeProgressByTitleIds, getDisplayStatusesByTitleIds } from "@sofa/core/tracking";
|
||||
import { getGenres, getPopular, getTrending } from "@sofa/tmdb/client";
|
||||
import { isTmdbConfigured } from "@sofa/tmdb/config";
|
||||
import { tmdbImageUrl } from "@sofa/tmdb/image";
|
||||
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
function requireTmdb() {
|
||||
if (!isTmdbConfigured()) {
|
||||
throw new ORPCError("PRECONDITION_FAILED", {
|
||||
message: "TMDB API key is not configured",
|
||||
data: { code: AppErrorCode.TMDB_NOT_CONFIGURED },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const trending = os.explore.trending.use(authed).handler(async ({ input, context }) => {
|
||||
requireTmdb();
|
||||
|
||||
const data = await getTrending(input.type, "day", input.page);
|
||||
const results = (data.results ?? []) as Record<string, unknown>[];
|
||||
|
||||
const baseItems = results
|
||||
.filter((r) => r.poster_path)
|
||||
.map((r) => {
|
||||
const mediaType = r.media_type === "movie" || r.media_type === "tv" ? r.media_type : "movie";
|
||||
return {
|
||||
tmdbId: r.id as number,
|
||||
type: mediaType as "movie" | "tv",
|
||||
title: ((r.title ?? r.name) as string) || "",
|
||||
posterPath: (r.poster_path as string) ?? null,
|
||||
releaseDate: (r.release_date as string | undefined) ?? null,
|
||||
firstAirDate: (r.first_air_date as string | undefined) ?? null,
|
||||
voteAverage: (r.vote_average as number | undefined) ?? null,
|
||||
};
|
||||
});
|
||||
const heroResult = results.find(
|
||||
(r) => r.backdrop_path && (r.media_type === "movie" || r.media_type === "tv"),
|
||||
);
|
||||
|
||||
// Batch-upsert all browse items (+ hero) into the titles table
|
||||
const allBrowseItems = [
|
||||
...baseItems,
|
||||
...(heroResult
|
||||
? [
|
||||
{
|
||||
tmdbId: heroResult.id as number,
|
||||
type: heroResult.media_type as "movie" | "tv",
|
||||
title: ((heroResult.title ?? heroResult.name) as string | undefined) ?? "",
|
||||
posterPath: (heroResult.poster_path as string) ?? null,
|
||||
releaseDate: (heroResult.release_date as string | undefined) ?? null,
|
||||
firstAirDate: (heroResult.first_air_date as string | undefined) ?? null,
|
||||
voteAverage: (heroResult.vote_average as number | undefined) ?? null,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
const titleMap = ensureBrowseTitlesExist(allBrowseItems);
|
||||
|
||||
const items = baseItems.map((item) => {
|
||||
const entry = titleMap.get(`${item.tmdbId}-${item.type}`);
|
||||
return Object.assign(item, {
|
||||
id: entry?.id ?? "",
|
||||
posterPath: tmdbImageUrl(item.posterPath, "posters"),
|
||||
posterThumbHash: entry?.posterThumbHash ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
const heroEntry = heroResult
|
||||
? titleMap.get(`${heroResult.id as number}-${heroResult.media_type as string}`)
|
||||
: undefined;
|
||||
const hero = heroResult
|
||||
? {
|
||||
id: heroEntry?.id ?? "",
|
||||
tmdbId: heroResult.id as number,
|
||||
type: heroResult.media_type as "movie" | "tv",
|
||||
title: ((heroResult.title ?? heroResult.name) as string | undefined) ?? "",
|
||||
overview: (heroResult.overview as string | undefined) ?? "",
|
||||
backdropPath: tmdbImageUrl((heroResult.backdrop_path as string) ?? null, "backdrops"),
|
||||
voteAverage: heroResult.vote_average as number,
|
||||
}
|
||||
: null;
|
||||
|
||||
const titleIds = items.map((r) => r.id);
|
||||
const [userStatuses, episodeProgress] =
|
||||
titleIds.length > 0
|
||||
? [
|
||||
getDisplayStatusesByTitleIds(context.user.id, titleIds),
|
||||
getEpisodeProgressByTitleIds(context.user.id, titleIds),
|
||||
]
|
||||
: [{}, {}];
|
||||
|
||||
return {
|
||||
items,
|
||||
hero,
|
||||
userStatuses,
|
||||
episodeProgress,
|
||||
page: (data as { page?: number }).page ?? input.page,
|
||||
totalPages: (data as { total_pages?: number }).total_pages ?? 1,
|
||||
totalResults: (data as { total_results?: number }).total_results ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
export const popular = os.explore.popular.use(authed).handler(async ({ input, context }) => {
|
||||
requireTmdb();
|
||||
|
||||
const data = await getPopular(input.type, input.page);
|
||||
const baseItems = ((data.results ?? []) as Record<string, unknown>[])
|
||||
.filter((r) => r.poster_path)
|
||||
.map((r) => ({
|
||||
tmdbId: r.id as number,
|
||||
type: input.type,
|
||||
title: ((r.title ?? r.name) as string) || "",
|
||||
posterPath: (r.poster_path as string) ?? null,
|
||||
releaseDate: (r.release_date as string | undefined) ?? null,
|
||||
firstAirDate: (r.first_air_date as string | undefined) ?? null,
|
||||
voteAverage: (r.vote_average as number | undefined) ?? null,
|
||||
}));
|
||||
|
||||
const titleMap = ensureBrowseTitlesExist(baseItems);
|
||||
const items = baseItems.map((item) => {
|
||||
const entry = titleMap.get(`${item.tmdbId}-${item.type}`);
|
||||
return Object.assign(item, {
|
||||
id: entry?.id ?? "",
|
||||
posterPath: tmdbImageUrl(item.posterPath, "posters"),
|
||||
posterThumbHash: entry?.posterThumbHash ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
const titleIds = items.map((r) => r.id);
|
||||
const [userStatuses, episodeProgress] =
|
||||
titleIds.length > 0
|
||||
? [
|
||||
getDisplayStatusesByTitleIds(context.user.id, titleIds),
|
||||
getEpisodeProgressByTitleIds(context.user.id, titleIds),
|
||||
]
|
||||
: [{}, {}];
|
||||
|
||||
return {
|
||||
items,
|
||||
userStatuses,
|
||||
episodeProgress,
|
||||
page: data.page ?? input.page,
|
||||
totalPages: data.total_pages ?? 1,
|
||||
totalResults: data.total_results ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
export const genres = os.explore.genres.use(authed).handler(async ({ input }) => {
|
||||
requireTmdb();
|
||||
const data = await getGenres(input.type);
|
||||
return {
|
||||
genres: (data.genres ?? []).map((g) => ({
|
||||
id: g.id,
|
||||
name: g.name ?? "",
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
export const watchProviders = os.explore.watchProviders.use(authed).handler(async () => {
|
||||
const allPlatforms = listPlatforms();
|
||||
return {
|
||||
providers: allPlatforms.map((p) => ({
|
||||
id: p.id,
|
||||
tmdbProviderId: p.tmdbProviderId,
|
||||
name: p.name,
|
||||
logoPath: tmdbImageUrl(p.logoPath, "logos"),
|
||||
})),
|
||||
};
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ORPCError } from "@orpc/server";
|
||||
|
||||
import { AppErrorCode } from "@sofa/api/errors";
|
||||
import {
|
||||
createOrUpdateIntegration,
|
||||
deleteIntegration as coreDeleteIntegration,
|
||||
listUserIntegrations,
|
||||
regenerateToken as coreRegenerateToken,
|
||||
serializeIntegration,
|
||||
} from "@sofa/core/integrations";
|
||||
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
export const list = os.integrations.list.use(authed).handler(({ context }) => {
|
||||
return listUserIntegrations(context.user.id);
|
||||
});
|
||||
|
||||
export const create = os.integrations.create.use(authed).handler(({ input, context }) => {
|
||||
return createOrUpdateIntegration(context.user.id, input.provider, input.enabled);
|
||||
});
|
||||
|
||||
export const deleteIntegration = os.integrations.delete
|
||||
.use(authed)
|
||||
.handler(({ input, context }) => {
|
||||
coreDeleteIntegration(context.user.id, input.provider);
|
||||
});
|
||||
|
||||
export const regenerateToken = os.integrations.regenerateToken
|
||||
.use(authed)
|
||||
.handler(({ input, context }) => {
|
||||
const row = coreRegenerateToken(context.user.id, input.provider);
|
||||
|
||||
if (!row) {
|
||||
throw new ORPCError("NOT_FOUND", {
|
||||
message: "Integration not found",
|
||||
data: { code: AppErrorCode.INTEGRATION_NOT_FOUND },
|
||||
});
|
||||
}
|
||||
|
||||
return serializeIntegration(row);
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getContinueWatchingFeed, getUserStats, getUpcomingFeed } from "@sofa/core/discovery";
|
||||
import { getFilteredLibraryFeed, getLibraryGenresList } from "@sofa/core/library";
|
||||
import { tmdbImageUrl } from "@sofa/tmdb/image";
|
||||
|
||||
@@ -45,3 +46,56 @@ export const list = os.library.list.use(authed).handler(({ input, context }) =>
|
||||
export const genres = os.library.genres.use(authed).handler(({ context }) => {
|
||||
return { genres: getLibraryGenresList(context.user.id) };
|
||||
});
|
||||
|
||||
export const stats = os.library.stats.use(authed).handler(({ context }) => {
|
||||
const userStats = getUserStats(context.user.id);
|
||||
return { size: userStats.librarySize, completed: userStats.completed };
|
||||
});
|
||||
|
||||
export const continueWatching = os.library.continueWatching.use(authed).handler(({ context }) => {
|
||||
const feed = getContinueWatchingFeed(context.user.id);
|
||||
const items = feed.map((item) => ({
|
||||
title: {
|
||||
id: item.title.id,
|
||||
title: item.title.title,
|
||||
backdropPath: tmdbImageUrl(item.title.backdropPath, "backdrops"),
|
||||
backdropThumbHash: item.title.backdropThumbHash,
|
||||
},
|
||||
nextEpisode: item.nextEpisode
|
||||
? {
|
||||
seasonNumber: item.nextEpisode.seasonNumber,
|
||||
episodeNumber: item.nextEpisode.episodeNumber,
|
||||
name: item.nextEpisode.name,
|
||||
stillPath: tmdbImageUrl(item.nextEpisode.stillPath, "stills"),
|
||||
stillThumbHash: item.nextEpisode.stillThumbHash,
|
||||
}
|
||||
: null,
|
||||
totalEpisodes: item.totalEpisodes,
|
||||
watchedEpisodes: item.watchedEpisodes,
|
||||
}));
|
||||
return { items };
|
||||
});
|
||||
|
||||
export const upcoming = os.library.upcoming.use(authed).handler(({ input, context }) => {
|
||||
const result = getUpcomingFeed(context.user.id, {
|
||||
days: input.days,
|
||||
limit: input.limit,
|
||||
cursor: input.cursor,
|
||||
mediaType: input.mediaType,
|
||||
statusFilter: input.statusFilter,
|
||||
});
|
||||
return {
|
||||
items: result.items.map((item) => ({
|
||||
...item,
|
||||
posterPath: tmdbImageUrl(item.posterPath, "posters"),
|
||||
backdropPath: tmdbImageUrl(item.backdropPath, "backdrops"),
|
||||
streamingProvider: item.streamingProvider
|
||||
? {
|
||||
...item.streamingProvider,
|
||||
logoPath: tmdbImageUrl(item.streamingProvider.logoPath, "logos"),
|
||||
}
|
||||
: null,
|
||||
})),
|
||||
nextCursor: result.nextCursor,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getDisplayStatusesByTitleIds } from "@sofa/core/tracking";
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
export const detail = os.people.detail.use(authed).handler(async ({ input, context }) => {
|
||||
export const get = os.people.get.use(authed).handler(async ({ input, context }) => {
|
||||
const person = await getOrFetchPerson(input.id);
|
||||
if (!person)
|
||||
throw new ORPCError("NOT_FOUND", {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { listPlatforms } from "@sofa/core/platforms";
|
||||
import { tmdbImageUrl } from "@sofa/tmdb/image";
|
||||
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
export const list = os.platforms.list.use(authed).handler(async () => {
|
||||
const allPlatforms = listPlatforms();
|
||||
return {
|
||||
platforms: allPlatforms.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
tmdbProviderId: p.tmdbProviderId,
|
||||
logoPath: tmdbImageUrl(p.logoPath, "logos"),
|
||||
displayOrder: p.displayOrder,
|
||||
})),
|
||||
};
|
||||
});
|
||||
@@ -1,161 +0,0 @@
|
||||
import { ORPCError } from "@orpc/server";
|
||||
|
||||
import { AppErrorCode } from "@sofa/api/errors";
|
||||
import { ensureBrowseTitlesExist } from "@sofa/core/metadata";
|
||||
import { ensureBrowsePersonsExist } from "@sofa/core/person";
|
||||
import { searchMovies, searchMulti, searchPerson, searchTv } from "@sofa/tmdb/client";
|
||||
import { isTmdbConfigured } from "@sofa/tmdb/config";
|
||||
import { tmdbImageUrl } from "@sofa/tmdb/image";
|
||||
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
export const search = os.search.use(authed).handler(async ({ input }) => {
|
||||
if (!isTmdbConfigured()) {
|
||||
throw new ORPCError("PRECONDITION_FAILED", {
|
||||
message: "TMDB API key is not configured",
|
||||
data: { code: AppErrorCode.TMDB_NOT_CONFIGURED },
|
||||
});
|
||||
}
|
||||
|
||||
const query = input.query.trim();
|
||||
if (!query) {
|
||||
return { results: [], page: 1, totalPages: 0, totalResults: 0 };
|
||||
}
|
||||
const type = input.type ?? null;
|
||||
|
||||
if (type === "person") {
|
||||
const personResults = await searchPerson(query, input.page);
|
||||
const personItems = (personResults.results ?? []).map((r) => ({
|
||||
tmdbId: r.id,
|
||||
type: "person" as const,
|
||||
title: r.name ?? "",
|
||||
posterPath: null,
|
||||
profilePath: r.profile_path ?? null,
|
||||
overview: null,
|
||||
releaseDate: null,
|
||||
popularity: r.popularity ?? null,
|
||||
voteAverage: null,
|
||||
knownForDepartment: r.known_for_department ?? null,
|
||||
knownFor:
|
||||
(r.known_for
|
||||
?.slice(0, 3)
|
||||
.map((k) => k.title ?? (k as { name?: string }).name)
|
||||
.filter((s): s is string => !!s) as string[]) ?? null,
|
||||
}));
|
||||
const personMap = ensureBrowsePersonsExist(
|
||||
personItems.map((r) => ({
|
||||
tmdbId: r.tmdbId,
|
||||
name: r.title,
|
||||
profilePath: r.profilePath,
|
||||
knownForDepartment: r.knownForDepartment,
|
||||
popularity: r.popularity,
|
||||
})),
|
||||
);
|
||||
return {
|
||||
results: personItems.map((r) =>
|
||||
Object.assign(r, {
|
||||
id: personMap.get(r.tmdbId),
|
||||
profilePath: tmdbImageUrl(r.profilePath, "profiles"),
|
||||
}),
|
||||
),
|
||||
page: personResults.page ?? input.page,
|
||||
totalPages: personResults.total_pages ?? 1,
|
||||
totalResults: personResults.total_results ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
const raw =
|
||||
type === "movie"
|
||||
? await searchMovies(query, input.page)
|
||||
: type === "tv"
|
||||
? await searchTv(query, input.page)
|
||||
: await searchMulti(query, input.page);
|
||||
|
||||
type SearchResult = {
|
||||
id: number;
|
||||
media_type?: string;
|
||||
title?: string;
|
||||
name?: string;
|
||||
overview?: string;
|
||||
poster_path?: string | null;
|
||||
profile_path?: string | null;
|
||||
release_date?: string;
|
||||
first_air_date?: string;
|
||||
popularity?: number;
|
||||
vote_average?: number;
|
||||
};
|
||||
|
||||
const mapped = ((raw.results ?? []) as SearchResult[])
|
||||
.map((r) => {
|
||||
if (r.media_type === "person") {
|
||||
return {
|
||||
tmdbId: r.id,
|
||||
type: "person" as const,
|
||||
title: r.name ?? "Unknown",
|
||||
posterPath: null,
|
||||
profilePath: r.profile_path ?? null,
|
||||
overview: null,
|
||||
releaseDate: null,
|
||||
popularity: r.popularity ?? null,
|
||||
voteAverage: null,
|
||||
knownForDepartment: null,
|
||||
knownFor: null,
|
||||
};
|
||||
}
|
||||
|
||||
const mediaType = r.media_type === "movie" || r.media_type === "tv" ? r.media_type : type;
|
||||
if (!mediaType) return null;
|
||||
|
||||
return {
|
||||
tmdbId: r.id,
|
||||
type: mediaType,
|
||||
title: r.title ?? r.name ?? "",
|
||||
overview: r.overview ?? null,
|
||||
releaseDate: r.release_date ?? r.first_air_date ?? null,
|
||||
posterPath: r.poster_path ?? null,
|
||||
profilePath: null,
|
||||
popularity: r.popularity ?? null,
|
||||
voteAverage: r.vote_average ?? null,
|
||||
knownForDepartment: null,
|
||||
knownFor: null,
|
||||
};
|
||||
})
|
||||
.filter((r): r is NonNullable<typeof r> => r !== null);
|
||||
|
||||
// Batch-import movie/TV results so they have internal IDs
|
||||
const titleResults = mapped.filter(
|
||||
(r): r is typeof r & { type: "movie" | "tv" } => r.type !== "person",
|
||||
);
|
||||
const titleMap = ensureBrowseTitlesExist(titleResults);
|
||||
|
||||
// Batch-import person results so they have internal IDs
|
||||
const personResults = mapped.filter((r) => r.type === "person");
|
||||
const personMap = ensureBrowsePersonsExist(
|
||||
personResults.map((r) => ({
|
||||
tmdbId: r.tmdbId,
|
||||
name: r.title,
|
||||
profilePath: r.profilePath,
|
||||
knownForDepartment: r.knownForDepartment,
|
||||
popularity: r.popularity,
|
||||
})),
|
||||
);
|
||||
|
||||
const results = mapped.map((r) => {
|
||||
if (r.type === "person") {
|
||||
return Object.assign(r, {
|
||||
id: personMap.get(r.tmdbId),
|
||||
profilePath: tmdbImageUrl(r.profilePath, "profiles"),
|
||||
});
|
||||
}
|
||||
const entry = titleMap.get(`${r.tmdbId}-${r.type}`);
|
||||
return Object.assign(r, { id: entry?.id, posterPath: tmdbImageUrl(r.posterPath, "posters") });
|
||||
});
|
||||
|
||||
return {
|
||||
results,
|
||||
page: raw.page ?? input.page,
|
||||
totalPages: raw.total_pages ?? 1,
|
||||
totalResults: raw.total_results ?? 0,
|
||||
};
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { unwatchSeason, watchSeason } from "@sofa/core/tracking";
|
||||
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
export const watch = os.seasons.watch.use(authed).handler(({ input, context }) => {
|
||||
watchSeason(context.user.id, input.id);
|
||||
});
|
||||
|
||||
export const unwatch = os.seasons.unwatch.use(authed).handler(({ input, context }) => {
|
||||
unwatchSeason(context.user.id, input.id);
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
export const status = os.system.status.use(authed).handler(() => {
|
||||
return {
|
||||
publicApiUrl: process.env.PUBLIC_API_URL || "https://public-api.sofa.watch",
|
||||
};
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { isTmdbConfigured } from "@sofa/tmdb/config";
|
||||
import { tmdbImageUrl } from "@sofa/tmdb/image";
|
||||
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
// Well-known TMDB poster paths for the background collage
|
||||
const posterPaths = [
|
||||
@@ -26,22 +27,20 @@ export const publicInfo = os.system.publicInfo.handler(async () => {
|
||||
.map((p) => tmdbImageUrl(p, "posters", "w300"))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const oidcEnabled = isOidcConfigured();
|
||||
|
||||
return {
|
||||
instanceId: getInstanceId(),
|
||||
tmdbConfigured: isTmdbConfigured(),
|
||||
userCount: getUserCount(),
|
||||
registrationOpen: isRegistrationOpen(),
|
||||
posterUrls,
|
||||
};
|
||||
});
|
||||
|
||||
export const authConfig = os.system.authConfig.handler(async () => {
|
||||
const oidcEnabled = isOidcConfigured();
|
||||
return {
|
||||
oidcEnabled,
|
||||
oidcProviderName: oidcEnabled ? getOidcProviderName() : null,
|
||||
passwordLoginDisabled: isPasswordLoginDisabled(),
|
||||
registrationOpen: isRegistrationOpen(),
|
||||
userCount: getUserCount(),
|
||||
};
|
||||
});
|
||||
|
||||
export const status = os.system.status.use(authed).handler(() => {
|
||||
return { publicApiUrl: process.env.PUBLIC_API_URL ?? "https://public-api.sofa.watch" };
|
||||
});
|
||||
|
||||
@@ -2,25 +2,13 @@ import { ORPCError } from "@orpc/server";
|
||||
|
||||
import { AppErrorCode } from "@sofa/api/errors";
|
||||
import { getRecommendationsForTitle } from "@sofa/core/discovery";
|
||||
import { getOrFetchTitle, getOrFetchTitleByTmdbId } from "@sofa/core/metadata";
|
||||
import {
|
||||
getDisplayStatusesByTitleIds,
|
||||
getUserTitleInfo,
|
||||
logMovieWatch,
|
||||
markAllEpisodesWatched,
|
||||
quickAddTitle,
|
||||
rateTitleStars,
|
||||
removeTitleStatus,
|
||||
setTitleStatus,
|
||||
} from "@sofa/core/tracking";
|
||||
import { createLogger } from "@sofa/logger";
|
||||
import { getOrFetchTitle } from "@sofa/core/metadata";
|
||||
import { getDisplayStatusesByTitleIds } from "@sofa/core/tracking";
|
||||
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
const log = createLogger("titles");
|
||||
|
||||
export const detail = os.titles.detail.use(authed).handler(async ({ input, context }) => {
|
||||
export const get = os.titles.get.use(authed).handler(async ({ input, context }) => {
|
||||
const result = await getOrFetchTitle(input.id, context.user.id);
|
||||
if (!result)
|
||||
throw new ORPCError("NOT_FOUND", {
|
||||
@@ -30,59 +18,11 @@ export const detail = os.titles.detail.use(authed).handler(async ({ input, conte
|
||||
return result;
|
||||
});
|
||||
|
||||
export const updateStatus = os.titles.updateStatus.use(authed).handler(({ input, context }) => {
|
||||
if (input.status === null) {
|
||||
removeTitleStatus(context.user.id, input.id);
|
||||
} else {
|
||||
setTitleStatus(context.user.id, input.id, input.status);
|
||||
}
|
||||
});
|
||||
|
||||
export const updateRating = os.titles.updateRating.use(authed).handler(({ input, context }) => {
|
||||
rateTitleStars(context.user.id, input.id, input.stars);
|
||||
});
|
||||
|
||||
export const watchMovie = os.titles.watchMovie.use(authed).handler(({ input, context }) => {
|
||||
logMovieWatch(context.user.id, input.id);
|
||||
});
|
||||
|
||||
export const watchAll = os.titles.watchAll.use(authed).handler(({ input, context }) => {
|
||||
markAllEpisodesWatched(context.user.id, input.id);
|
||||
});
|
||||
|
||||
export const userInfo = os.titles.userInfo.use(authed).handler(({ input, context }) => {
|
||||
const info = getUserTitleInfo(context.user.id, input.id);
|
||||
if (!info.status) return { ...info, status: null };
|
||||
|
||||
// Convert stored status to display status
|
||||
const displayStatuses = getDisplayStatusesByTitleIds(context.user.id, [input.id]);
|
||||
return { ...info, status: displayStatuses[input.id] ?? null };
|
||||
});
|
||||
|
||||
export const recommendations = os.titles.recommendations
|
||||
.use(authed)
|
||||
.handler(({ input, context }) => {
|
||||
const recs = getRecommendationsForTitle(input.id);
|
||||
const userStatuses = getDisplayStatusesByTitleIds(
|
||||
context.user.id,
|
||||
recs.map((r) => r.id),
|
||||
);
|
||||
return { recommendations: recs, userStatuses };
|
||||
});
|
||||
|
||||
export const quickAdd = os.titles.quickAdd.use(authed).handler(async ({ input, context }) => {
|
||||
const result = quickAddTitle(context.user.id, input.id);
|
||||
if (!result) {
|
||||
throw new ORPCError("NOT_FOUND", {
|
||||
message: "Title not found",
|
||||
data: { code: AppErrorCode.TITLE_NOT_FOUND },
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger full TMDB import if still a shell (fire-and-forget)
|
||||
getOrFetchTitleByTmdbId(result.tmdbId, result.type as "movie" | "tv").catch((err) => {
|
||||
log.warn(`Failed to import ${result.type} TMDB ${result.tmdbId}:`, err);
|
||||
});
|
||||
|
||||
return { id: result.id, alreadyAdded: result.alreadyAdded };
|
||||
export const similar = os.titles.similar.use(authed).handler(({ input, context }) => {
|
||||
const recs = getRecommendationsForTitle(input.id);
|
||||
const userStatuses = getDisplayStatusesByTitleIds(
|
||||
context.user.id,
|
||||
recs.map((r) => r.id),
|
||||
);
|
||||
return { recommendations: recs, userStatuses };
|
||||
});
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { ORPCError } from "@orpc/server";
|
||||
|
||||
import { AppErrorCode } from "@sofa/api/errors";
|
||||
import type { WatchScopeType } from "@sofa/api/schemas";
|
||||
import { getWatchCount, getWatchHistory } from "@sofa/core/discovery";
|
||||
import { getOrFetchTitleByTmdbId } from "@sofa/core/metadata";
|
||||
import {
|
||||
getDisplayStatusesByTitleIds,
|
||||
getUserTitleInfo,
|
||||
logEpisodeWatch,
|
||||
logEpisodeWatchBatch,
|
||||
logMovieWatch,
|
||||
markAllEpisodesWatched,
|
||||
quickAddTitle,
|
||||
rateTitleStars,
|
||||
removeTitleStatus,
|
||||
unwatchEpisode,
|
||||
unwatchMovie,
|
||||
unwatchSeason,
|
||||
unwatchSeries,
|
||||
watchSeason,
|
||||
} from "@sofa/core/tracking";
|
||||
import { createLogger } from "@sofa/logger";
|
||||
|
||||
import { os } from "../context";
|
||||
import { authed } from "../middleware";
|
||||
|
||||
const log = createLogger("tracking");
|
||||
|
||||
const watchHistoryTypeMap = { movie: "movies", episode: "episodes" } as const;
|
||||
|
||||
// ─── Watch handlers by scope ──────────────────────────────────
|
||||
|
||||
// All tracking core functions are synchronous (bun:sqlite). If any become
|
||||
// async, these loops need to be awaited to surface errors properly.
|
||||
function handleWatch(userId: string, scope: WatchScopeType, ids: string[]) {
|
||||
switch (scope) {
|
||||
case "movie":
|
||||
for (const id of ids) logMovieWatch(userId, id);
|
||||
break;
|
||||
case "episode":
|
||||
if (ids.length === 1) {
|
||||
logEpisodeWatch(userId, ids[0]);
|
||||
} else {
|
||||
logEpisodeWatchBatch(userId, ids);
|
||||
}
|
||||
break;
|
||||
case "season":
|
||||
for (const id of ids) watchSeason(userId, id);
|
||||
break;
|
||||
case "series":
|
||||
for (const id of ids) markAllEpisodesWatched(userId, id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnwatch(userId: string, scope: WatchScopeType, ids: string[]) {
|
||||
switch (scope) {
|
||||
case "movie":
|
||||
for (const id of ids) unwatchMovie(userId, id);
|
||||
break;
|
||||
case "episode":
|
||||
for (const id of ids) unwatchEpisode(userId, id);
|
||||
break;
|
||||
case "season":
|
||||
for (const id of ids) unwatchSeason(userId, id);
|
||||
break;
|
||||
case "series":
|
||||
for (const id of ids) unwatchSeries(userId, id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Procedures ───────────────────────────────────────────────
|
||||
|
||||
export const watch = os.tracking.watch.use(authed).handler(({ input, context }) => {
|
||||
handleWatch(context.user.id, input.scope, input.ids);
|
||||
});
|
||||
|
||||
export const unwatch = os.tracking.unwatch.use(authed).handler(({ input, context }) => {
|
||||
handleUnwatch(context.user.id, input.scope, input.ids);
|
||||
});
|
||||
|
||||
export const updateStatus = os.tracking.updateStatus
|
||||
.use(authed)
|
||||
.handler(async ({ input, context }) => {
|
||||
if (input.status === null) {
|
||||
removeTitleStatus(context.user.id, input.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-import from TMDB if the title is a shell (absorbs quickAdd logic)
|
||||
const result = quickAddTitle(context.user.id, input.id);
|
||||
if (!result) {
|
||||
throw new ORPCError("NOT_FOUND", {
|
||||
message: "Title not found",
|
||||
data: { code: AppErrorCode.TITLE_NOT_FOUND },
|
||||
});
|
||||
}
|
||||
if (!result.alreadyAdded) {
|
||||
getOrFetchTitleByTmdbId(result.tmdbId, result.type as "movie" | "tv").catch((err) => {
|
||||
log.warn(`Failed to import ${result.type} TMDB ${result.tmdbId}:`, err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const rate = os.tracking.rate.use(authed).handler(({ input, context }) => {
|
||||
rateTitleStars(context.user.id, input.id, input.stars);
|
||||
});
|
||||
|
||||
export const userInfo = os.tracking.userInfo.use(authed).handler(({ input, context }) => {
|
||||
const info = getUserTitleInfo(context.user.id, input.id);
|
||||
if (!info.status) return { ...info, status: null };
|
||||
|
||||
const displayStatuses = getDisplayStatusesByTitleIds(context.user.id, [input.id]);
|
||||
return { ...info, status: displayStatuses[input.id] ?? null };
|
||||
});
|
||||
|
||||
export const stats = os.tracking.stats.use(authed).handler(({ input, context }) => {
|
||||
const coreType = watchHistoryTypeMap[input.type];
|
||||
const count = getWatchCount(context.user.id, coreType, input.period);
|
||||
const history = getWatchHistory(context.user.id, coreType, input.period);
|
||||
return { count, history };
|
||||
});
|
||||
@@ -1,75 +1,68 @@
|
||||
import { os } from "./context";
|
||||
import * as account from "./procedures/account";
|
||||
import * as admin from "./procedures/admin";
|
||||
import * as dashboard from "./procedures/dashboard";
|
||||
import { discover } from "./procedures/discover";
|
||||
import * as episodes from "./procedures/episodes";
|
||||
import * as explore from "./procedures/explore";
|
||||
import * as discover from "./procedures/discover";
|
||||
import * as imports from "./procedures/imports";
|
||||
import * as integrations from "./procedures/integrations";
|
||||
import * as library from "./procedures/library";
|
||||
import * as people from "./procedures/people";
|
||||
import * as platformProcs from "./procedures/platforms";
|
||||
import { search } from "./procedures/search";
|
||||
import * as seasons from "./procedures/seasons";
|
||||
import * as status from "./procedures/status";
|
||||
import * as system from "./procedures/system";
|
||||
import * as titles from "./procedures/titles";
|
||||
import * as tracking from "./procedures/tracking";
|
||||
|
||||
export const implementedRouter = {
|
||||
titles: {
|
||||
get: titles.get,
|
||||
similar: titles.similar,
|
||||
},
|
||||
tracking: {
|
||||
watch: tracking.watch,
|
||||
unwatch: tracking.unwatch,
|
||||
updateStatus: tracking.updateStatus,
|
||||
rate: tracking.rate,
|
||||
userInfo: tracking.userInfo,
|
||||
stats: tracking.stats,
|
||||
},
|
||||
library: {
|
||||
list: library.list,
|
||||
genres: library.genres,
|
||||
stats: library.stats,
|
||||
continueWatching: library.continueWatching,
|
||||
upcoming: library.upcoming,
|
||||
},
|
||||
titles: {
|
||||
detail: titles.detail,
|
||||
updateStatus: titles.updateStatus,
|
||||
updateRating: titles.updateRating,
|
||||
watchMovie: titles.watchMovie,
|
||||
watchAll: titles.watchAll,
|
||||
userInfo: titles.userInfo,
|
||||
recommendations: titles.recommendations,
|
||||
quickAdd: titles.quickAdd,
|
||||
},
|
||||
episodes: {
|
||||
watch: episodes.watch,
|
||||
unwatch: episodes.unwatch,
|
||||
batchWatch: episodes.batchWatch,
|
||||
},
|
||||
seasons: {
|
||||
watch: seasons.watch,
|
||||
unwatch: seasons.unwatch,
|
||||
discover: {
|
||||
trending: discover.trending,
|
||||
popular: discover.popular,
|
||||
search: discover.search,
|
||||
browse: discover.browse,
|
||||
genres: discover.genres,
|
||||
platforms: discover.platforms,
|
||||
recommendations: discover.recommendations,
|
||||
},
|
||||
people: {
|
||||
detail: people.detail,
|
||||
get: people.get,
|
||||
},
|
||||
dashboard: {
|
||||
stats: dashboard.stats,
|
||||
continueWatching: dashboard.continueWatching,
|
||||
upcoming: dashboard.upcoming,
|
||||
recommendations: dashboard.recommendations,
|
||||
watchHistory: dashboard.watchHistory,
|
||||
account: {
|
||||
updateName: account.updateName,
|
||||
uploadAvatar: account.uploadAvatar,
|
||||
removeAvatar: account.removeAvatar,
|
||||
platforms: account.platforms,
|
||||
updatePlatforms: account.updatePlatformsHandler,
|
||||
integrations: {
|
||||
list: account.integrationsList,
|
||||
create: account.integrationsCreate,
|
||||
delete: account.integrationsDelete,
|
||||
regenerateToken: account.integrationsRegenerateToken,
|
||||
},
|
||||
},
|
||||
explore: {
|
||||
trending: explore.trending,
|
||||
popular: explore.popular,
|
||||
genres: explore.genres,
|
||||
watchProviders: explore.watchProviders,
|
||||
},
|
||||
search,
|
||||
discover,
|
||||
system: {
|
||||
publicInfo: system.publicInfo,
|
||||
authConfig: system.authConfig,
|
||||
status: status.status,
|
||||
},
|
||||
integrations: {
|
||||
list: integrations.list,
|
||||
create: integrations.create,
|
||||
delete: integrations.deleteIntegration,
|
||||
regenerateToken: integrations.regenerateToken,
|
||||
status: system.status,
|
||||
},
|
||||
admin: {
|
||||
settings: {
|
||||
get: admin.settingsGet,
|
||||
update: admin.settingsUpdate,
|
||||
},
|
||||
backups: {
|
||||
list: admin.backupsList,
|
||||
create: admin.backupsCreate,
|
||||
@@ -78,27 +71,11 @@ export const implementedRouter = {
|
||||
schedule: admin.backupsSchedule,
|
||||
updateSchedule: admin.backupsUpdateSchedule,
|
||||
},
|
||||
registration: admin.registration,
|
||||
toggleRegistration: admin.toggleRegistration,
|
||||
updateCheck: admin.updateCheck,
|
||||
toggleUpdateCheck: admin.toggleUpdateCheck,
|
||||
telemetry: admin.telemetry,
|
||||
toggleTelemetry: admin.toggleTelemetry,
|
||||
triggerJob: admin.triggerJob,
|
||||
purgeMetadataCache: admin.purgeMetadataCache,
|
||||
purgeImageCache: admin.purgeImageCache,
|
||||
systemHealth: admin.systemHealth,
|
||||
},
|
||||
account: {
|
||||
updateName: account.updateName,
|
||||
uploadAvatar: account.uploadAvatar,
|
||||
removeAvatar: account.removeAvatar,
|
||||
platforms: account.platforms,
|
||||
updatePlatforms: account.updatePlatformsHandler,
|
||||
},
|
||||
platforms: {
|
||||
list: platformProcs.list,
|
||||
},
|
||||
imports: {
|
||||
parseFile: imports.parseFile,
|
||||
parsePayload: imports.parsePayload,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sofa/web",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -29,9 +29,9 @@
|
||||
"@sofa/i18n": "workspace:*",
|
||||
"@tabler/icons-react": "3.40.0",
|
||||
"@tanstack/react-form": "catalog:",
|
||||
"@tanstack/react-hotkeys": "0.6.0",
|
||||
"@tanstack/react-hotkeys": "0.8.1",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-router": "1.168.3",
|
||||
"@tanstack/react-router": "1.168.5",
|
||||
"@tanstack/zod-adapter": "1.166.9",
|
||||
"better-auth": "catalog:",
|
||||
"class-variance-authority": "0.7.1",
|
||||
@@ -57,7 +57,7 @@
|
||||
"@tanstack/react-devtools": "0.10.0",
|
||||
"@tanstack/react-query-devtools": "5.95.2",
|
||||
"@tanstack/react-router-devtools": "1.166.11",
|
||||
"@tanstack/router-plugin": "1.167.4",
|
||||
"@tanstack/router-plugin": "1.167.6",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
@@ -68,7 +68,7 @@
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "8.0.2",
|
||||
"vite": "8.0.3",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ export function CommandPalette() {
|
||||
const debouncedQuery = useDebounce(query, 300);
|
||||
const trimmedQuery = debouncedQuery.trim();
|
||||
const { data: searchData, isLoading: loading } = useQuery(
|
||||
orpc.search.queryOptions({
|
||||
orpc.discover.search.queryOptions({
|
||||
input: trimmedQuery ? { query: trimmedQuery } : skipToken,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ContinueWatchingList, ContinueWatchingSectionSkeleton } from "./continu
|
||||
import { FeedSection } from "./feed-section";
|
||||
|
||||
export function ContinueWatchingSection() {
|
||||
const { data, isPending } = useQuery(orpc.dashboard.continueWatching.queryOptions());
|
||||
const { data, isPending } = useQuery(orpc.library.continueWatching.queryOptions());
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FeedSection } from "./feed-section";
|
||||
import { TitleGrid, TitleGridSectionSkeleton } from "./title-grid";
|
||||
|
||||
export function RecommendationsSection() {
|
||||
const { data, isPending } = useQuery(orpc.dashboard.recommendations.queryOptions());
|
||||
const { data, isPending } = useQuery(orpc.discover.recommendations.queryOptions());
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
|
||||
@@ -109,8 +109,6 @@ export function Sparkline({ data, color }: SparklineProps) {
|
||||
return computeMonotonePath(points, size.height);
|
||||
}, [data, size.width, size.height]);
|
||||
|
||||
if (!data.some((d) => d.count > 0)) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { orpc } from "@/lib/orpc/client";
|
||||
import type { DashboardStats, HistoryBucket, TimePeriod } from "@sofa/api/schemas";
|
||||
import type { HistoryBucket, LibraryStats, TimePeriod } from "@sofa/api/schemas";
|
||||
|
||||
import { Sparkline } from "./sparkline";
|
||||
|
||||
@@ -108,7 +108,7 @@ function PeriodSelect({
|
||||
{(value: TimePeriod | null) => (value ? periodLabels[value] : null)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start" alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent align="start" className="p-1">
|
||||
{periods.map((p) => (
|
||||
<SelectItem key={p} value={p}>
|
||||
{periodLabels[p]}
|
||||
@@ -152,27 +152,38 @@ function PeriodSelector({
|
||||
);
|
||||
}
|
||||
|
||||
export function StatsDisplay({ stats }: { stats: DashboardStats }) {
|
||||
interface WatchStats {
|
||||
count: number;
|
||||
history: HistoryBucket[];
|
||||
}
|
||||
|
||||
interface StatsDisplayProps {
|
||||
movieStats: WatchStats;
|
||||
episodeStats: WatchStats;
|
||||
libraryStats: LibraryStats;
|
||||
}
|
||||
|
||||
export function StatsDisplay({ movieStats, episodeStats, libraryStats }: StatsDisplayProps) {
|
||||
const { t } = useLingui();
|
||||
const [moviePeriod, setMoviePeriod] = useState<TimePeriod>("this_month");
|
||||
const [episodePeriod, setEpisodePeriod] = useState<TimePeriod>("this_week");
|
||||
|
||||
const { data: movieStats } = useQuery(
|
||||
orpc.dashboard.watchHistory.queryOptions({
|
||||
const { data: moviePeriodStats } = useQuery(
|
||||
orpc.tracking.stats.queryOptions({
|
||||
input: { type: "movie", period: moviePeriod },
|
||||
}),
|
||||
);
|
||||
const { data: episodeStats } = useQuery(
|
||||
orpc.dashboard.watchHistory.queryOptions({
|
||||
const { data: episodePeriodStats } = useQuery(
|
||||
orpc.tracking.stats.queryOptions({
|
||||
input: { type: "episode", period: episodePeriod },
|
||||
}),
|
||||
);
|
||||
|
||||
const movieCount = movieStats?.count ?? stats.moviesThisMonth;
|
||||
const movieHistory = movieStats?.history;
|
||||
const movieCount = moviePeriodStats?.count ?? movieStats.count;
|
||||
const movieHistory = moviePeriodStats?.history ?? movieStats.history;
|
||||
|
||||
const episodeCount = episodeStats?.count ?? stats.episodesThisWeek;
|
||||
const episodeHistory = episodeStats?.history;
|
||||
const episodeCount = episodePeriodStats?.count ?? episodeStats.count;
|
||||
const episodeHistory = episodePeriodStats?.history ?? episodeStats.history;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
@@ -206,7 +217,7 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
|
||||
icon={IconBooks}
|
||||
color="text-status-watchlist"
|
||||
bgColor="bg-status-watchlist/10"
|
||||
value={stats.librarySize}
|
||||
value={libraryStats.size}
|
||||
index={2}
|
||||
label={t`In Library`}
|
||||
/>
|
||||
@@ -214,7 +225,7 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
|
||||
icon={IconCheck}
|
||||
color="text-status-completed"
|
||||
bgColor="bg-status-completed/10"
|
||||
value={stats.completed}
|
||||
value={libraryStats.completed}
|
||||
index={3}
|
||||
label={t`Completed`}
|
||||
/>
|
||||
|
||||
@@ -8,20 +8,34 @@ import { orpc } from "@/lib/orpc/client";
|
||||
import { StatsDisplay, StatsSectionSkeleton } from "./stats-display";
|
||||
|
||||
export function StatsSection() {
|
||||
const { data: stats, isPending } = useQuery(orpc.dashboard.stats.queryOptions());
|
||||
const { data: movieStats, isPending: moviesPending } = useQuery(
|
||||
orpc.tracking.stats.queryOptions({ input: { type: "movie", period: "this_month" } }),
|
||||
);
|
||||
const { data: episodeStats, isPending: episodesPending } = useQuery(
|
||||
orpc.tracking.stats.queryOptions({ input: { type: "episode", period: "this_week" } }),
|
||||
);
|
||||
const { data: libraryStats, isPending: libraryPending } = useQuery(
|
||||
orpc.library.stats.queryOptions(),
|
||||
);
|
||||
|
||||
const isPending = moviesPending || episodesPending || libraryPending;
|
||||
|
||||
if (isPending) return <StatsSectionSkeleton />;
|
||||
if (!stats) return null;
|
||||
if (!movieStats || !episodeStats || !libraryStats) return null;
|
||||
|
||||
const isEmpty =
|
||||
stats.moviesThisMonth === 0 &&
|
||||
stats.episodesThisWeek === 0 &&
|
||||
stats.librarySize === 0 &&
|
||||
stats.completed === 0;
|
||||
movieStats.count === 0 &&
|
||||
episodeStats.count === 0 &&
|
||||
libraryStats.size === 0 &&
|
||||
libraryStats.completed === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatsDisplay stats={stats} />
|
||||
<StatsDisplay
|
||||
movieStats={movieStats}
|
||||
episodeStats={episodeStats}
|
||||
libraryStats={libraryStats}
|
||||
/>
|
||||
{isEmpty && (
|
||||
<div className="border-border/50 flex flex-col items-center gap-4 rounded-xl border border-dashed py-16 text-center">
|
||||
<div className="bg-primary/10 rounded-full p-4">
|
||||
|
||||
@@ -9,7 +9,7 @@ import { UpcomingRow } from "./upcoming-item";
|
||||
|
||||
export function UpcomingSection() {
|
||||
const { data, isPending } = useQuery(
|
||||
orpc.dashboard.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
|
||||
orpc.library.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
|
||||
);
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -67,14 +67,13 @@ export function DiscoverSection() {
|
||||
| "primary_release_date.asc";
|
||||
const [sortBy, setSortBy] = useState<DiscoverSortBy | undefined>(undefined);
|
||||
const [language, setLanguage] = useState<string | undefined>(undefined);
|
||||
const [providerId, setProviderId] = useState<number | undefined>(undefined);
|
||||
const [selectedPlatformId, setSelectedPlatformId] = useState("");
|
||||
const [platformId, setPlatformId] = useState<string | undefined>(undefined);
|
||||
|
||||
const { data: genreData } = useQuery(orpc.explore.genres.queryOptions({ input: { type } }));
|
||||
const { data: providerData } = useQuery(orpc.platforms.list.queryOptions());
|
||||
const { data: genreData } = useQuery(orpc.discover.genres.queryOptions({ input: { type } }));
|
||||
const { data: providerData } = useQuery(orpc.discover.platforms.queryOptions());
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isPending } = useInfiniteQuery(
|
||||
orpc.discover.infiniteOptions({
|
||||
orpc.discover.browse.infiniteOptions({
|
||||
input: (pageParam: number) => ({
|
||||
type,
|
||||
genreId,
|
||||
@@ -83,7 +82,7 @@ export function DiscoverSection() {
|
||||
ratingMin,
|
||||
sortBy,
|
||||
language,
|
||||
providerId,
|
||||
platformId,
|
||||
page: pageParam,
|
||||
}),
|
||||
initialPageParam: 1,
|
||||
@@ -154,14 +153,7 @@ export function DiscoverSection() {
|
||||
}
|
||||
|
||||
function handleProviderChange(value: string | null) {
|
||||
setSelectedPlatformId(value ?? "");
|
||||
if (!value) {
|
||||
setProviderId(undefined);
|
||||
return;
|
||||
}
|
||||
// Resolve platform UUID to TMDB provider ID for the discover API
|
||||
const platform = providers.find((p) => p.id === value);
|
||||
setProviderId(platform?.tmdbProviderId ?? undefined);
|
||||
setPlatformId(value || undefined);
|
||||
}
|
||||
|
||||
function handleGenreChange(value: string | null) {
|
||||
@@ -183,8 +175,7 @@ export function DiscoverSection() {
|
||||
if (next === "movie" || next === "tv") {
|
||||
setType(next);
|
||||
setGenreId(undefined);
|
||||
setProviderId(undefined);
|
||||
setSelectedPlatformId("");
|
||||
setPlatformId(undefined);
|
||||
}
|
||||
}}
|
||||
variant="outline"
|
||||
@@ -217,7 +208,7 @@ export function DiscoverSection() {
|
||||
}}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent className="p-1">
|
||||
<SelectItem value="">{t`All genres`}</SelectItem>
|
||||
{genres.map((genre) => (
|
||||
<SelectItem key={genre.id} value={String(genre.id)}>
|
||||
@@ -248,7 +239,7 @@ export function DiscoverSection() {
|
||||
}}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent className="p-1">
|
||||
<SelectItem value="">{t`Any year`}</SelectItem>
|
||||
{DECADE_PRESETS.map((d) => (
|
||||
<SelectItem key={d.min} value={String(d.min)}>
|
||||
@@ -277,7 +268,7 @@ export function DiscoverSection() {
|
||||
}}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent className="p-1">
|
||||
<SelectItem value="">{t`Any rating`}</SelectItem>
|
||||
{RATING_PRESETS.map((r) => (
|
||||
<SelectItem key={r.value} value={String(r.value)}>
|
||||
@@ -306,7 +297,7 @@ export function DiscoverSection() {
|
||||
}}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent className="p-1">
|
||||
<SelectItem value="">{t`Default`}</SelectItem>
|
||||
{SORT_OPTIONS.map((s) => (
|
||||
<SelectItem key={s.value} value={s.value}>
|
||||
@@ -335,7 +326,7 @@ export function DiscoverSection() {
|
||||
}}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent className="p-1">
|
||||
<SelectItem value="">{t`Any language`}</SelectItem>
|
||||
{LANGUAGE_OPTIONS.map((lang) => (
|
||||
<SelectItem key={lang.code} value={lang.code}>
|
||||
@@ -347,14 +338,14 @@ export function DiscoverSection() {
|
||||
|
||||
{/* Provider select */}
|
||||
<Select
|
||||
value={selectedPlatformId}
|
||||
value={platformId ?? ""}
|
||||
onValueChange={handleProviderChange}
|
||||
modal={false}
|
||||
aria-label={t`Provider`}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
data-active={selectedPlatformId ? "" : undefined}
|
||||
data-active={platformId ? "" : undefined}
|
||||
className="data-[active]:border-primary/40 data-[active]:text-foreground"
|
||||
>
|
||||
<SelectValue>
|
||||
@@ -365,10 +356,10 @@ export function DiscoverSection() {
|
||||
}}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent className="p-1">
|
||||
<SelectItem value="">{t`All providers`}</SelectItem>
|
||||
{providers
|
||||
.filter((p) => p.tmdbProviderId != null)
|
||||
.filter((p) => p.tmdbProviderIds.length > 0)
|
||||
.map((platform) => (
|
||||
<SelectItem key={platform.id} value={platform.id}>
|
||||
{platform.name}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function FilterableTitleRow({
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useInfiniteQuery(
|
||||
orpc.discover.infiniteOptions({
|
||||
orpc.discover.browse.infiniteOptions({
|
||||
input:
|
||||
selectedGenre != null
|
||||
? (pageParam: number) => ({
|
||||
|
||||
@@ -206,7 +206,7 @@ export function LibraryFilters({
|
||||
}}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent className="p-1">
|
||||
<SelectItem value="">{t`All genres`}</SelectItem>
|
||||
{genreData?.genres.map((genre) => (
|
||||
<SelectItem key={genre.id} value={String(genre.id)}>
|
||||
@@ -235,7 +235,7 @@ export function LibraryFilters({
|
||||
}}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent className="p-1">
|
||||
<SelectItem value="">{t`Any`}</SelectItem>
|
||||
<SelectItem value="1">1★+</SelectItem>
|
||||
<SelectItem value="2">2★+</SelectItem>
|
||||
@@ -261,14 +261,14 @@ export function LibraryFilters({
|
||||
}}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent className="p-1">
|
||||
<SelectItem value="">{t`Any year`}</SelectItem>
|
||||
{DECADES.map((d) => (
|
||||
<SelectItem key={d.yearMin} value={String(d.yearMin)}>
|
||||
{d.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="older">{t`Pre-1980`}</SelectItem>
|
||||
<SelectItem value="older">{t`70s and earlier`}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -286,7 +286,7 @@ export function LibraryFilters({
|
||||
>
|
||||
<SelectValue>{(value: string | null) => (value ? value : t`Age`)}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent className="p-1">
|
||||
<SelectItem value="">{t`All`}</SelectItem>
|
||||
{CONTENT_RATINGS.map((rating) => (
|
||||
<SelectItem key={rating} value={rating}>
|
||||
|
||||
@@ -68,20 +68,20 @@ export function LibraryToolbar({
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder={t`Search library...`}
|
||||
className="pl-7"
|
||||
className="py-3 pl-7"
|
||||
aria-label={t`Search library`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
<span className="text-muted-foreground text-[13px]">
|
||||
{plural(totalResults, { one: "# result", other: "# results" })}
|
||||
</span>
|
||||
|
||||
{/* Filter toggle */}
|
||||
<CollapsibleTrigger
|
||||
render={
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline">
|
||||
<IconFilter aria-hidden={true} className="size-3.5" />
|
||||
{t`Filters`}
|
||||
{activeFilterCount > 0 && (
|
||||
@@ -101,7 +101,7 @@ export function LibraryToolbar({
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline">
|
||||
<IconSortDescending aria-hidden={true} className="size-3.5" />
|
||||
{t`Sort`}
|
||||
</Button>
|
||||
|
||||
@@ -82,7 +82,7 @@ export function FilmographyGrid({ credits, userStatuses }: FilmographyGridProps)
|
||||
}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end" alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent align="end" className="p-1">
|
||||
<SelectItem value="newest">
|
||||
<Trans>Newest</Trans>
|
||||
</SelectItem>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function PersonDetailSkeleton() {
|
||||
|
||||
export function PersonDetailClient({ id }: { id: string }) {
|
||||
const { data, isPending, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery(
|
||||
orpc.people.detail.infiniteOptions({
|
||||
orpc.people.get.infiniteOptions({
|
||||
input: (pageParam: number) => ({ id, page: pageParam, limit: 20 }),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) =>
|
||||
|
||||
@@ -25,7 +25,7 @@ export function PlatformGrid({ platforms, selectedIds, onToggle }: PlatformGridP
|
||||
className={`group relative flex flex-col items-center gap-2 rounded-xl border p-3 motion-safe:transition-colors motion-safe:duration-150 ${
|
||||
isSelected
|
||||
? "border-primary/50 bg-primary/8"
|
||||
: "border-border/20 hover:border-primary/30 hover:bg-primary/5"
|
||||
: "border-border/50 hover:border-primary/30 hover:bg-primary/5"
|
||||
}`}
|
||||
aria-label={(() => {
|
||||
const name = platform.name;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
IconLogout,
|
||||
IconPencil,
|
||||
IconTrash,
|
||||
IconUser,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
@@ -164,208 +163,197 @@ export function AccountSection({
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<IconUser aria-hidden={true} className="text-muted-foreground size-4" />
|
||||
<h2 className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
|
||||
<Trans>Account</Trans>
|
||||
</h2>
|
||||
</div>
|
||||
<Card className="pb-0">
|
||||
<CardContent className="flex items-center gap-4">
|
||||
{/* Avatar: click to upload (no avatar) or remove (has avatar) */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
onClick={avatarUrl ? handleRemoveAvatar : () => fileInputRef.current?.click()}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
disabled={isAvatarPending}
|
||||
/>
|
||||
}
|
||||
className="focus-visible:ring-ring focus-visible:ring-offset-background relative shrink-0 cursor-pointer rounded-full focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||
aria-label={avatarUrl ? t`Remove profile picture` : t`Upload profile picture`}
|
||||
>
|
||||
<Avatar className="size-12 overflow-hidden">
|
||||
<AvatarImage src={isAvatarPending ? undefined : avatarUrl} alt={displayName} />
|
||||
<AvatarFallback className="bg-primary/10 font-display text-primary text-lg">
|
||||
{initial}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<Card className="pb-0">
|
||||
<CardContent className="flex items-center gap-4">
|
||||
{/* Avatar: click to upload (no avatar) or remove (has avatar) */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
onClick={avatarUrl ? handleRemoveAvatar : () => fileInputRef.current?.click()}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
disabled={isAvatarPending}
|
||||
/>
|
||||
}
|
||||
className="focus-visible:ring-ring focus-visible:ring-offset-background relative shrink-0 cursor-pointer rounded-full focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||
aria-label={avatarUrl ? t`Remove profile picture` : t`Upload profile picture`}
|
||||
>
|
||||
<Avatar className="size-12 overflow-hidden">
|
||||
<AvatarImage src={isAvatarPending ? undefined : avatarUrl} alt={displayName} />
|
||||
<AvatarFallback className="bg-primary/10 font-display text-primary text-lg">
|
||||
{initial}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<AnimatePresence>
|
||||
{(isHovered || isAvatarPending) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className={`text-foreground/70 absolute inset-0 flex items-center justify-center rounded-full backdrop-blur-sm ${
|
||||
avatarUrl && !isAvatarPending ? "bg-destructive/40" : "bg-black/50"
|
||||
}`}
|
||||
>
|
||||
{isAvatarPending ? (
|
||||
<Spinner className="size-4.5" />
|
||||
) : avatarUrl ? (
|
||||
<IconTrash className="size-4.5" />
|
||||
) : (
|
||||
<IconCamera className="size-4.5" />
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{avatarUrl ? <Trans>Remove picture</Trans> : <Trans>Upload picture</Trans>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp,image/gif"
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<CardTitle className="mb-0.5">
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{isEditingName ? (
|
||||
<motion.div
|
||||
key="editing"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
<div className="relative inline-grid items-center">
|
||||
<span
|
||||
className="invisible col-start-1 row-start-1 text-sm font-medium whitespace-pre"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{editValue || " "}
|
||||
</span>
|
||||
<input
|
||||
ref={nameInputRef}
|
||||
type="text"
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleNameKeyDown}
|
||||
onBlur={handleNameSave}
|
||||
disabled={isNamePending}
|
||||
maxLength={100}
|
||||
className="border-primary/40 focus:border-primary col-start-1 row-start-1 min-w-4 border-0 border-b border-dashed bg-transparent text-sm font-medium transition-colors outline-none"
|
||||
/>
|
||||
</div>
|
||||
{isNamePending ? (
|
||||
<Spinner className="text-muted-foreground size-3.5 shrink-0" />
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
handleNameSave();
|
||||
}}
|
||||
className="text-muted-foreground hover:text-primary shrink-0 rounded-md p-0.5 transition-colors"
|
||||
aria-label={t`Save name`}
|
||||
>
|
||||
<IconCheck className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
handleNameCancel();
|
||||
}}
|
||||
className="text-muted-foreground hover:text-destructive shrink-0 rounded-md p-0.5 transition-colors"
|
||||
aria-label={t`Cancel editing`}
|
||||
>
|
||||
<IconX className="size-3.5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.button
|
||||
key="display"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
type="button"
|
||||
onClick={() => setIsEditingName(true)}
|
||||
className="group/name hover:text-primary inline-flex items-center gap-1.5 rounded-md px-0 text-start transition-colors"
|
||||
>
|
||||
{displayName}
|
||||
<IconPencil className="group-hover/name:text-muted-foreground size-3 text-transparent transition-colors" />
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{user.email}
|
||||
{user.role === "admin" && (
|
||||
<Badge className="bg-primary/10 text-primary ms-1.5 rounded-md border-0 align-middle">
|
||||
<Trans>Admin</Trans>
|
||||
</Badge>
|
||||
<AnimatePresence>
|
||||
{(isHovered || isAvatarPending) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className={`text-foreground/70 absolute inset-0 flex items-center justify-center rounded-full backdrop-blur-sm ${
|
||||
avatarUrl && !isAvatarPending ? "bg-destructive/40" : "bg-black/50"
|
||||
}`}
|
||||
>
|
||||
{isAvatarPending ? (
|
||||
<Spinner className="size-4.5" />
|
||||
) : avatarUrl ? (
|
||||
<IconTrash className="size-4.5" />
|
||||
) : (
|
||||
<IconCamera className="size-4.5" />
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</CardDescription>
|
||||
<p className="text-muted-foreground/60 mt-0.5 text-xs">
|
||||
<Trans>Member since {memberSince}</Trans>
|
||||
</AnimatePresence>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{avatarUrl ? <Trans>Remove picture</Trans> : <Trans>Upload picture</Trans>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp,image/gif"
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<CardTitle className="mb-0.5">
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{isEditingName ? (
|
||||
<motion.div
|
||||
key="editing"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
<div className="relative inline-grid items-center">
|
||||
<span
|
||||
className="invisible col-start-1 row-start-1 text-sm font-medium whitespace-pre"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{editValue || " "}
|
||||
</span>
|
||||
<input
|
||||
ref={nameInputRef}
|
||||
type="text"
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleNameKeyDown}
|
||||
onBlur={handleNameSave}
|
||||
disabled={isNamePending}
|
||||
maxLength={100}
|
||||
className="border-primary/40 focus:border-primary col-start-1 row-start-1 min-w-4 border-0 border-b border-dashed bg-transparent text-sm font-medium transition-colors outline-none"
|
||||
/>
|
||||
</div>
|
||||
{isNamePending ? (
|
||||
<Spinner className="text-muted-foreground size-3.5 shrink-0" />
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
handleNameSave();
|
||||
}}
|
||||
className="text-muted-foreground hover:text-primary shrink-0 rounded-md p-0.5 transition-colors"
|
||||
aria-label={t`Save name`}
|
||||
>
|
||||
<IconCheck className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
handleNameCancel();
|
||||
}}
|
||||
className="text-muted-foreground hover:text-destructive shrink-0 rounded-md p-0.5 transition-colors"
|
||||
aria-label={t`Cancel editing`}
|
||||
>
|
||||
<IconX className="size-3.5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.button
|
||||
key="display"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
type="button"
|
||||
onClick={() => setIsEditingName(true)}
|
||||
className="group/name hover:text-primary inline-flex items-center gap-1.5 rounded-md px-0 text-start transition-colors"
|
||||
>
|
||||
{displayName}
|
||||
<IconPencil className="group-hover/name:text-muted-foreground size-3 text-transparent transition-colors" />
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{user.email}
|
||||
{user.role === "admin" && (
|
||||
<Badge className="bg-primary/10 text-primary ms-1.5 rounded-md border-0 align-middle">
|
||||
<Trans>Admin</Trans>
|
||||
</Badge>
|
||||
)}
|
||||
</CardDescription>
|
||||
<p className="text-muted-foreground/60 mt-0.5 text-xs">
|
||||
<Trans>Member since {memberSince}</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end gap-2 sm:flex-row sm:items-center">
|
||||
<ChangePasswordDialog />
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={async () => {
|
||||
await signOut();
|
||||
void navigate({ to: "/" });
|
||||
}}
|
||||
>
|
||||
<IconLogout aria-hidden={true} />
|
||||
<Trans>Sign out</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<div className="border-border/30 space-y-px border-t">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
window.location.href = "/api/export/user-data";
|
||||
}}
|
||||
className="group hover:bg-muted/40 flex w-full items-center gap-3 px-4 py-3 text-start transition-colors"
|
||||
>
|
||||
<div className="bg-muted group-hover:bg-primary/10 flex size-7.5 shrink-0 items-center justify-center rounded-lg">
|
||||
<IconCloudDownload
|
||||
aria-hidden={true}
|
||||
className="text-muted-foreground group-hover:text-primary size-3.5"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<p className="text-[13px] leading-none font-medium">
|
||||
<Trans>Export</Trans>
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[11px]">
|
||||
<Trans>Download your library, watch history, and ratings as JSON</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end gap-2 sm:flex-row sm:items-center">
|
||||
<ChangePasswordDialog />
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={async () => {
|
||||
await signOut();
|
||||
void navigate({ to: "/" });
|
||||
}}
|
||||
>
|
||||
<IconLogout aria-hidden={true} />
|
||||
<Trans>Sign out</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<div className="border-border/30 space-y-px border-t">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
window.location.href = "/api/export/user-data";
|
||||
}}
|
||||
className="group hover:bg-muted/40 flex w-full items-center gap-3 px-4 py-3 text-start transition-colors"
|
||||
>
|
||||
<div className="bg-muted group-hover:bg-primary/10 flex size-7.5 shrink-0 items-center justify-center rounded-lg">
|
||||
<IconCloudDownload
|
||||
aria-hidden={true}
|
||||
className="text-muted-foreground group-hover:text-primary size-3.5"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<p className="text-[13px] leading-none font-medium">
|
||||
<Trans>Export</Trans>
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[11px]">
|
||||
<Trans>Download your library, watch history, and ratings as JSON</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<IconArrowRight
|
||||
aria-hidden={true}
|
||||
className="text-muted-foreground size-3.5 shrink-0"
|
||||
/>
|
||||
</button>
|
||||
<SofaImportDialog />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<IconArrowRight aria-hidden={true} className="text-muted-foreground size-3.5 shrink-0" />
|
||||
</button>
|
||||
<SofaImportDialog />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ export function BackupScheduleSection() {
|
||||
}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start" alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent align="start" className="p-1">
|
||||
{[3, 5, 7, 14, 30, 0].map((n) => (
|
||||
<SelectItem key={n} value={String(n)}>
|
||||
{n === 0 ? t`unlimited` : t`last ${n}`}
|
||||
@@ -319,7 +319,7 @@ export function BackupScheduleSection() {
|
||||
}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start" alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent align="start" className="p-1">
|
||||
{DAYS_OF_WEEK.map((day, i) => (
|
||||
<SelectItem key={day} value={String(i)}>
|
||||
{day}
|
||||
@@ -368,7 +368,7 @@ export function BackupScheduleSection() {
|
||||
}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start" alignItemWithTrigger={false} className="p-1">
|
||||
<SelectContent align="start" className="p-1">
|
||||
{HOURS.map((h) => {
|
||||
const val = `${String(h).padStart(2, "0")}:00`;
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { IconCloudUpload, IconFileImport, IconLink } from "@tabler/icons-react";
|
||||
import { IconCloudUpload, IconLink } from "@tabler/icons-react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -99,7 +99,7 @@ const SOURCES: SourceConfig[] = [
|
||||
{
|
||||
source: "trakt",
|
||||
label: "Trakt",
|
||||
description: "Connect your Trakt account or upload a JSON export.",
|
||||
description: "Connect your Trakt account or upload a JSON export",
|
||||
accept: ".json",
|
||||
icon: <TraktLogo className="text-primary size-4" />,
|
||||
supportsOAuth: true,
|
||||
@@ -107,7 +107,7 @@ const SOURCES: SourceConfig[] = [
|
||||
{
|
||||
source: "simkl",
|
||||
label: "Simkl",
|
||||
description: "Connect your Simkl account or upload a JSON export.",
|
||||
description: "Connect your Simkl account or upload a JSON export",
|
||||
accept: ".json",
|
||||
icon: <SimklLogo className="text-primary size-4" />,
|
||||
supportsOAuth: true,
|
||||
@@ -115,7 +115,7 @@ const SOURCES: SourceConfig[] = [
|
||||
{
|
||||
source: "letterboxd",
|
||||
label: "Letterboxd",
|
||||
description: "Upload the ZIP export from your Letterboxd account settings.",
|
||||
description: "Upload the ZIP export from your Letterboxd account settings",
|
||||
accept: ".zip",
|
||||
icon: <LetterboxdLogo className="text-primary size-4" />,
|
||||
supportsOAuth: false,
|
||||
@@ -168,18 +168,10 @@ type DialogStep =
|
||||
|
||||
export function ImportsSection() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<IconFileImport aria-hidden className="text-muted-foreground size-4" />
|
||||
<h2 className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
|
||||
<Trans>Import</Trans>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-2.5">
|
||||
{SOURCES.map((config) => (
|
||||
<ImportSourceCard key={config.source} config={config} />
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-2.5">
|
||||
{SOURCES.map((config) => (
|
||||
<ImportSourceCard key={config.source} config={config} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export function IntegrationCard({
|
||||
const providerInput = provider as "plex" | "jellyfin" | "emby" | "sonarr" | "radarr";
|
||||
|
||||
const connectMutation = useMutation(
|
||||
orpc.integrations.create.mutationOptions({
|
||||
orpc.account.integrations.create.mutationOptions({
|
||||
onSuccess: (result) => {
|
||||
setConnections((prev) => [...prev, { ...result, recentEvents: [] }]);
|
||||
toast.success(t`${label} connected`);
|
||||
@@ -93,7 +93,7 @@ export function IntegrationCard({
|
||||
);
|
||||
|
||||
const deleteMutation = useMutation(
|
||||
orpc.integrations.delete.mutationOptions({
|
||||
orpc.account.integrations.delete.mutationOptions({
|
||||
onMutate: () => {
|
||||
let previous: IntegrationConnection[] = [];
|
||||
setConnections((prev) => {
|
||||
@@ -111,7 +111,7 @@ export function IntegrationCard({
|
||||
);
|
||||
|
||||
const regenerateTokenMutation = useMutation(
|
||||
orpc.integrations.regenerateToken.mutationOptions({
|
||||
orpc.account.integrations.regenerateToken.mutationOptions({
|
||||
onSuccess: (result) => {
|
||||
setConnections((prev) =>
|
||||
prev.map((c) => (c.provider === provider ? { ...c, token: result.token } : c)),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { IconWebhook } from "@tabler/icons-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -10,7 +8,7 @@ import { IntegrationCard, type IntegrationConnection } from "./integration-card"
|
||||
import { INTEGRATION_CONFIGS } from "./integration-configs";
|
||||
|
||||
export function IntegrationsSection() {
|
||||
const { data, isPending } = useQuery(orpc.integrations.list.queryOptions());
|
||||
const { data, isPending } = useQuery(orpc.account.integrations.list.queryOptions());
|
||||
const [localConnections, setLocalConnections] = useState<IntegrationConnection[] | null>(null);
|
||||
|
||||
// Use local state if user has modified connections, else use query data
|
||||
@@ -25,35 +23,24 @@ export function IntegrationsSection() {
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<IconWebhook aria-hidden={true} className="text-muted-foreground size-4" />
|
||||
<h2 className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
|
||||
<Trans>Integrations</Trans>
|
||||
</h2>
|
||||
</div>
|
||||
{isPending ? (
|
||||
<div className="space-y-2.5">
|
||||
{INTEGRATION_CONFIGS.map((c) => (
|
||||
<Skeleton key={c.provider} className="h-20 w-full rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2.5">
|
||||
{INTEGRATION_CONFIGS.map((config) => (
|
||||
<IntegrationCard
|
||||
key={config.provider}
|
||||
config={config}
|
||||
connection={
|
||||
connections.find((c: IntegrationConnection) => c.provider === config.provider) ??
|
||||
null
|
||||
}
|
||||
setConnections={handleSetConnections}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
return isPending ? (
|
||||
<div className="space-y-2.5">
|
||||
{INTEGRATION_CONFIGS.map((c) => (
|
||||
<Skeleton key={c.provider} className="h-20 w-full rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2.5">
|
||||
{INTEGRATION_CONFIGS.map((config) => (
|
||||
<IntegrationCard
|
||||
key={config.provider}
|
||||
config={config}
|
||||
connection={
|
||||
connections.find((c: IntegrationConnection) => c.provider === config.provider) ?? null
|
||||
}
|
||||
setConnections={handleSetConnections}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ import { orpc } from "@/lib/orpc/client";
|
||||
|
||||
export function RegistrationSection() {
|
||||
const { t } = useLingui();
|
||||
const { data, isPending: isLoading } = useQuery(orpc.admin.registration.queryOptions());
|
||||
const { data, isPending: isLoading } = useQuery(orpc.admin.settings.get.queryOptions());
|
||||
const [registrationOpen, setRegistrationOpen] = useState<boolean | null>(null);
|
||||
const currentOpen = registrationOpen ?? data?.open ?? false;
|
||||
const currentOpen = registrationOpen ?? data?.registration.open ?? false;
|
||||
const [optimisticOpen, setOptimisticOpen] = useOptimistic(currentOpen);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const toggleMutation = useMutation(orpc.admin.toggleRegistration.mutationOptions());
|
||||
const toggleMutation = useMutation(orpc.admin.settings.update.mutationOptions());
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -30,7 +30,7 @@ export function RegistrationSection() {
|
||||
startTransition(async () => {
|
||||
setOptimisticOpen(checked);
|
||||
try {
|
||||
await toggleMutation.mutateAsync({ open: checked });
|
||||
await toggleMutation.mutateAsync({ registration: { open: checked } });
|
||||
setRegistrationOpen(checked);
|
||||
toast.success(checked ? t`Registration opened` : t`Registration closed`);
|
||||
} catch {
|
||||
|
||||
@@ -15,7 +15,7 @@ const sectionVariants = {
|
||||
export function SettingsShell({ children, footer }: { children: ReactNode; footer?: ReactNode }) {
|
||||
return (
|
||||
<motion.div
|
||||
className="mx-auto max-w-2xl space-y-6"
|
||||
className="mx-auto max-w-2xl space-y-8"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { IconChevronDown, IconDeviceTv } from "@tabler/icons-react";
|
||||
import { IconChevronDown, IconCircleCheck, IconDeviceTv } from "@tabler/icons-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { SVGProps, useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { PlatformGrid } from "@/components/platforms/platform-grid";
|
||||
import { Card, CardContent, CardDescription, CardTitle } from "@/components/ui/card";
|
||||
@@ -20,7 +20,7 @@ export function StreamingServicesSection() {
|
||||
const saveCounterRef = useRef(0);
|
||||
const initialized = useRef(false);
|
||||
|
||||
const platformsQuery = useQuery(orpc.platforms.list.queryOptions());
|
||||
const platformsQuery = useQuery(orpc.discover.platforms.queryOptions());
|
||||
const userPlatformsQuery = useQuery(orpc.account.platforms.queryOptions());
|
||||
|
||||
// Initialize selected IDs from server data
|
||||
@@ -86,26 +86,27 @@ export function StreamingServicesSection() {
|
||||
</div>
|
||||
<div className="text-start">
|
||||
<CardTitle>
|
||||
<Trans>Streaming Services</Trans>
|
||||
<Trans>Subscriptions</Trans>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{selectedCount > 0 ? (
|
||||
t`${selectedCount} selected`
|
||||
) : (
|
||||
<Trans>Choose your subscriptions</Trans>
|
||||
<Trans>Choose your preferred streaming platforms</Trans>
|
||||
)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<AnimatePresence>
|
||||
{saved && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, x: 4 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 4 }}
|
||||
className="text-primary text-xs"
|
||||
className="text-primary flex items-center gap-1.5 text-xs"
|
||||
>
|
||||
<IconCircleCheck className="size-3.5" />
|
||||
<Trans>Saved</Trans>
|
||||
</motion.span>
|
||||
)}
|
||||
@@ -121,9 +122,38 @@ export function StreamingServicesSection() {
|
||||
<CollapsibleContent className="h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0">
|
||||
<CardContent className="border-border/30 border-t pt-4">
|
||||
<PlatformGrid platforms={platforms} selectedIds={selectedIds} onToggle={handleToggle} />
|
||||
<div className="text-muted-foreground mt-4 text-center text-xs">
|
||||
<a
|
||||
href="https://justwatch.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-primary"
|
||||
>
|
||||
<Trans>
|
||||
Streaming data provided by{" "}
|
||||
<JustWatchLogo
|
||||
className="mx-0.5 inline-block size-3.5 translate-y-[-1px] text-[#FBD446]"
|
||||
aria-hidden={true}
|
||||
/>
|
||||
<span className="font-semibold">JustWatch</span>
|
||||
</Trans>
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function JustWatchLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
{/* Icon from Custom Brand Icons by Emanuele & rchiileea - https://github.com/elax46/custom-brand-icons/blob/main/LICENSE */}
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M11.727 11.7L8.1 9.85s-.646-.344-.617.38s0 3.591 0 3.591s-.1.767.612.427s3.094-1.571 3.627-1.841c.715-.367.005-.707.005-.707m-.006 5.023l-3.628-1.854s-.647-.345-.618.38s0 3.591 0 3.591s-.1.768.613.427s3.093-1.571 3.626-1.841c.717-.367.007-.703.007-.703m4.953-2.464l-3.628-1.854s-.647-.344-.617.38s-.005 3.591-.005 3.591s-.1.768.612.427s3.094-1.57 3.626-1.841c.722-.362.012-.703.012-.703m4.205-2.918L17.844 9.8c-.506-.257-.457.333-.457.333v3.824s-.054.564.468.3l3.025-1.526c1.398-.702-.001-1.39-.001-1.39m-4.203-2.229l-3.629-1.855s-.647-.343-.617.38s0 3.592 0 3.592s-.1.767.612.427s3.094-1.571 3.626-1.841c.717-.367.008-.703.008-.703M7.012 4.257s-1.494-.74-2.945-1.479s-1.539.693-1.539.693v3.128c0 .369.373.217.373.217s3.7-1.886 4.113-2.1s-.002-.459-.002-.459m1.071 4.924c.715-.34 3.094-1.57 3.626-1.841c.722-.366.013-.7.013-.7L8.093 4.783s-.646-.344-.618.38s0 3.591 0 3.591s-.107.768.608.427m-4.961 2.573c.716-.341 3.094-1.571 3.626-1.841c.723-.367.013-.7.013-.7L3.133 7.355s-.647-.343-.618.38s-.005 3.592-.005 3.592s-.103.767.612.427m-.005 4.954c.716-.34 3.094-1.57 3.627-1.841c.722-.366.012-.7.012-.7L3.128 12.31s-.647-.343-.618.38s0 3.591 0 3.591v.101c-.01.196.058.588.607.326M7 19.334c-.418-.216-4.115-2.1-4.115-2.1s-.374-.151-.373.218l.006 3.128s.09 1.434 1.54.692S7 19.791 7 19.791s.415-.24 0-.457"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export function SystemHealthCards() {
|
||||
if (isPending || !data) return <SkeletonCards />;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2.5">
|
||||
<SystemStatusCard
|
||||
checkedAt={data.checkedAt}
|
||||
database={data.database}
|
||||
|
||||
@@ -11,12 +11,12 @@ import { orpc } from "@/lib/orpc/client";
|
||||
|
||||
export function UpdateCheckSection() {
|
||||
const { t } = useLingui();
|
||||
const { data, isPending: isLoading } = useQuery(orpc.admin.updateCheck.queryOptions());
|
||||
const { data, isPending: isLoading } = useQuery(orpc.admin.settings.get.queryOptions());
|
||||
const [localEnabled, setLocalEnabled] = useState<boolean | null>(null);
|
||||
const currentEnabled = localEnabled ?? data?.enabled ?? true;
|
||||
const currentEnabled = localEnabled ?? data?.updateCheck.enabled ?? true;
|
||||
const [optimisticEnabled, setOptimisticEnabled] = useOptimistic(currentEnabled);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const toggleMutation = useMutation(orpc.admin.toggleUpdateCheck.mutationOptions());
|
||||
const toggleMutation = useMutation(orpc.admin.settings.update.mutationOptions());
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -30,7 +30,7 @@ export function UpdateCheckSection() {
|
||||
startTransition(async () => {
|
||||
setOptimisticEnabled(checked);
|
||||
try {
|
||||
await toggleMutation.mutateAsync({ enabled: checked });
|
||||
await toggleMutation.mutateAsync({ updateCheck: { enabled: checked } });
|
||||
setLocalEnabled(checked);
|
||||
toast.success(checked ? t`Update checks enabled` : t`Update checks disabled`);
|
||||
} catch {
|
||||
|
||||
@@ -88,8 +88,8 @@ function QuickAddButton({ id, userStatus }: { id: string; userStatus?: TitleStat
|
||||
const statusConfig = useStatusConfig();
|
||||
const [optimisticStatus, setOptimisticStatus] = useState<TitleStatus | null>(null);
|
||||
|
||||
const quickAddMutation = useMutation(
|
||||
orpc.titles.quickAdd.mutationOptions({
|
||||
const addToWatchlistMutation = useMutation(
|
||||
orpc.tracking.updateStatus.mutationOptions({
|
||||
onSuccess: () => setOptimisticStatus("in_watchlist"),
|
||||
}),
|
||||
);
|
||||
@@ -101,8 +101,8 @@ function QuickAddButton({ id, userStatus }: { id: string; userStatus?: TitleStat
|
||||
function handleClick(e: React.MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (quickAddMutation.isPending || isAdded) return;
|
||||
quickAddMutation.mutate({ id });
|
||||
if (addToWatchlistMutation.isPending || isAdded) return;
|
||||
addToWatchlistMutation.mutate({ id, status: "watchlist" });
|
||||
}
|
||||
|
||||
if (isAdded && config) {
|
||||
@@ -127,8 +127,8 @@ function QuickAddButton({ id, userStatus }: { id: string; userStatus?: TitleStat
|
||||
className="absolute end-2 top-2 z-10 flex size-8 items-center justify-center rounded-full bg-black/50 text-white opacity-60 backdrop-blur-sm transition-opacity hover:bg-black/70 focus-visible:opacity-100 sm:opacity-0 sm:group-hover:opacity-100"
|
||||
render={<button type="button" />}
|
||||
>
|
||||
{!quickAddMutation.isPending && <IconPlus className="size-4" />}
|
||||
{quickAddMutation.isPending && <IconLoader className="size-4 animate-spin" />}
|
||||
{!addToWatchlistMutation.isPending && <IconPlus className="size-4" />}
|
||||
{addToWatchlistMutation.isPending && <IconLoader className="size-4 animate-spin" />}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{t`Add to Watchlist`}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
@@ -12,19 +12,31 @@ import { useTitleActions } from "./use-title-actions";
|
||||
export function TitleActions() {
|
||||
const { titleType } = useTitleContext();
|
||||
const { userStatus, userRating } = useTitleUserInfo();
|
||||
const { handleStatusChange, handleRating, handleWatchMovie } = useTitleActions();
|
||||
const { handleStatusChange, handleRating, handleWatchMovie, handleUnwatchMovie } =
|
||||
useTitleActions();
|
||||
const isMovieWatched = userStatus === "completed";
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<StatusButton currentStatus={userStatus ?? null} onChange={handleStatusChange} />
|
||||
{titleType === "movie" && (
|
||||
<Button
|
||||
onClick={handleWatchMovie}
|
||||
onClick={isMovieWatched ? handleUnwatchMovie : handleWatchMovie}
|
||||
variant={isMovieWatched ? "destructive" : "default"}
|
||||
size="lg"
|
||||
className="hover:shadow-primary/20 h-9 rounded-lg px-4 text-sm hover:shadow-md active:scale-[0.97]"
|
||||
className={`h-9 rounded-lg px-4 text-sm hover:shadow-md active:scale-[0.97] ${isMovieWatched ? "hover:shadow-destructive/20" : "hover:shadow-primary/20"}`}
|
||||
>
|
||||
<IconCheck aria-hidden={true} className="size-3.5" />
|
||||
<Trans>Mark Watched</Trans>
|
||||
{isMovieWatched ? (
|
||||
<>
|
||||
<IconX aria-hidden={true} className="size-3.5" />
|
||||
<Trans>Mark Unwatched</Trans>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconCheck aria-hidden={true} className="size-3.5" />
|
||||
<Trans>Mark Watched</Trans>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<Separator orientation="vertical" className="bg-border/50 mx-0.5 my-auto h-6" />
|
||||
|
||||
@@ -17,19 +17,13 @@ function ProviderBadge({
|
||||
logoPath: string | null;
|
||||
watchUrl: string | null;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
{...(watchUrl
|
||||
? {
|
||||
render: (
|
||||
<a
|
||||
href={watchUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t`Watch on ${name}`}
|
||||
/>
|
||||
<a href={watchUrl} target="_blank" rel="noopener noreferrer" aria-label={name} />
|
||||
),
|
||||
}
|
||||
: {})}
|
||||
@@ -50,7 +44,7 @@ function ProviderBadge({
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-popover text-popover-foreground px-2 py-1 text-[10px] font-medium shadow-md [&>:last-child]:hidden">
|
||||
{watchUrl ? t`Watch on ${name}` : name}
|
||||
{name}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -159,11 +153,8 @@ function OffersByType({
|
||||
export function TitleAvailability({ availability }: { availability: AvailabilityOffer[] }) {
|
||||
const { t } = useLingui();
|
||||
const offerLabels: Record<string, string> = {
|
||||
flatrate: t`Stream`,
|
||||
rent: t`Rent`,
|
||||
buy: t`Buy`,
|
||||
free: t`Free`,
|
||||
ads: t`With Ads`,
|
||||
stream: t`Stream`,
|
||||
purchase: t`Buy or Rent`,
|
||||
};
|
||||
|
||||
if (availability.length === 0) return null;
|
||||
|
||||
@@ -27,7 +27,7 @@ export function useTitleUserInfo() {
|
||||
const { titleId } = useTitleContext();
|
||||
const { data: session } = useSession();
|
||||
const { data } = useQuery({
|
||||
...orpc.titles.userInfo.queryOptions({ input: { id: titleId } }),
|
||||
...orpc.tracking.userInfo.queryOptions({ input: { id: titleId } }),
|
||||
enabled: !!session,
|
||||
});
|
||||
return {
|
||||
|
||||
@@ -9,7 +9,8 @@ import { useTitleActions } from "./use-title-actions";
|
||||
export function TitleKeyboardShortcuts() {
|
||||
const { titleType } = useTitleContext();
|
||||
const { userStatus } = useTitleUserInfo();
|
||||
const { handleStatusChange, handleRating, handleWatchMovie } = useTitleActions();
|
||||
const { handleStatusChange, handleRating, handleWatchMovie, handleUnwatchMovie } =
|
||||
useTitleActions();
|
||||
|
||||
const commandPaletteOpen = useAtomValue(commandPaletteOpenAtom);
|
||||
const enabled = !commandPaletteOpen;
|
||||
@@ -21,7 +22,13 @@ export function TitleKeyboardShortcuts() {
|
||||
useHotkey(
|
||||
"M",
|
||||
() => {
|
||||
if (titleType === "movie") handleWatchMovie();
|
||||
if (titleType === "movie") {
|
||||
if (userStatus === "completed") {
|
||||
handleUnwatchMovie();
|
||||
} else {
|
||||
handleWatchMovie();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ enabled },
|
||||
);
|
||||
|
||||
@@ -27,7 +27,7 @@ function RecommendationsSkeleton() {
|
||||
|
||||
export function TitleRecommendations({ titleId }: { titleId: string }) {
|
||||
const { data, isLoading } = useQuery(
|
||||
orpc.titles.recommendations.queryOptions({ input: { id: titleId } }),
|
||||
orpc.titles.similar.queryOptions({ input: { id: titleId } }),
|
||||
);
|
||||
|
||||
if (isLoading) return <RecommendationsSkeleton />;
|
||||
|
||||
@@ -19,7 +19,7 @@ export function useTitleActions() {
|
||||
const { t } = useLingui();
|
||||
const { titleId, titleName, seasons, setWatchingEp } = useTitleContext();
|
||||
const queryClient = useQueryClient();
|
||||
const userInfoKey = orpc.titles.userInfo.queryKey({ input: { id: titleId } });
|
||||
const userInfoKey = orpc.tracking.userInfo.queryKey({ input: { id: titleId } });
|
||||
|
||||
const getUserInfo = useCallback(
|
||||
() =>
|
||||
@@ -40,15 +40,10 @@ export function useTitleActions() {
|
||||
[queryClient, userInfoKey],
|
||||
);
|
||||
|
||||
const { mutateAsync: batchWatch } = useMutation(orpc.episodes.batchWatch.mutationOptions());
|
||||
const { mutateAsync: updateStatus } = useMutation(orpc.titles.updateStatus.mutationOptions());
|
||||
const { mutateAsync: updateRating } = useMutation(orpc.titles.updateRating.mutationOptions());
|
||||
const { mutateAsync: watchMovie } = useMutation(orpc.titles.watchMovie.mutationOptions());
|
||||
const { mutateAsync: unwatchEp } = useMutation(orpc.episodes.unwatch.mutationOptions());
|
||||
const { mutateAsync: watchEp } = useMutation(orpc.episodes.watch.mutationOptions());
|
||||
const { mutateAsync: watchSeason } = useMutation(orpc.seasons.watch.mutationOptions());
|
||||
const { mutateAsync: unwatchSeason } = useMutation(orpc.seasons.unwatch.mutationOptions());
|
||||
const { mutateAsync: watchAll } = useMutation(orpc.titles.watchAll.mutationOptions());
|
||||
const { mutateAsync: watch } = useMutation(orpc.tracking.watch.mutationOptions());
|
||||
const { mutateAsync: unwatch } = useMutation(orpc.tracking.unwatch.mutationOptions());
|
||||
const { mutateAsync: updateStatus } = useMutation(orpc.tracking.updateStatus.mutationOptions());
|
||||
const { mutateAsync: updateRating } = useMutation(orpc.tracking.rate.mutationOptions());
|
||||
|
||||
const catchUp = useCallback(
|
||||
async (episodeIds: string[]) => {
|
||||
@@ -64,7 +59,7 @@ export function useTitleActions() {
|
||||
}));
|
||||
|
||||
try {
|
||||
await batchWatch({ episodeIds });
|
||||
await watch({ scope: "episode", ids: episodeIds });
|
||||
await queryClient.invalidateQueries({ queryKey: userInfoKey });
|
||||
const count = episodeIds.length;
|
||||
toast.success(
|
||||
@@ -79,7 +74,7 @@ export function useTitleActions() {
|
||||
toast.error(t`Failed to catch up`);
|
||||
}
|
||||
},
|
||||
[getUserInfo, setUserInfo, batchWatch, queryClient, userInfoKey, t],
|
||||
[getUserInfo, setUserInfo, watch, queryClient, userInfoKey, t],
|
||||
);
|
||||
|
||||
const handleStatusChange = useCallback(
|
||||
@@ -132,13 +127,26 @@ export function useTitleActions() {
|
||||
const prevStatus = getUserInfo().status;
|
||||
setUserInfo((old) => ({ ...old, status: "completed" }));
|
||||
try {
|
||||
await watchMovie({ id: titleId });
|
||||
await watch({ scope: "movie", ids: [titleId] });
|
||||
toast.success(t`Marked "${titleName}" as watched`);
|
||||
} catch {
|
||||
setUserInfo((old) => ({ ...old, status: prevStatus }));
|
||||
toast.error(t`Failed to mark as watched`);
|
||||
}
|
||||
}, [getUserInfo, setUserInfo, queryClient, userInfoKey, titleId, titleName, watchMovie, t]);
|
||||
}, [getUserInfo, setUserInfo, queryClient, userInfoKey, titleId, titleName, watch, t]);
|
||||
|
||||
const handleUnwatchMovie = useCallback(async () => {
|
||||
await queryClient.cancelQueries({ queryKey: userInfoKey });
|
||||
const prevStatus = getUserInfo().status;
|
||||
setUserInfo((old) => ({ ...old, status: "in_watchlist" }));
|
||||
try {
|
||||
await unwatch({ scope: "movie", ids: [titleId] });
|
||||
toast.success(t`Marked "${titleName}" as unwatched`);
|
||||
} catch {
|
||||
setUserInfo((old) => ({ ...old, status: prevStatus }));
|
||||
toast.error(t`Failed to mark as unwatched`);
|
||||
}
|
||||
}, [getUserInfo, setUserInfo, queryClient, userInfoKey, titleId, titleName, unwatch, t]);
|
||||
|
||||
const handleWatchEpisode = useCallback(
|
||||
async (episodeId: string, seasonNum: number, epNum: number, isWatched: boolean) => {
|
||||
@@ -156,7 +164,7 @@ export function useTitleActions() {
|
||||
}));
|
||||
|
||||
try {
|
||||
await unwatchEp({ id: episodeId });
|
||||
await unwatch({ scope: "episode", ids: [episodeId] });
|
||||
toast.success(t`Unwatched S${seasonNum} E${epNum}`);
|
||||
} catch {
|
||||
setUserInfo((old) => ({
|
||||
@@ -181,7 +189,7 @@ export function useTitleActions() {
|
||||
}));
|
||||
|
||||
try {
|
||||
await watchEp({ id: episodeId });
|
||||
await watch({ scope: "episode", ids: [episodeId] });
|
||||
|
||||
const watchedSet = new Set(getUserInfo().episodeWatches);
|
||||
const previousUnwatched: string[] = [];
|
||||
@@ -231,8 +239,8 @@ export function useTitleActions() {
|
||||
setWatchingEp,
|
||||
seasons,
|
||||
catchUp,
|
||||
unwatchEp,
|
||||
watchEp,
|
||||
unwatch,
|
||||
watch,
|
||||
t,
|
||||
],
|
||||
);
|
||||
@@ -257,7 +265,7 @@ export function useTitleActions() {
|
||||
}));
|
||||
|
||||
try {
|
||||
await watchSeason({ id: season.id });
|
||||
await watch({ scope: "season", ids: [season.id] });
|
||||
await queryClient.invalidateQueries({ queryKey: userInfoKey });
|
||||
|
||||
const currentWatchSet = new Set(getUserInfo().episodeWatches);
|
||||
@@ -296,7 +304,7 @@ export function useTitleActions() {
|
||||
toast.error(t`Failed to mark some episodes`);
|
||||
}
|
||||
},
|
||||
[getUserInfo, setUserInfo, seasons, catchUp, watchSeason, queryClient, userInfoKey, t],
|
||||
[getUserInfo, setUserInfo, seasons, catchUp, watch, queryClient, userInfoKey, t],
|
||||
);
|
||||
|
||||
const handleUnmarkSeason = useCallback(
|
||||
@@ -312,7 +320,7 @@ export function useTitleActions() {
|
||||
}));
|
||||
|
||||
try {
|
||||
await unwatchSeason({ id: season.id });
|
||||
await unwatch({ scope: "season", ids: [season.id] });
|
||||
const seasonNumber = season.seasonNumber;
|
||||
const seasonLabel = season.name ?? t`Season ${seasonNumber}`;
|
||||
toast.success(t`Unwatched all of ${seasonLabel}`);
|
||||
@@ -325,7 +333,7 @@ export function useTitleActions() {
|
||||
toast.error(t`Failed to unmark some episodes`);
|
||||
}
|
||||
},
|
||||
[getUserInfo, setUserInfo, queryClient, userInfoKey, unwatchSeason, t],
|
||||
[getUserInfo, setUserInfo, queryClient, userInfoKey, unwatch, t],
|
||||
);
|
||||
|
||||
const handleMarkAllWatched = useCallback(async () => {
|
||||
@@ -339,7 +347,7 @@ export function useTitleActions() {
|
||||
status: old.status ?? "watching",
|
||||
}));
|
||||
try {
|
||||
await watchAll({ id: titleId });
|
||||
await watch({ scope: "series", ids: [titleId] });
|
||||
// Refresh to get server-derived display status (caught_up / completed)
|
||||
await queryClient.invalidateQueries({ queryKey: userInfoKey });
|
||||
toast.success(t`Marked all episodes as watched`);
|
||||
@@ -351,12 +359,13 @@ export function useTitleActions() {
|
||||
}));
|
||||
toast.error(t`Failed to mark all episodes as watched`);
|
||||
}
|
||||
}, [getUserInfo, setUserInfo, seasons, titleId, watchAll, queryClient, userInfoKey, t]);
|
||||
}, [getUserInfo, setUserInfo, seasons, titleId, watch, queryClient, userInfoKey, t]);
|
||||
|
||||
return {
|
||||
handleStatusChange,
|
||||
handleRating,
|
||||
handleWatchMovie,
|
||||
handleUnwatchMovie,
|
||||
handleWatchEpisode,
|
||||
handleMarkSeason,
|
||||
handleUnmarkSeason,
|
||||
|
||||
@@ -4,9 +4,9 @@ import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { updateToastDismissedVersionAtom } from "@/lib/atoms/update-check";
|
||||
import type { UpdateCheckResult } from "@sofa/api/schemas";
|
||||
import type { AdminSettings } from "@sofa/api/schemas";
|
||||
|
||||
export function UpdateToast({ data }: { data: UpdateCheckResult | null }) {
|
||||
export function UpdateToast({ data }: { data: AdminSettings["updateCheck"] | null }) {
|
||||
const { t } = useLingui();
|
||||
const [dismissedVersion, setDismissedVersion] = useAtom(updateToastDismissedVersionAtom);
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ export const Route = createFileRoute("/_app")({
|
||||
let updateCheck = null;
|
||||
if (session.user.role === "admin") {
|
||||
try {
|
||||
({ updateCheck } = await client.admin.updateCheck({}));
|
||||
const settings = await client.admin.settings.get({});
|
||||
updateCheck = settings.updateCheck;
|
||||
} catch {
|
||||
// Silently ignore — update check is non-critical
|
||||
}
|
||||
|
||||
@@ -17,11 +17,17 @@ export const Route = createFileRoute("/_app/dashboard")({
|
||||
staleTime: 30_000,
|
||||
loader: async ({ context }) => {
|
||||
await Promise.all([
|
||||
context.queryClient.ensureQueryData(orpc.dashboard.stats.queryOptions()),
|
||||
context.queryClient.ensureQueryData(orpc.dashboard.continueWatching.queryOptions()),
|
||||
context.queryClient.ensureQueryData(orpc.dashboard.recommendations.queryOptions()),
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.dashboard.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
|
||||
orpc.tracking.stats.queryOptions({ input: { type: "movie", period: "this_month" } }),
|
||||
),
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.tracking.stats.queryOptions({ input: { type: "episode", period: "this_week" } }),
|
||||
),
|
||||
context.queryClient.ensureQueryData(orpc.library.stats.queryOptions()),
|
||||
context.queryClient.ensureQueryData(orpc.library.continueWatching.queryOptions()),
|
||||
context.queryClient.ensureQueryData(orpc.discover.recommendations.queryOptions()),
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.library.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
|
||||
),
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.library.list.queryOptions({ input: { page: 1, limit: 10 } }),
|
||||
|
||||
@@ -17,7 +17,7 @@ export const Route = createFileRoute("/_app/explore")({
|
||||
loader: async ({ context }) => {
|
||||
await Promise.all([
|
||||
context.queryClient.ensureInfiniteQueryData(
|
||||
orpc.explore.trending.infiniteOptions({
|
||||
orpc.discover.trending.infiniteOptions({
|
||||
input: (pageParam: number) => ({ type: "all" as const, page: pageParam }),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) =>
|
||||
@@ -26,16 +26,16 @@ export const Route = createFileRoute("/_app/explore")({
|
||||
}),
|
||||
),
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.explore.popular.queryOptions({ input: { type: "movie" } }),
|
||||
orpc.discover.popular.queryOptions({ input: { type: "movie" } }),
|
||||
),
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.explore.popular.queryOptions({ input: { type: "tv" } }),
|
||||
orpc.discover.popular.queryOptions({ input: { type: "tv" } }),
|
||||
),
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.explore.genres.queryOptions({ input: { type: "movie" } }),
|
||||
orpc.discover.genres.queryOptions({ input: { type: "movie" } }),
|
||||
),
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.explore.genres.queryOptions({ input: { type: "tv" } }),
|
||||
orpc.discover.genres.queryOptions({ input: { type: "tv" } }),
|
||||
),
|
||||
]);
|
||||
},
|
||||
@@ -81,7 +81,7 @@ function ExplorePage() {
|
||||
hasNextPage: hasNextTrending,
|
||||
isFetchingNextPage: isFetchingNextTrending,
|
||||
} = useInfiniteQuery(
|
||||
orpc.explore.trending.infiniteOptions({
|
||||
orpc.discover.trending.infiniteOptions({
|
||||
input: (pageParam: number) => ({ type: "all" as const, page: pageParam }),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) =>
|
||||
@@ -91,16 +91,16 @@ function ExplorePage() {
|
||||
);
|
||||
|
||||
const { data: popularMoviesData, isPending: moviesPending } = useQuery(
|
||||
orpc.explore.popular.queryOptions({ input: { type: "movie" } }),
|
||||
orpc.discover.popular.queryOptions({ input: { type: "movie" } }),
|
||||
);
|
||||
const { data: popularTvData, isPending: tvPending } = useQuery(
|
||||
orpc.explore.popular.queryOptions({ input: { type: "tv" } }),
|
||||
orpc.discover.popular.queryOptions({ input: { type: "tv" } }),
|
||||
);
|
||||
const { data: movieGenreData } = useQuery(
|
||||
orpc.explore.genres.queryOptions({ input: { type: "movie" } }),
|
||||
orpc.discover.genres.queryOptions({ input: { type: "movie" } }),
|
||||
);
|
||||
const { data: tvGenreData } = useQuery(
|
||||
orpc.explore.genres.queryOptions({ input: { type: "tv" } }),
|
||||
orpc.discover.genres.queryOptions({ input: { type: "tv" } }),
|
||||
);
|
||||
|
||||
const isPending = trendingPending || moviesPending || tvPending;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { client, orpc } from "@/lib/orpc/client";
|
||||
|
||||
export const Route = createFileRoute("/_app/onboarding")({
|
||||
loader: async ({ context }) => {
|
||||
await context.queryClient.ensureQueryData(orpc.platforms.list.queryOptions());
|
||||
await context.queryClient.ensureQueryData(orpc.discover.platforms.queryOptions());
|
||||
},
|
||||
head: () => ({ meta: [{ title: "Get Started — Sofa" }] }),
|
||||
errorComponent: RouteError,
|
||||
@@ -23,7 +23,7 @@ function OnboardingPage() {
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const platformsQuery = useQuery(orpc.platforms.list.queryOptions());
|
||||
const platformsQuery = useQuery(orpc.discover.platforms.queryOptions());
|
||||
const platforms = platformsQuery.data?.platforms ?? [];
|
||||
|
||||
const handleToggle = useCallback((id: string) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ export const Route = createFileRoute("/_app/people/$id")({
|
||||
loader: async ({ params, context }) => {
|
||||
try {
|
||||
const data = await context.queryClient.ensureInfiniteQueryData(
|
||||
orpc.people.detail.infiniteOptions({
|
||||
orpc.people.get.infiniteOptions({
|
||||
input: (pageParam: number) => ({
|
||||
id: params.id,
|
||||
page: pageParam,
|
||||
|
||||
@@ -2,9 +2,11 @@ import { Trans } from "@lingui/react/macro";
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconDatabaseExport,
|
||||
IconDeviceDesktopCog,
|
||||
IconFileImport,
|
||||
IconServer2,
|
||||
IconShieldLock,
|
||||
IconUser,
|
||||
IconWebhook,
|
||||
} from "@tabler/icons-react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
@@ -33,9 +35,9 @@ export const Route = createFileRoute("/_app/settings")({
|
||||
staleTime: 30_000,
|
||||
loader: async ({ context }) => {
|
||||
const promises: Promise<unknown>[] = [
|
||||
context.queryClient.ensureQueryData(orpc.integrations.list.queryOptions()),
|
||||
context.queryClient.ensureQueryData(orpc.account.integrations.list.queryOptions()),
|
||||
context.queryClient.ensureQueryData(orpc.system.status.queryOptions()),
|
||||
context.queryClient.ensureQueryData(orpc.platforms.list.queryOptions()),
|
||||
context.queryClient.ensureQueryData(orpc.discover.platforms.queryOptions()),
|
||||
context.queryClient.ensureQueryData(orpc.account.platforms.queryOptions()),
|
||||
];
|
||||
const isAdmin = context.session.user.role === "admin";
|
||||
@@ -77,78 +79,50 @@ function SettingsPage() {
|
||||
const isAdmin = session.user.role === "admin";
|
||||
|
||||
return (
|
||||
<SettingsShell
|
||||
footer={
|
||||
<footer className="border-border/50 text-muted-foreground border-t pt-6 pb-2 text-center text-xs">
|
||||
<p>
|
||||
<a
|
||||
href="https://sofa.watch"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary/80 hover:text-primary font-medium transition-colors"
|
||||
>
|
||||
Sofa
|
||||
</a>{" "}
|
||||
v{__APP_VERSION__}
|
||||
{__GIT_COMMIT_SHA__ && (
|
||||
<>
|
||||
{" "}
|
||||
(
|
||||
<a
|
||||
href={`https://github.com/${GITHUB_REPO}/commit/${__GIT_COMMIT_SHA__}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-primary font-mono transition-colors"
|
||||
>
|
||||
{__GIT_COMMIT_SHA__}
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-4 flex flex-col items-center gap-2">
|
||||
<a
|
||||
href="https://www.themoviedb.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="transition-opacity hover:opacity-70"
|
||||
>
|
||||
<TmdbLogo className="h-3" />
|
||||
</a>
|
||||
<p className="text-muted-foreground text-[10px] leading-relaxed">
|
||||
<Trans>
|
||||
This product uses the TMDB API but is not endorsed or certified by TMDB.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
>
|
||||
<AccountSection
|
||||
user={{
|
||||
name: session.user.name,
|
||||
email: session.user.email,
|
||||
image: session.user.image || undefined,
|
||||
createdAt: new Date(session.user.createdAt).toISOString(),
|
||||
role: session.user.role ?? undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* App Settings */}
|
||||
<SettingsShell>
|
||||
<div>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<IconDeviceDesktopCog aria-hidden={true} className="text-muted-foreground size-4" />
|
||||
<IconUser aria-hidden={true} className="text-muted-foreground size-4" />
|
||||
<h2 className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
|
||||
<Trans>App Settings</Trans>
|
||||
<Trans>Account</Trans>
|
||||
</h2>
|
||||
</div>
|
||||
<LanguageSection />
|
||||
<StreamingServicesSection />
|
||||
<div className="space-y-2.5">
|
||||
<AccountSection
|
||||
user={{
|
||||
name: session.user.name,
|
||||
email: session.user.email,
|
||||
image: session.user.image || undefined,
|
||||
createdAt: new Date(session.user.createdAt).toISOString(),
|
||||
role: session.user.role ?? undefined,
|
||||
}}
|
||||
/>
|
||||
<StreamingServicesSection />
|
||||
<LanguageSection />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IntegrationsSection />
|
||||
{/* Integrations */}
|
||||
<div>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<IconWebhook aria-hidden={true} className="text-muted-foreground size-4" />
|
||||
<h2 className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
|
||||
<Trans>Integrations</Trans>
|
||||
</h2>
|
||||
</div>
|
||||
<IntegrationsSection />
|
||||
</div>
|
||||
|
||||
<ImportsSection />
|
||||
{/* Import */}
|
||||
<div>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<IconFileImport aria-hidden className="text-muted-foreground size-4" />
|
||||
<h2 className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
|
||||
<Trans>Import</Trans>
|
||||
</h2>
|
||||
</div>
|
||||
<ImportsSection />
|
||||
</div>
|
||||
|
||||
{/* Server health */}
|
||||
{isAdmin && (
|
||||
@@ -178,7 +152,7 @@ function SettingsPage() {
|
||||
<Trans>Admin only</Trans>
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2.5">
|
||||
<Card className="border-s-primary/30 border-s-2">
|
||||
<RegistrationSection />
|
||||
</Card>
|
||||
@@ -201,7 +175,7 @@ function SettingsPage() {
|
||||
<Trans>Admin only</Trans>
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2.5">
|
||||
<Card className="border-s-primary/30 border-s-2">
|
||||
<BackupSection />
|
||||
</Card>
|
||||
@@ -232,6 +206,48 @@ function SettingsPage() {
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<footer className="border-border/50 text-muted-foreground border-t pt-6 pb-2 text-center text-xs">
|
||||
<p>
|
||||
<a
|
||||
href="https://sofa.watch"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary/80 hover:text-primary font-medium transition-colors"
|
||||
>
|
||||
Sofa
|
||||
</a>{" "}
|
||||
v{__APP_VERSION__}
|
||||
{__GIT_COMMIT_SHA__ && (
|
||||
<>
|
||||
{" "}
|
||||
(
|
||||
<a
|
||||
href={`https://github.com/${GITHUB_REPO}/commit/${__GIT_COMMIT_SHA__}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-primary font-mono transition-colors"
|
||||
>
|
||||
{__GIT_COMMIT_SHA__}
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-4 flex flex-col items-center gap-2">
|
||||
<a
|
||||
href="https://www.themoviedb.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="transition-opacity hover:opacity-70"
|
||||
>
|
||||
<TmdbLogo className="h-3" />
|
||||
</a>
|
||||
<p className="text-muted-foreground text-[10px] leading-relaxed">
|
||||
<Trans>This product uses the TMDB API but is not endorsed or certified by TMDB.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</SettingsShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ export const Route = createFileRoute("/_app/titles/$id")({
|
||||
try {
|
||||
const [titleResult, userInfo] = await Promise.all([
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.titles.detail.queryOptions({ input: { id: params.id } }),
|
||||
orpc.titles.get.queryOptions({ input: { id: params.id } }),
|
||||
),
|
||||
context.queryClient
|
||||
.ensureQueryData(orpc.titles.userInfo.queryOptions({ input: { id: params.id } }))
|
||||
.ensureQueryData(orpc.tracking.userInfo.queryOptions({ input: { id: params.id } }))
|
||||
.catch(() => null),
|
||||
]);
|
||||
return { ...titleResult, userInfo };
|
||||
|
||||
@@ -23,7 +23,7 @@ export const Route = createFileRoute("/_app/upcoming")({
|
||||
staleTime: 30_000,
|
||||
loader: async ({ context }) => {
|
||||
await context.queryClient.ensureInfiniteQueryData(
|
||||
orpc.dashboard.upcoming.infiniteOptions({
|
||||
orpc.library.upcoming.infiniteOptions({
|
||||
input: (pageParam: string | undefined) => ({
|
||||
days: 90,
|
||||
limit: 20,
|
||||
@@ -70,7 +70,7 @@ function UpcomingPage() {
|
||||
const statusFilter = search.status ?? "all";
|
||||
|
||||
const { data, isPending, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery(
|
||||
orpc.dashboard.upcoming.infiniteOptions({
|
||||
orpc.library.upcoming.infiniteOptions({
|
||||
input: (pageParam: string | undefined) => ({
|
||||
days: 90,
|
||||
limit: 20,
|
||||
|
||||
@@ -5,25 +5,25 @@ import { client } from "@/lib/orpc/client";
|
||||
|
||||
export const Route = createFileRoute("/_auth/login")({
|
||||
beforeLoad: async () => {
|
||||
const authConfig = await client.system.authConfig({});
|
||||
if (authConfig.userCount === 0) throw redirect({ to: "/register" });
|
||||
const publicInfo = await client.system.publicInfo({});
|
||||
if (publicInfo.userCount === 0) throw redirect({ to: "/register" });
|
||||
|
||||
return { authConfig };
|
||||
return { publicInfo };
|
||||
},
|
||||
head: () => ({ meta: [{ title: "Sign in — Sofa" }] }),
|
||||
component: LoginPage,
|
||||
});
|
||||
|
||||
function LoginPage() {
|
||||
const { authConfig } = Route.useRouteContext();
|
||||
const { publicInfo } = Route.useRouteContext();
|
||||
return (
|
||||
<AuthForm
|
||||
mode="login"
|
||||
authConfig={{
|
||||
oidcEnabled: authConfig.oidcEnabled,
|
||||
oidcProviderName: authConfig.oidcProviderName,
|
||||
passwordLoginDisabled: authConfig.passwordLoginDisabled,
|
||||
registrationOpen: authConfig.registrationOpen,
|
||||
oidcEnabled: publicInfo.oidcEnabled,
|
||||
oidcProviderName: publicInfo.oidcProviderName,
|
||||
passwordLoginDisabled: publicInfo.passwordLoginDisabled,
|
||||
registrationOpen: publicInfo.registrationOpen,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,17 +7,17 @@ import { client } from "@/lib/orpc/client";
|
||||
|
||||
export const Route = createFileRoute("/_auth/register")({
|
||||
beforeLoad: async () => {
|
||||
const authConfig = await client.system.authConfig({});
|
||||
return { authConfig };
|
||||
const publicInfo = await client.system.publicInfo({});
|
||||
return { publicInfo };
|
||||
},
|
||||
head: () => ({ meta: [{ title: "Create account — Sofa" }] }),
|
||||
component: RegisterPage,
|
||||
});
|
||||
|
||||
function RegisterPage() {
|
||||
const { authConfig } = Route.useRouteContext();
|
||||
const { publicInfo } = Route.useRouteContext();
|
||||
|
||||
if (!authConfig.registrationOpen) {
|
||||
if (!publicInfo.registrationOpen) {
|
||||
return (
|
||||
<div className="relative mx-auto w-full max-w-sm">
|
||||
<div className="bg-primary/3 absolute -inset-4 rounded-2xl blur-2xl" />
|
||||
@@ -50,9 +50,9 @@ function RegisterPage() {
|
||||
<AuthForm
|
||||
mode="register"
|
||||
authConfig={{
|
||||
oidcEnabled: authConfig.oidcEnabled,
|
||||
oidcProviderName: authConfig.oidcProviderName,
|
||||
passwordLoginDisabled: authConfig.passwordLoginDisabled,
|
||||
oidcEnabled: publicInfo.oidcEnabled,
|
||||
oidcProviderName: publicInfo.oidcProviderName,
|
||||
passwordLoginDisabled: publicInfo.passwordLoginDisabled,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"@vitest/browser-playwright": "catalog:",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"eslint-plugin-drizzle": "0.2.3",
|
||||
"eslint-plugin-lingui": "0.11.0",
|
||||
"eslint-plugin-lingui": "0.12.0",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
"oxfmt": "0.42.0",
|
||||
"oxlint": "1.57.0",
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"apps/native": {
|
||||
"name": "@sofa/native",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@better-auth/expo": "catalog:",
|
||||
"@expo/metro-runtime": "55.0.6",
|
||||
@@ -46,7 +46,7 @@
|
||||
"@react-native-menu/menu": "2.0.0",
|
||||
"@react-navigation/elements": "2.9.11",
|
||||
"@react-navigation/native": "7.1.34",
|
||||
"@sentry/react-native": "8.5.0",
|
||||
"@sentry/react-native": "8.6.0",
|
||||
"@shopify/flash-list": "2.0.2",
|
||||
"@sofa/api": "workspace:*",
|
||||
"@sofa/i18n": "workspace:*",
|
||||
@@ -98,7 +98,7 @@
|
||||
"react-native-worklets": "0.7.2",
|
||||
"tailwind-merge": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"uniwind": "1.6.0",
|
||||
"uniwind": "1.6.1",
|
||||
"zeego": "3.0.6",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -112,7 +112,7 @@
|
||||
},
|
||||
"apps/public-api": {
|
||||
"name": "@sofa/public-api",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@hono/zod-validator": "0.7.6",
|
||||
"@vercel/firewall": "1.1.2",
|
||||
@@ -127,7 +127,7 @@
|
||||
},
|
||||
"apps/server": {
|
||||
"name": "@sofa/server",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@orpc/contract": "catalog:",
|
||||
"@orpc/json-schema": "catalog:",
|
||||
@@ -153,7 +153,7 @@
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@sofa/web",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "1.3.0",
|
||||
"@fontsource-variable/dm-sans": "5.2.8",
|
||||
@@ -170,9 +170,9 @@
|
||||
"@sofa/i18n": "workspace:*",
|
||||
"@tabler/icons-react": "3.40.0",
|
||||
"@tanstack/react-form": "catalog:",
|
||||
"@tanstack/react-hotkeys": "0.6.0",
|
||||
"@tanstack/react-hotkeys": "0.8.1",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-router": "1.168.3",
|
||||
"@tanstack/react-router": "1.168.5",
|
||||
"@tanstack/zod-adapter": "1.166.9",
|
||||
"better-auth": "catalog:",
|
||||
"class-variance-authority": "0.7.1",
|
||||
@@ -198,7 +198,7 @@
|
||||
"@tanstack/react-devtools": "0.10.0",
|
||||
"@tanstack/react-query-devtools": "5.95.2",
|
||||
"@tanstack/react-router-devtools": "1.166.11",
|
||||
"@tanstack/router-plugin": "1.167.4",
|
||||
"@tanstack/router-plugin": "1.167.6",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
@@ -209,13 +209,13 @@
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "8.0.2",
|
||||
"vite": "8.0.3",
|
||||
"vitest": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/api": {
|
||||
"name": "@sofa/api",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@orpc/contract": "catalog:",
|
||||
"zod": "catalog:",
|
||||
@@ -227,7 +227,7 @@
|
||||
},
|
||||
"packages/auth": {
|
||||
"name": "@sofa/auth",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@better-auth/drizzle-adapter": "1.5.6",
|
||||
"@better-auth/expo": "catalog:",
|
||||
@@ -244,7 +244,7 @@
|
||||
},
|
||||
"packages/config": {
|
||||
"name": "@sofa/config",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"devDependencies": {
|
||||
"@sofa/tsconfig": "workspace:*",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -253,7 +253,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@sofa/core",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@orpc/server": "catalog:",
|
||||
"@sofa/api": "workspace:*",
|
||||
@@ -277,7 +277,7 @@
|
||||
},
|
||||
"packages/db": {
|
||||
"name": "@sofa/db",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@sofa/config": "workspace:*",
|
||||
"@sofa/logger": "workspace:*",
|
||||
@@ -292,7 +292,7 @@
|
||||
},
|
||||
"packages/i18n": {
|
||||
"name": "@sofa/i18n",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@lingui/core": "catalog:",
|
||||
"@lingui/react": "catalog:",
|
||||
@@ -310,7 +310,7 @@
|
||||
},
|
||||
"packages/logger": {
|
||||
"name": "@sofa/logger",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"pino": "10.3.1",
|
||||
"pino-pretty": "13.1.3",
|
||||
@@ -323,7 +323,7 @@
|
||||
},
|
||||
"packages/test": {
|
||||
"name": "@sofa/test",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@sofa/db": "workspace:*",
|
||||
"better-sqlite3": "12.8.0",
|
||||
@@ -338,7 +338,7 @@
|
||||
},
|
||||
"packages/tmdb": {
|
||||
"name": "@sofa/tmdb",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@sofa/logger": "workspace:*",
|
||||
"openapi-fetch": "0.17.0",
|
||||
@@ -351,10 +351,14 @@
|
||||
},
|
||||
"packages/tsconfig": {
|
||||
"name": "@sofa/tsconfig",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
},
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"sharp",
|
||||
"better-sqlite3",
|
||||
"esbuild",
|
||||
"msw",
|
||||
"@sentry/cli",
|
||||
],
|
||||
"catalog": {
|
||||
@@ -367,31 +371,31 @@
|
||||
"@lingui/metro-transformer": "5.9.3",
|
||||
"@lingui/react": "5.9.3",
|
||||
"@lingui/vite-plugin": "5.9.3",
|
||||
"@orpc/client": "1.13.10",
|
||||
"@orpc/contract": "1.13.10",
|
||||
"@orpc/json-schema": "1.13.10",
|
||||
"@orpc/openapi": "1.13.10",
|
||||
"@orpc/server": "1.13.10",
|
||||
"@orpc/tanstack-query": "1.13.10",
|
||||
"@orpc/zod": "1.13.10",
|
||||
"@orpc/client": "1.13.13",
|
||||
"@orpc/contract": "1.13.13",
|
||||
"@orpc/json-schema": "1.13.13",
|
||||
"@orpc/openapi": "1.13.13",
|
||||
"@orpc/server": "1.13.13",
|
||||
"@orpc/tanstack-query": "1.13.13",
|
||||
"@orpc/zod": "1.13.13",
|
||||
"@tanstack/react-form": "1.28.5",
|
||||
"@tanstack/react-query": "5.95.2",
|
||||
"@types/bun": "1.3.11",
|
||||
"@types/node": "25.5.0",
|
||||
"@types/react": "19.2.14",
|
||||
"@vitest/browser": "4.1.1",
|
||||
"@vitest/browser-playwright": "4.1.1",
|
||||
"@vitest/coverage-v8": "4.1.1",
|
||||
"@vitest/browser": "4.1.2",
|
||||
"@vitest/browser-playwright": "4.1.2",
|
||||
"@vitest/coverage-v8": "4.1.2",
|
||||
"better-auth": "1.5.6",
|
||||
"drizzle-kit": "1.0.0-beta.18-7eb39f0",
|
||||
"drizzle-orm": "1.0.0-beta.18-7eb39f0",
|
||||
"drizzle-kit": "1.0.0-beta.19",
|
||||
"drizzle-orm": "1.0.0-beta.19",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"tailwind-merge": "3.5.0",
|
||||
"tailwindcss": "4.2.2",
|
||||
"thumbhash": "0.1.1",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.1.1",
|
||||
"vitest": "4.1.2",
|
||||
"zod": "4.3.6",
|
||||
},
|
||||
"packages": {
|
||||
@@ -981,7 +985,7 @@
|
||||
|
||||
"@messageformat/parser": ["@messageformat/parser@5.1.1", "", { "dependencies": { "moo": "^0.5.1" } }, "sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.28.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw=="],
|
||||
|
||||
"@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="],
|
||||
|
||||
@@ -1005,41 +1009,41 @@
|
||||
|
||||
"@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||
|
||||
"@orpc/client": ["@orpc/client@1.13.10", "", { "dependencies": { "@orpc/shared": "1.13.10", "@orpc/standard-server": "1.13.10", "@orpc/standard-server-fetch": "1.13.10", "@orpc/standard-server-peer": "1.13.10" } }, "sha512-G16KsNeKn+5+CKyY47VvAGCn04AQ5Wt+2oqqjU56lAJcNvII+Xn3cWwtva/dv6u2lNXVG4JOO1TT1cVgCAc9Aw=="],
|
||||
"@orpc/client": ["@orpc/client@1.13.13", "", { "dependencies": { "@orpc/shared": "1.13.13", "@orpc/standard-server": "1.13.13", "@orpc/standard-server-fetch": "1.13.13", "@orpc/standard-server-peer": "1.13.13" } }, "sha512-jagx/Sa+9K4HEC5lBrUlMSrmR/06hvZctWh93/sKZc8GBk4zM0+71oT1kXQVw1oRYFV2XAq3xy3m6NdM6gfKYA=="],
|
||||
|
||||
"@orpc/contract": ["@orpc/contract@1.13.10", "", { "dependencies": { "@orpc/client": "1.13.10", "@orpc/shared": "1.13.10", "@standard-schema/spec": "^1.1.0", "openapi-types": "^12.1.3" } }, "sha512-5kSW34UZ3UTxcP0uw4COQd7wb2qFT2M4RXo/HahdAxa9U+uuLGSBC4Q6E0und45Vk5DkT8LnVESixK18uK9mCg=="],
|
||||
"@orpc/contract": ["@orpc/contract@1.13.13", "", { "dependencies": { "@orpc/client": "1.13.13", "@orpc/shared": "1.13.13", "@standard-schema/spec": "^1.1.0", "openapi-types": "^12.1.3" } }, "sha512-md6iyrYkePBSJNs1VnVEEnAUORMDPHIf3JGRSHxyssIcNakev/iOjP0HvpH0Sx0MlTBhihAJo6uFL8Vpth58Nw=="],
|
||||
|
||||
"@orpc/interop": ["@orpc/interop@1.13.10", "", {}, "sha512-h9mHeYkXkZJidRFAtx/jSzWbQBR0nTVo6iYg66siXyDTz7r4uimOBFAFPn1TYAl3jY1JHW8EXNDr7pNOaeNv/g=="],
|
||||
"@orpc/interop": ["@orpc/interop@1.13.13", "", {}, "sha512-S8edIRA7ekD3/gHx5MrsO9CxZ0QvCaVDkVbNf42SZbLYPXuvxtdH0CYwaB1q9uQg2Jk0PBP8AquLH6WFwjMSbQ=="],
|
||||
|
||||
"@orpc/json-schema": ["@orpc/json-schema@1.13.10", "", { "dependencies": { "@orpc/contract": "1.13.10", "@orpc/interop": "1.13.10", "@orpc/openapi": "1.13.10", "@orpc/server": "1.13.10", "@orpc/shared": "1.13.10", "json-schema-typed": "^8.0.2" } }, "sha512-6mG8KKOw3ctzG8nVyObU0OFxSkCM41Q814Ol5nOmHA00+QCam03ZTZXsnYywijonUaSfJmW4KsjKJ9ID7txXzQ=="],
|
||||
"@orpc/json-schema": ["@orpc/json-schema@1.13.13", "", { "dependencies": { "@orpc/contract": "1.13.13", "@orpc/interop": "1.13.13", "@orpc/openapi": "1.13.13", "@orpc/server": "1.13.13", "@orpc/shared": "1.13.13", "json-schema-typed": "^8.0.2" } }, "sha512-48CgMrWIKG/t8l4OBlFRcYOj2ZCrIXfw1LA2Q6qRGdyBPzSLmQCMhh3Y4eoFXfz/wKvQIfyoUmBIdd91BxSyLg=="],
|
||||
|
||||
"@orpc/openapi": ["@orpc/openapi@1.13.10", "", { "dependencies": { "@orpc/client": "1.13.10", "@orpc/contract": "1.13.10", "@orpc/interop": "1.13.10", "@orpc/openapi-client": "1.13.10", "@orpc/server": "1.13.10", "@orpc/shared": "1.13.10", "@orpc/standard-server": "1.13.10", "json-schema-typed": "^8.0.2", "rou3": "^0.7.12" } }, "sha512-U9E+vb24p1TYRK9/l+/4EzzI1khuTBsffVByDn1/dCpMQTvrFytzbr88LDHsXnpGz6fKnc2x+5RLVwSMWVzyGg=="],
|
||||
"@orpc/openapi": ["@orpc/openapi@1.13.13", "", { "dependencies": { "@orpc/client": "1.13.13", "@orpc/contract": "1.13.13", "@orpc/interop": "1.13.13", "@orpc/openapi-client": "1.13.13", "@orpc/server": "1.13.13", "@orpc/shared": "1.13.13", "@orpc/standard-server": "1.13.13", "json-schema-typed": "^8.0.2", "rou3": "^0.7.12" } }, "sha512-lm3cAR1KJ1xp5WyDKCIqvhzYlDC914oevwYPWC7GFvTU1cTEmFP4lkQRdzLfvo06BprCg+UtHjzdLUDrBOF/zQ=="],
|
||||
|
||||
"@orpc/openapi-client": ["@orpc/openapi-client@1.13.10", "", { "dependencies": { "@orpc/client": "1.13.10", "@orpc/contract": "1.13.10", "@orpc/shared": "1.13.10", "@orpc/standard-server": "1.13.10" } }, "sha512-EKnmm4lwVhqVt5jW8wtn4eyx3Kl5Va3sEoMqqmUZHjv2NzYj+qZMBTDzatZoN001MGABkrrIwtdYrOqTNPD/qQ=="],
|
||||
"@orpc/openapi-client": ["@orpc/openapi-client@1.13.13", "", { "dependencies": { "@orpc/client": "1.13.13", "@orpc/contract": "1.13.13", "@orpc/shared": "1.13.13", "@orpc/standard-server": "1.13.13" } }, "sha512-k8od+bD7MqysKPPybAkxgfaNIaNseFPXtbidWkZAdCZ5w34SnDc7QPZJ0PQbyt9n9B+jOXSADNwQSTWSuGpjyA=="],
|
||||
|
||||
"@orpc/server": ["@orpc/server@1.13.10", "", { "dependencies": { "@orpc/client": "1.13.10", "@orpc/contract": "1.13.10", "@orpc/interop": "1.13.10", "@orpc/shared": "1.13.10", "@orpc/standard-server": "1.13.10", "@orpc/standard-server-aws-lambda": "1.13.10", "@orpc/standard-server-fastify": "1.13.10", "@orpc/standard-server-fetch": "1.13.10", "@orpc/standard-server-node": "1.13.10", "@orpc/standard-server-peer": "1.13.10", "cookie": "^1.1.1" }, "peerDependencies": { "crossws": ">=0.3.4", "ws": ">=8.18.1" }, "optionalPeers": ["crossws", "ws"] }, "sha512-SVSyqMGILZ81a0aiCAeR740JRaiCHScK1e+rDYP9dyp1KpOQ+gy8qZbR4QwK7MpsSH58SRZPPxWgEqzYPGUKLg=="],
|
||||
"@orpc/server": ["@orpc/server@1.13.13", "", { "dependencies": { "@orpc/client": "1.13.13", "@orpc/contract": "1.13.13", "@orpc/interop": "1.13.13", "@orpc/shared": "1.13.13", "@orpc/standard-server": "1.13.13", "@orpc/standard-server-aws-lambda": "1.13.13", "@orpc/standard-server-fastify": "1.13.13", "@orpc/standard-server-fetch": "1.13.13", "@orpc/standard-server-node": "1.13.13", "@orpc/standard-server-peer": "1.13.13", "cookie": "^1.1.1" }, "peerDependencies": { "crossws": ">=0.3.4", "ws": ">=8.18.1" }, "optionalPeers": ["crossws", "ws"] }, "sha512-l3OfiaqfLiLQqamXufKiEzT8K9Zq8VHvijADV5eDJOaz6sFzK1Mg6q2D8l9wcmuyG7ga0HfMT+cf3JQsh5WDIA=="],
|
||||
|
||||
"@orpc/shared": ["@orpc/shared@1.13.10", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.4.4" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-EvlsgN2Ll7mgrgL51UqoWcu8jJXLZeuqL2f5q/OfUTFk5VJ+wwmNep8Ig2s5655sHoFydhnLGHi216GqNkqwRw=="],
|
||||
"@orpc/shared": ["@orpc/shared@1.13.13", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.4.4" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-kNpYOBjHvmgKHla6munWOaEeA0utEfAvoiZpXjiRjjt1RxTibdwQvVHgxRIBNMXfQsb+ON3Q/wDkoaUhvvSnIw=="],
|
||||
|
||||
"@orpc/standard-server": ["@orpc/standard-server@1.13.10", "", { "dependencies": { "@orpc/shared": "1.13.10" } }, "sha512-AUKKAqXMb3X1s7oYSe7ok1AAHc+S/n43+QMOko+9McUKo62StMp5SAbLMgBN1BUaC0RVzAmgV9nnFkqjwNpUKA=="],
|
||||
"@orpc/standard-server": ["@orpc/standard-server@1.13.13", "", { "dependencies": { "@orpc/shared": "1.13.13" } }, "sha512-9pgS8XvauuRQElkyuD8F3om+nN0KBEnTkhblDHCBzkZERjWkmfirJmshQrWHoFaDTk+nnXHIaY6d7TBTxXdPRw=="],
|
||||
|
||||
"@orpc/standard-server-aws-lambda": ["@orpc/standard-server-aws-lambda@1.13.10", "", { "dependencies": { "@orpc/shared": "1.13.10", "@orpc/standard-server": "1.13.10", "@orpc/standard-server-fetch": "1.13.10", "@orpc/standard-server-node": "1.13.10" } }, "sha512-8TtB4+Sc8plNKlGYUOSXkyidl2BrGiXQNqQl/pUXEwvgw6zxcX5JsHJhHFZdlP7UyfQnFrOgpEMz8Uds+kZ8Yw=="],
|
||||
"@orpc/standard-server-aws-lambda": ["@orpc/standard-server-aws-lambda@1.13.13", "", { "dependencies": { "@orpc/shared": "1.13.13", "@orpc/standard-server": "1.13.13", "@orpc/standard-server-fetch": "1.13.13", "@orpc/standard-server-node": "1.13.13" } }, "sha512-e7310DnN6FSpRgR5gQpwvwg2B3uR+NoT7qlnyQWsjxQGr95L4GST3hRNSsQdQyXmFx1jvCCp0i3kAE9SJyvU6A=="],
|
||||
|
||||
"@orpc/standard-server-fastify": ["@orpc/standard-server-fastify@1.13.10", "", { "dependencies": { "@orpc/shared": "1.13.10", "@orpc/standard-server": "1.13.10", "@orpc/standard-server-node": "1.13.10" }, "peerDependencies": { "fastify": ">=5.6.1" }, "optionalPeers": ["fastify"] }, "sha512-tFujlelDF6S+SR6XzG7H62uro9uuPXYLVN4HgOhk75cNp/Bdn6/KatE3IQZB90nu3LgO6EmVwtIZR8jcjhtjmA=="],
|
||||
"@orpc/standard-server-fastify": ["@orpc/standard-server-fastify@1.13.13", "", { "dependencies": { "@orpc/shared": "1.13.13", "@orpc/standard-server": "1.13.13", "@orpc/standard-server-node": "1.13.13" }, "peerDependencies": { "fastify": ">=5.6.1" }, "optionalPeers": ["fastify"] }, "sha512-G5Sq+rZDPwHvWJHsAA6bYWoJ7LcGyBby+SjjEGwz+viRdBk/inZnZafKpM+pL89aDdq6iw99Bcbug5WFPP6KdA=="],
|
||||
|
||||
"@orpc/standard-server-fetch": ["@orpc/standard-server-fetch@1.13.10", "", { "dependencies": { "@orpc/shared": "1.13.10", "@orpc/standard-server": "1.13.10" } }, "sha512-zEb67B/CPvMw1ZsDx4iwi0zSZ/gHuy4oo2icnteiw6U0RMHzbyYSeSu7fTEUDYqkQnsRDRyw28GCyyZl7ePRxQ=="],
|
||||
"@orpc/standard-server-fetch": ["@orpc/standard-server-fetch@1.13.13", "", { "dependencies": { "@orpc/shared": "1.13.13", "@orpc/standard-server": "1.13.13" } }, "sha512-Lffy26+WtCQkwOUacsrdyeJF1GNzrhm75O3LXKVFXqmSdyVVdyI6zuqLn/YKGODU2L9IqGxZ2CwsV2tE298SSA=="],
|
||||
|
||||
"@orpc/standard-server-node": ["@orpc/standard-server-node@1.13.10", "", { "dependencies": { "@orpc/shared": "1.13.10", "@orpc/standard-server": "1.13.10", "@orpc/standard-server-fetch": "1.13.10" } }, "sha512-nZ41/yx112/FfUgUBXh88N/yEFxen6mxde14zgr5+WjZA7MAifdj9HNd3fqkep06sWNiMgXzQj6rgCglFpQuWQ=="],
|
||||
"@orpc/standard-server-node": ["@orpc/standard-server-node@1.13.13", "", { "dependencies": { "@orpc/shared": "1.13.13", "@orpc/standard-server": "1.13.13", "@orpc/standard-server-fetch": "1.13.13" } }, "sha512-wI45h/bVWn7lgvqqm5aM7vj5BblmmBdFG2wbEhmpY85DslbCtMNJrIw/x+zxm1XgYAzsKLExLp6L01hzjeHmEA=="],
|
||||
|
||||
"@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.13.10", "", { "dependencies": { "@orpc/shared": "1.13.10", "@orpc/standard-server": "1.13.10" } }, "sha512-/jHacwIeeNm0l5cPG1CfVZEa69TZIv4Pgxd7PmLqFdjUwHnX0AOb+3klaOOOj7V6UxNFyPbB8EXOcQDm7KuIXQ=="],
|
||||
"@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.13.13", "", { "dependencies": { "@orpc/shared": "1.13.13", "@orpc/standard-server": "1.13.13" } }, "sha512-FeWAbXfnZDPYQRajM0hD6GJvHeC3DZILngAjdcLHy5zt3riu6nL2lLPSWDv5yNWWscmYU+CfKmXWd0Z01BOeWA=="],
|
||||
|
||||
"@orpc/tanstack-query": ["@orpc/tanstack-query@1.13.10", "", { "dependencies": { "@orpc/shared": "1.13.10" }, "peerDependencies": { "@orpc/client": "1.13.10", "@tanstack/query-core": ">=5.80.2" } }, "sha512-K8SFPmlpnz/LY6uUX5UPERHYmcytibRRubtA6ZvQ09vv+ZwhAHBtzon0ExJtzifRQEPPQhX1Uf+vWUSKwHxhhw=="],
|
||||
"@orpc/tanstack-query": ["@orpc/tanstack-query@1.13.13", "", { "dependencies": { "@orpc/shared": "1.13.13" }, "peerDependencies": { "@orpc/client": "1.13.13", "@tanstack/query-core": ">=5.80.2" } }, "sha512-6+Cheaiu+RDPdszdeRKoBINrF8MQp64zSeZB+L3gqgF43zlYDhLOgELZMzYa6U3U6bLk4rmIeubpk+i1kACfRg=="],
|
||||
|
||||
"@orpc/zod": ["@orpc/zod@1.13.10", "", { "dependencies": { "@orpc/json-schema": "1.13.10", "@orpc/openapi": "1.13.10", "@orpc/shared": "1.13.10", "escape-string-regexp": "^5.0.0", "wildcard-match": "^5.1.4" }, "peerDependencies": { "@orpc/contract": "1.13.10", "@orpc/server": "1.13.10", "zod": ">=3.25.0" } }, "sha512-1SizAwPVc2gh+ajlOSVCjOyBV1vEKrc4mHqrcYXuzLcCdZ1vCQAU89dEpps/C+5/RbPSJn8JBbsF1pdg4LUfqQ=="],
|
||||
"@orpc/zod": ["@orpc/zod@1.13.13", "", { "dependencies": { "@orpc/json-schema": "1.13.13", "@orpc/openapi": "1.13.13", "@orpc/shared": "1.13.13", "escape-string-regexp": "^5.0.0", "wildcard-match": "^5.1.4" }, "peerDependencies": { "@orpc/contract": "1.13.13", "@orpc/server": "1.13.13", "zod": ">=3.25.0" } }, "sha512-K2iWGHopi3apExqNVb6EWJJqmq7cgytY3PjJyJ/Uy98UfJJ17Kwz7rAZKffNc7E967rNk9IH8MIcUcGkT7dmzQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
|
||||
|
||||
@@ -1211,47 +1215,47 @@
|
||||
|
||||
"@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.83.2", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-N7mRjHLW/+KWxMp9IHRWyE3VIkeG1m3PnZJAGEFLCN8VFb7e4VfI567o7tE/HYcdcXCylw+Eqhlciz8gDeQ71g=="],
|
||||
|
||||
"@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.15.7", "", { "dependencies": { "@react-navigation/elements": "^2.9.12", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.2.0", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-OXNvU0esvyZf//KpaIsFh2GD1otxqG+Lv48VfkNIh+3DEbOnqni8pOrKdICgoC4T0R8oVln7pnVeHl89Gipv8w=="],
|
||||
"@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.15.8", "", { "dependencies": { "@react-navigation/elements": "^2.9.13", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.2.1", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-Fz/AAPE6Be0CimOXvon75RNgpFCbZvzF2RPcNeZOdOxIYyHDGxDdtsfTxLHB0tOp9HHXkT0xXOX8Rk001jdpbg=="],
|
||||
|
||||
"@react-navigation/core": ["@react-navigation/core@7.17.0", "", { "dependencies": { "@react-navigation/routers": "^7.5.3", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-E4Kr1PRrhKiVn1RdMdPIG1rCfrKh+HiVJ2smdLsh9D95Q2z0a9dGE9yHpRQ2pAUiiwOfgloLqegkPb8g+TcCBA=="],
|
||||
"@react-navigation/core": ["@react-navigation/core@7.17.1", "", { "dependencies": { "@react-navigation/routers": "^7.5.3", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-P1kL4FVTVYEf9Cvmb+WFxQ2UkbaXc9psj6OE0BsZ+hutPqZVbmiN6v/TI5QPf4qtg40a02yYo3vo+Mob9vJKtg=="],
|
||||
|
||||
"@react-navigation/elements": ["@react-navigation/elements@2.9.11", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.34", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-O5KiwaVCcEVuqZgQ77xiBFSl1sha77rNMTFlLWYnom33ZHPDarV3bM9WNyVnMZxU8ZVTi02X3+ZhO0fSn5QYyg=="],
|
||||
|
||||
"@react-navigation/native": ["@react-navigation/native@7.1.34", "", { "dependencies": { "@react-navigation/core": "^7.16.2", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-zzQ0mKAhLsjTIsaoLfILKZVMObJzE0F+bOi0hl2Glt+1Rd2GtaWJ1Z024c3yLmX+Oc79pqoCQLBXpyxtrZu9NQ=="],
|
||||
|
||||
"@react-navigation/native-stack": ["@react-navigation/native-stack@7.14.7", "", { "dependencies": { "@react-navigation/elements": "^2.9.12", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.2.0", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-MxKdKS817YK7iirlyW+XZnXJ339eRE7aA3E55zHVDS+R+bqro+PwRwNGqL1Y9e3w0KjAKZVsOfn5erJRWrO4iQ=="],
|
||||
"@react-navigation/native-stack": ["@react-navigation/native-stack@7.14.9", "", { "dependencies": { "@react-navigation/elements": "^2.9.13", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.2.1", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-s76NyRr/VSPRqXaLtaKUj9Q1qZ5ym0831QZFFXJcRyom6QYpo9eESB9/dfeN+tTEnH7kP77CwoCuR0THKMuk3w=="],
|
||||
|
||||
"@react-navigation/routers": ["@react-navigation/routers@7.5.3", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.11", "", { "os": "android", "cpu": "arm64" }, "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A=="],
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg=="],
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ=="],
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag=="],
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm" }, "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ=="],
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q=="],
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg=="],
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw=="],
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw=="],
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "x64" }, "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA=="],
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg=="],
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.11", "", { "os": "none", "cpu": "arm64" }, "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g=="],
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.11", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw=="],
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q=="],
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.11", "", { "os": "win32", "cpu": "x64" }, "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA=="],
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="],
|
||||
|
||||
"@rolldown/plugin-babel": ["@rolldown/plugin-babel@0.2.2", "", { "dependencies": { "picomatch": "^4.0.3" }, "peerDependencies": { "@babel/core": "^7.29.0 || ^8.0.0-rc.1", "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1", "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1", "rolldown": "^1.0.0-rc.5", "vite": "^8.0.0" }, "optionalPeers": ["@babel/plugin-transform-runtime", "@babel/runtime", "vite"] }, "sha512-q9pE8+47bQNHb5eWVcE6oXppA+JTSwvnrhH53m0ZuHuK5MLvwsLoWrWzBTFQqQ06BVxz1gp0HblLsch8o6pvZw=="],
|
||||
|
||||
@@ -1259,43 +1263,43 @@
|
||||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.44.0", "", { "dependencies": { "@sentry/core": "10.44.0" } }, "sha512-z9xz3T/v+MnfHY6kdUCmOZI8CiAl3LlKYtGH2p3rAsrxhwX+BTnUp01VhMVnEZIDgUXNt3AhJac+4kcDIPu1Hg=="],
|
||||
"@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.46.0", "", { "dependencies": { "@sentry/core": "10.46.0" } }, "sha512-WB1gBT9G13V02ekZ6NpUhoI1aGHV2eNfjEPthkU2bGBvFpQKnstwzjg7waIRGR7cu+YSW2Q6UI6aQLgBeOPD1g=="],
|
||||
|
||||
"@sentry-internal/feedback": ["@sentry-internal/feedback@10.44.0", "", { "dependencies": { "@sentry/core": "10.44.0" } }, "sha512-yNS2EGK1bNm8YUI+Orzpa7yr05Da+b1VEe/9x7dl7gTjw/+tfutoXlG6Y+iFZBB3gQ9QU+nxZAhU+KcxiPEURw=="],
|
||||
"@sentry-internal/feedback": ["@sentry-internal/feedback@10.46.0", "", { "dependencies": { "@sentry/core": "10.46.0" } }, "sha512-c4pI/z9nZCQXe9GYEw/hE/YTY9AxGBp8/wgKI+T8zylrN35SGHaXv63szzE1WbI8lacBY8lBF7rstq9bQVCaHw=="],
|
||||
|
||||
"@sentry-internal/replay": ["@sentry-internal/replay@10.44.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.44.0", "@sentry/core": "10.44.0" } }, "sha512-KDmoqBsRmkaoc+eKLR2CbScd2eBmLcw+1+D441lLttAO3WWhvYyCaYdu/HIGGUoybuSgt+IcpCJdi7hFuCvYqw=="],
|
||||
"@sentry-internal/replay": ["@sentry-internal/replay@10.46.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.46.0", "@sentry/core": "10.46.0" } }, "sha512-JBsWeXG6bRbxBFK8GzWymWGOB9QE7Kl57BeF3jzgdHTuHSWZ2mRnAmb1K05T4LU+gVygk6yW0KmdC8Py9Qzg9A=="],
|
||||
|
||||
"@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.44.0", "", { "dependencies": { "@sentry-internal/replay": "10.44.0", "@sentry/core": "10.44.0" } }, "sha512-RA7XgYZWHY7M+vaHvuMxDFT51wCs4puS2smElM5oh+j3YqbFXY7P16fOCwIAGoyI4gVsj8aTeBgVqUmrmzhAXQ=="],
|
||||
"@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.46.0", "", { "dependencies": { "@sentry-internal/replay": "10.46.0", "@sentry/core": "10.46.0" } }, "sha512-ub314MWUsekVCuoH0/HJbbimlI24SkV745UW2pj9xRbxOAEf1wjkmIzxKrMDbTgJGuEunug02XZVdJFJUzOcDw=="],
|
||||
|
||||
"@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@5.1.1", "", {}, "sha512-x2wEpBHwsTyTF2rWsLKJlzrRF1TTIGOfX+ngdE+Yd5DBkoS58HwQv824QOviPGQRla4/ypISqAXzjdDPL/zalg=="],
|
||||
|
||||
"@sentry/browser": ["@sentry/browser@10.44.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.44.0", "@sentry-internal/feedback": "10.44.0", "@sentry-internal/replay": "10.44.0", "@sentry-internal/replay-canvas": "10.44.0", "@sentry/core": "10.44.0" } }, "sha512-UpMx5forbVKieNULma3gT2SsLYqsYT4nLXa6s1io/Y8BFej9sH2dD5ExA8TrkQThQwAWFI3qKsQzYnF+EX/Bfg=="],
|
||||
"@sentry/browser": ["@sentry/browser@10.46.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.46.0", "@sentry-internal/feedback": "10.46.0", "@sentry-internal/replay": "10.46.0", "@sentry-internal/replay-canvas": "10.46.0", "@sentry/core": "10.46.0" } }, "sha512-80DmGlTk5Z2/OxVOzLNxwolMyouuAYKqG8KUcoyintZqHbF6kO1RulI610HmyUt3OagKeBCqt9S7w0VIfCRL+Q=="],
|
||||
|
||||
"@sentry/cli": ["@sentry/cli@3.3.3", "", { "dependencies": { "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "undici": "^6.22.0", "which": "^2.0.2" }, "optionalDependencies": { "@sentry/cli-darwin": "3.3.3", "@sentry/cli-linux-arm": "3.3.3", "@sentry/cli-linux-arm64": "3.3.3", "@sentry/cli-linux-i686": "3.3.3", "@sentry/cli-linux-x64": "3.3.3", "@sentry/cli-win32-arm64": "3.3.3", "@sentry/cli-win32-i686": "3.3.3", "@sentry/cli-win32-x64": "3.3.3" }, "bin": { "sentry-cli": "bin/sentry-cli" } }, "sha512-4CZtfgiOraX+BntMjYQhfLDArXwpqt3sEo5Zdj2pqWSZSd4yI3ncfQ21CsxLcI/sUQrjmD5Vzidu4/1OShyxtA=="],
|
||||
"@sentry/cli": ["@sentry/cli@3.3.4", "", { "dependencies": { "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "undici": "^6.22.0", "which": "^2.0.2" }, "optionalDependencies": { "@sentry/cli-darwin": "3.3.4", "@sentry/cli-linux-arm": "3.3.4", "@sentry/cli-linux-arm64": "3.3.4", "@sentry/cli-linux-i686": "3.3.4", "@sentry/cli-linux-x64": "3.3.4", "@sentry/cli-win32-arm64": "3.3.4", "@sentry/cli-win32-i686": "3.3.4", "@sentry/cli-win32-x64": "3.3.4" }, "bin": { "sentry-cli": "bin/sentry-cli" } }, "sha512-r97H1GTdaRs1qhTvbzyomclPesrt4vpjY2W7KGtgSOa5ynQsXKajsM5oJOtNW99O1pNNMZFlR1mmGDMHOxYm4g=="],
|
||||
|
||||
"@sentry/cli-darwin": ["@sentry/cli-darwin@3.3.3", "", { "os": "darwin" }, "sha512-P8DoL79eX5fhKCfBHHl7xwwTShDPOb2drJC8lizZ3v1iS1JLPrNweM1KEzDefR30zH1wghbLSwsYv/svWdM3wA=="],
|
||||
"@sentry/cli-darwin": ["@sentry/cli-darwin@3.3.4", "", { "os": "darwin" }, "sha512-1cFHgwJq0yJ4lAvxQISag2R5R/wRtY7e4YX54Da4n+Isw1WHdF2CLmdT0ufOyT04iiF406UD2d7qsPEVDniAmw=="],
|
||||
|
||||
"@sentry/cli-linux-arm": ["@sentry/cli-linux-arm@3.3.3", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm" }, "sha512-a7o/huozveLIImXHe0HDwEMVhvDopOP2tLcopvV7sQsVE8f/QOShR5FudKjmiaZz2opdLzPJO9pv5WuF9jAZPg=="],
|
||||
"@sentry/cli-linux-arm": ["@sentry/cli-linux-arm@3.3.4", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm" }, "sha512-8XDDmUZ/4X7Dw2hoSU6T9tpD8qMwtVKHYLQjcY+xNBQujPrSq+YCrNXK/iIN9UgX8Rza2q4IftsIkJADOxLFow=="],
|
||||
|
||||
"@sentry/cli-linux-arm64": ["@sentry/cli-linux-arm64@3.3.3", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm64" }, "sha512-9jaX9RGyTpjo9u2urNi5ciBDpRdTt107YJpFXev+BFHJ6Lwz/owgRuYzPRfAen8hKkOOFheZ3iy07kl576eZzw=="],
|
||||
"@sentry/cli-linux-arm64": ["@sentry/cli-linux-arm64@3.3.4", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm64" }, "sha512-SD9YQjUPXjIBkt4q41lHMopeL9lKskaxc7qpt1ZuQpoHOszDOUNP3WPvxpeiaMjFMmgMkGojyDBk2XY9eyfGNQ=="],
|
||||
|
||||
"@sentry/cli-linux-i686": ["@sentry/cli-linux-i686@3.3.3", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "ia32" }, "sha512-VngQYzR2kDm2oojCuYF20ebLTK8HKvEwxe785J6gxob8Ef9JvZkERyUqENYppBa9aVgN0pandqPAqOECWykTMA=="],
|
||||
"@sentry/cli-linux-i686": ["@sentry/cli-linux-i686@3.3.4", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "ia32" }, "sha512-LNQlRDPrHLTDgxxJsAzT+1+sJ8Kv/Lq8E4Ob8RjqkwuZxl/wR6QJ4O83cxYGJPPnmjEAT+lOUQt1pTPXAwIwLQ=="],
|
||||
|
||||
"@sentry/cli-linux-x64": ["@sentry/cli-linux-x64@3.3.3", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "x64" }, "sha512-rBxXQeIYGefUNI2cXHxEr0y3bhxDQjOD4G6j/gqLz/Dj+l8gJ/iKP64kTudnoViNIpn0pdYccG69th7zmzM/Fg=="],
|
||||
"@sentry/cli-linux-x64": ["@sentry/cli-linux-x64@3.3.4", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "x64" }, "sha512-yEOVI4a0RTBYcHJBEaiFU2s4GzcfkXDToMUeLlUg4B3Bgz8AX76163RTEJH5dKavKkoMLKzOrKgVylXPxo1JLQ=="],
|
||||
|
||||
"@sentry/cli-win32-arm64": ["@sentry/cli-win32-arm64@3.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-c52g+YS6BO0rzH8AEHqQPmpqZrw0GJjMWqy0tQ5jcqaGdaLVnxk0mMEubv8R6Dv5MR2LShoKjiNsaeVfrWIMUg=="],
|
||||
"@sentry/cli-win32-arm64": ["@sentry/cli-win32-arm64@3.3.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-aLGgnIf7FHK+yRsemXGQ1yF0Q4R3D/jwCf/20k1miUgFP9fn5mZt+fArGDHr5k3vFfh3bUTf22Ga4CUwXqwkvQ=="],
|
||||
|
||||
"@sentry/cli-win32-i686": ["@sentry/cli-win32-i686@3.3.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-DygYzSY/+tS7oFj/mfeg/yzYxsQx3fO8cI+IWc2pns/at+JcJ9O5xyM/x/q55wOxpnwla7RL1D3rsqK2mqkYfg=="],
|
||||
"@sentry/cli-win32-i686": ["@sentry/cli-win32-i686@3.3.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-mLD5NpgI3G3+f1iBWGqTTC1kvdQ0CzmkvM9aIRiXUYWXZiaZVd4YuqhoDvTU6zNFEUXI+9jUEp84VF171B0Pqg=="],
|
||||
|
||||
"@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@3.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-i0glPcHwkqbVA2Y+0Yz7CD/l8TSkfft1a+lTU9yk/+DDU8WGkyArEAxAji9bGo4p+k5HIFC8OC2MwpKdcdFM4Q=="],
|
||||
"@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@3.3.4", "", { "os": "win32", "cpu": "x64" }, "sha512-dNWifGo3VLx7n3N3/m+7+rLGNZEb7JmnLwLLAHoz11DneCa6OTBSMCKFABArxqLinlzTbiSOYc8QbvCTcLk5FA=="],
|
||||
|
||||
"@sentry/core": ["@sentry/core@10.44.0", "", {}, "sha512-aa7CiDaNFZvHpqd97LJhuskolfJ/4IH5xyuVVLnv7l6B0v9KTwskPUxb0tH1ej3FxuzfH+i8iTiTFuqpfHS3QA=="],
|
||||
"@sentry/core": ["@sentry/core@10.46.0", "", {}, "sha512-N3fj4zqBQOhXliS1Ne9euqIKuciHCGOJfPGQLwBoW9DNz03jF+NB8+dUKtrJ79YLoftjVgf8nbgwtADK7NR+2Q=="],
|
||||
|
||||
"@sentry/react": ["@sentry/react@10.44.0", "", { "dependencies": { "@sentry/browser": "10.44.0", "@sentry/core": "10.44.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-blaYoLk/UgFZXj9ieKZeY1JIiqzeL2VegQt22S9IQk8gHpunDZux5XC4CdcPdavcVusddaB/SmHAmhy2RCBdPQ=="],
|
||||
"@sentry/react": ["@sentry/react@10.46.0", "", { "dependencies": { "@sentry/browser": "10.46.0", "@sentry/core": "10.46.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-Rb1S+9OuUPVwsz7GWnQ6Kgf3azbsseUymIegg3JZHNcW/fM1nPpaljzTBnuineia113DH0pgMBcdrrZDLaosFQ=="],
|
||||
|
||||
"@sentry/react-native": ["@sentry/react-native@8.5.0", "", { "dependencies": { "@sentry/babel-plugin-component-annotate": "5.1.1", "@sentry/browser": "10.44.0", "@sentry/cli": "3.3.3", "@sentry/core": "10.44.0", "@sentry/react": "10.44.0", "@sentry/types": "10.44.0" }, "peerDependencies": { "expo": ">=49.0.0", "react": ">=17.0.0", "react-native": ">=0.65.0" }, "optionalPeers": ["expo"], "bin": { "sentry-eas-build-on-complete": "scripts/eas-build-hook.js", "sentry-eas-build-on-error": "scripts/eas-build-hook.js", "sentry-eas-build-on-success": "scripts/eas-build-hook.js", "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" } }, "sha512-ZMemYiEIIXV7p4PcpRmDf9+dxWTbWgfSTgJ/cVIB3xiU3PtfLefupjpaNuwNNDK7pTA0amHzPgFDY5mdm/KE5w=="],
|
||||
"@sentry/react-native": ["@sentry/react-native@8.6.0", "", { "dependencies": { "@sentry/babel-plugin-component-annotate": "5.1.1", "@sentry/browser": "10.46.0", "@sentry/cli": "3.3.4", "@sentry/core": "10.46.0", "@sentry/react": "10.46.0", "@sentry/types": "10.46.0" }, "peerDependencies": { "expo": ">=49.0.0", "react": ">=17.0.0", "react-native": ">=0.65.0" }, "optionalPeers": ["expo"], "bin": { "sentry-eas-build-on-complete": "scripts/eas-build-hook.js", "sentry-eas-build-on-error": "scripts/eas-build-hook.js", "sentry-eas-build-on-success": "scripts/eas-build-hook.js", "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" } }, "sha512-sT44HU09BCxlFndTJaYJjxkd6OGAiTLd85rgXkShlllur5ITDg7GZmVgTv/ja4bPOqSMHAGMI9Tx3/VTDq3dgQ=="],
|
||||
|
||||
"@sentry/types": ["@sentry/types@10.44.0", "", { "dependencies": { "@sentry/core": "10.44.0" } }, "sha512-eON+xfpPVZr246cPqByW2yoRiZsURTjNnXJ9qPiDf64lCUxAj1IZE4v14Ns+ZcrhUPadGMUVBwmDNnrfmEAy3A=="],
|
||||
"@sentry/types": ["@sentry/types@10.46.0", "", { "dependencies": { "@sentry/core": "10.46.0" } }, "sha512-ZJWEuevCTzxA2z+C5NQ+bXKxA+oVnZM5QacBZ+RX6NHItwgYOcp4GNM6Wc9xTymOUvVmBC0Vcj1wbx7TA2JX0Q=="],
|
||||
|
||||
"@shopify/flash-list": ["@shopify/flash-list@2.0.2", "", { "dependencies": { "tslib": "2.8.1" }, "peerDependencies": { "@babel/runtime": "*", "react": "*", "react-native": "*" } }, "sha512-zhlrhA9eiuEzja4wxVvotgXHtqd3qsYbXkQ3rsBfOgbFA9BVeErpDE/yEwtlIviRGEqpuFj/oU5owD6ByaNX+w=="],
|
||||
|
||||
@@ -1405,7 +1409,7 @@
|
||||
|
||||
"@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="],
|
||||
|
||||
"@tanstack/hotkeys": ["@tanstack/hotkeys@0.5.0", "", { "dependencies": { "@tanstack/store": "^0.9.2" } }, "sha512-OByddYW3N7Y9yHOWzf5cR7mqq1OKa9S1DMGLciUAlmZrt1xw31GpbauabcO0FicAkaDa7AqFm7+gXX93PVmxrA=="],
|
||||
"@tanstack/hotkeys": ["@tanstack/hotkeys@0.6.1", "", { "dependencies": { "@tanstack/store": "^0.9.3" } }, "sha512-9JSqG0Go2r0Hj2z7UfK05YOGsWK2bwfoa880emVe340o0Vp4hrDHSDNV2bmR72wHmuf9dLVms9Gkx2ZCDjEpkA=="],
|
||||
|
||||
"@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="],
|
||||
|
||||
@@ -1421,7 +1425,7 @@
|
||||
|
||||
"@tanstack/react-form": ["@tanstack/react-form@1.28.5", "", { "dependencies": { "@tanstack/form-core": "1.28.5", "@tanstack/react-store": "^0.9.1" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-CL8IeWkeXnEEDsHt5wBuIOZvSYrKiLRtsC9ca0LzfJJ22SYCma9cBmh1UX1EBX0o3gH2U21PmUf+y5f9OJNoEQ=="],
|
||||
|
||||
"@tanstack/react-hotkeys": ["@tanstack/react-hotkeys@0.6.0", "", { "dependencies": { "@tanstack/hotkeys": "0.5.0", "@tanstack/react-store": "^0.9.2" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-9c/31SV5VmuhPKUJdSSseDBSlmBzqK7Lae9ig9SQI8nvsZDEDZ48L/k2IkWaYsxrPQPVDL1ZIuQjlw/xhz4LZg=="],
|
||||
"@tanstack/react-hotkeys": ["@tanstack/react-hotkeys@0.8.1", "", { "dependencies": { "@tanstack/hotkeys": "0.6.1", "@tanstack/react-store": "^0.9.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-U5j2ulCAJ6FaEctYt5loNOFicrzYrESQXctx/tgPbgysR/YZSvI9UdBIRuvyHW0unwlnei8MEgYkNLGZFufDCw=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.95.2", "", { "dependencies": { "@tanstack/query-core": "5.95.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA=="],
|
||||
|
||||
@@ -1429,23 +1433,23 @@
|
||||
|
||||
"@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.95.2", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.95.2" }, "peerDependencies": { "@tanstack/react-query": "^5.95.2", "react": "^18 || ^19" } }, "sha512-i3fvzD8gaLgQyFvRc/+iSUr60aL31tMN+5QM11zdPRg0K9CirIQjHD7WgXFBnD29KJDvcjcv7OrIBaPwZ+H9xw=="],
|
||||
|
||||
"@tanstack/react-router": ["@tanstack/react-router@1.168.3", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.2", "@tanstack/router-core": "1.168.3", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-hMWXhckeaSvjepHT5x9tUYJVXMvT/kUjaVHOUDmCfyOBtjxJNYJKbEWClXoopGwWlHjRTAzhsndhnQQRbIiKmA=="],
|
||||
"@tanstack/react-router": ["@tanstack/react-router@1.168.5", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.5", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-jh+S5bqvHxH7ng4KKsN3wTYegJIP4V68dGrgUUwmYSmipb4cJyq+09TqCWVRXQ9f9xVeESRAqotAgK7GOS1uFQ=="],
|
||||
|
||||
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.166.11", "", { "dependencies": { "@tanstack/router-devtools-core": "1.167.1" }, "peerDependencies": { "@tanstack/react-router": "^1.168.2", "@tanstack/router-core": "^1.168.2", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-WYR3q4Xui5yPT/5PXtQh8i03iUA7q8dONBjWpV3nsGdM8Cs1FxpfhLstW0wZO1dOvSyElscwTRCJ6nO5N8r3Lg=="],
|
||||
|
||||
"@tanstack/react-store": ["@tanstack/react-store@0.9.2", "", { "dependencies": { "@tanstack/store": "0.9.2", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Vt5usJE5sHG/cMechQfmwvwne6ktGCELe89Lmvoxe3LKRoFrhPa8OCKWs0NliG8HTJElEIj7PLtaBQIcux5pAQ=="],
|
||||
"@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="],
|
||||
|
||||
"@tanstack/router-core": ["@tanstack/router-core@1.168.3", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2" }, "bin": { "intent": "bin/intent.js" } }, "sha512-qcjArls3v12UQQkEpU0+todc0/MCyrEZeXxhtgZZ0e5gxZDG25BUe/HlNcIjzyb7NZaw0TQAUBXbTClmFaHZiw=="],
|
||||
"@tanstack/router-core": ["@tanstack/router-core@1.168.5", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2" }, "bin": { "intent": "bin/intent.js" } }, "sha512-k378TPlJBM1x+7ixgQL+gC+v6omN6vF31QuhOBzcISQ1oW/oyHsJf8D4OWnZ4wJD63vD2P5kXLb8QcHM419oeg=="],
|
||||
|
||||
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.167.1", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.168.2", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-ECMM47J4KmifUvJguGituSiBpfN8SyCUEoxQks5RY09hpIBfR2eswCv2e6cJimjkKwBQXOVTPkTUk/yRvER+9w=="],
|
||||
|
||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.166.17", "", { "dependencies": { "@tanstack/router-core": "1.168.3", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-sBs6lyvA+B51hpUWYLx0KdaAIO/m9Ml2bsAdfVYyvs5DZXiAZZEbVD0myndyIkWaPR5x+kzuBakkrgTxJ9/m9Q=="],
|
||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.166.19", "", { "dependencies": { "@tanstack/router-core": "1.168.5", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-fSmeX99DOZrhL0fcc5S0VL1RNyImzTKzrP2SPXoee9dOGypWSln3tDce7LfbZS2JdwIXpyE+jQZfe67nXYUWig=="],
|
||||
|
||||
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.4", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.3", "@tanstack/router-generator": "1.166.17", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.168.3", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-VChByI+CHdHMW350E6winbgqdX4tzmZIHovys8vXidRZkxGAhlygj/zhbnepF/TGX88rubj+SXDwSHY25qEcpQ=="],
|
||||
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.6", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.5", "@tanstack/router-generator": "1.166.19", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.168.5", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-ktl4ErXjY1a08EAcvIwddnDTjbx7wmFbcYFtSLAx09ZPOxpMbK5wiHu7dlzw9Acv0hc176OzV1o4rrGV6t4RqQ=="],
|
||||
|
||||
"@tanstack/router-utils": ["@tanstack/router-utils@1.161.6", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw=="],
|
||||
|
||||
"@tanstack/store": ["@tanstack/store@0.9.2", "", {}, "sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA=="],
|
||||
"@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
|
||||
|
||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="],
|
||||
|
||||
@@ -1503,7 +1507,7 @@
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/mssql": ["@types/mssql@9.1.10", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-h3ImCiKdO7SM+2P1ToAZ+O0Ip23iISUHplH5GgMGnM9AwjmusDv8QqoNztoNj6vXpFbX29axsIybte7J2Q3XNg=="],
|
||||
"@types/mssql": ["@types/mssql@9.1.11", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-vcujgrDbDezCxNDO4KY6gjwduLYOKfrexpRUwhoysRvcXZ3+IgZ/PMYFDgh8c3cQIxZ6skAwYo+H6ibMrBWPjQ=="],
|
||||
|
||||
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||
|
||||
@@ -1567,25 +1571,25 @@
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
||||
|
||||
"@vitest/browser": ["@vitest/browser@4.1.1", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.1", "@vitest/utils": "4.1.1", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.0.3", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.1" } }, "sha512-gjjrFC4+kPVK/fN9URDJWrssU5Gqh8Az8pKG/NSfQ2V+ky8b/y1BgBg0Ug13+hOGp5pzInonmGRPn7vOgSLgzA=="],
|
||||
"@vitest/browser": ["@vitest/browser@4.1.2", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.2", "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.2" } }, "sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ=="],
|
||||
|
||||
"@vitest/browser-playwright": ["@vitest/browser-playwright@4.1.1", "", { "dependencies": { "@vitest/browser": "4.1.1", "@vitest/mocker": "4.1.1", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "playwright": "*", "vitest": "4.1.1" } }, "sha512-dtVSBZZha2k/7P7EAXXrEAoxuIKl8Yv9f2Dk4GN/DGfmhf4DQvkvu+57okR2wq/gan1xppKjL/aBxK/kbYrbGw=="],
|
||||
"@vitest/browser-playwright": ["@vitest/browser-playwright@4.1.2", "", { "dependencies": { "@vitest/browser": "4.1.2", "@vitest/mocker": "4.1.2", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "playwright": "*", "vitest": "4.1.2" } }, "sha512-N0Z2HzMLvMR6k/tWPTS6Q/DaRscrkax/f2f9DIbNQr+Cd1l4W4wTf/I6S983PAMr0tNqqoTL+xNkLh9M5vbkLg=="],
|
||||
|
||||
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.1", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.1", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.1.1", "vitest": "4.1.1" }, "optionalPeers": ["@vitest/browser"] }, "sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw=="],
|
||||
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.2", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.2", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.2", "vitest": "4.1.2" }, "optionalPeers": ["@vitest/browser"] }, "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@4.1.1", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A=="],
|
||||
"@vitest/expect": ["@vitest/expect@4.1.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@4.1.1", "", { "dependencies": { "@vitest/spy": "4.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A=="],
|
||||
"@vitest/mocker": ["@vitest/mocker@4.1.2", "", { "dependencies": { "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q=="],
|
||||
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.1", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ=="],
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.2", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@4.1.1", "", { "dependencies": { "@vitest/utils": "4.1.1", "pathe": "^2.0.3" } }, "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A=="],
|
||||
"@vitest/runner": ["@vitest/runner@4.1.2", "", { "dependencies": { "@vitest/utils": "4.1.2", "pathe": "^2.0.3" } }, "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ=="],
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "@vitest/utils": "4.1.1", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg=="],
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.1.2", "", { "dependencies": { "@vitest/pretty-format": "4.1.2", "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@4.1.1", "", {}, "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA=="],
|
||||
"@vitest/spy": ["@vitest/spy@4.1.2", "", {}, "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ=="],
|
||||
"@vitest/utils": ["@vitest/utils@4.1.2", "", { "dependencies": { "@vitest/pretty-format": "4.1.2", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ=="],
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
|
||||
|
||||
@@ -1663,11 +1667,11 @@
|
||||
|
||||
"babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="],
|
||||
|
||||
"better-auth": ["better-auth@1.5.6", "", { "dependencies": { "@better-auth/core": "1.5.6", "@better-auth/drizzle-adapter": "1.5.6", "@better-auth/kysely-adapter": "1.5.6", "@better-auth/memory-adapter": "1.5.6", "@better-auth/mongo-adapter": "1.5.6", "@better-auth/prisma-adapter": "1.5.6", "@better-auth/telemetry": "1.5.6", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.12", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ=="],
|
||||
|
||||
@@ -1695,7 +1699,7 @@
|
||||
|
||||
"bplist-parser": ["bplist-parser@0.3.2", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
"brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
@@ -1875,9 +1879,9 @@
|
||||
|
||||
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@1.0.0-beta.18-7eb39f0", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-F3ozFBjlUr/VWHR0NIz06jM9cKmfFafoXnCDhKvwjhH91hRK/l6sxIjiDOwMxjGuoJH+/p9W8DE6juoGvn7kXA=="],
|
||||
"drizzle-kit": ["drizzle-kit@1.0.0-beta.19", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-LO9cz8iipjyOROaBLmVAhu1WT4JGd8QKSc0XYhnO0FGXP5FappHII52lHcxfj2j+vVuDCTjoqeTTvW1N9NJYZQ=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@1.0.0-beta.18-7eb39f0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-piQ+esfm3krAiwXw3d8U559VdBjzA178jqazAkb0tSo7lJPZUL4SbB5uqg77ZUuCvY37hJuQ/o/gQoMX4m0E6Q=="],
|
||||
"drizzle-orm": ["drizzle-orm@1.0.0-beta.19", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-wL8FoHl9NRVYdrescFWyAVY5rNuEmbY4KDqO6Qo4RTVt1rYMbz7btq5Mhg91GN3x+qeUbQ83l9QkHYd4QmVTvg=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
@@ -1887,7 +1891,7 @@
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.322", "", {}, "sha512-vFU34OcrvMcH66T+dYC3G4nURmgfDVewMIu6Q2urXpumAPSMmzvcn04KVVV8Opikq8Vs5nUbO/8laNhNRqSzYw=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.325", "", {}, "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
@@ -1929,7 +1933,7 @@
|
||||
|
||||
"eslint-plugin-drizzle": ["eslint-plugin-drizzle@0.2.3", "", { "peerDependencies": { "eslint": ">=8.0.0" } }, "sha512-BO+ymHo33IUNoJlC0rbd7HP9EwwpW4VIp49R/tWQF/d2E1K2kgTf0tCXT0v9MSiBr6gGR1LtPwMLapTKEWSg9A=="],
|
||||
|
||||
"eslint-plugin-lingui": ["eslint-plugin-lingui@0.11.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.0.0", "micromatch": "^4.0.0" }, "peerDependencies": { "eslint": "^8.37.0 || ^9.0.0" } }, "sha512-O2Ixoapt5fa4VKZJgXhVwb6BHnzByIUDNMfZOhHWGMYk40GfGCho4MUfspLVrHAFLimgBPKXtCcJ8GC4YNZmfg=="],
|
||||
"eslint-plugin-lingui": ["eslint-plugin-lingui@0.12.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.57.2", "micromatch": "^4.0.8" }, "peerDependencies": { "eslint": "^8.37.0 || ^9.0.0", "typescript": "^5.0.0 || ^6.0.0" } }, "sha512-2+9P3thudGIBI10sDWYUIrGs3HIP09gv0XF98RDzZs34GAsAsGTUoSgrttKS1knAU5dwbrFKhNKu2LMrjRECag=="],
|
||||
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
|
||||
|
||||
@@ -2081,7 +2085,7 @@
|
||||
|
||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"fetch-nodeshim": ["fetch-nodeshim@0.4.9", "", {}, "sha512-XIQWlB2A4RZ7NebXWGxS0uDMdvRHkiUDTghBVJKFg9yEOd45w/PP8cZANuPf2H08W6Cor3+2n7Q6TTZgAS3Fkw=="],
|
||||
"fetch-nodeshim": ["fetch-nodeshim@0.4.10", "", {}, "sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w=="],
|
||||
|
||||
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
||||
|
||||
@@ -2157,7 +2161,7 @@
|
||||
|
||||
"glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
@@ -2431,7 +2435,7 @@
|
||||
|
||||
"mdn-data": ["mdn-data@2.0.14", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="],
|
||||
|
||||
"media-chrome": ["media-chrome@4.18.2", "", { "dependencies": { "ce-la-react": "^0.3.2" } }, "sha512-Ywy34ZalIXr/Ss1efv3KU9I02hb8PF7l+62kKwhxDkUjwcWM8Ashl0P7Ya6YcuxENaJLycsRgikG3hz52GhpIA=="],
|
||||
"media-chrome": ["media-chrome@4.18.3", "", { "dependencies": { "ce-la-react": "^0.3.2" } }, "sha512-YuS2wY0Fn+2nXGijJYn4+IE0n9wFe3v6SvOZHGNkoxh32T/cCcrXHUWskA+9tyYTONa6JKwKAOJJeO6QOlJLKw=="],
|
||||
|
||||
"media-played-ranges-mixin": ["media-played-ranges-mixin@0.1.0", "", {}, "sha512-zTsvkleu5sAyTsPVxDI+KUbCwy/lXwHgOPi3ER9S3lhtAWhGTQH6qxvfrVMym1cvoLU36SPbVr6Qe8Zxyc0WpA=="],
|
||||
|
||||
@@ -2487,7 +2491,7 @@
|
||||
|
||||
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
@@ -2537,7 +2541,7 @@
|
||||
|
||||
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="],
|
||||
"node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="],
|
||||
|
||||
"node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
|
||||
|
||||
@@ -2815,7 +2819,7 @@
|
||||
|
||||
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-rc.11", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.11" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.11", "@rolldown/binding-darwin-arm64": "1.0.0-rc.11", "@rolldown/binding-darwin-x64": "1.0.0-rc.11", "@rolldown/binding-freebsd-x64": "1.0.0-rc.11", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw=="],
|
||||
"rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
|
||||
|
||||
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||
|
||||
@@ -2899,7 +2903,7 @@
|
||||
|
||||
"slugify": ["slugify@1.6.8", "", {}, "sha512-HVk9X1E0gz3mSpoi60h/saazLKXKaZThMLU3u/aNwoYn8/xQyX2MGxL0ui2eaokkD7tF+Zo+cKTHUbe1mmmGzA=="],
|
||||
|
||||
"solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="],
|
||||
"solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="],
|
||||
|
||||
"sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
|
||||
|
||||
@@ -3075,7 +3079,7 @@
|
||||
|
||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||
|
||||
"uniwind": ["uniwind@1.6.0", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "culori": "4.0.2", "lightningcss": "1.30.1" }, "peerDependencies": { "react": ">=19.0.0", "react-native": ">=0.81.0", "tailwindcss": ">=4" } }, "sha512-D4jX0594cDnjeFCd0qDXJNQ5xT09W+zWkzq9yjUngNQQ4lCc7sN7Ht5a3+S752iT2D1r/Sul1eYPvjrTwmgSfg=="],
|
||||
"uniwind": ["uniwind@1.6.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "culori": "4.0.2", "lightningcss": "1.30.1" }, "peerDependencies": { "react": ">=19.0.0", "react-native": ">=0.81.0", "tailwindcss": ">=4" } }, "sha512-aR7vEGccEHDCvjyNM5BE5ZVdbdVTVL6yIffYe65mgjedzOj82TcdWrvae+A3PSHwQTuC1E3/3HbOX/Y5GqMvRQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
@@ -3109,9 +3113,9 @@
|
||||
|
||||
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
|
||||
|
||||
"vite": ["vite@8.0.2", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.11", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA=="],
|
||||
"vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
|
||||
|
||||
"vitest": ["vitest@4.1.1", "", { "dependencies": { "@vitest/expect": "4.1.1", "@vitest/mocker": "4.1.1", "@vitest/pretty-format": "4.1.1", "@vitest/runner": "4.1.1", "@vitest/snapshot": "4.1.1", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.1", "@vitest/browser-preview": "4.1.1", "@vitest/browser-webdriverio": "4.1.1", "@vitest/ui": "4.1.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA=="],
|
||||
"vitest": ["vitest@4.1.2", "", { "dependencies": { "@vitest/expect": "4.1.2", "@vitest/mocker": "4.1.2", "@vitest/pretty-format": "4.1.2", "@vitest/runner": "4.1.2", "@vitest/snapshot": "4.1.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.2", "@vitest/browser-preview": "4.1.2", "@vitest/browser-webdriverio": "4.1.2", "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg=="],
|
||||
|
||||
"vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="],
|
||||
|
||||
@@ -3209,6 +3213,10 @@
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/config-array/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"@eslint/eslintrc/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"@expo/cli/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||
@@ -3229,8 +3237,6 @@
|
||||
|
||||
"@expo/fingerprint/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
|
||||
|
||||
"@expo/fingerprint/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"@expo/metro/metro-runtime": ["metro-runtime@0.83.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw=="],
|
||||
|
||||
"@expo/metro/metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="],
|
||||
@@ -3295,15 +3301,21 @@
|
||||
|
||||
"@react-native/codegen/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro": ["metro@0.83.5", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.33.3", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.5", "metro-cache": "0.83.5", "metro-cache-key": "0.83.5", "metro-config": "0.83.5", "metro-core": "0.83.5", "metro-file-map": "0.83.5", "metro-resolver": "0.83.5", "metro-runtime": "0.83.5", "metro-source-map": "0.83.5", "metro-symbolicate": "0.83.5", "metro-transform-plugins": "0.83.5", "metro-transform-worker": "0.83.5", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-BgsXevY1MBac/3ZYv/RfNFf/4iuW9X7f4H8ZNkiH+r667HD9sVujxcmu4jvEzGCAm4/WyKdZCuyhAcyhTHOucQ=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro-config": ["metro-config@0.83.5", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.5", "metro-cache": "0.83.5", "metro-core": "0.83.5", "metro-runtime": "0.83.5", "yaml": "^2.6.1" } }, "sha512-JQ/PAASXH7yczgV6OCUSRhZYME+NU8NYjI2RcaG5ga4QfQ3T/XdiLzpSb3awWZYlDCcQb36l4Vl7i0Zw7/Tf9w=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro-core": ["metro-core@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.5" } }, "sha512-YcVcLCrf0ed4mdLa82Qob0VxYqfhmlRxUS8+TO4gosZo/gLwSvtdeOjc/Vt0pe/lvMNrBap9LlmvZM8FIsMgJQ=="],
|
||||
|
||||
"@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
|
||||
|
||||
"@react-native/dev-middleware/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
"@react-navigation/bottom-tabs/@react-navigation/elements": ["@react-navigation/elements@2.9.12", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.2.0", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-LSaQUj5SV9OXVRcxT8mqETDoM7BOKCveCvuLjdAr9NZnPDM5HW8uDnvW/sCa8oEFy+22+ojoXtHFKsfnesgBbw=="],
|
||||
"@react-navigation/bottom-tabs/@react-navigation/elements": ["@react-navigation/elements@2.9.13", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.2.1", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-ZD8fPwhujgs3SwgaPRse+shLCFkeLhlfk9BHtQ604Qa7/p0/sRQV9HkTfREP8gtbt6nwk6WE+0vWfX2iqxOCKA=="],
|
||||
|
||||
"@react-navigation/core/react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="],
|
||||
|
||||
"@react-navigation/native-stack/@react-navigation/elements": ["@react-navigation/elements@2.9.12", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.2.0", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-LSaQUj5SV9OXVRcxT8mqETDoM7BOKCveCvuLjdAr9NZnPDM5HW8uDnvW/sCa8oEFy+22+ojoXtHFKsfnesgBbw=="],
|
||||
"@react-navigation/native-stack/@react-navigation/elements": ["@react-navigation/elements@2.9.13", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.2.1", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-ZD8fPwhujgs3SwgaPRse+shLCFkeLhlfk9BHtQ604Qa7/p0/sRQV9HkTfREP8gtbt6nwk6WE+0vWfX2iqxOCKA=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||
|
||||
@@ -3335,10 +3347,6 @@
|
||||
|
||||
"@tanstack/zod-adapter/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@ts-morph/common/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
@@ -3361,12 +3369,12 @@
|
||||
|
||||
"chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"chromium-edge-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"compressible/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"compression/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
|
||||
@@ -3379,6 +3387,10 @@
|
||||
|
||||
"eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
|
||||
"eslint/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"eslint/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||
|
||||
"expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
|
||||
@@ -3399,8 +3411,6 @@
|
||||
|
||||
"express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"figures/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
@@ -3411,8 +3421,6 @@
|
||||
|
||||
"finalhandler/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
@@ -3509,9 +3517,9 @@
|
||||
|
||||
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.11", "", {}, "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ=="],
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
|
||||
|
||||
"router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||
"router/path-to-regexp": ["path-to-regexp@8.4.0", "", {}, "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg=="],
|
||||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
@@ -3539,6 +3547,8 @@
|
||||
|
||||
"test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"test-exclude/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"tsx/esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
|
||||
|
||||
"tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
@@ -3571,7 +3581,9 @@
|
||||
|
||||
"@dotenvx/dotenvx/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
||||
|
||||
"@expo/cli/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"@expo/cli/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
||||
|
||||
@@ -3581,14 +3593,6 @@
|
||||
|
||||
"@expo/cli/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="],
|
||||
|
||||
"@expo/config-plugins/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"@expo/config/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"@expo/fingerprint/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"@expo/metro-config/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"@expo/metro-config/hermes-parser/hermes-estree": ["hermes-estree@0.32.1", "", {}, "sha512-ne5hkuDxheNBAikDjqvCZCwihnz0vVu9YsBzAEO1puiyFR4F1+PAz/SiPHSsNTuOveCYGRMX8Xbx4LOubeC0Qg=="],
|
||||
|
||||
"@expo/metro-config/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
@@ -3649,8 +3653,42 @@
|
||||
|
||||
"@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@react-native/codegen/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/hermes-parser": ["hermes-parser@0.33.3", "", { "dependencies": { "hermes-estree": "0.33.3" } }, "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/metro-babel-transformer": ["metro-babel-transformer@0.83.5", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.33.3", "nullthrows": "^1.1.1" } }, "sha512-d9FfmgUEVejTiSb7bkQeLRGl6aeno2UpuPm3bo3rCYwxewj03ymvOn8s8vnS4fBqAPQ+cE9iQM40wh7nGXR+eA=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/metro-cache": ["metro-cache@0.83.5", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.5" } }, "sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/metro-cache-key": ["metro-cache-key@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-Ycl8PBajB7bhbAI7Rt0xEyiF8oJ0RWX8EKkolV1KfCUlC++V/GStMSGpPLwnnBZXZWkCC5edBPzv1Hz1Yi0Euw=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/metro-file-map": ["metro-file-map@0.83.5", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-ZEt8s3a1cnYbn40nyCD+CsZdYSlwtFh2kFym4lo+uvfM+UMMH+r/BsrC6rbNClSrt+B7rU9T+Te/sh/NL8ZZKQ=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/metro-resolver": ["metro-resolver@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/metro-symbolicate": ["metro-symbolicate@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/metro-transform-plugins": ["metro-transform-plugins@0.83.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-KxYKzZL+lt3Os5H2nx7YkbkWVduLZL5kPrE/Yq+Prm/DE1VLhpfnO6HtPs8vimYFKOa58ncl60GpoX0h7Wm0Vw=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/metro-transform-worker": ["metro-transform-worker@0.83.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.83.5", "metro-babel-transformer": "0.83.5", "metro-cache": "0.83.5", "metro-cache-key": "0.83.5", "metro-minify-terser": "0.83.5", "metro-source-map": "0.83.5", "metro-transform-plugins": "0.83.5", "nullthrows": "^1.1.1" } }, "sha512-8N4pjkNXc6ytlP9oAM6MwqkvUepNSW39LKYl9NjUMpRDazBQ7oBpQDc8Sz4aI8jnH6AGhF7s1m/ayxkN1t04yA=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro-config/metro-cache": ["metro-cache@0.83.5", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.5" } }, "sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro-core/metro-resolver": ["metro-resolver@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A=="],
|
||||
|
||||
"@react-native/dev-middleware/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
@@ -3705,14 +3743,8 @@
|
||||
|
||||
"@tanstack/router-plugin/chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"@tanstack/router-plugin/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"@tanstack/router-plugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
|
||||
@@ -3731,7 +3763,7 @@
|
||||
|
||||
"connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"expo-updates/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
"eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
@@ -3739,8 +3771,6 @@
|
||||
|
||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
|
||||
@@ -3757,8 +3787,12 @@
|
||||
|
||||
"node-vibrant/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"react-native/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
|
||||
"rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"shadcn/ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
@@ -3779,6 +3813,8 @@
|
||||
|
||||
"tedious/bl/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
|
||||
|
||||
"test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="],
|
||||
@@ -3853,7 +3889,9 @@
|
||||
|
||||
"vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"@expo/cli/glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
"@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"@expo/cli/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
|
||||
@@ -3865,14 +3903,6 @@
|
||||
|
||||
"@expo/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
|
||||
|
||||
"@expo/config-plugins/glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"@expo/config/glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"@expo/fingerprint/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"@expo/metro-config/glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"@expo/package-manager/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
|
||||
"@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||
@@ -3887,6 +3917,16 @@
|
||||
|
||||
"@istanbuljs/load-nyc-config/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"@react-native/codegen/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/hermes-parser/hermes-estree": ["hermes-estree@0.33.3", "", {}, "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-Toe4Md1wS1PBqbvB0cFxBzKEVyyuYTUb0sgifAZh/mSvLH84qA1NAWik9sISWatzvfWf3rOGoUoO5E3f193a3Q=="],
|
||||
|
||||
"@react-native/community-cli-plugin/metro/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
@@ -3921,13 +3961,11 @@
|
||||
|
||||
"@tanstack/router-plugin/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
"eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
"react-native/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"expo-updates/glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"shadcn/ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||
|
||||
@@ -3937,7 +3975,7 @@
|
||||
|
||||
"shadcn/ora/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"@expo/cli/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
"test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"@expo/cli/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
@@ -3947,12 +3985,6 @@
|
||||
|
||||
"@expo/cli/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"@expo/config-plugins/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"@expo/config/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"@expo/metro-config/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"@expo/package-manager/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
"@expo/package-manager/ora/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
||||
@@ -3963,7 +3995,11 @@
|
||||
|
||||
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
|
||||
"expo-updates/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
"@react-native/codegen/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"react-native/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"shadcn/ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
|
||||
+10
-6
@@ -7,10 +7,10 @@
|
||||
"dependencies": {
|
||||
"@takumi-rs/image-response": "^0.73.1",
|
||||
"@vercel/analytics": "^2.0.1",
|
||||
"fumadocs-core": "^16.7.5",
|
||||
"fumadocs-core": "^16.7.6",
|
||||
"fumadocs-mdx": "^14.2.11",
|
||||
"fumadocs-openapi": "^10.4.1",
|
||||
"fumadocs-ui": "^16.7.5",
|
||||
"fumadocs-openapi": "^10.5.0",
|
||||
"fumadocs-ui": "^16.7.6",
|
||||
"lucide-react": "^0.577.0",
|
||||
"next": "16.2.1",
|
||||
"react": "19.2.4",
|
||||
@@ -568,13 +568,13 @@
|
||||
|
||||
"framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="],
|
||||
|
||||
"fumadocs-core": ["fumadocs-core@16.7.5", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.8.2", "@orama/orama": "^3.1.18", "@shikijs/rehype": "^4.0.2", "@shikijs/transformers": "^4.0.2", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "path-to-regexp": "^8.3.0", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.0.2", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@mdx-js/mdx": "*", "@mixedbread/sdk": "^0.46.0", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/estree-jsx": "*", "@types/hast": "*", "@types/mdast": "*", "@types/react": "*", "algoliasearch": "5.x.x", "flexsearch": "*", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "^0.26.0 || ^0.27.0 || ^1.0.0", "zod": "4.x.x" }, "optionalPeers": ["@mdx-js/mdx", "@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/estree-jsx", "@types/hast", "@types/mdast", "@types/react", "algoliasearch", "flexsearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-qOAKQyNUCZQ0wRun2Jnw7f6VLEgCiGFomU+kQXUZI36cX9bJPiymXboKaSjCxT1PuIusjzUyxYTGapVZxzd9TA=="],
|
||||
"fumadocs-core": ["fumadocs-core@16.7.6", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.8.2", "@orama/orama": "^3.1.18", "@shikijs/rehype": "^4.0.2", "@shikijs/transformers": "^4.0.2", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "path-to-regexp": "^8.3.0", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.0.2", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@mdx-js/mdx": "*", "@mixedbread/sdk": "^0.46.0", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/estree-jsx": "*", "@types/hast": "*", "@types/mdast": "*", "@types/react": "*", "algoliasearch": "5.x.x", "flexsearch": "*", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "^0.26.0 || ^0.27.0 || ^1.0.0", "zod": "4.x.x" }, "optionalPeers": ["@mdx-js/mdx", "@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/estree-jsx", "@types/hast", "@types/mdast", "@types/react", "algoliasearch", "flexsearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-d4HtGupFpcSWQqLbWh184yoEg6D70pH68NP77Ct4mI0N61t/Uy63wYj9sbS1h/m6jlijUIXC6rz8D5JApOB9Wg=="],
|
||||
|
||||
"fumadocs-mdx": ["fumadocs-mdx@14.2.11", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.27.3", "estree-util-value-to-estree": "^3.5.0", "js-yaml": "^4.1.1", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "picocolors": "^1.1.1", "picomatch": "^4.0.3", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "zod": "^4.3.6" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^15.0.0 || ^16.0.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "*", "vite": "6.x.x || 7.x.x || 8.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "vite"], "bin": { "fumadocs-mdx": "dist/bin.js" } }, "sha512-j0gHKs45c62ARteE8/yBM2Nu2I8AE2Cs37ktPEdc/8EX7TL66XP74un5OpHp6itLyWTu8Jur0imOiiIDq8+rDg=="],
|
||||
|
||||
"fumadocs-openapi": ["fumadocs-openapi@10.4.1", "", { "dependencies": { "@fastify/deepmerge": "^3.2.1", "@fumari/json-schema-ts": "^0.0.2", "@fumari/stf": "1.0.3", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@scalar/json-magic": "^0.12.4", "@scalar/openapi-upgrader": "^0.2.1", "ajv": "^8.18.0", "class-variance-authority": "^0.7.1", "dereference-json-schema": "^0.2.2", "github-slugger": "^2.0.0", "hast-util-to-jsx-runtime": "^2.3.6", "js-yaml": "^4.1.1", "lucide-react": "^0.577.0", "next-themes": "^0.4.6", "openapi-sampler": "^1.7.2", "react-hook-form": "^7.71.2", "remark": "^15.0.1", "remark-rehype": "^11.1.2", "tailwind-merge": "^3.5.0", "xml-js": "^1.6.11" }, "peerDependencies": { "@scalar/api-client-react": "*", "@types/react": "*", "fumadocs-core": "^16.7.0", "fumadocs-ui": "^16.7.0", "json-schema-typed": "*", "react": "^19.2.0", "react-dom": "^19.2.0", "shiki": "*" }, "optionalPeers": ["@scalar/api-client-react", "@types/react", "json-schema-typed", "shiki"] }, "sha512-iMEDyANcikDePUFlBE5sffjTbrUI5LELuIla+6kTZJYEuoQMLUK+plOnXq1iacPBMeCS7cSFadvE/Vp/h7oA+g=="],
|
||||
"fumadocs-openapi": ["fumadocs-openapi@10.5.0", "", { "dependencies": { "@fastify/deepmerge": "^3.2.1", "@fumari/json-schema-ts": "^0.0.2", "@fumari/stf": "1.0.3", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@scalar/json-magic": "^0.12.4", "@scalar/openapi-upgrader": "^0.2.2", "ajv": "^8.18.0", "class-variance-authority": "^0.7.1", "dereference-json-schema": "^0.2.2", "github-slugger": "^2.0.0", "hast-util-to-jsx-runtime": "^2.3.6", "js-yaml": "^4.1.1", "lucide-react": "^1.6.0", "next-themes": "^0.4.6", "openapi-sampler": "^1.7.2", "react-hook-form": "^7.72.0", "remark": "^15.0.1", "remark-rehype": "^11.1.2", "tailwind-merge": "^3.5.0", "xml-js": "^1.6.11" }, "peerDependencies": { "@scalar/api-client-react": "*", "@types/react": "*", "fumadocs-core": "^16.7.0", "fumadocs-ui": "^16.7.0", "json-schema-typed": "*", "react": "^19.2.0", "react-dom": "^19.2.0", "shiki": "*" }, "optionalPeers": ["@scalar/api-client-react", "@types/react", "json-schema-typed", "shiki"] }, "sha512-EG3/U/NI/ha0xhIUiUIKiRSmLS4AOjn+4xkZXOoiLB5rlV+HyHsq9qcN+wKgbK9u4qoNuAcMzTEYUXScoSUbXA=="],
|
||||
|
||||
"fumadocs-ui": ["fumadocs-ui@16.7.5", "", { "dependencies": { "@fumadocs/tailwind": "0.0.3", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "lucide-react": "^0.577.0", "motion": "^12.38.0", "next-themes": "^0.4.6", "react-medium-image-zoom": "^5.4.1", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.5.0", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.7.5", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "shiki": "*" }, "optionalPeers": ["@takumi-rs/image-response", "@types/mdx", "@types/react", "next", "shiki"] }, "sha512-i9zvslCFc25hxYsAmkBCJUgAwvWbKwMn4Xd5L7LcnQwW60r8VPihkfeWB/DDtHv4wJTGOPl+neb1AgNkm1Cujw=="],
|
||||
"fumadocs-ui": ["fumadocs-ui@16.7.6", "", { "dependencies": { "@fumadocs/tailwind": "0.0.3", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "lucide-react": "^1.6.0", "motion": "^12.38.0", "next-themes": "^0.4.6", "react-medium-image-zoom": "^5.4.1", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.5.0", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.7.6", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "shiki": "*" }, "optionalPeers": ["@takumi-rs/image-response", "@types/mdx", "@types/react", "next", "shiki"] }, "sha512-wjZnm8SiX2lj5zWOlOHnzSZ0YBFwNqYGBX1u5F3mZtdIkmkDVs+3+JngCkRHNZzYJVBulXjp8t5wzBz0yDJa8w=="],
|
||||
|
||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||
|
||||
@@ -968,6 +968,10 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"fumadocs-openapi/lucide-react": ["lucide-react@1.7.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg=="],
|
||||
|
||||
"fumadocs-ui/lucide-react": ["lucide-react@1.7.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg=="],
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
+1
-1
@@ -16,4 +16,4 @@ _openapi:
|
||||
|
||||
Create a new media server integration or update an existing one. Generates a unique webhook token for the provider.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/integrations","method":"post"}]} />
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/integrations","method":"post"}]} />
|
||||
+1
-1
@@ -14,4 +14,4 @@ _openapi:
|
||||
|
||||
Remove a media server integration and all its event history.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/integrations/{provider}","method":"delete"}]} />
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/integrations/{provider}","method":"delete"}]} />
|
||||
+1
-1
@@ -16,4 +16,4 @@ _openapi:
|
||||
|
||||
Fetch all configured media server integrations for the current user, including recent webhook/sync events for each.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/integrations","method":"get"}]} />
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/integrations","method":"get"}]} />
|
||||
+1
-1
@@ -16,4 +16,4 @@ _openapi:
|
||||
|
||||
Generate a new webhook token for an integration. The old token is immediately invalidated.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/integrations/{provider}/regenerate-token","method":"post"}]} />
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/integrations/{provider}/regenerate-token","method":"post"}]} />
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: Get registration status
|
||||
full: true
|
||||
_openapi:
|
||||
method: GET
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: Check whether new user registration is currently open or closed.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Check whether new user registration is currently open or closed.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/registration","method":"get"}]} />
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Get telemetry status
|
||||
title: Get admin settings
|
||||
full: true
|
||||
_openapi:
|
||||
method: GET
|
||||
@@ -8,12 +8,12 @@ _openapi:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch whether anonymous telemetry is enabled and when the last report
|
||||
was sent.
|
||||
Fetch all admin-configurable settings: registration, update checks,
|
||||
and telemetry.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch whether anonymous telemetry is enabled and when the last report was sent.
|
||||
Fetch all admin-configurable settings: registration, update checks, and telemetry.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/telemetry","method":"get"}]} />
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/settings","method":"get"}]} />
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Update admin settings
|
||||
full: true
|
||||
_openapi:
|
||||
method: PATCH
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Partially update admin settings. Only provided sections are changed;
|
||||
omitted sections keep their current values.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Partially update admin settings. Only provided sections are changed; omitted sections keep their current values.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/settings","method":"patch"}]} />
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: Toggle registration
|
||||
full: true
|
||||
_openapi:
|
||||
method: PUT
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: Open or close new user registration.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Open or close new user registration.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/registration","method":"put"}]} />
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: Toggle telemetry
|
||||
full: true
|
||||
_openapi:
|
||||
method: PUT
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: Enable or disable anonymous telemetry reporting.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Enable or disable anonymous telemetry reporting.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/telemetry","method":"put"}]} />
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user