From ad84b66ccdf88dc9a29eb7a8722c0bfb81ab5727 Mon Sep 17 00:00:00 2001 From: Aaron Leopold <36278431+aaronleopold@users.noreply.github.com> Date: Sat, 28 Sep 2024 14:41:55 -0700 Subject: [PATCH 01/35] :recycle: Rewrite `api` package as standalone `sdk` (#457) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * just messing around * progress with new patterns * wip more type gen * working tirlessly before these layover flights 🥵 * library API * add interceptor and build out new api * continue building out sdk * fully ported functions to classes * start using sdk * wip: replace api stuff with sdk * wip: almost done with hooks! * finish primary hooks * wip: fix the world * wip: fix the world * la la laaaa la la * its alive! * fix scan method * add api tests and fix existing tests * fix lint * try fix weird dotenv error --- Cargo.lock | 2 +- README.md | 8 +- apps/desktop/src-tauri/src/main.rs | 2 +- apps/expo/package.json | 2 +- apps/expo/src/App.tsx | 2 +- .../expo/src/components/book/BookListItem.tsx | 38 +-- .../components/reader/epub/EpubJSReader.tsx | 12 +- .../reader/image/ImageBasedReader.tsx | 7 +- apps/expo/src/screens/authenticated/Home.tsx | 6 +- apps/expo/tsconfig.json | 4 +- apps/server/Cargo.toml | 1 + apps/server/src/filter/basic_filter.rs | 43 ++-- apps/server/src/routers/api/mod.rs | 37 ++- apps/server/src/routers/api/v1/library.rs | 7 +- apps/server/src/routers/api/v1/media.rs | 5 +- apps/server/src/routers/utoipa.rs | 9 +- core/src/db/filter/smart_filter.rs | 4 +- core/src/db/query/pagination.rs | 4 + core/src/lib.rs | 4 +- crates/integrations/.env.template | 3 - crates/integrations/Cargo.toml | 1 - .../src/notifier/discord_client.rs | 2 - .../src/notifier/telegram_client.rs | 2 - packages/api/src/auth.ts | 36 --- packages/api/src/axios.ts | 68 ----- packages/api/src/bookClub.ts | 178 ------------- packages/api/src/config.ts | 12 - packages/api/src/emailer.ts | 122 --------- packages/api/src/epub.ts | 70 ------ packages/api/src/filesystem.ts | 26 -- packages/api/src/index.ts | 33 --- packages/api/src/job.ts | 56 ----- packages/api/src/library.ts | 171 ------------- packages/api/src/log.ts | 45 ---- packages/api/src/media.ts | 151 ----------- packages/api/src/metadata.ts | 79 ------ packages/api/src/series.ts | 119 --------- packages/api/src/server.ts | 34 --- packages/api/src/smartList.ts | 98 -------- packages/api/src/tag.ts | 22 -- packages/api/src/types.ts | 16 -- packages/api/src/user.ts | 147 ----------- packages/browser/package.json | 2 +- packages/browser/src/App.tsx | 32 ++- packages/browser/src/AppLayout.tsx | 2 +- .../browser/src/components/book/BookCard.tsx | 12 +- .../src/components/book/BookSearch.tsx | 7 +- .../components/book/table/CoverImageCell.tsx | 8 +- .../src/components/explorer/FileThumbnail.tsx | 25 +- .../src/components/filters/FilterProvider.tsx | 2 +- .../filters/form/MediaFilterForm.tsx | 15 +- .../src/components/filters/useFilterScene.ts | 2 +- .../library/DeleteLibraryConfirmation.tsx | 6 +- .../components/library/LastVisitedLibrary.tsx | 12 +- .../components/navigation/sidebar/Logout.tsx | 9 +- .../components/navigation/sidebar/SignOut.tsx | 8 +- .../sidebar/sections/library/LibraryEmoji.tsx | 5 +- .../sections/library/LibraryOptionsMenu.tsx | 2 +- .../navigation/topbar/sections/UserMenu.tsx | 8 +- .../components/readers/epub/EpubJsReader.tsx | 25 +- .../readers/epub/EpubStreamReader.tsx | 25 +- .../readers/epub/controls/useEpubBookmark.ts | 22 +- .../readers/imageBased/ImageBasedReader.tsx | 12 +- .../imageBased/container/ReaderFooter.tsx | 5 +- .../readers/pdf/NativePDFViewer.tsx | 8 +- .../ConfiguredServersList.tsx | 4 +- .../__tests__/ConfiguredServersList.tests.tsx | 20 +- .../src/components/series/SeriesCard.tsx | 12 +- .../series/table/CoverImageCell.tsx | 7 +- .../src/scenes/auth/LoginOrClaimScene.tsx | 2 +- .../book/BookCompletionToggleButton.tsx | 19 +- .../src/scenes/book/DownloadMediaButton.tsx | 5 +- .../scenes/book/reader/BookReaderScene.tsx | 1 - .../book/settings/BookManagementScene.tsx | 6 +- .../src/scenes/book/settings/BookPageGrid.tsx | 5 +- .../book/settings/BookThumbnailSelector.tsx | 9 +- .../bookClub/home/BookClubNavigation.tsx | 7 +- .../home/tabs/chat-board/ChatMessage.tsx | 11 +- .../overview/BookClubScheduleTimelineItem.tsx | 8 +- .../tabs/settings/scheduler/AddBookCard.tsx | 8 +- .../src/scenes/bookSearch/BookSearchScene.tsx | 7 +- .../createLibrary/CreateLibraryScene.tsx | 2 +- .../src/scenes/home/RecentlyAddedSeries.tsx | 6 +- .../src/scenes/library/LibraryHeader.tsx | 5 +- .../src/scenes/library/LibraryLayout.tsx | 4 +- .../src/scenes/library/LibraryNavigation.tsx | 19 +- .../library/LibraryOverviewTitleSection.tsx | 5 +- .../library/tabs/books/LibraryBooksScene.tsx | 9 +- .../tabs/series/LibrarySeriesScene.tsx | 13 +- .../tabs/settings/LibrarySeriesGrid.tsx | 6 +- .../tabs/settings/LibrarySettingsRouter.tsx | 10 +- .../accessControl/LibraryExclusions.tsx | 10 +- .../settings/danger/deletion/CleanLibrary.tsx | 9 +- .../options/analysis/AnalyzeMedia.tsx | 8 +- .../thumbnails/DeleteLibraryThumbnails.tsx | 7 +- .../thumbnails/LibraryThumbnailSelector.tsx | 13 +- .../thumbnails/RegenerateThumbnails.tsx | 9 +- .../src/scenes/series/SeriesHeader.tsx | 5 +- .../src/scenes/series/SeriesLibraryLink.tsx | 4 +- .../src/scenes/series/SeriesNavigation.tsx | 12 +- .../series/tabs/books/SeriesBooksScene.tsx | 9 +- .../series/tabs/settings/SeriesBookGrid.tsx | 6 +- .../tabs/settings/SeriesSettingsScene.tsx | 5 +- .../tabs/settings/SeriesThumbnailSelector.tsx | 11 +- .../settings/app/general/ProfileForm.tsx | 2 +- .../server/email/CreateEmailerScene.tsx | 10 +- .../devices/CreateOrUpdateDeviceModal.tsx | 19 +- .../devices/DeleteDeviceConfirmation.tsx | 10 +- .../server/email/emailers/EmailerListItem.tsx | 5 +- .../settings/server/jobs/DeleteAllSection.tsx | 8 +- .../settings/server/jobs/JobActionMenu.tsx | 25 +- .../server/logs/live-logs/LiveLogsFeed.tsx | 7 +- .../CreateOrUpdateUserForm.tsx | 8 +- .../login-activity/ClearActivitySection.tsx | 8 +- .../users/user-table/DeleteUserModal.tsx | 6 +- .../users/user-table/UserActionMenu.tsx | 12 +- .../src/scenes/smartList/SmartListCard.tsx | 5 +- .../smartList/UserSmartListNavigation.tsx | 9 +- .../GroupedSmartListItemListGroupContent.tsx | 5 +- .../smartList/items/table/CoverImageCell.tsx | 7 +- .../settings/DeleteSmartListConfirmation.tsx | 2 +- packages/browser/src/utils/prefetch.ts | 7 +- packages/browser/tsconfig.json | 6 +- packages/client/package.json | 2 +- packages/client/src/client.ts | 11 +- packages/client/src/hooks/useCoreEvent.ts | 30 +-- packages/client/src/hooks/useStumpSse.ts | 13 +- packages/client/src/hooks/useStumpWs.ts | 9 +- packages/client/src/index.ts | 1 + packages/client/src/queries/auth.ts | 47 ++-- packages/client/src/queries/bookClub.ts | 120 +++++---- packages/client/src/queries/emailers.ts | 104 ++++---- packages/client/src/queries/epub.ts | 17 +- packages/client/src/queries/filesystem.ts | 51 ++-- packages/client/src/queries/index.ts | 105 +------- packages/client/src/queries/job.ts | 24 +- packages/client/src/queries/library.ts | 214 +++++++--------- packages/client/src/queries/log.ts | 18 +- packages/client/src/queries/media.ts | 217 ++++++++-------- packages/client/src/queries/series.ts | 154 +++++++----- packages/client/src/queries/server.ts | 27 +- packages/client/src/queries/smartList.ts | 125 +++++----- packages/client/src/queries/tag.ts | 20 +- packages/client/src/queries/user.ts | 111 +++------ packages/client/src/sdk/SDKProvider.tsx | 29 +++ packages/client/src/sdk/context.ts | 16 ++ packages/client/src/sdk/index.ts | 2 + packages/client/tsconfig.json | 6 +- packages/sdk/babel.config.json | 6 + packages/{api => sdk}/package.json | 12 +- packages/sdk/src/__tests__/api.test.ts | 100 ++++++++ packages/sdk/src/api.ts | 231 +++++++++++++++++ packages/sdk/src/base.ts | 21 ++ packages/sdk/src/configuration.ts | 12 + packages/sdk/src/controllers/auth-api.ts | 88 +++++++ packages/sdk/src/controllers/bookclub-api.ts | 228 +++++++++++++++++ packages/sdk/src/controllers/emailer-api.ts | 186 ++++++++++++++ packages/sdk/src/controllers/epub-api.ts | 123 +++++++++ .../sdk/src/controllers/filesystem-api.ts | 38 +++ packages/sdk/src/controllers/index.ts | 16 ++ packages/sdk/src/controllers/job-api.ts | 90 +++++++ packages/sdk/src/controllers/library-api.ts | 235 ++++++++++++++++++ packages/sdk/src/controllers/log-api.ts | 61 +++++ packages/sdk/src/controllers/media-api.ts | 170 +++++++++++++ packages/sdk/src/controllers/metadata-api.ts | 128 ++++++++++ packages/sdk/src/controllers/series-api.ts | 155 ++++++++++++ packages/sdk/src/controllers/server-api.ts | 58 +++++ packages/sdk/src/controllers/smartlist-api.ts | 137 ++++++++++ packages/sdk/src/controllers/tag-api.ts | 45 ++++ packages/sdk/src/controllers/types.ts | 35 +++ packages/sdk/src/controllers/user-api.ts | 204 +++++++++++++++ .../{api/src => sdk/src/controllers}/utils.ts | 18 ++ packages/sdk/src/index.ts | 5 + packages/sdk/src/utils.ts | 39 +++ packages/{api => sdk}/tsconfig.json | 0 packages/types/generated.ts | 70 +++++- tsconfig.json | 3 + tsconfig.options.json | 2 +- yarn.lock | 36 ++- 179 files changed, 3783 insertions(+), 2663 deletions(-) delete mode 100644 crates/integrations/.env.template delete mode 100644 packages/api/src/auth.ts delete mode 100644 packages/api/src/axios.ts delete mode 100644 packages/api/src/bookClub.ts delete mode 100644 packages/api/src/config.ts delete mode 100644 packages/api/src/emailer.ts delete mode 100644 packages/api/src/epub.ts delete mode 100644 packages/api/src/filesystem.ts delete mode 100644 packages/api/src/index.ts delete mode 100644 packages/api/src/job.ts delete mode 100644 packages/api/src/library.ts delete mode 100644 packages/api/src/log.ts delete mode 100644 packages/api/src/media.ts delete mode 100644 packages/api/src/metadata.ts delete mode 100644 packages/api/src/series.ts delete mode 100644 packages/api/src/server.ts delete mode 100644 packages/api/src/smartList.ts delete mode 100644 packages/api/src/tag.ts delete mode 100644 packages/api/src/types.ts delete mode 100644 packages/api/src/user.ts create mode 100644 packages/client/src/sdk/SDKProvider.tsx create mode 100644 packages/client/src/sdk/context.ts create mode 100644 packages/client/src/sdk/index.ts create mode 100644 packages/sdk/babel.config.json rename packages/{api => sdk}/package.json (59%) create mode 100644 packages/sdk/src/__tests__/api.test.ts create mode 100644 packages/sdk/src/api.ts create mode 100644 packages/sdk/src/base.ts create mode 100644 packages/sdk/src/configuration.ts create mode 100644 packages/sdk/src/controllers/auth-api.ts create mode 100644 packages/sdk/src/controllers/bookclub-api.ts create mode 100644 packages/sdk/src/controllers/emailer-api.ts create mode 100644 packages/sdk/src/controllers/epub-api.ts create mode 100644 packages/sdk/src/controllers/filesystem-api.ts create mode 100644 packages/sdk/src/controllers/index.ts create mode 100644 packages/sdk/src/controllers/job-api.ts create mode 100644 packages/sdk/src/controllers/library-api.ts create mode 100644 packages/sdk/src/controllers/log-api.ts create mode 100644 packages/sdk/src/controllers/media-api.ts create mode 100644 packages/sdk/src/controllers/metadata-api.ts create mode 100644 packages/sdk/src/controllers/series-api.ts create mode 100644 packages/sdk/src/controllers/server-api.ts create mode 100644 packages/sdk/src/controllers/smartlist-api.ts create mode 100644 packages/sdk/src/controllers/tag-api.ts create mode 100644 packages/sdk/src/controllers/types.ts create mode 100644 packages/sdk/src/controllers/user-api.ts rename packages/{api/src => sdk/src/controllers}/utils.ts (85%) create mode 100644 packages/sdk/src/index.ts create mode 100644 packages/sdk/src/utils.ts rename packages/{api => sdk}/tsconfig.json (100%) diff --git a/Cargo.lock b/Cargo.lock index ef6f8d7bb..b835172e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3665,7 +3665,6 @@ name = "integrations" version = "0.0.5" dependencies = [ "async-trait", - "dotenv", "lettre", "reqwest 0.12.7", "serde_json", @@ -7846,6 +7845,7 @@ dependencies = [ "serde-untagged", "serde_json", "serde_qs", + "serde_with", "specta", "stump_core", "thiserror", diff --git a/README.md b/README.md index c528a8ad4..be26eb485 100644 --- a/README.md +++ b/README.md @@ -182,11 +182,11 @@ A NextJS application for the Stump documentation site at `/docs` in the root of Various TypeScript packages, at `/packages` in the root of the project: -- `api`: All of the API functions used by the `client` package -- `client`: React-query config, hooks, and other client-side utilities +- `sdk`: A TypeScript SDK for interfacing with Stump's API +- `client`: React-query config, hooks, and other client-side utilities for React-based applications - `components`: Shared React components for the web and desktop applications -- `browser`: A React component that is essentially the "main" UI for Stump on browser-based platforms. It is isolated in order to be re-used in the two browser-based apps: `web` and `desktop` -- `types`: Shared TypeScript types for interfacing with Stump's core and API +- `browser`: A React component that is essentially the "main" UI for Stump on browser-based platforms. This package is shared between both the `web` and `desktop` apps +- `types`: Shared TypeScript types generated by the various codegen tools in the project diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index a60bdb638..c2ac4167f 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -84,7 +84,7 @@ mod tests { where T: NamedType, { - export::(&ExportConfiguration::new().bigint(BigIntExportBehavior::BigInt)) + export::(&ExportConfiguration::new().bigint(BigIntExportBehavior::Number)) } #[test] diff --git a/apps/expo/package.json b/apps/expo/package.json index 3e726485f..dea5ea476 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -15,7 +15,7 @@ "@react-navigation/bottom-tabs": "^6.5.12", "@react-navigation/native": "^6.1.10", "@react-navigation/native-stack": "^6.9.18", - "@stump/api": "*", + "@stump/sdk": "*", "@tanstack/react-query": "^4.20.4", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", diff --git a/apps/expo/src/App.tsx b/apps/expo/src/App.tsx index 2d21c7c11..c4abc970d 100644 --- a/apps/expo/src/App.tsx +++ b/apps/expo/src/App.tsx @@ -1,7 +1,7 @@ import { NavigationContainer } from '@react-navigation/native' import { createNativeStackNavigator } from '@react-navigation/native-stack' -import { checkUrl, initializeApi, isAxiosError, isUrl } from '@stump/api' import { useAuthQuery } from '@stump/client' +import { checkUrl, initializeApi, isAxiosError, isUrl } from '@stump/sdk' import * as SplashScreen from 'expo-splash-screen' import { useEffect, useState } from 'react' import { SafeAreaProvider } from 'react-native-safe-area-context' diff --git a/apps/expo/src/components/book/BookListItem.tsx b/apps/expo/src/components/book/BookListItem.tsx index f5502e113..1386f2861 100644 --- a/apps/expo/src/components/book/BookListItem.tsx +++ b/apps/expo/src/components/book/BookListItem.tsx @@ -1,4 +1,4 @@ -import { getMediaThumbnail } from '@stump/api' +import { useSDK } from '@stump/client' import { Media } from '@stump/types' import React from 'react' import { TouchableOpacity } from 'react-native' @@ -12,20 +12,24 @@ type BookListItemProps = { book: Media navigate: (id: string) => void } -export const BookListItem = React.memo(({ book, navigate }: BookListItemProps) => ( - navigate(book.id)} - > - - - {book.metadata?.title || book.name} - - -)) +export const BookListItem = React.memo(({ book, navigate }: BookListItemProps) => { + const { sdk } = useSDK() + + return ( + navigate(book.id)} + > + + + {book.metadata?.title || book.name} + + + ) +}) BookListItem.displayName = 'BookListItem' diff --git a/apps/expo/src/components/reader/epub/EpubJSReader.tsx b/apps/expo/src/components/reader/epub/EpubJSReader.tsx index d1c269138..d16979bb9 100644 --- a/apps/expo/src/components/reader/epub/EpubJSReader.tsx +++ b/apps/expo/src/components/reader/epub/EpubJSReader.tsx @@ -1,6 +1,7 @@ import { Location, Reader } from '@epubjs-react-native/core' import { useFileSystem } from '@epubjs-react-native/expo-file-system' -import { API, isAxiosError, updateEpubProgress } from '@stump/api' +import { useSDK } from '@stump/client' +import { isAxiosError } from '@stump/sdk' import { Media } from '@stump/types' import { useColorScheme } from 'nativewind' import React, { useCallback, useEffect, useState } from 'react' @@ -30,6 +31,7 @@ type Props = { * the long run */ export default function EpubJSReader({ book, initialCfi, incognito }: Props) { + const { sdk } = useSDK() /** * The base64 representation of the book file. The reader component does not accept * credentials in the fetch, so we just have to fetch manually and pass the base64 @@ -47,7 +49,7 @@ export default function EpubJSReader({ book, initialCfi, incognito }: Props) { useEffect(() => { async function fetchBook() { try { - const response = await fetch(`${API.getUri()}/media/${book.id}/file`) + const response = await fetch(sdk.media.downloadURL(book.id)) const data = await response.blob() const reader = new FileReader() reader.onloadend = () => { @@ -64,7 +66,7 @@ export default function EpubJSReader({ book, initialCfi, incognito }: Props) { } fetchBook() - }, [book.id]) + }, [book.id, sdk.media]) /** * A callback that updates the read progress of the current location @@ -79,7 +81,7 @@ export default function EpubJSReader({ book, initialCfi, incognito }: Props) { } = currentLocation try { - await updateEpubProgress({ + await sdk.epub.updateProgress({ epubcfi: cfi, id: book.id, is_complete: progress >= 1.0, @@ -93,7 +95,7 @@ export default function EpubJSReader({ book, initialCfi, incognito }: Props) { } } }, - [incognito, book.id], + [incognito, book.id, sdk.epub], ) if (!base64) { diff --git a/apps/expo/src/components/reader/image/ImageBasedReader.tsx b/apps/expo/src/components/reader/image/ImageBasedReader.tsx index 632add20e..5935d8fdf 100644 --- a/apps/expo/src/components/reader/image/ImageBasedReader.tsx +++ b/apps/expo/src/components/reader/image/ImageBasedReader.tsx @@ -1,5 +1,5 @@ -import { getMediaPage, isAxiosError } from '@stump/api' -import { useUpdateMediaProgress } from '@stump/client' +import { useSDK, useUpdateMediaProgress } from '@stump/client' +import { isAxiosError } from '@stump/sdk' import { Media } from '@stump/types' import { useColorScheme } from 'nativewind' import React, { useCallback, useMemo, useState } from 'react' @@ -166,6 +166,7 @@ const Page = React.memo( maxHeight, readingDirection, }: PageProps) => { + const { sdk } = useSDK() const insets = useSafeAreaInsets() const { @@ -224,7 +225,7 @@ const Page = React.memo( }} > state.setUser) const handleLogout = async () => { try { - await authApi.logout() + await sdk.auth.logout() setUser(null) } catch (err) { console.error(err) diff --git a/apps/expo/tsconfig.json b/apps/expo/tsconfig.json index d3b291a7a..5a2c85fed 100644 --- a/apps/expo/tsconfig.json +++ b/apps/expo/tsconfig.json @@ -5,8 +5,8 @@ "jsx": "preserve", "module": "esnext", "paths": { - "@stump/api": ["../../packages/api/src/index.ts"], - "@stump/api/*": ["../../packages/api/src/*"], + "@stump/sdk": ["../../packages/sdk/src/index.ts"], + "@stump/sdk/*": ["../../packages/sdk/src/*"], "@stump/client": ["../../packages/client/src/index.ts"], "@stump/client/*": ["../../packages/client/src/*"], "@stump/types": ["../../packages/types/index.ts"], diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index fc2e6e07d..b94c91dd7 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -33,6 +33,7 @@ serde = { workspace = true } serde_json = { workspace = true } serde_qs = { version = "0.13.0", features = ["axum"] } serde-untagged = "0.1.2" +serde_with = { workspace = true } specta = { workspace = true } stump_core = { path = "../../core" } tower-http = { version = "0.5.2", features = [ diff --git a/apps/server/src/filter/basic_filter.rs b/apps/server/src/filter/basic_filter.rs index df15ad43a..8c9ef22da 100644 --- a/apps/server/src/filter/basic_filter.rs +++ b/apps/server/src/filter/basic_filter.rs @@ -2,6 +2,8 @@ use std::str::FromStr; use serde::{de, Deserialize, Serialize}; use serde_untagged::UntaggedEnumVisitor; +use serde_with::skip_serializing_none; +use specta::Type; use stump_core::db::{ entity::{age_rating_deserializer, LogLevel}, query::ordering::QueryOrder, @@ -39,7 +41,7 @@ where } } -#[derive(Deserialize, Debug, Clone, Serialize)] +#[derive(Deserialize, Debug, Clone, Serialize, Type)] pub struct Range where T: std::str::FromStr, @@ -63,7 +65,7 @@ where } } -#[derive(Debug, Clone, Serialize, ToSchema)] +#[derive(Debug, Clone, Serialize, ToSchema, Type)] #[serde(untagged)] pub enum ValueOrRange where @@ -121,7 +123,7 @@ pub struct BaseFilter { pub search: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct LibraryBaseFilter { #[serde(default, deserialize_with = "string_or_seq_string")] pub id: Vec, @@ -133,13 +135,13 @@ pub struct LibraryBaseFilter { pub search: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct LibraryRelationFilter { #[serde(skip_serializing_if = "Option::is_none")] pub series: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct LibraryFilter { #[serde(flatten)] pub base_filter: LibraryBaseFilter, @@ -147,14 +149,16 @@ pub struct LibraryFilter { pub relation_filter: LibraryRelationFilter, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[skip_serializing_none] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct SeriesQueryRelation { pub load_media: Option, pub load_library: Option, pub count_media: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[skip_serializing_none] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct UserQueryRelation { pub include_read_progresses: Option, pub include_session_count: Option, @@ -162,7 +166,7 @@ pub struct UserQueryRelation { } // TODO: decide what others to include -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct SeriesMedataFilter { #[serde(default, deserialize_with = "string_or_seq_string")] pub meta_type: Vec, @@ -182,7 +186,7 @@ pub struct SeriesMedataFilter { pub volume: Option>, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct SeriesBaseFilter { #[serde(default, deserialize_with = "string_or_seq_string")] pub id: Vec, @@ -197,7 +201,7 @@ pub struct SeriesBaseFilter { pub metadata: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct SeriesRelationFilter { #[serde(skip_serializing_if = "Option::is_none")] pub library: Option, @@ -205,7 +209,7 @@ pub struct SeriesRelationFilter { pub media: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct SeriesFilter { #[serde(flatten)] pub base_filter: SeriesBaseFilter, @@ -213,7 +217,7 @@ pub struct SeriesFilter { pub relation_filter: SeriesRelationFilter, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct MediaMetadataBaseFilter { #[serde(default, deserialize_with = "string_or_seq_string")] pub publisher: Vec, @@ -243,13 +247,13 @@ pub struct MediaMetadataBaseFilter { pub year: Option>, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct MediaMetadataRelationFilter { #[serde(skip_serializing_if = "Option::is_none")] pub media: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct MediaMetadataFilter { #[serde(flatten)] pub base_filter: MediaMetadataBaseFilter, @@ -259,7 +263,7 @@ pub struct MediaMetadataFilter { /// A user-friendly representation of a media's read_progress. This will map to /// a query condition that will be used to filter the media. -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub enum ReadStatus { #[default] #[serde(alias = "unread")] @@ -283,7 +287,7 @@ impl FromStr for ReadStatus { } } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct MediaBaseFilter { #[serde(default, deserialize_with = "string_or_seq_string")] pub id: Vec, @@ -304,13 +308,13 @@ pub struct MediaBaseFilter { pub metadata: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct MediaRelationFilter { #[serde(skip_serializing_if = "Option::is_none")] pub series: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct MediaFilter { #[serde(flatten)] pub base_filter: MediaBaseFilter, @@ -330,7 +334,8 @@ impl MediaFilter { } } -#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +#[skip_serializing_none] +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema, Type)] pub struct LogFilter { pub level: Option, pub job_id: Option, diff --git a/apps/server/src/routers/api/mod.rs b/apps/server/src/routers/api/mod.rs index dc8687e6a..6864e8b91 100644 --- a/apps/server/src/routers/api/mod.rs +++ b/apps/server/src/routers/api/mod.rs @@ -17,7 +17,7 @@ mod tests { NamedType, }; - use crate::config::jwt::CreatedToken; + use crate::{config::jwt::CreatedToken, filter::*}; use super::v1::{ auth::*, book_club::*, emailer::*, epub::*, job::*, library::*, media::*, @@ -30,7 +30,7 @@ mod tests { where T: NamedType, { - export::(&ExportConfiguration::new().bigint(BigIntExportBehavior::BigInt)) + export::(&ExportConfiguration::new().bigint(BigIntExportBehavior::Number)) } #[test] @@ -92,6 +92,36 @@ mod tests { )?; file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all( + format!("{}\n\n", ts_export::()?).as_bytes(), + )?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all( + format!("{}\n\n", ts_export::()?).as_bytes(), + )?; + file.write_all( + format!("{}\n\n", ts_export::()?).as_bytes(), + )?; + file.write_all( + format!("{}\n\n", ts_export::()?).as_bytes(), + )?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all( + format!("{}\n\n", ts_export::>()?).as_bytes(), + )?; + file.write_all(format!("{}\n\n", ts_export::>()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all( + format!("{}\n\n", ts_export::()?).as_bytes(), + )?; file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; file.write_all( @@ -100,6 +130,9 @@ mod tests { file.write_all( format!("{}\n\n", ts_export::()?).as_bytes(), )?; + file.write_all( + format!("{}\n\n", ts_export::()?).as_bytes(), + )?; file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; file.write_all( diff --git a/apps/server/src/routers/api/v1/library.rs b/apps/server/src/routers/api/v1/library.rs index 0e8351dd1..fb3d3f8fd 100644 --- a/apps/server/src/routers/api/v1/library.rs +++ b/apps/server/src/routers/api/v1/library.rs @@ -8,6 +8,7 @@ use chrono::Duration; use prisma_client_rust::{chrono::Utc, not, or, raw, Direction, PrismaValue}; use serde::{Deserialize, Serialize}; use serde_qs::axum::QsQuery; +use serde_with::skip_serializing_none; use specta::Type; use std::path; use tokio::fs; @@ -402,6 +403,7 @@ async fn get_library_by_id( Ok(Json(library.into())) } +// TODO: remove? Not used on client #[utoipa::path( get, path = "/api/v1/libraries/:id/series", @@ -503,6 +505,7 @@ async fn get_library_series( Ok(Json((series, series_count, pagination).into())) } +// TODO: remove? Not used on client async fn get_library_media( filter_query: Query>, pagination_query: Query, @@ -802,6 +805,7 @@ async fn replace_library_thumbnail( ))) } +// TODO: support all vs just library thumb /// Deletes all media thumbnails in a library by id, if the current user has access to it. #[utoipa::path( delete, @@ -849,7 +853,8 @@ async fn delete_library_thumbnails( Ok(Json(())) } -#[derive(Debug, Deserialize, ToSchema)] +#[skip_serializing_none] +#[derive(Debug, Deserialize, ToSchema, Type)] pub struct GenerateLibraryThumbnails { pub image_options: Option, #[serde(default)] diff --git a/apps/server/src/routers/api/v1/media.rs b/apps/server/src/routers/api/v1/media.rs index b1bec928f..fc46db85e 100644 --- a/apps/server/src/routers/api/v1/media.rs +++ b/apps/server/src/routers/api/v1/media.rs @@ -15,6 +15,7 @@ use prisma_client_rust::{ }; use serde::{Deserialize, Serialize}; use serde_qs::axum::QsQuery; +use specta::Type; use stump_core::{ config::StumpConfig, db::{ @@ -728,8 +729,8 @@ async fn get_media_by_path( Ok(Json(Media::from(book))) } -#[derive(Deserialize)] -struct BookRelations { +#[derive(Deserialize, Type)] +pub struct BookRelations { #[serde(default)] load_series: Option, #[serde(default)] diff --git a/apps/server/src/routers/utoipa.rs b/apps/server/src/routers/utoipa.rs index bd6ab8add..269e54df1 100644 --- a/apps/server/src/routers/utoipa.rs +++ b/apps/server/src/routers/utoipa.rs @@ -14,10 +14,7 @@ use utoipa_swagger_ui::SwaggerUi; use crate::config::state::AppState; use crate::errors::APIError; -use crate::filter::{ - FilterableLibraryQuery, FilterableMediaQuery, FilterableSeriesQuery, LibraryFilter, - MediaFilter, SeriesFilter, SeriesQueryRelation, -}; +use crate::filter::*; use crate::middleware::auth::auth_middleware; use super::api::{ @@ -149,7 +146,9 @@ use super::api::{ CreateOrUpdateSmartListView, SmartListItemGrouping, SmartFilter, FilterJoin, EntityVisibility, SmartListViewConfig, ReactTableColumnSort, ReactTableGlobalSort, MediaSmartFilter, MediaMetadataSmartFilter, SeriesSmartFilter, SeriesMetadataSmartFilter, - LibrarySmartFilter, Notifier, CreateOrUpdateNotifier, PatchNotifier + LibrarySmartFilter, Notifier, CreateOrUpdateNotifier, PatchNotifier, LibraryBaseFilter, LibraryRelationFilter, + MediaBaseFilter, MediaRelationFilter, SeriesBaseFilter, SeriesRelationFilter, NotifierConfig, NotifierType, + ReadingListItem, ReadingListVisibility, SeriesMedataFilter ) ), tags( diff --git a/core/src/db/filter/smart_filter.rs b/core/src/db/filter/smart_filter.rs index 2c5f21a8f..a0b369bcf 100644 --- a/core/src/db/filter/smart_filter.rs +++ b/core/src/db/filter/smart_filter.rs @@ -18,7 +18,7 @@ use crate::prisma::{library, media, media_metadata, series, series_metadata}; // consolidate them into a single macro. I'm not sure if this is possible, but it's worth looking into. This will get exponentially // worse as things like sorting and sorting on relations are added... :weary: -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema, Type)] #[serde(untagged)] /// A filter for a single value, e.g. `name = "test"` pub enum Filter { @@ -48,7 +48,7 @@ pub struct NumericRange { pub inclusive: bool, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema, Type)] #[serde(untagged)] pub enum NumericFilter { Gt { gt: T }, diff --git a/core/src/db/query/pagination.rs b/core/src/db/query/pagination.rs index 7ed883b3c..8bc925934 100644 --- a/core/src/db/query/pagination.rs +++ b/core/src/db/query/pagination.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use specta::Type; use tracing::trace; use utoipa::ToSchema; @@ -11,6 +12,7 @@ use crate::{ // TODO: this entire file belongs in server app, not here. It is currently used by DAOs, which are // very much going BYE BYE +#[skip_serializing_none] #[derive( Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq, Type, ToSchema, )] @@ -20,6 +22,7 @@ pub struct PageQuery { pub page_size: Option, } +#[skip_serializing_none] #[derive( Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq, Type, ToSchema, )] @@ -28,6 +31,7 @@ pub struct CursorQuery { pub limit: Option, } +#[skip_serializing_none] #[derive(Default, Debug, Deserialize, Serialize, Type, ToSchema)] pub struct PaginationQuery { pub zero_based: Option, diff --git a/core/src/lib.rs b/core/src/lib.rs index 95fbe2c50..651fdc447 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -259,7 +259,7 @@ mod tests { where T: NamedType, { - export::(&ExportConfiguration::new().bigint(BigIntExportBehavior::BigInt)) + export::(&ExportConfiguration::new().bigint(BigIntExportBehavior::Number)) } #[test] @@ -278,6 +278,8 @@ mod tests { file.write_all(b"// CORE TYPE GENERATION\n\n")?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; diff --git a/crates/integrations/.env.template b/crates/integrations/.env.template deleted file mode 100644 index 82f4d5cb6..000000000 --- a/crates/integrations/.env.template +++ /dev/null @@ -1,3 +0,0 @@ -DUMMY_DISCORD_WEBHOOK_URL= -DUMMY_TG_TOKEN= -DUMMY_TG_CHAT_ID= \ No newline at end of file diff --git a/crates/integrations/Cargo.toml b/crates/integrations/Cargo.toml index 4e70a9fb4..653de9422 100644 --- a/crates/integrations/Cargo.toml +++ b/crates/integrations/Cargo.toml @@ -12,4 +12,3 @@ thiserror = { workspace = true } [dev-dependencies] tokio = { workspace = true } -dotenv = "0.15.0" diff --git a/crates/integrations/src/notifier/discord_client.rs b/crates/integrations/src/notifier/discord_client.rs index a98b9675a..8fab8f53b 100644 --- a/crates/integrations/src/notifier/discord_client.rs +++ b/crates/integrations/src/notifier/discord_client.rs @@ -67,10 +67,8 @@ impl Notifier for DiscordClient { #[cfg(test)] mod tests { use super::*; - use dotenv::dotenv; fn get_debug_client() -> DiscordClient { - dotenv().ok(); let webhook_url = std::env::var("DUMMY_DISCORD_WEBHOOK_URL") .expect("Failed to load webhook URL"); DiscordClient::new(webhook_url) diff --git a/crates/integrations/src/notifier/telegram_client.rs b/crates/integrations/src/notifier/telegram_client.rs index 8f67cc65d..dc397b5db 100644 --- a/crates/integrations/src/notifier/telegram_client.rs +++ b/crates/integrations/src/notifier/telegram_client.rs @@ -53,10 +53,8 @@ impl Notifier for TelegramClient { #[cfg(test)] mod tests { use super::*; - use dotenv::dotenv; fn get_debug_client() -> TelegramClient { - dotenv().ok(); let token = std::env::var("DUMMY_TG_TOKEN").expect("Failed to load token"); let chat_id = std::env::var("DUMMY_TG_CHAT_ID").expect("Failed to load chat ID"); TelegramClient::new(token, chat_id) diff --git a/packages/api/src/auth.ts b/packages/api/src/auth.ts deleted file mode 100644 index 089550ad3..000000000 --- a/packages/api/src/auth.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { LoginOrRegisterArgs, User } from '@stump/types' - -import { API } from './axios' -import { APIResult } from './types' - -// TODO: types - -export function me(): Promise> { - return API.get('/auth/me') -} - -export function login(input: LoginOrRegisterArgs): Promise> { - return API.post('/auth/login', input) -} - -export function register(payload: LoginOrRegisterArgs): Promise> { - return API.post('/auth/register', payload) -} - -export function logout(): Promise> { - return API.post('/auth/logout') -} - -export const authApi = { - login, - logout, - me, - register, -} - -export const authQueryKeys: Record = { - login: 'auth.login', - logout: 'auth.logout', - me: 'auth.me', - register: 'auth.register', -} diff --git a/packages/api/src/axios.ts b/packages/api/src/axios.ts deleted file mode 100644 index 46145b331..000000000 --- a/packages/api/src/axios.ts +++ /dev/null @@ -1,68 +0,0 @@ -import axios, { AxiosInstance } from 'axios' - -export let API: AxiosInstance - -// TODO: make not bad -export function initializeApi(baseUrl: string, version: string) { - let correctedUrl = baseUrl - - // remove trailing slash - if (correctedUrl.endsWith('/')) { - correctedUrl = correctedUrl.slice(0, -1) - } - - const isValid = correctedUrl.endsWith(`/api/${version}`) - const hasApiPiece = !isValid && correctedUrl.endsWith('/api') - - if (!isValid && !hasApiPiece) { - correctedUrl += `/api/${version}` - } else if (hasApiPiece) { - correctedUrl += `/${version}` - } - - // remove all double slashes AFTER the initial http:// or https:// or whatever - correctedUrl = correctedUrl.replace(/([^:]\/)\/+/g, '$1') - - API = axios.create({ - baseURL: correctedUrl, - // FIXME: react-native seems to ignore this option, causing brackets to be encoded which - // the backend doesn't support - // paramsSerializer: { - // encode: (params) => qs.stringify(params, { arrayFormat: 'repeat' }), - // }, - withCredentials: true, - }) -} - -export function apiIsInitialized() { - return !!API -} - -// TODO: make not bad -export function isUrl(url: string) { - return url.startsWith('http://') || url.startsWith('https://') -} - -export async function checkUrl(url: string, version = 'v1') { - if (!isUrl(url)) { - return false - } - - const res = await fetch(`${url}/api/${version}/ping`).catch((err) => err) - - return res.status === 200 -} - -export const formatServiceURL = (url: string) => { - let adjustedUrl = url - - if (adjustedUrl.endsWith('/')) { - adjustedUrl = url.slice(0, -1) - } - - if (!url.endsWith('/api')) { - adjustedUrl += '/api' - } - - return adjustedUrl -} diff --git a/packages/api/src/bookClub.ts b/packages/api/src/bookClub.ts deleted file mode 100644 index 867caffe3..000000000 --- a/packages/api/src/bookClub.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { - BookClub, - BookClubBook, - BookClubChatBoard, - BookClubChatMessage, - BookClubInvitation, - BookClubInvitationAnswer, - BookClubMember, - BookClubSchedule, - CreateBookClub, - CreateBookClubInvitation, - CreateBookClubMember, - GetBookClubsParams, - UpdateBookClub, - UpdateBookClubMember, -} from '@stump/types' - -import { API } from './axios' -import { APIResult } from './types' -import { toUrlParams, urlWithParams } from './utils' - -export async function getBookClubs(params?: GetBookClubsParams): Promise> { - if (params) { - const searchParams = toUrlParams(params) - return API.get(urlWithParams('/book-clubs', searchParams)) - } - - return API.get('/book-clubs') -} - -export async function createBookClub(payload: CreateBookClub): Promise> { - return API.post('/book-clubs', payload) -} - -export async function getBookClubById(id: string): Promise> { - return API.get(`/book-clubs/${id}`) -} - -export async function updateBookClub( - id: string, - payload: UpdateBookClub, -): Promise> { - return API.put(`/book-clubs/${id}`, payload) -} - -export async function getBookClubInvitations( - bookClubId: string, -): Promise> { - return API.get(`/book-clubs/${bookClubId}/invitations`) -} - -export async function createBookClubInvitation( - bookClubId: string, - payload: CreateBookClubInvitation, -): Promise> { - return API.post(`/book-clubs/${bookClubId}/invitations`, payload) -} - -export async function respondToBookClubInvitation( - bookClubId: string, - invitationId: string, - payload: BookClubInvitationAnswer, -): Promise> { - return API.put(`/book-clubs/${bookClubId}/invitations/${invitationId}`, payload) -} - -export async function getBookClubMembers(bookClubId: string): Promise> { - return API.get(`/book-clubs/${bookClubId}/members`) -} - -export async function createBookClubMember( - bookClubId: string, - payload: CreateBookClubMember, -): Promise> { - return API.post(`/book-clubs/${bookClubId}/members`, payload) -} - -export async function getBookClubMember( - bookClubId: string, - memberId: string, -): Promise> { - return API.get(`/book-clubs/${bookClubId}/members/${memberId}`) -} - -export async function updateBookClubMember( - bookClubId: string, - memberId: string, - payload: CreateBookClubMember, -): Promise> { - return API.put(`/book-clubs/${bookClubId}/members/${memberId}`, payload) -} - -export async function deleteBookClubMember( - bookClubId: string, - memberId: string, -): Promise> { - return API.delete(`/book-clubs/${bookClubId}/members/${memberId}`) -} - -export async function getBookClubSchedule( - bookClubId: string, -): Promise> { - return API.get(`/book-clubs/${bookClubId}/schedule`) -} - -export async function createBookClubSchedule( - bookClubId: string, -): Promise> { - return API.post(`/book-clubs/${bookClubId}/schedule`) -} - -export async function getBookClubCurrentBook(bookClubId: string): Promise> { - // TODO: maybe remove /schedule from the endpoint - return API.get(`/book-clubs/${bookClubId}/schedule/current-book`) -} - -export async function getBookClubCurrentChat( - bookClubId: string, -): Promise> { - return API.get(`/book-clubs/${bookClubId}/chats/current`) -} - -export async function getBookClubChatById( - bookClubId: string, - chatId: string, -): Promise> { - return API.get(`/book-clubs/${bookClubId}/chats/${chatId}`) -} - -export async function getBookClubChatThread( - bookClubId: string, - chatId: string, - threadId: string, -): Promise> { - return API.get(`/book-clubs/${bookClubId}/chats/${chatId}/threads/${threadId}`) -} - -export const bookClubApi = { - createBookClub, - createBookClubInvitation, - createBookClubMember, - createBookClubSchedule, - deleteBookClubMember, - getBookClubById, - getBookClubChatById, - getBookClubChatThread, - getBookClubCurrentBook, - getBookClubCurrentChat, - getBookClubInvitations, - getBookClubMember, - getBookClubMembers, - getBookClubSchedule, - getBookClubs, - respondToBookClubInvitation, - updateBookClub, - updateBookClubMember, -} - -export const bookClubQueryKeys: Record = { - createBookClub: 'bookClub.create', - createBookClubInvitation: 'bookClub.createInvitation', - createBookClubMember: 'bookClub.createMember', - createBookClubSchedule: 'bookClub.createSchedule', - deleteBookClubMember: 'bookClub.deleteMember', - getBookClubById: 'bookClub.getById', - getBookClubChatById: 'bookClub.getChatById', - getBookClubChatThread: 'bookClub.getChatThread', - getBookClubCurrentBook: 'bookClub.getCurrentBook', - getBookClubCurrentChat: 'bookClub.getCurrentChat', - getBookClubInvitations: 'bookClub.getInvitations', - getBookClubMember: 'bookClub.getMember', - getBookClubMembers: 'bookClub.getMembers', - getBookClubSchedule: 'bookClub.getSchedule', - getBookClubs: 'bookClub.get', - respondToBookClubInvitation: 'bookClub.respondToInvitation', - updateBookClub: 'bookClub.update', - updateBookClubMember: 'bookClub.updateMember', -} diff --git a/packages/api/src/config.ts b/packages/api/src/config.ts deleted file mode 100644 index 423fb561a..000000000 --- a/packages/api/src/config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ClaimResponse } from '@stump/types' - -import { API } from '.' -import { APIResult } from './types' - -export function ping(): Promise> { - return API.get('/ping') -} - -export async function checkIsClaimed(): Promise> { - return API.get('/claim') -} diff --git a/packages/api/src/emailer.ts b/packages/api/src/emailer.ts deleted file mode 100644 index b89eea9b7..000000000 --- a/packages/api/src/emailer.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { - CreateOrUpdateEmailDevice, - CreateOrUpdateEmailer, - EmailerSendRecord, - PatchEmailDevice, - RegisteredEmailDevice, - SendAttachmentEmailResponse, - SendAttachmentEmailsPayload, - SMTPEmailer, -} from '@stump/types' - -import { API } from './axios' -import { APIResult } from './types' -import { toUrlParams } from './utils' - -function getEmailers(params?: Record): Promise> { - if (params) { - return API.get(`/emailers?${toUrlParams(params)}`) - } else { - return API.get('/emailers') - } -} - -function getEmailerById(id: number): Promise> { - return API.get(`/emailers/${id}`) -} - -function createEmailer(payload: CreateOrUpdateEmailer): Promise> { - return API.post('/emailers', payload) -} - -function updateEmailer( - id: number, - payload: CreateOrUpdateEmailer, -): Promise> { - return API.put(`/emailers/${id}`, payload) -} - -function deleteEmailer(id: number): Promise> { - return API.delete(`/emailers/${id}`) -} - -function getEmailDevices(): Promise> { - return API.get('/email-devices') -} - -function getEmailDeviceById(id: number): Promise> { - return API.get(`/email-devices/${id}`) -} - -function getEmailerSendHistory( - emailerId: number, - params?: Record, -): Promise> { - if (params) { - return API.get(`/emailers/${emailerId}/send-history?${toUrlParams(params)}`) - } else { - return API.get(`/emailers/${emailerId}/send-history`) - } -} - -function createEmailDevice( - payload: CreateOrUpdateEmailDevice, -): Promise> { - return API.post('/email-devices', payload) -} - -function updateEmailDevice( - id: number, - payload: CreateOrUpdateEmailDevice, -): Promise> { - return API.put(`/email-devices/${id}`, payload) -} - -function patchEmailDevice( - id: number, - payload: PatchEmailDevice, -): Promise> { - return API.patch(`/email-devices/${id}`, payload) -} - -function deleteEmailDevice(id: number): Promise> { - return API.delete(`/email-devices/${id}`) -} - -function sendAttachmentEmail( - payload: SendAttachmentEmailsPayload, -): Promise> { - return API.post('/emailers/send-attachment', payload) -} - -export const emailerApi = { - createEmailDevice, - createEmailer, - deleteEmailDevice, - deleteEmailer, - getEmailDeviceById, - getEmailDevices, - getEmailerById, - getEmailerSendHistory, - getEmailers, - patchEmailDevice, - sendAttachmentEmail, - updateEmailDevice, - updateEmailer, -} - -export const emailerQueryKeys: Record = { - createEmailDevice: 'emailDevice.create', - createEmailer: 'emailer.create', - deleteEmailDevice: 'emailDevice.delete', - deleteEmailer: 'emailer.delete', - getEmailDeviceById: 'emailDevice.getById', - getEmailDevices: 'emailDevices.get', - getEmailerById: 'emailer.getById', - getEmailerSendHistory: 'emailer.sendHistory', - getEmailers: 'emailer.get', - patchEmailDevice: 'emailDevice.patch', - sendAttachmentEmail: 'emailer.sendAttachment', - updateEmailDevice: 'emailDevice.update', - updateEmailer: 'emailer.update', -} diff --git a/packages/api/src/epub.ts b/packages/api/src/epub.ts deleted file mode 100644 index 51e6f0874..000000000 --- a/packages/api/src/epub.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { - Bookmark, - CreateOrUpdateBookmark, - DeleteBookmark, - Epub, - ProgressUpdateReturn, - UpdateEpubProgress, -} from '@stump/types' - -import { API } from './axios' -import { APIResult } from './types' - -export function getEpubBaseUrl(id: string): string { - return `${API.getUri()}/epub/${id}` -} - -export function getEpubById(id: string): Promise> { - return API.get(`/epub/${id}`) -} - -export function updateEpubProgress( - payload: UpdateEpubProgress & { id: string }, -): Promise> { - return API.put(`/epub/${payload.id}/progress`, payload) -} - -// This returns raw epub data (e.g. HTML, XHTML, CSS, etc.) -// TODO: type this?? -export function getEpubResource(payload: { - id: string - root?: string - resourceId: string -}): Promise> { - return API.get(`/epub/${payload.id}/${payload.root ?? 'META-INF'}/${payload.resourceId}`) -} - -export function getBookmarks(id: string): Promise> { - return API.get(`/epub/${id}/bookmarks`) -} - -export function createBookmark( - id: string, - payload: CreateOrUpdateBookmark, -): Promise> { - return API.post(`/epub/${id}/bookmarks`, payload) -} - -export function deleteBookmark(id: string, payload: DeleteBookmark): Promise> { - return API.delete(`/epub/${id}/bookmarks`, { data: payload }) -} - -export const epubApi = { - createBookmark, - deleteBookmark, - getBookmarks, - getEpubBaseUrl, - getEpubById, - getEpubResource, - updateEpubProgress, -} - -export const epubQueryKeys: Record = { - createBookmark: 'epub.createBookmark', - deleteBookmark: 'epub.deleteBookmark', - getBookmarks: 'epub.getBookmarks', - getEpubBaseUrl: 'epub.getEpubBaseUrl', - getEpubById: 'epub.getEpubById', - getEpubResource: 'epub.getEpubResource', - updateEpubProgress: 'epub.updateEpubProgress', -} diff --git a/packages/api/src/filesystem.ts b/packages/api/src/filesystem.ts deleted file mode 100644 index 5492ef8ee..000000000 --- a/packages/api/src/filesystem.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { DirectoryListing, DirectoryListingInput, Pageable } from '@stump/types' - -import { API } from './axios' -import { APIResult } from './types' - -interface ListDirectoryFnInput extends DirectoryListingInput { - page?: number -} - -export function listDirectory( - input?: ListDirectoryFnInput, -): Promise>> { - if (input?.page != null) { - return API.post(`/filesystem?page=${input.page}`, input) - } - - return API.post('/filesystem', input) -} - -export const filesystemApi = { - listDirectory, -} - -export const filesystemQueryKeys: Record = { - listDirectory: 'filesystem.listDirectory', -} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts deleted file mode 100644 index b853dd6e0..000000000 --- a/packages/api/src/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -export { authApi, authQueryKeys } from './auth' -export { API, apiIsInitialized, checkUrl, formatServiceURL, initializeApi, isUrl } from './axios' -export { bookClubApi, bookClubQueryKeys } from './bookClub' -export { emailerApi, emailerQueryKeys } from './emailer' -export { epubApi, epubQueryKeys, getEpubResource, updateEpubProgress } from './epub' -export { filesystemApi, filesystemQueryKeys } from './filesystem' -export * from './job' -export { getLibraryThumbnail, libraryApi, libraryQueryKeys } from './library' -export { logApi, logQueryKeys } from './log' -export { - getMediaDownloadUrl, - getMediaPage, - getMediaThumbnail, - mediaApi, - mediaQueryKeys, -} from './media' -export { metadataApi, metadataQueryKeys } from './metadata' -export { - getNextInSeries, - getNextMediaInSeries, - getRecentlyAddedSeries, - getSeriesById, - getSeriesMedia, - getSeriesThumbnail, - seriesApi, - seriesQueryKeys, -} from './series' -export { checkIsClaimed, getStumpVersion, ping, serverApi, serverQueryKeys } from './server' -export { getSmartListById, getSmartLists, smartListApi, smartListQueryKeys } from './smartList' -export * from './tag' -export * from './types' -export * from './user' -export * from './utils' diff --git a/packages/api/src/job.ts b/packages/api/src/job.ts deleted file mode 100644 index f1f74f5d1..000000000 --- a/packages/api/src/job.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { JobSchedulerConfig, PersistedJob, UpdateSchedulerConfig } from '@stump/types' - -import { API } from './axios' -import { APIResult, PageableAPIResult } from './types' -import { toUrlParams } from './utils' - -export function getJobs( - params?: Record, -): Promise> { - if (params) { - const searchParams = toUrlParams(params) - return API.get(`/jobs?${searchParams.toString()}`) - } else { - return API.get('/jobs') - } -} - -export function cancelJob(id: string): Promise> { - return API.delete(`/jobs/${id}/cancel`) -} - -export function deleteJob(id: string): Promise> { - return API.delete(`/jobs/${id}`) -} - -export function deleteAllJobs(): Promise> { - return API.delete('/jobs') -} - -export function getJobSchedulerConfig(): Promise> { - return API.get('/jobs/scheduler-config') -} - -export function updateJobSchedulerConfig( - config: UpdateSchedulerConfig, -): Promise> { - return API.post('/jobs/scheduler-config', config) -} - -export const jobApi = { - cancelJob, - deleteAllJobs, - deleteJob, - getJobSchedulerConfig, - getJobs, - updateJobSchedulerConfig, -} - -export const jobQueryKeys: Record = { - cancelJob: 'job.cancelJob', - deleteAllJobs: 'job.deleteAll', - deleteJob: 'job.delete', - getJobSchedulerConfig: 'job.getSchedulerConfig', - getJobs: 'job.get', - updateJobSchedulerConfig: 'job.updateSchedulerConfig', -} diff --git a/packages/api/src/library.ts b/packages/api/src/library.ts deleted file mode 100644 index 9cc84e71b..000000000 --- a/packages/api/src/library.ts +++ /dev/null @@ -1,171 +0,0 @@ -import type { - CleanLibraryResponse, - CreateLibrary, - Library, - LibraryScanMode, - LibraryStats, - PatchLibraryThumbnail, - Series, - UpdateLibrary, -} from '@stump/types' - -import { API } from './axios' -import { APIResult, PageableAPIResult, PagedQueryParams } from './types' -import { mergePageParams, toUrlParams, urlWithParams } from './utils' - -export function getLibraries( - params: Record = { unpaged: true }, -): Promise> { - return API.get(urlWithParams('/libraries', toUrlParams(params))) -} - -export function getTotalLibraryStats(): Promise> { - return API.get('/libraries/stats') -} - -export function getLibraryStats( - id: string, - params?: Record, -): Promise> { - if (params) { - return API.get(`/libraries/${id}/stats?${toUrlParams(params)}`) - } else { - return API.get(`/libraries/${id}/stats`) - } -} - -export function getLibraryById(id: string): Promise> { - return API.get(`/libraries/${id}`) -} - -export function getLibraryThumbnail(id: string): string { - return `${API.defaults.baseURL}/libraries/${id}/thumbnail` -} - -export function getLibrarySeries( - id: string, - { page, page_size, params }: PagedQueryParams, -): Promise> { - const searchParams = mergePageParams({ page, page_size, params }) - return API.get(urlWithParams(`/libraries/${id}/series`, searchParams)) -} - -// FIXME: type this lol -// TODO: narrow mode type to exclude NONE -// TODO: fix function signature to work with react-query -export function scanLibary(params: { - id: string - mode?: LibraryScanMode -}): Promise> { - return API.get(`/libraries/${params.id}/scan?scan_mode=${params.mode ?? 'BATCHED'}`) -} - -export function cleanLibrary(id: string): Promise> { - return API.put(`/libraries/${id}/clean`) -} - -// TODO: type this -export function deleteLibrary(id: string) { - return API.delete(`/libraries/${id}`) -} - -export function deleteLibraryThumbnails(id: string) { - // TODO: libraries don't have a configurable thumbnail, but eventually - // they might. So this endpoint might need to change. - return API.delete(`/libraries/${id}/thumbnail`) -} - -export function regenerateThumbnails(id: string, force?: boolean) { - return API.post(`/libraries/${id}/thumbnail/generate`, { force_regenerate: !!force }) -} - -export function createLibrary(payload: CreateLibrary): Promise> { - return API.post('/libraries', payload) -} - -export function editLibrary(payload: UpdateLibrary): Promise> { - return API.put(`/libraries/${payload.id}`, payload) -} - -export function patchLibraryThumbnail(id: string, params: PatchLibraryThumbnail) { - return API.patch(`/libraries/${id}/thumbnail`, params) -} - -export function uploadLibraryThumbnail(id: string, file: File) { - const formData = new FormData() - formData.append('file', file) - return API.post(`/libraries/${id}/thumbnail`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) -} - -export function visitLibrary(id: string) { - return API.put(`/libraries/last-visited/${id}`) -} - -export function getLastVisitedLibrary(): Promise> { - return API.get('/libraries/last-visited') -} - -export function getExcludedUsers(id: string) { - return API.get(`/libraries/${id}/excluded-users`) -} - -export function updateExcludedUsers(id: string, user_ids: string[]) { - return API.post(`/libraries/${id}/excluded-users`, { user_ids }) -} - -/** - * Start the analysis of a library by library id. - * - * @param id The id for the library to analyze - */ -export function startMediaAnalysis(id: string) { - API.post(`/libraries/${id}/analyze`) -} - -export const libraryApi = { - cleanLibrary, - createLibrary, - deleteLibrary, - deleteLibraryThumbnails, - editLibrary, - getExcludedUsers, - getLastVisitedLibrary, - getLibraries, - getLibraryById, - getLibrarySeries, - getLibraryStats, - getTotalLibraryStats, - patchLibraryThumbnail, - regenerateThumbnails, - scanLibary, - startMediaAnalysis, - updateExcludedUsers, - uploadLibraryThumbnail, - visitLibrary, -} - -export const libraryQueryKeys: Record = { - cleanLibrary: 'library.cleanLibrary', - createLibrary: 'library.createLibrary', - deleteLibrary: 'library.deleteLibrary', - deleteLibraryThumbnails: 'library.deleteLibraryThumbnails', - editLibrary: 'library.editLibrary', - getExcludedUsers: 'library.getExcludedUsers', - getLastVisitedLibrary: 'library.getLastVisitedLibrary', - getLibraries: 'library.getLibraries', - getLibraryById: 'library.getLibraryById', - getLibrarySeries: 'library.getLibrarySeries', - getLibraryStats: 'library.getLibraryStats', - getTotalLibraryStats: 'library.getTotalLibraryStats', - patchLibraryThumbnail: 'library.patchLibraryThumbnail', - regenerateThumbnails: 'library.regenerateThumbnails', - scanLibary: 'library.scanLibary', - startMediaAnalysis: 'library.startAnalysis', - updateExcludedUsers: 'library.updateExcludedUsers', - uploadLibraryThumbnail: 'library.uploadLibraryThumbnail', - visitLibrary: 'library.visitLibrary', -} diff --git a/packages/api/src/log.ts b/packages/api/src/log.ts deleted file mode 100644 index 9b40ecf77..000000000 --- a/packages/api/src/log.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Log, LogMetadata } from '@stump/types' - -import { API } from './axios' -import { APIResult, PageableAPIResult } from './types' -import { toUrlParams } from './utils' - -export function getLogs(params?: Record): Promise> { - if (params) { - const searchParams = toUrlParams(params) - return API.get(`/logs?${searchParams.toString()}`) - } else { - return API.get('/logs') - } -} - -export function clearPersistedLogs(params?: Record): Promise> { - if (params) { - const searchParams = toUrlParams(params) - return API.delete(`/logs?${searchParams.toString()}`) - } else { - return API.delete('/logs') - } -} - -export function getLogFileMeta(): Promise> { - return API.get('/logs/file/info') -} - -export function clearLogFile() { - return API.delete('/logs/file') -} - -export const logApi = { - clearLogFile, - clearPersistedLogs, - getLogFileMeta, - getLogs, -} - -export const logQueryKeys: Record = { - clearLogFile: 'log.clearLogFile', - clearPersistedLogs: 'log.clearPersistedLogs', - getLogFileMeta: 'log.getLogFileMeta', - getLogs: 'log.getLogs', -} diff --git a/packages/api/src/media.ts b/packages/api/src/media.ts deleted file mode 100644 index 5d1fa922f..000000000 --- a/packages/api/src/media.ts +++ /dev/null @@ -1,151 +0,0 @@ -import type { - Media, - MediaIsComplete, - PatchMediaThumbnail, - ProgressUpdateReturn, - PutMediaCompletionStatus, -} from '@stump/types' - -import { API } from './axios' -import { APIResult, CursorQueryParams, PageableAPIResult } from './types' -import { mergeCursorParams, toUrlParams, urlWithParams } from './utils' - -type GetMediaById = APIResult - -export function getMedia(filters?: Record): Promise> { - const params = toUrlParams(filters) - return API.get(urlWithParams('/media', params)) -} - -export function getMediaWithCursor({ - afterId, - limit, - params, -}: CursorQueryParams): Promise> { - const searchParams = mergeCursorParams({ afterId, limit, params }) - return API.get(urlWithParams('/media', searchParams)) -} - -export function getPaginatedMedia(page: number): Promise> { - return API.get(`/media?page=${page}`) -} - -export function getMediaById(id: string, params?: Record): Promise { - if (params) { - return API.get(`/media/${id}?${toUrlParams(params)}`) - } else { - return API.get(`/media/${id}?load_series=true`) - } -} - -export function getMediaByPath(path: string): Promise> { - return API.get(`/media/path/${encodeURIComponent(path)}`) -} - -export function getRecentlyAddedMedia({ - afterId, - limit, - params, -}: CursorQueryParams): Promise> { - const searchParams = mergeCursorParams({ afterId, limit, params }) - return API.get(urlWithParams('/media/recently-added', searchParams)) -} - -export function getInProgressMedia({ - afterId, - limit, - params, -}: CursorQueryParams): Promise> { - const searchParams = mergeCursorParams({ afterId, limit, params }) - return API.get(urlWithParams('/media/keep-reading', searchParams)) -} - -export function getMediaThumbnail(id: string): string { - return `${API.getUri()}/media/${id}/thumbnail` -} - -export function getMediaDownloadUrl(id: string): string { - return `${API.getUri()}/media/${id}/file` -} - -// TODO: misleading, sounds like paged API response but is actualy a page image -export function getMediaPage(id: string, page: number): string { - return `${API.getUri()}/media/${id}/page/${page}` -} - -export function updateMediaProgress( - id: string, - page: number, -): Promise> { - return API.put(`/media/${id}/progress/${page}`) -} - -export function patchMediaThumbnail(id: string, params: PatchMediaThumbnail) { - return API.patch(`/media/${id}/thumbnail`, params) -} - -export function uploadMediaThumbnail(id: string, file: File) { - const formData = new FormData() - formData.append('file', file) - return API.post(`/media/${id}/thumbnail`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) -} - -export function putMediaCompletion( - id: string, - payload: PutMediaCompletionStatus, -): Promise> { - return API.put(`/media/${id}/progress/complete`, payload) -} - -export function deleteActiveReadingSession(bookId: string) { - return API.delete(`/media/${bookId}/progress`) -} - -/** - * Start the analysis of a book by media id. - * - * @param id The id for the book to analyze - */ -export function startMediaAnalysis(id: string) { - API.post(`/media/${id}/analyze`) -} - -export const mediaApi = { - deleteActiveReadingSession, - getInProgressMedia, - getMedia, - getMediaById, - getMediaByPath, - getMediaPage, - getMediaThumbnail, - getMediaWithCursor, - getPaginatedMedia, - getRecentlyAddedMedia, - patchMediaThumbnail, - putMediaCompletion, - startMediaAnalysis, - updateMediaProgress, - uploadMediaThumbnail, -} - -export const mediaQueryKeys: Record = { - deleteActiveReadingSession: 'media.deleteActiveReadingSession', - getInProgressMedia: 'media.getInProgress', - getMedia: 'media.get', - getMediaById: 'media.getById', - getMediaByPath: 'media.getByPath', - getMediaPage: 'media.getPage', - getMediaThumbnail: 'media.getThumbnail', - getMediaWithCursor: 'media.getWithCursor', - getPaginatedMedia: 'media.getPaginated', - getRecentlyAddedMedia: 'media.getRecentlyAdded', - patchMediaThumbnail: 'media.patchThumbnail', - putMediaCompletion: 'media.putCompletion', - startMediaAnalysis: 'media.startAnalysis', - updateMediaProgress: 'media.updateProgress', - uploadMediaThumbnail: 'media.uploadThumbnail', -} diff --git a/packages/api/src/metadata.ts b/packages/api/src/metadata.ts deleted file mode 100644 index 2ed8c0f05..000000000 --- a/packages/api/src/metadata.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { MediaMetadataOverview } from '@stump/types' - -import { API } from './axios' -import { APIResult } from './types' -import { toUrlParams, urlWithParams } from './utils' - -export function getMediaMetadataOverview( - params?: Record, -): Promise> { - return API.get(urlWithParams('/metadata/media', toUrlParams(params))) -} - -export function getGenres(): Promise> { - return API.get('/metadata/media/genres') -} - -export function getWriters(): Promise> { - return API.get('/metadata/media/writers') -} - -export function getPencillers(): Promise> { - return API.get('/metadata/media/pencillers') -} - -export function getInkers(): Promise> { - return API.get('/metadata/media/inkers') -} - -export function getColorists(): Promise> { - return API.get('/metadata/media/colorists') -} - -export function getLetterers(): Promise> { - return API.get('/metadata/media/letterers') -} - -export function getEditors(): Promise> { - return API.get('/metadata/media/editors') -} - -export function getPublishers(): Promise> { - return API.get('/metadata/media/publishers') -} - -export function getCharacters(): Promise> { - return API.get('/metadata/media/characters') -} - -export function getTeams(): Promise> { - return API.get('/metadata/media/teams') -} - -export const metadataApi = { - getCharacters, - getColorists, - getEditors, - getGenres, - getInkers, - getLetterers, - getMediaMetadataOverview, - getPencillers, - getPublishers, - getTeams, - getWriters, -} - -export const metadataQueryKeys: Record = { - getCharacters: 'metadata.getCharacters', - getColorists: 'metadata.getColorists', - getEditors: 'metadata.getEditors', - getGenres: 'metadata.getGenres', - getInkers: 'metadata.getInkers', - getLetterers: 'metadata.getLetterers', - getMediaMetadataOverview: 'metadata.getMediaMetadataOverview', - getPencillers: 'metadata.getPencillers', - getPublishers: 'metadata.getPublishers', - getTeams: 'metadata.getTeams', - getWriters: 'metadata.getWriters', -} diff --git a/packages/api/src/series.ts b/packages/api/src/series.ts deleted file mode 100644 index 191e31aee..000000000 --- a/packages/api/src/series.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { Media, PatchSeriesThumbnail, Series } from '@stump/types' - -import { API } from './axios' -import { mediaApi } from './media' -import { APIResult, CursorQueryParams, PageableAPIResult, PagedQueryParams } from './types' -import { mergeCursorParams, mergePageParams, toUrlParams, urlWithParams } from './utils' - -export function getSeries(filters?: Record): Promise> { - const params = toUrlParams(filters) - return API.get(urlWithParams('/series', params)) -} - -export function getSeriesById( - id: string, - params?: Record, -): Promise> { - return API.get(urlWithParams(`/series/${id}`, toUrlParams(params))) -} - -export function getSeriesWithCursor( - params: CursorQueryParams, -): Promise> { - const searchParams = mergeCursorParams(params) - return API.get(urlWithParams('/series', searchParams)) -} - -export function getSeriesMedia( - id: string, - { page, page_size, params }: PagedQueryParams, -): Promise> { - const searchParams = mergePageParams({ page, page_size, params }) - return API.get(urlWithParams(`/series/${id}/media`, searchParams)) -} - -export function getRecentlyAddedSeries( - page: number, - params?: URLSearchParams, -): Promise> { - if (params) { - params.set('page', page.toString()) - return API.get(`/series/recently-added?${params.toString()}`) - } - - return API.get(`/series/recently-added?page=${page}`) -} - -export function getNextInSeries(id: string): Promise> { - return API.get(`/series/${id}/media/next`) -} - -/** Returns a list of media within a series, ordered after the cursor. - * Default limit is 25. - * */ -export function getNextMediaInSeries( - series_id: string, - media_id: string, - limit = 25, -): Promise> { - return mediaApi.getMedia({ - cursor: media_id, - limit: limit.toString(), - series_id, - }) -} - -export function getSeriesThumbnail(id: string): string { - return `${API.getUri()}/series/${id}/thumbnail` -} - -export function patchSeriesThumbnail(id: string, params: PatchSeriesThumbnail) { - return API.patch(`/series/${id}/thumbnail`, params) -} - -export function uploadSeriesThumbnail(id: string, file: File) { - const formData = new FormData() - formData.append('file', file) - return API.post(`/series/${id}/thumbnail`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) -} - -/** - * Start the analysis of a series by series id. - * - * @param id The id for the series to analyze - */ -export function startMediaAnalysis(id: string) { - API.post(`/series/${id}/analyze`) -} - -export const seriesApi = { - getNextInSeries, - getNextMediaInSeries, - getRecentlyAddedSeries, - getSeries, - getSeriesById, - getSeriesMedia, - getSeriesThumbnail, - getSeriesWithCursor, - patchSeriesThumbnail, - startMediaAnalysis, - uploadSeriesThumbnail, -} - -export const seriesQueryKeys: Record = { - getNextInSeries: 'series.getNextInSeries', - getNextMediaInSeries: 'series.getNextMediaInSeries', - getRecentlyAddedSeries: 'series.getRecentlyAddedSeries', - getSeries: 'series.getSeries', - getSeriesById: 'series.getSeriesById', - getSeriesMedia: 'series.getSeriesMedia', - getSeriesThumbnail: 'series.getSeriesThumbnail', - getSeriesWithCursor: 'series.getSeriesWithCursor', - patchSeriesThumbnail: 'series.patchSeriesThumbnail', - startMediaAnalysis: 'series.Analysis', - uploadSeriesThumbnail: 'series.uploadSeriesThumbnail', -} diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts deleted file mode 100644 index ff4d0314a..000000000 --- a/packages/api/src/server.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { ClaimResponse, StumpVersion, UpdateCheck } from '@stump/types' - -import { API } from './axios' -import { APIResult } from './types' - -export function getStumpVersion(): Promise> { - return API.post('/version') -} - -export function checkForServerUpdate(): Promise> { - return API.get('/check-for-update') -} - -export function ping() { - return API.get('/ping') -} - -export async function checkIsClaimed(): Promise> { - return API.get('/claim') -} - -export const serverApi = { - checkForServerUpdate, - checkIsClaimed, - getStumpVersion, - ping, -} - -export const serverQueryKeys: Record = { - checkForServerUpdate: 'server.checkForServerUpdate', - checkIsClaimed: 'server.checkIsClaimed', - getStumpVersion: 'server.getStumpVersion', - ping: 'server.ping', -} diff --git a/packages/api/src/smartList.ts b/packages/api/src/smartList.ts deleted file mode 100644 index 78208c3cd..000000000 --- a/packages/api/src/smartList.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - CreateOrUpdateSmartList, - CreateOrUpdateSmartListView, - GetSmartListsParams, - SmartList, - SmartListItems, - SmartListMeta, - SmartListRelationOptions, - SmartListView, -} from '@stump/types' - -import { API } from './axios' -import { APIResult } from './types' -import { toUrlParams, urlWithParams } from './utils' - -export async function getSmartLists(params?: GetSmartListsParams): Promise> { - if (params) { - const searchParams = toUrlParams(params) - return API.get(urlWithParams('/smart-lists', searchParams)) - } - - return API.get('/smart-lists') -} - -export async function createSmartList(payload: SmartList): Promise> { - return API.post('/smart-lists', payload) -} - -export async function getSmartListById( - id: string, - params?: SmartListRelationOptions, -): Promise> { - if (params) { - const searchParams = toUrlParams(params) - return API.get(urlWithParams(`/smart-lists/${id}`, searchParams)) - } else { - return API.get(`/smart-lists/${id}`) - } -} - -export async function getSmartListMeta(id: string): Promise> { - return API.get(`/smart-lists/${id}/meta`) -} - -export async function getSmartListItems(id: string): Promise> { - return API.get(`/smart-lists/${id}/items`) -} - -// TODO: different types! -export async function updateSmartList( - id: string, - payload: CreateOrUpdateSmartList, -): Promise> { - return API.put(`/smart-lists/${id}`, payload) -} - -export async function deleteSmartList(id: string): Promise> { - return API.delete(`/smart-lists/${id}`) -} - -export async function createSmartListView( - listId: string, - params: CreateOrUpdateSmartListView, -): Promise> { - return API.post(`/smart-lists/${listId}/views`, params) -} - -export async function updateSmartListView( - listId: string, - name: string, - params: CreateOrUpdateSmartListView, -): Promise> { - return API.put(`/smart-lists/${listId}/views/${name}`, params) -} - -export const smartListApi = { - createSmartList, - createSmartListView, - deleteSmartList, - getSmartListById, - getSmartListItems, - getSmartListMeta, - getSmartLists, - updateSmartList, - updateSmartListView, -} - -export const smartListQueryKeys: Record = { - createSmartList: 'smartlist.create', - createSmartListView: 'smartlist.createView', - deleteSmartList: 'smartlist.delete', - getSmartListById: 'smartlist.getById', - getSmartListItems: 'smartlist.getItems', - getSmartListMeta: 'smartlist.getMeta', - getSmartLists: 'smartlist.get', - updateSmartList: 'smartlist.update', - updateSmartListView: 'smartlist.updateView', -} diff --git a/packages/api/src/tag.ts b/packages/api/src/tag.ts deleted file mode 100644 index 437e36040..000000000 --- a/packages/api/src/tag.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Tag } from '@stump/types' - -import { API } from './axios' -import { APIResult } from './types' - -export function getAllTags(): Promise> { - return API.get('/tags') -} - -export function createTags(tags: string[]): Promise> { - return API.post('/tags', { tags }) -} - -const tagApi = { - createTags, - getAllTags, -} - -export const tagQueryKeys: Record = { - createTags: 'tag.createTags', - getAllTags: 'tag.getAllTags', -} diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts deleted file mode 100644 index 5afb8f02f..000000000 --- a/packages/api/src/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { APIError, Pageable } from '@stump/types' - -export type APIResult = import('axios').AxiosResponse> -export type PageableAPIResult = APIResult> - -export type PagedQueryParams = { - page?: number - page_size?: number - params?: Record -} - -export type CursorQueryParams = { - afterId?: string - limit?: number - params?: Record -} diff --git a/packages/api/src/user.ts b/packages/api/src/user.ts deleted file mode 100644 index d77f90253..000000000 --- a/packages/api/src/user.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type { - Arrangement, - CreateUser, - LoginActivity, - NavigationItem, - UpdateUser, - UpdateUserPreferences, - User, - UserPreferences, -} from '@stump/types' - -import { API } from './axios' -import { APIResult, PageableAPIResult } from './types' -import { toUrlParams } from './utils' - -export function getUsers(params?: Record): Promise> { - if (params) { - const searchParams = toUrlParams(params) - return API.get(`/users?${searchParams.toString()}`) - } else { - return API.get('/users') - } -} - -export function getUserById(userId: string): Promise> { - return API.get(`/users/${userId}`) -} - -export function getUserPreferences(userId: string): Promise> { - return API.get(`/users/${userId}/preferences`) -} - -/** - * Update the current user's preferences - */ -export function updatePreferences( - preferences: UpdateUserPreferences, -): Promise> { - return API.put(`/users/me/preferences`, preferences) -} - -/** - * Update the a user's preferences by their ID - */ -export function updateUserPreferences( - userId: string, - preferences: UserPreferences, -): Promise> { - return API.put(`/users/${userId}/preferences`, preferences) -} - -export function createUser(params: CreateUser): Promise> { - return API.post(`/users`, params) -} - -export function updateUser(userId: string, params: UpdateUser): Promise> { - return API.put(`/users/${userId}`, params) -} - -export function updateViewer(params: UpdateUser): Promise> { - return API.put(`/users/me`, params) -} - -type DeleteUser = { - userId: string - hardDelete?: boolean -} - -export function deleteUser({ userId, hardDelete }: DeleteUser): Promise> { - return API.delete(`/users/${userId}`, { - data: { - hard_delete: hardDelete, - }, - }) -} - -export function getLoginActivityForUser(userId: string): Promise> { - return API.get(`/users/${userId}/login-activity`) -} - -export function getLoginActivity(): Promise> { - return API.get(`/users/login-activity`) -} - -export function deleteAllLoginActivity(): Promise> { - return API.delete(`/users/login-activity`) -} - -export function setLockStatus(userId: string, lock: boolean): Promise> { - return API.put(`/users/${userId}/lock`, { - lock, - }) -} - -export function deleteUserSessions(userId: string): Promise> { - return API.delete(`/users/${userId}/sessions`) -} - -export function getPreferredNavigationArrangement(): Promise< - APIResult> -> { - return API.get('/users/me/navigation-arrangement') -} - -export function setPreferredNavigationArrangement( - arrangement: Arrangement, -): Promise>> { - return API.put('/users/me/navigation-arrangement', arrangement) -} - -export const userApi = { - createUser, - deleteAllLoginActivity, - deleteUser, - deleteUserSessions, - getLoginActivity, - getLoginActivityForUser, - getPreferredNavigationArrangement, - getUserById, - getUserPreferences, - getUsers, - setLockStatus, - setPreferredNavigationArrangement, - updatePreferences, - updateUser, - updateUserPreferences, - updateViewer, -} - -export const userQueryKeys: Record = { - createUser: 'user.createUser', - deleteAllLoginActivity: 'user.deleteAllLoginActivity', - deleteUser: 'user.deleteUser', - deleteUserSessions: 'user.deleteUserSessions', - getLoginActivity: 'user.getLoginActivity', - getLoginActivityForUser: 'user.getLoginActivityForUser', - getPreferredNavigationArrangement: 'user.getPreferredNavigationArrangement', - getUserById: 'user.getUserById', - getUserPreferences: 'user.getUserPreferences', - getUsers: 'user.getUsers', - setLockStatus: 'user.setLockStatus', - setPreferredNavigationArrangement: 'user.setPreferredNavigationArrangement', - updatePreferences: 'user.updatePreferences', - updateUser: 'user.updateUser', - updateUserPreferences: 'user.updateUserPreferences', - updateViewer: 'user.updateViewer', -} diff --git a/packages/browser/package.json b/packages/browser/package.json index aab331245..6ca4e7245 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -19,7 +19,7 @@ "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@hookform/resolvers": "^3.3.4", - "@stump/api": "*", + "@stump/sdk": "*", "@stump/client": "*", "@stump/components": "*", "@stump/i18n": "*", diff --git a/packages/browser/src/App.tsx b/packages/browser/src/App.tsx index f01b3cd9f..f571770a7 100644 --- a/packages/browser/src/App.tsx +++ b/packages/browser/src/App.tsx @@ -1,8 +1,7 @@ import './styles/index.css' import '@stump/components/styles/overrides.css' -import { initializeApi } from '@stump/api' -import { StumpClientContextProvider, StumpClientProps } from '@stump/client' +import { SDKProvider, StumpClientContextProvider, StumpClientProps } from '@stump/client' import { defaultContext } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { useEffect, useState } from 'react' @@ -15,7 +14,6 @@ import Notifications from '@/components/Notifications' import { AppRouter } from './AppRouter' import { useApplyTheme } from './hooks' -import { API_VERSION } from './index' import { useAppStore, useUserStore } from './stores' const IS_DEVELOPMENT = import.meta.env.MODE === 'development' @@ -51,8 +49,6 @@ function RouterContainer(props: StumpClientProps) { () => { if (!baseUrl && props.baseUrl) { setBaseUrl(props.baseUrl) - } else if (baseUrl) { - initializeApi(baseUrl, API_VERSION) } setMounted(true) @@ -103,17 +99,19 @@ function RouterContainer(props: StumpClientProps) { } return ( - - {IS_DEVELOPMENT && } - - Stump - - - - + + + {IS_DEVELOPMENT && } + + Stump + + + + + ) } diff --git a/packages/browser/src/AppLayout.tsx b/packages/browser/src/AppLayout.tsx index 7411dea13..89f267e38 100644 --- a/packages/browser/src/AppLayout.tsx +++ b/packages/browser/src/AppLayout.tsx @@ -1,6 +1,6 @@ -import { isAxiosError } from '@stump/api' import { useAuthQuery, useCoreEventHandler } from '@stump/client' import { cn, cx } from '@stump/components' +import { isAxiosError } from '@stump/sdk' import { UserPermission, UserPreferences } from '@stump/types' import { useOverlayScrollbars } from 'overlayscrollbars-react' import { Suspense, useCallback, useEffect, useMemo, useRef } from 'react' diff --git a/packages/browser/src/components/book/BookCard.tsx b/packages/browser/src/components/book/BookCard.tsx index 498210d83..1a646379a 100644 --- a/packages/browser/src/components/book/BookCard.tsx +++ b/packages/browser/src/components/book/BookCard.tsx @@ -1,5 +1,4 @@ -import { getMediaThumbnail } from '@stump/api' -import { prefetchMedia } from '@stump/client' +import { usePrefetchMediaByID, useSDK } from '@stump/client' import { EntityCard, Text } from '@stump/components' import { FileStatus, Media } from '@stump/types' import pluralize from 'pluralize' @@ -26,16 +25,19 @@ export default function BookCard({ variant = 'default', onSelect, }: BookCardProps) { + const { sdk } = useSDK() + const { prefetch } = usePrefetchMediaByID(media.id) + const isCoverOnly = variant === 'cover' const handleHover = () => { if (!readingLink) { - prefetchMedia(media.id) + prefetch() } const currentPage = media.current_page || -1 if (currentPage > 0) { - prefetchMediaPage(media.id, currentPage) + prefetchMediaPage(sdk, media.id, currentPage) } } @@ -146,7 +148,7 @@ export default function BookCard({ title={formatBookName(media)} href={href} fullWidth={fullWidth} - imageUrl={getMediaThumbnail(media.id)} + imageUrl={sdk.media.thumbnailURL(media.id)} progress={getProgress()} subtitle={getSubtitle(media)} onMouseEnter={handleHover} diff --git a/packages/browser/src/components/book/BookSearch.tsx b/packages/browser/src/components/book/BookSearch.tsx index b06f05718..9b22abdac 100644 --- a/packages/browser/src/components/book/BookSearch.tsx +++ b/packages/browser/src/components/book/BookSearch.tsx @@ -1,4 +1,4 @@ -import { prefetchPagedMedia, usePagedMediaQuery } from '@stump/client' +import { usePagedMediaQuery, usePrefetchMediaPaged } from '@stump/client' import { usePreviousIsDifferent } from '@stump/components' import { Media } from '@stump/types' import React, { useCallback, useEffect, useMemo } from 'react' @@ -40,6 +40,7 @@ export default function BookSearch({ page, page_size, setPage, onBookSelect, sho media, pageData: { current_page, total_pages } = {}, } = usePagedMediaQuery(params) + const { prefetch } = usePrefetchMediaPaged() const differentSearch = usePreviousIsDifferent(filters?.search as string) useEffect(() => { @@ -67,12 +68,12 @@ export default function BookSearch({ page, page_size, setPage, onBookSelect, sho const handlePrefetchPage = useCallback( (page: number) => { - prefetchPagedMedia({ + prefetch({ ...params, page, }) }, - [params], + [prefetch, params], ) return ( diff --git a/packages/browser/src/components/book/table/CoverImageCell.tsx b/packages/browser/src/components/book/table/CoverImageCell.tsx index af123374f..7626014fa 100644 --- a/packages/browser/src/components/book/table/CoverImageCell.tsx +++ b/packages/browser/src/components/book/table/CoverImageCell.tsx @@ -1,4 +1,4 @@ -import { getMediaThumbnail } from '@stump/api' +import { useSDK } from '@stump/client' import { Book } from 'lucide-react' import React, { useState } from 'react' @@ -6,13 +6,15 @@ type Props = { id: string title?: string } + export default function CoverImageCell({ id, title }: Props) { + const { sdk } = useSDK() const [showFallback, setShowFallback] = useState(false) const loadImage = () => { const image = new Image() return new Promise((resolve, reject) => { - image.src = getMediaThumbnail(id) + image.src = sdk.media.thumbnailURL(id) image.onload = () => resolve(image) image.onerror = (e) => { console.error('Image failed to load:', e) @@ -46,7 +48,7 @@ export default function CoverImageCell({ id, title }: Props) { setShowFallback(true)} /> ) diff --git a/packages/browser/src/components/explorer/FileThumbnail.tsx b/packages/browser/src/components/explorer/FileThumbnail.tsx index 4d2378823..7df34a306 100644 --- a/packages/browser/src/components/explorer/FileThumbnail.tsx +++ b/packages/browser/src/components/explorer/FileThumbnail.tsx @@ -1,6 +1,6 @@ -import { getMediaThumbnail, mediaApi, mediaQueryKeys } from '@stump/api' -import { queryClient } from '@stump/client' +import { queryClient, useSDK } from '@stump/client' import { cn } from '@stump/components' +import { Api } from '@stump/sdk' import { Media } from '@stump/types' import { Book, Folder } from 'lucide-react' import { useCallback, useEffect, useRef, useState } from 'react' @@ -18,6 +18,7 @@ export default function FileThumbnail({ size = 'sm', containerClassName, }: Props) { + const { sdk } = useSDK() /** * A boolean state to keep track of whether or not we should show the fallback icon. This * will be set to true if the image fails to load @@ -39,9 +40,9 @@ export default function FileThumbnail({ useEffect(() => { if (!book && !didFetchRef.current && !isDirectory) { didFetchRef.current = true - getBook(path).then(setBook) + getBook(path, sdk).then(setBook) } - }, [book, path, isDirectory]) + }, [book, path, isDirectory, sdk]) /** * A function that attempts to load the image associated with the book, @@ -51,7 +52,7 @@ export default function FileThumbnail({ if (book) { const image = new Image() return new Promise((resolve, reject) => { - image.src = getMediaThumbnail(book.id) + image.src = sdk.media.thumbnailURL(book.id) image.onload = () => resolve(image) image.onerror = (e) => { console.error('Image failed to load:', e) @@ -61,7 +62,7 @@ export default function FileThumbnail({ } else { return Promise.reject('No book found') } - }, [book]) + }, [book, sdk.media]) /** * A function that attempts to reload the image @@ -102,7 +103,7 @@ export default function FileThumbnail({ return ( setShowFallback(true)} /> ) @@ -112,20 +113,20 @@ export default function FileThumbnail({ * A function that attempts to fetch the book associated with the file, if any exists. * The queryClient is used in order to properly cache the result. */ -export const getBook = async (path: string) => { +export const getBook = async (path: string, sdk: Api) => { try { const response = await queryClient.fetchQuery( - [mediaQueryKeys.getMedia, { path }], + [sdk.media.keys.get, { path }], () => - mediaApi.getMedia({ - path, + sdk.media.get({ + path: [path], }), { // 15 minutes cacheTime: 1000 * 60 * 15, }, ) - return response.data.data?.at(0) ?? null + return response.data?.at(0) ?? null } catch (error) { console.error(error) return null diff --git a/packages/browser/src/components/filters/FilterProvider.tsx b/packages/browser/src/components/filters/FilterProvider.tsx index 1c042b5f9..9b3663766 100644 --- a/packages/browser/src/components/filters/FilterProvider.tsx +++ b/packages/browser/src/components/filters/FilterProvider.tsx @@ -1,4 +1,4 @@ -import { toObjectParams, toUrlParams } from '@stump/api' +import { toObjectParams, toUrlParams } from '@stump/sdk' import React, { useMemo } from 'react' import { useSearchParams } from 'react-router-dom' diff --git a/packages/browser/src/components/filters/form/MediaFilterForm.tsx b/packages/browser/src/components/filters/form/MediaFilterForm.tsx index 5409984ac..7307d07e0 100644 --- a/packages/browser/src/components/filters/form/MediaFilterForm.tsx +++ b/packages/browser/src/components/filters/form/MediaFilterForm.tsx @@ -1,7 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { metadataApi, metadataQueryKeys } from '@stump/api' -import { useQuery } from '@stump/client' +import { useQuery, useSDK } from '@stump/client' import { CheckBox, Form } from '@stump/components' +import { MediaMetadataFilter } from '@stump/types' import React, { useEffect, useMemo, useState } from 'react' import { FieldValues, useForm } from 'react-hook-form' import z from 'zod' @@ -41,6 +41,7 @@ export type MediaFilterFormSchema = z.infer type ReadStatus = NonNullable['read_status']>[number] export default function MediaFilterForm() { + const { sdk } = useSDK() const { filters, setFilters } = useFilterContext() const seriesContext = useSeriesContextSafe() @@ -51,16 +52,16 @@ export default function MediaFilterForm() { return { media: { series: { - id: seriesContext.series.id, + id: [seriesContext.series.id], }, }, - } + } satisfies MediaMetadataFilter } - return {} + return undefined }, [onlyFromSeries, seriesContext]) - const { data } = useQuery([metadataQueryKeys.getMediaMetadataOverview, params], () => - metadataApi.getMediaMetadataOverview(params).then((res) => res.data), + const { data } = useQuery([sdk.metadata.keys.overview, params], () => + sdk.metadata.overview(params), ) const form = useForm({ diff --git a/packages/browser/src/components/filters/useFilterScene.ts b/packages/browser/src/components/filters/useFilterScene.ts index db277a9b2..9de2bbe41 100644 --- a/packages/browser/src/components/filters/useFilterScene.ts +++ b/packages/browser/src/components/filters/useFilterScene.ts @@ -1,4 +1,4 @@ -import { toObjectParams, toUrlParams } from '@stump/api' +import { toObjectParams, toUrlParams } from '@stump/sdk' import { useCallback, useMemo } from 'react' import { useSearchParams } from 'react-router-dom' import { useMediaMatch } from 'rooks' diff --git a/packages/browser/src/components/library/DeleteLibraryConfirmation.tsx b/packages/browser/src/components/library/DeleteLibraryConfirmation.tsx index dd5dd9cd7..e91fe0c87 100644 --- a/packages/browser/src/components/library/DeleteLibraryConfirmation.tsx +++ b/packages/browser/src/components/library/DeleteLibraryConfirmation.tsx @@ -1,6 +1,6 @@ -import { isAxiosError } from '@stump/api' -import { useDeleteLibraryMutation } from '@stump/client' +import { useDeleteLibrary } from '@stump/client' import { ConfirmationModal } from '@stump/components' +import { isAxiosError } from '@stump/sdk' import { toast } from 'react-hot-toast' import { useNavigate } from 'react-router' @@ -16,7 +16,7 @@ type Props = { export default function DeleteLibraryConfirmation({ isOpen, libraryId, onClose, trigger }: Props) { const navigate = useNavigate() - const { deleteLibraryAsync, isLoading } = useDeleteLibraryMutation({ + const { deleteLibraryAsync, isLoading } = useDeleteLibrary({ onSuccess: () => { navigate(paths.home()) }, diff --git a/packages/browser/src/components/library/LastVisitedLibrary.tsx b/packages/browser/src/components/library/LastVisitedLibrary.tsx index 29fb542bf..1893886fc 100644 --- a/packages/browser/src/components/library/LastVisitedLibrary.tsx +++ b/packages/browser/src/components/library/LastVisitedLibrary.tsx @@ -1,5 +1,4 @@ -import { getLibraryThumbnail, libraryApi, libraryQueryKeys } from '@stump/api' -import { useQuery } from '@stump/client' +import { useQuery, useSDK } from '@stump/client' import { EntityCard, Label, Text } from '@stump/components' import React from 'react' @@ -8,11 +7,10 @@ import paths from '@/paths' type Props = { container?: (children: React.ReactNode) => React.ReactNode } + export default function LastVisitedLibrary({ container }: Props) { - const { data: library } = useQuery([libraryQueryKeys.getLastVisitedLibrary], async () => { - const { data } = await libraryApi.getLastVisitedLibrary() - return data - }) + const { sdk } = useSDK() + const { data: library } = useQuery([sdk.library.keys.getLastVisited], sdk.library.getLastVisited) if (!library) { return null @@ -24,7 +22,7 @@ export default function LastVisitedLibrary({ container }: Props) { !imageFailed} diff --git a/packages/browser/src/components/navigation/sidebar/Logout.tsx b/packages/browser/src/components/navigation/sidebar/Logout.tsx index 994ed9bad..332180ec6 100644 --- a/packages/browser/src/components/navigation/sidebar/Logout.tsx +++ b/packages/browser/src/components/navigation/sidebar/Logout.tsx @@ -1,5 +1,4 @@ -import { authApi, serverQueryKeys } from '@stump/api' -import { invalidateQueries } from '@stump/client' +import { invalidateQueries, useSDK } from '@stump/client' import { ConfirmationModal, IconButton, ToolTip, useBoolean } from '@stump/components' import { LogOut } from 'lucide-react' import toast from 'react-hot-toast' @@ -10,7 +9,9 @@ import { useUserStore } from '@/stores' type Props = { trigger?: (setOpen: (state: boolean) => void) => JSX.Element } + export default function Logout({ trigger }: Props) { + const { sdk } = useSDK() const [isOpen, { on, off }] = useBoolean() const setUser = useUserStore((store) => store.setUser) @@ -18,13 +19,13 @@ export default function Logout({ trigger }: Props) { async function handleLogout() { toast - .promise(authApi.logout(), { + .promise(sdk.auth.logout(), { error: 'There was an error logging you out. Please try again.', loading: null, success: 'You have been logged out. Redirecting...', }) .then(() => { - invalidateQueries({ keys: [serverQueryKeys.checkIsClaimed] }) + invalidateQueries({ keys: [sdk.server.keys.claimedStatus] }) setUser(null) navigate('/auth') }) diff --git a/packages/browser/src/components/navigation/sidebar/SignOut.tsx b/packages/browser/src/components/navigation/sidebar/SignOut.tsx index 29d5eb511..4bb019603 100644 --- a/packages/browser/src/components/navigation/sidebar/SignOut.tsx +++ b/packages/browser/src/components/navigation/sidebar/SignOut.tsx @@ -1,5 +1,4 @@ -import { authApi, serverQueryKeys } from '@stump/api' -import { invalidateQueries } from '@stump/client' +import { invalidateQueries, useSDK } from '@stump/client' import { ConfirmationModal, Text, useBoolean } from '@stump/components' import { LogOut } from 'lucide-react' import toast from 'react-hot-toast' @@ -8,6 +7,7 @@ import { useNavigate } from 'react-router-dom' import { useUserStore } from '@/stores' export default function SignOut() { + const { sdk } = useSDK() const [isOpen, { on, off }] = useBoolean() const setUser = useUserStore((store) => store.setUser) @@ -15,13 +15,13 @@ export default function SignOut() { async function handleLogout() { toast - .promise(authApi.logout(), { + .promise(sdk.auth.logout(), { error: 'There was an error logging you out. Please try again.', loading: null, success: 'You have been logged out. Redirecting...', }) .then(() => { - invalidateQueries({ keys: [serverQueryKeys.checkIsClaimed] }) + invalidateQueries({ keys: [sdk.server.keys.claimedStatus] }) setUser(null) navigate('/auth') }) diff --git a/packages/browser/src/components/navigation/sidebar/sections/library/LibraryEmoji.tsx b/packages/browser/src/components/navigation/sidebar/sections/library/LibraryEmoji.tsx index b94ac8a8b..b31a4aa2b 100644 --- a/packages/browser/src/components/navigation/sidebar/sections/library/LibraryEmoji.tsx +++ b/packages/browser/src/components/navigation/sidebar/sections/library/LibraryEmoji.tsx @@ -1,4 +1,4 @@ -import { useEditLibraryMutation } from '@stump/client' +import { useUpdateLibrary } from '@stump/client' import { EmojiPicker } from '@stump/components' import { Library } from '@stump/types' import React from 'react' @@ -10,7 +10,7 @@ type Props = { disabled?: boolean } export default function LibraryEmoji({ emoji, placeholder, library, disabled }: Props) { - const { editLibraryAsync } = useEditLibraryMutation() + const { editLibraryAsync } = useUpdateLibrary({ id: library.id }) const handleEmojiSelect = (emoji?: { native: string }) => { if (disabled) { @@ -21,6 +21,7 @@ export default function LibraryEmoji({ emoji, placeholder, library, disabled }: ...library, emoji: emoji?.native ?? null, scan_mode: 'NONE', + tags: library.tags?.map((tag) => tag.name) ?? [], }) } diff --git a/packages/browser/src/components/navigation/sidebar/sections/library/LibraryOptionsMenu.tsx b/packages/browser/src/components/navigation/sidebar/sections/library/LibraryOptionsMenu.tsx index 581b482cb..329b4f34b 100644 --- a/packages/browser/src/components/navigation/sidebar/sections/library/LibraryOptionsMenu.tsx +++ b/packages/browser/src/components/navigation/sidebar/sections/library/LibraryOptionsMenu.tsx @@ -44,7 +44,7 @@ export default function LibraryOptionsMenu({ library }: Props) { // The UI will receive updates from SSE in fractions of ms lol and it can get bogged down. // So, add a slight delay so the close animation of the menu can finish cleanly. setTimeout(async () => { - await scanAsync({ id: library.id, mode: 'DEFAULT' }) + await scanAsync(library.id) await queryClient.invalidateQueries(['getJobReports']) }, 50) }, [canScan, library.id, scanAsync]) diff --git a/packages/browser/src/components/navigation/topbar/sections/UserMenu.tsx b/packages/browser/src/components/navigation/topbar/sections/UserMenu.tsx index 5f26d6a9b..d5fa83ee7 100644 --- a/packages/browser/src/components/navigation/topbar/sections/UserMenu.tsx +++ b/packages/browser/src/components/navigation/topbar/sections/UserMenu.tsx @@ -1,5 +1,4 @@ -import { authApi, serverQueryKeys } from '@stump/api' -import { invalidateQueries } from '@stump/client' +import { invalidateQueries, useSDK } from '@stump/client' import { Avatar, cn, NavigationMenu } from '@stump/components' import { Bell, LogOut } from 'lucide-react' import React from 'react' @@ -14,6 +13,7 @@ import TopBarButtonItem from '../TopBarButtonItem' import TopBarLinkListItem from '../TopBarLinkListItem' export default function UserMenu() { + const { sdk } = useSDK() const { user } = useAppContext() const setUser = useUserStore((store) => store.setUser) @@ -21,8 +21,8 @@ export default function UserMenu() { const logout = async () => { try { - await authApi.logout() - await invalidateQueries({ keys: [serverQueryKeys.checkIsClaimed] }) + await sdk.auth.logout() + await invalidateQueries({ keys: [sdk.server.keys.claimedStatus] }) setUser(null) navigate('/auth') } catch (error) { diff --git a/packages/browser/src/components/readers/epub/EpubJsReader.tsx b/packages/browser/src/components/readers/epub/EpubJsReader.tsx index 5727f1cce..28c49f984 100644 --- a/packages/browser/src/components/readers/epub/EpubJsReader.tsx +++ b/packages/browser/src/components/readers/epub/EpubJsReader.tsx @@ -1,10 +1,10 @@ -import { API, epubApi, epubQueryKeys, mediaQueryKeys, updateEpubProgress } from '@stump/api' import { type EpubReaderPreferences, queryClient, useEpubLazy, useEpubReader, useQuery, + useSDK, } from '@stump/client' import { Bookmark, UpdateEpubProgress } from '@stump/types' import { Book, Rendition } from 'epubjs' @@ -65,6 +65,7 @@ type EpubLocationState = { * epub reader as an additional option. */ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) { + const { sdk } = useSDK() const { theme } = useTheme() const ref = useRef(null) @@ -78,15 +79,9 @@ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) { epubPreferences: state.preferences, })) - const { data: bookmarks } = useQuery([epubQueryKeys.getBookmarks, id], async () => { - try { - const { data } = await epubApi.getBookmarks(id) - return data - } catch (error) { - console.error('Failed to fetch bookmarks', error) - return [] - } - }) + const { data: bookmarks } = useQuery([sdk.epub.keys.getBookmarks, id], () => + sdk.epub.getBookmarks(id), + ) const existingBookmarks = useMemo( () => (bookmarks ?? []).reduce( @@ -167,14 +162,14 @@ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) { useEffect(() => { if (!book) { setBook( - new Book(`${API.getUri()}/media/${id}/file`, { + new Book(sdk.media.downloadURL(id), { openAs: 'epub', // @ts-expect-error: epubjs has incorrect types requestCredentials: true, }), ) } - }, [book, epub, id]) + }, [book, epub, id, sdk.media]) /** * A function for applying the epub reader preferences to the epubjs rendition instance @@ -308,9 +303,9 @@ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) { */ useEffect(() => { return () => { - queryClient.invalidateQueries([mediaQueryKeys.getInProgressMedia], { exact: false }) + queryClient.invalidateQueries([sdk.media.keys.inProgress], { exact: false }) } - }, []) + }, [sdk.media]) /** * A callback for when the reader should paginate forward. This will only run if the @@ -442,7 +437,7 @@ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) { */ const handleUpdateProgress = async (payload: UpdateEpubProgress) => { try { - await updateEpubProgress({ ...payload, id: epub.media_entity.id }) + await sdk.epub.updateProgress({ ...payload, id: epub.media_entity.id }) } catch (err) { console.error(err) } diff --git a/packages/browser/src/components/readers/epub/EpubStreamReader.tsx b/packages/browser/src/components/readers/epub/EpubStreamReader.tsx index dcfef54f1..4d3b449d9 100644 --- a/packages/browser/src/components/readers/epub/EpubStreamReader.tsx +++ b/packages/browser/src/components/readers/epub/EpubStreamReader.tsx @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ // FIXME: this file is a mess -import { getEpubResource } from '@stump/api' -import { UseEpubReturn } from '@stump/client' +import { UseEpubReturn, useSDK } from '@stump/client' import React, { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' @@ -22,6 +21,7 @@ import { useNavigate } from 'react-router-dom' Some of this has been started, but not finished. */ export default function EpubStreamReader({ epub, actions, ...rest }: UseEpubReturn) { + const { sdk } = useSDK() const navigate = useNavigate() // const { isLoading: isFetchingResource, data: content } = useQuery( @@ -35,16 +35,17 @@ export default function EpubStreamReader({ epub, actions, ...rest }: UseEpubRetu useEffect( () => { - getEpubResource({ - id: epub.media_entity.id, - resourceId: actions.currentResource()?.content!, - root: epub.root_base, - }).then((res) => { - console.debug(res) - - // FIXME: don't cast - setContent(rest.correctHtmlUrls(res.data as string)) - }) + sdk.epub + .fetchResource({ + id: epub.media_entity.id, + resourceId: actions.currentResource()?.content!, + root: epub.root_base, + }) + .then((res) => { + console.debug(res) + // FIXME: don't cast + setContent(rest.correctHtmlUrls(res as string)) + }) }, // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/browser/src/components/readers/epub/controls/useEpubBookmark.ts b/packages/browser/src/components/readers/epub/controls/useEpubBookmark.ts index 694ac5d26..5445b44d5 100644 --- a/packages/browser/src/components/readers/epub/controls/useEpubBookmark.ts +++ b/packages/browser/src/components/readers/epub/controls/useEpubBookmark.ts @@ -1,5 +1,4 @@ -import { epubApi, epubQueryKeys } from '@stump/api' -import { queryClient, useMutation } from '@stump/client' +import { queryClient, useMutation, useSDK } from '@stump/client' import { CreateOrUpdateBookmark, DeleteBookmark } from '@stump/types' import { useCallback, useMemo } from 'react' @@ -9,6 +8,7 @@ import { useEpubReaderContext } from '../context' * A hook for creating and deleting bookmarks within an epub reader */ export function useEpubBookmark() { + const { sdk } = useSDK() const { readerMeta: { bookEntity: { id: bookId }, @@ -34,8 +34,8 @@ export function useEpubBookmark() { * A callback to invalidate the bookmarks query after a bookmark is created or deleted */ const onSuccess = useCallback( - () => queryClient.invalidateQueries({ queryKey: [epubQueryKeys.getBookmarks, bookId] }), - [bookId], + () => queryClient.invalidateQueries({ queryKey: [sdk.epub.keys.getBookmarks, bookId] }), + [bookId, sdk.epub], ) /** @@ -52,11 +52,8 @@ export function useEpubBookmark() { }, [cfiRange, getCfiPreviewText]) const { mutate: createMutation, isLoading: isCreating } = useMutation( - [epubQueryKeys.createBookmark, bookId], - async (payload: CreateOrUpdateBookmark) => { - const { data } = await epubApi.createBookmark(bookId, payload) - return data - }, + [sdk.epub.keys.createBookmark, bookId], + async (payload: CreateOrUpdateBookmark) => sdk.epub.createBookmark(bookId, payload), { onSuccess, }, @@ -77,11 +74,8 @@ export function useEpubBookmark() { ) const { mutate: deleteMutation, isLoading: isDeleting } = useMutation( - [epubQueryKeys.deleteBookmark, bookId], - async (payload: DeleteBookmark) => { - const { data } = await epubApi.deleteBookmark(bookId, payload) - return data - }, + [sdk.epub.keys.deleteBookmark, bookId], + async (payload: DeleteBookmark) => sdk.epub.deleteBookmark(bookId, payload), { onSuccess, }, diff --git a/packages/browser/src/components/readers/imageBased/ImageBasedReader.tsx b/packages/browser/src/components/readers/imageBased/ImageBasedReader.tsx index a9dad748e..534204354 100644 --- a/packages/browser/src/components/readers/imageBased/ImageBasedReader.tsx +++ b/packages/browser/src/components/readers/imageBased/ImageBasedReader.tsx @@ -1,5 +1,4 @@ -import { getMediaPage, mediaQueryKeys } from '@stump/api' -import { queryClient, useUpdateMediaProgress } from '@stump/client' +import { queryClient, useSDK, useUpdateMediaProgress } from '@stump/client' import { Media } from '@stump/types' import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router' @@ -41,6 +40,7 @@ export default function ImageBasedReader({ isIncognito, initialPage, }: Props) { + const { sdk } = useSDK() const navigate = useNavigate() /** @@ -91,7 +91,7 @@ export default function ImageBasedReader({ /** * A callback to get the URL of a page. This is *not* 0-indexed, so the first page is 1. */ - const getPageUrl = (pageNumber: number) => getMediaPage(media.id, pageNumber) + const getPageUrl = (pageNumber: number) => sdk.media.bookPageURL(media.id, pageNumber) const lastPage = media.pages /** @@ -134,9 +134,9 @@ export default function ImageBasedReader({ setSettings({ showToolBar: false, }) - queryClient.invalidateQueries([mediaQueryKeys.getInProgressMedia], { exact: false }) + queryClient.invalidateQueries([sdk.media.keys.inProgress], { exact: false }) } - }, [setSettings]) + }, [setSettings, sdk.media]) const renderReader = () => { if (readingMode.startsWith('continuous')) { @@ -156,7 +156,7 @@ export default function ImageBasedReader({ getMediaPage(media.id, pageNumber)} + getPageUrl={(pageNumber) => sdk.media.bookPageURL(media.id, pageNumber)} onPageChange={handleChangePage} /> ) diff --git a/packages/browser/src/components/readers/imageBased/container/ReaderFooter.tsx b/packages/browser/src/components/readers/imageBased/container/ReaderFooter.tsx index 79ce287fb..96be6a7a6 100644 --- a/packages/browser/src/components/readers/imageBased/container/ReaderFooter.tsx +++ b/packages/browser/src/components/readers/imageBased/container/ReaderFooter.tsx @@ -1,4 +1,4 @@ -import { getMediaPage } from '@stump/api' +import { useSDK } from '@stump/client' import { AspectRatio, cn, usePrevious } from '@stump/components' import { motion } from 'framer-motion' import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -16,6 +16,7 @@ import { useBookPreferences } from '@/scenes/book/reader/useBookPreferences' import { useImageBaseReaderContext } from '../context' export default function ReaderFooter() { + const { sdk } = useSDK() const { book, currentPage, setCurrentPage } = useImageBaseReaderContext() const { settings: { showToolBar, preload }, @@ -74,7 +75,7 @@ export default function ReaderFooter() { ref={virtuosoRef} style={{ height: '100%' }} horizontalDirection - data={pageArray.map((page) => getMediaPage(book.id, page))} + data={pageArray.map((page) => sdk.media.bookPageURL(book.id, page))} components={{ Item, List, diff --git a/packages/browser/src/components/readers/pdf/NativePDFViewer.tsx b/packages/browser/src/components/readers/pdf/NativePDFViewer.tsx index 42c768f25..c1f58225a 100644 --- a/packages/browser/src/components/readers/pdf/NativePDFViewer.tsx +++ b/packages/browser/src/components/readers/pdf/NativePDFViewer.tsx @@ -1,4 +1,4 @@ -import { getMediaDownloadUrl } from '@stump/api' +import { useSDK } from '@stump/client' import { Link, Text } from '@stump/components' import { useEffect, useState } from 'react' @@ -18,6 +18,8 @@ type Props = { * such as reading progress, bookmarks, and annotations. */ export default function NativePDFViewer({ id }: Props) { + const { sdk } = useSDK() + const [pdfObjectUrl, setPdfObjectUrl] = useState() /** @@ -27,7 +29,7 @@ export default function NativePDFViewer({ id }: Props) { useEffect( () => { async function fetchPdf() { - const response = await fetch(getMediaDownloadUrl(id), { + const response = await fetch(sdk.media.downloadURL(id), { credentials: 'include', }) const blob = await response.blob() @@ -54,7 +56,7 @@ export default function NativePDFViewer({ id }: Props) { return ( - PDF failed to load. Click here attempt + PDF failed to load. Click here attempt downloading it directly. diff --git a/packages/browser/src/components/savedServer/configuredServers/ConfiguredServersList.tsx b/packages/browser/src/components/savedServer/configuredServers/ConfiguredServersList.tsx index 89fc72d96..2758c3baa 100644 --- a/packages/browser/src/components/savedServer/configuredServers/ConfiguredServersList.tsx +++ b/packages/browser/src/components/savedServer/configuredServers/ConfiguredServersList.tsx @@ -1,7 +1,7 @@ -import { checkUrl, formatServiceURL } from '@stump/api' import { QueryClientContext, useLogout } from '@stump/client' import { Card, Heading, Text } from '@stump/components' import { useLocaleContext } from '@stump/i18n' +import { checkUrl, formatApiURL } from '@stump/sdk' import { SavedServer } from '@stump/types' import { useQueries } from '@tanstack/react-query' import React, { useCallback, useMemo, useState } from 'react' @@ -63,7 +63,7 @@ export default function ConfiguredServersSection() { queryFn: async () => ({ name: server.name, - status: await checkUrl(formatServiceURL(server.uri), 'v1'), + status: await checkUrl(formatApiURL(server.uri, 'v1')), }) as PingResult, queryKey: ['ping', server.uri, server.name], refetchInterval: (result?: PingResult) => { diff --git a/packages/browser/src/components/savedServer/configuredServers/__tests__/ConfiguredServersList.tests.tsx b/packages/browser/src/components/savedServer/configuredServers/__tests__/ConfiguredServersList.tests.tsx index 11bea8e70..2003ecaea 100644 --- a/packages/browser/src/components/savedServer/configuredServers/__tests__/ConfiguredServersList.tests.tsx +++ b/packages/browser/src/components/savedServer/configuredServers/__tests__/ConfiguredServersList.tests.tsx @@ -1,5 +1,5 @@ -import { checkUrl } from '@stump/api' -import { queryClient, QueryClientContext, StumpClientContext } from '@stump/client' +import { queryClient, QueryClientContext, SDKProvider, StumpClientContext } from '@stump/client' +import { checkUrl } from '@stump/sdk' import { fireEvent, render, screen } from '@testing-library/react' import { useLocation, useNavigate } from 'react-router' @@ -59,17 +59,19 @@ jest.mock('@stump/client', () => ({ })) const useClientContextRet = {} as any -jest.mock('@stump/api', () => ({ - ...jest.requireActual('@stump/api'), +jest.mock('@stump/sdk', () => ({ + ...jest.requireActual('@stump/sdk'), checkUrl: jest.fn(), })) const Subject = () => ( - - - - - + + + + + + + ) describe('ConfiguredServersList', () => { diff --git a/packages/browser/src/components/series/SeriesCard.tsx b/packages/browser/src/components/series/SeriesCard.tsx index e55726cfa..e1f8e9840 100644 --- a/packages/browser/src/components/series/SeriesCard.tsx +++ b/packages/browser/src/components/series/SeriesCard.tsx @@ -1,5 +1,4 @@ -import { getSeriesThumbnail } from '@stump/api' -import { prefetchSeries } from '@stump/client' +import { usePrefetchSeries, useSDK } from '@stump/client' import { EntityCard, Text } from '@stump/components' import { FileStatus, Series } from '@stump/types' @@ -13,14 +12,15 @@ export type SeriesCardProps = { } export default function SeriesCard({ series, fullWidth, variant = 'default' }: SeriesCardProps) { + const { sdk } = useSDK() const isCoverOnly = variant === 'cover' const bookCount = Number(series.media ? series.media.length : series.media_count ?? 0) const booksUnread = series.unread_media_count - const handleHover = () => { - prefetchSeries(series.id) - } + const { prefetch } = usePrefetchSeries({ id: series.id }) + + const handleHover = () => prefetch() function getProgress() { if (isCoverOnly || booksUnread == null) { @@ -73,7 +73,7 @@ export default function SeriesCard({ series, fullWidth, variant = 'default' }: S key={series.id} title={series.name} href={paths.seriesOverview(series.id)} - imageUrl={getSeriesThumbnail(series.id)} + imageUrl={sdk.series.thumbnailURL(series.id)} progress={getProgress()} subtitle={getSubtitle(series)} onMouseEnter={handleHover} diff --git a/packages/browser/src/components/series/table/CoverImageCell.tsx b/packages/browser/src/components/series/table/CoverImageCell.tsx index a16bb6409..bfdbec60e 100644 --- a/packages/browser/src/components/series/table/CoverImageCell.tsx +++ b/packages/browser/src/components/series/table/CoverImageCell.tsx @@ -1,4 +1,4 @@ -import { seriesApi } from '@stump/api' +import { useSDK } from '@stump/client' import { Book } from 'lucide-react' import React, { useState } from 'react' @@ -14,12 +14,13 @@ type Props = { } export default function CoverImageCell({ id, title }: Props) { + const { sdk } = useSDK() const [showFallback, setShowFallback] = useState(false) const loadImage = () => { const image = new Image() return new Promise((resolve, reject) => { - image.src = seriesApi.getSeriesThumbnail(id) + image.src = sdk.series.thumbnailURL(id) image.onload = () => resolve(image) image.onerror = (e) => { console.error('Image failed to load:', e) @@ -53,7 +54,7 @@ export default function CoverImageCell({ id, title }: Props) { setShowFallback(true)} /> ) diff --git a/packages/browser/src/scenes/auth/LoginOrClaimScene.tsx b/packages/browser/src/scenes/auth/LoginOrClaimScene.tsx index cad436abe..0edbd3a8b 100644 --- a/packages/browser/src/scenes/auth/LoginOrClaimScene.tsx +++ b/packages/browser/src/scenes/auth/LoginOrClaimScene.tsx @@ -1,5 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { isAxiosError } from '@stump/api' +import { isAxiosError } from '@stump/sdk' import { queryClient, useLoginOrRegister } from '@stump/client' import { Alert, Button, cx, Form, Heading, Input } from '@stump/components' import { useLocaleContext } from '@stump/i18n' diff --git a/packages/browser/src/scenes/book/BookCompletionToggleButton.tsx b/packages/browser/src/scenes/book/BookCompletionToggleButton.tsx index c294c0ffc..a2b8d3f4f 100644 --- a/packages/browser/src/scenes/book/BookCompletionToggleButton.tsx +++ b/packages/browser/src/scenes/book/BookCompletionToggleButton.tsx @@ -1,5 +1,4 @@ -import { mediaApi, mediaQueryKeys } from '@stump/api' -import { invalidateQueries, useMutation } from '@stump/client' +import { invalidateQueries, useMutation, useSDK } from '@stump/client' import { Button } from '@stump/components' import { Media, PutMediaCompletionStatus } from '@stump/types' import React, { useCallback, useMemo } from 'react' @@ -14,14 +13,16 @@ type Props = { } export default function BookCompletionToggleButton({ book }: Props) { + const { sdk } = useSDK() + const { mutateAsync: completeBook } = useMutation( - [mediaQueryKeys.putMediaCompletion, book.id], - (payload: PutMediaCompletionStatus) => mediaApi.putMediaCompletion(book.id, payload), + [sdk.media.keys.complete, book.id], + (payload: PutMediaCompletionStatus) => sdk.media.complete(book.id, payload), ) const { mutateAsync: deleteCurrentSession } = useMutation( - [mediaQueryKeys.deleteActiveReadingSession, book.id], - () => mediaApi.deleteActiveReadingSession(book.id), + [sdk.media.keys.deleteActiveReadingSession, book.id], + () => sdk.media.deleteActiveReadingSession(book.id), ) const isCompleted = useMemo(() => isReadAgainPrompt(book), [book]) @@ -34,7 +35,7 @@ export default function BookCompletionToggleButton({ book }: Props) { if (hasProgress && isCompleted) { try { await deleteCurrentSession() - invalidateQueries({ keys: [mediaQueryKeys.getMediaById] }) + invalidateQueries({ keys: [sdk.media.keys.getByID] }) } catch (error) { console.error(error) toast.error('Failed to clear progress') @@ -47,13 +48,13 @@ export default function BookCompletionToggleButton({ book }: Props) { is_complete: willBeComplete, page, }) - invalidateQueries({ keys: [mediaQueryKeys.getMediaById] }) + invalidateQueries({ keys: [sdk.media.keys.getByID] }) } catch (error) { console.error(error) toast.error('Failed to update book completion status') } } - }, [book, completeBook, isCompleted, isEpub, hasProgress, deleteCurrentSession]) + }, [book, completeBook, isCompleted, isEpub, hasProgress, deleteCurrentSession, sdk.media]) // There really isn't anything to do here if the book is completed and has no progress. Eventually, // we will support clearing the completion history. diff --git a/packages/browser/src/scenes/book/DownloadMediaButton.tsx b/packages/browser/src/scenes/book/DownloadMediaButton.tsx index 357a70552..234d2630b 100644 --- a/packages/browser/src/scenes/book/DownloadMediaButton.tsx +++ b/packages/browser/src/scenes/book/DownloadMediaButton.tsx @@ -1,4 +1,4 @@ -import { getMediaDownloadUrl } from '@stump/api' +import { useSDK } from '@stump/client' import { Button, IconButton } from '@stump/components' import { Media } from '@stump/types' import { DownloadCloud } from 'lucide-react' @@ -10,6 +10,7 @@ type Props = { media: Media } export default function DownloadMediaButton({ media }: Props) { + const { sdk } = useSDK() const isAtLeastMedium = useMediaMatch('(min-width: 768px)') const bookTitle = formatBookName(media) @@ -31,7 +32,7 @@ export default function DownloadMediaButton({ media }: Props) { } return ( - + {renderButton()} ) diff --git a/packages/browser/src/scenes/book/reader/BookReaderScene.tsx b/packages/browser/src/scenes/book/reader/BookReaderScene.tsx index 889929d94..c68c6be33 100644 --- a/packages/browser/src/scenes/book/reader/BookReaderScene.tsx +++ b/packages/browser/src/scenes/book/reader/BookReaderScene.tsx @@ -1,4 +1,3 @@ -import { mediaQueryKeys } from '@stump/api' import { invalidateQueries, useMediaByIdQuery, useUpdateMediaProgress } from '@stump/client' import { Media } from '@stump/types' import { Suspense, useEffect } from 'react' diff --git a/packages/browser/src/scenes/book/settings/BookManagementScene.tsx b/packages/browser/src/scenes/book/settings/BookManagementScene.tsx index 6d00412af..72b86360d 100644 --- a/packages/browser/src/scenes/book/settings/BookManagementScene.tsx +++ b/packages/browser/src/scenes/book/settings/BookManagementScene.tsx @@ -1,5 +1,4 @@ -import { mediaApi } from '@stump/api' -import { useMediaByIdQuery } from '@stump/client' +import { useMediaByIdQuery, useSDK } from '@stump/client' import { Alert, Breadcrumbs, Button, Heading, Text } from '@stump/components' import { Construction } from 'lucide-react' import React, { useMemo } from 'react' @@ -12,6 +11,7 @@ import { formatBookName } from '@/utils/format' import BookThumbnailSelector from './BookThumbnailSelector' export default function BookManagementScene() { + const { sdk } = useSDK() const { id } = useParams() if (!id) { @@ -56,7 +56,7 @@ export default function BookManagementScene() { function handleAnalyze() { if (id != undefined) { - mediaApi.startMediaAnalysis(id) + sdk.media.analyze(id) } } diff --git a/packages/browser/src/scenes/book/settings/BookPageGrid.tsx b/packages/browser/src/scenes/book/settings/BookPageGrid.tsx index 65f791672..f1f90a4f1 100644 --- a/packages/browser/src/scenes/book/settings/BookPageGrid.tsx +++ b/packages/browser/src/scenes/book/settings/BookPageGrid.tsx @@ -1,4 +1,4 @@ -import { getMediaPage } from '@stump/api' +import { useSDK } from '@stump/client' import { cx } from '@stump/components' import { useVirtualizer } from '@tanstack/react-virtual' import React, { useCallback, useEffect } from 'react' @@ -13,6 +13,7 @@ type Props = { } // TODO: Create generlized VirtualizedGrid component and trim the reused logic export default function BookPageGrid({ bookId, pages, selectedPage, onSelectPage }: Props) { + const { sdk } = useSDK() const parentRef = React.useRef(null) const isAtLeastSmall = useMediaMatch('(min-width: 640px)') @@ -78,7 +79,7 @@ export default function BookPageGrid({ bookId, pages, selectedPage, onSelectPage {columnVirtualizer.getVirtualItems().map((virtualColumn) => { const virtualPage = virtualRow.index * 4 + virtualColumn.index + 1 - const imageUrl = getMediaPage(bookId, virtualPage) + const imageUrl = sdk.media.bookPageURL(bookId, virtualPage) return (
() @@ -31,7 +32,7 @@ export default function BookThumbnailSelector({ book }: Props) { const handleUploadImage = async (file: File) => { try { - await mediaApi.uploadMediaThumbnail(book.id, file) + await sdk.media.uploadThumbnail(book.id, file) setIsOpen(false) } catch (error) { console.error(error) @@ -43,7 +44,7 @@ export default function BookThumbnailSelector({ book }: Props) { if (!page) return try { - await mediaApi.patchMediaThumbnail(book.id, { page }) + await sdk.media.patchThumbnail(book.id, { page }) // TODO: The browser is caching the image, so we need to force remove it and ensure // the new one is loaded instead @@ -58,7 +59,7 @@ export default function BookThumbnailSelector({ book }: Props) { return (
!imageFailed} diff --git a/packages/browser/src/scenes/bookClub/home/BookClubNavigation.tsx b/packages/browser/src/scenes/bookClub/home/BookClubNavigation.tsx index 1ac3c7726..d96a7f96b 100644 --- a/packages/browser/src/scenes/bookClub/home/BookClubNavigation.tsx +++ b/packages/browser/src/scenes/bookClub/home/BookClubNavigation.tsx @@ -1,4 +1,4 @@ -import { prefetchBookClubChat } from '@stump/client' +import { usePrefetchClubChat } from '@stump/client' import { cn, cx, Link } from '@stump/components' import React, { useMemo } from 'react' import { useLocation } from 'react-router' @@ -17,6 +17,7 @@ export default function BookClubNavigation() { bookClub: { id }, viewerIsMember, } = useBookClubContext() + const { prefetch } = usePrefetchClubChat({ id }) const tabs = useMemo(() => { const base = [ @@ -36,7 +37,7 @@ export default function BookClubNavigation() { { isActive: location.pathname.match(/\/book-clubs\/[^/]+\/chat-board(\/.*)?$/), label: 'Chat Board', - onHover: () => prefetchBookClubChat(id), + onHover: () => prefetch(), to: 'chat-board', }, { @@ -50,7 +51,7 @@ export default function BookClubNavigation() { to: 'settings', }, ] - }, [location, viewerIsMember, id]) + }, [location, viewerIsMember, prefetch]) const preferTopBar = primary_navigation_mode === 'TOPBAR' diff --git a/packages/browser/src/scenes/bookClub/home/tabs/chat-board/ChatMessage.tsx b/packages/browser/src/scenes/bookClub/home/tabs/chat-board/ChatMessage.tsx index 7d5b10b01..f84f2e0d5 100644 --- a/packages/browser/src/scenes/bookClub/home/tabs/chat-board/ChatMessage.tsx +++ b/packages/browser/src/scenes/bookClub/home/tabs/chat-board/ChatMessage.tsx @@ -1,4 +1,4 @@ -import { prefetchThread } from '@stump/client' +import { usePrefetchClubThread } from '@stump/client' import { Avatar, Button, cx, IconButton, Text, ToolTip } from '@stump/components' import { BookClubChatMessage } from '@stump/types' import dayjs from 'dayjs' @@ -18,6 +18,7 @@ type Props = { isArchived?: boolean } export default function ChatMessage({ message, chatId, isArchived }: Props) { + const { prefetch } = usePrefetchClubThread() const { bookClub, viewerMember } = useBookClubContext() const displayName = message.member?.display_name ?? message.member?.user?.username ?? 'Unknown' @@ -59,7 +60,13 @@ export default function ChatMessage({ message, chatId, isArchived }: Props) {