diff --git a/apps/frontend/app/lib/state/media.ts b/apps/frontend/app/lib/state/media.ts index 11d3e946b9..da725a27d2 100644 --- a/apps/frontend/app/lib/state/media.ts +++ b/apps/frontend/app/lib/state/media.ts @@ -20,7 +20,7 @@ export type UpdateProgressData = { showEpisodeNumber?: number | null; podcastEpisodeNumber?: number | null; animeEpisodeNumber?: number | null; - mangaChapterNumber?: number | null; + mangaChapterNumber?: string | null; mangaVolumeNumber?: number | null; }; diff --git a/apps/frontend/app/lib/utilities.server.ts b/apps/frontend/app/lib/utilities.server.ts index 3a6d41247b..b3ef4f2f41 100644 --- a/apps/frontend/app/lib/utilities.server.ts +++ b/apps/frontend/app/lib/utilities.server.ts @@ -156,6 +156,11 @@ const emptyNumberString = z .transform((v) => (!v ? undefined : Number.parseInt(v))) .nullable(); +const emptyDecimalString = z + .any() + .transform((v) => (!v ? undefined : Number.parseFloat(v).toString())) + .nullable(); + export const MetadataIdSchema = z.object({ metadataId: z.string() }); export const MetadataSpecificsSchema = z.object({ @@ -163,7 +168,7 @@ export const MetadataSpecificsSchema = z.object({ showEpisodeNumber: emptyNumberString, podcastEpisodeNumber: emptyNumberString, animeEpisodeNumber: emptyNumberString, - mangaChapterNumber: emptyNumberString, + mangaChapterNumber: emptyDecimalString, mangaVolumeNumber: emptyNumberString, }); diff --git a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx index 92341ca4ea..780a16ad92 100644 --- a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx +++ b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx @@ -57,7 +57,9 @@ import { changeCase, formatDateToNaiveDate, humanizeDuration, + isInteger, isNumber, + isString, processSubmission, } from "@ryot/ts-utils"; import { @@ -1367,13 +1369,24 @@ const HistoryItem = (props: { history: History; index: number }) => { ) ? `EP-${props.history.animeExtraInformation.episode}` : null; - const displayMangaExtraInformation = isNumber( - props.history.mangaExtraInformation?.chapter, - ) - ? `CH-${props.history.mangaExtraInformation.chapter}` - : isNumber(props.history.mangaExtraInformation?.volume) - ? `VOL-${props.history.mangaExtraInformation.volume}` - : null; + const displayMangaExtraInformation = (() => { + const { chapter, volume } = props.history.mangaExtraInformation || {}; + + if (chapter != null) { + const chapterNum = isString(chapter) + ? Number.parseFloat(chapter) + : chapter; + + if (!Number.isNaN(chapterNum)) { + const isWholeNumber = isInteger(chapterNum); + return `CH-${isWholeNumber ? Math.floor(chapterNum) : chapterNum}`; + } + } + + if (isNumber(volume)) return `VOL-${volume}`; + + return null; + })(); const watchedOnInformation = props.history.providerWatchedOn; const filteredDisplayInformation = [ diff --git a/apps/frontend/app/routes/_dashboard.tsx b/apps/frontend/app/routes/_dashboard.tsx index 9cf5b89f52..0e94cee785 100644 --- a/apps/frontend/app/routes/_dashboard.tsx +++ b/apps/frontend/app/routes/_dashboard.tsx @@ -897,12 +897,12 @@ const MetadataInProgressUpdateForm = ({ { - const value = (Number(v) / (total || 1)) * 100; + const value = (Number(v) / (Number(total) || 1)) * 100; setValue(value); }} - max={total} + max={Number(total)} min={0} step={1} hideControls @@ -999,7 +999,8 @@ const NewProgressUpdateForm = ({ onChange={(e) => { setMetadataToUpdate( produce(metadataToUpdate, (draft) => { - draft.mangaChapterNumber = Number(e); + draft.mangaChapterNumber = + e === "" ? undefined : Number(e).toString(); }), ); }} @@ -1014,7 +1015,8 @@ const NewProgressUpdateForm = ({ onChange={(e) => { setMetadataToUpdate( produce(metadataToUpdate, (draft) => { - draft.mangaVolumeNumber = Number(e); + draft.mangaVolumeNumber = + e === "" ? undefined : Number(e); }), ); }} diff --git a/apps/frontend/app/routes/actions.tsx b/apps/frontend/app/routes/actions.tsx index 03d36bec1e..4938fd14a5 100644 --- a/apps/frontend/app/routes/actions.tsx +++ b/apps/frontend/app/routes/actions.tsx @@ -270,10 +270,15 @@ export const action = unstable_defineAction(async ({ request }) => { } } if (submission.metadataLot === MediaLot.Manga) { + const isValidNumber = (value: unknown): boolean => { + const num = Number(value); + return !Number.isNaN(num) && Number.isFinite(num); + }; + if ( - (isNumber(submission.mangaChapterNumber) && + (isValidNumber(submission.mangaChapterNumber) && isNumber(submission.mangaVolumeNumber)) || - (!isNumber(submission.mangaChapterNumber) && + (!isValidNumber(submission.mangaChapterNumber) && !isNumber(submission.mangaVolumeNumber)) ) throw Response.json({ @@ -293,14 +298,26 @@ export const action = unstable_defineAction(async ({ request }) => { } } if (submission.mangaChapterNumber) { - const lastSeenChapter = - latestHistoryItem?.mangaExtraInformation?.chapter || 0; - for ( - let i = lastSeenChapter + 1; - i < submission.mangaChapterNumber; - i++ - ) { - updates.push({ ...variables, mangaChapterNumber: i }); + const targetChapter = Number(submission.mangaChapterNumber); + const markedChapters = new Set(); + + for (const historyItem of userMetadataDetails?.history ?? []) { + const chapter = Number( + historyItem?.mangaExtraInformation?.chapter, + ); + + if (!Number.isNaN(chapter) && chapter < targetChapter) { + markedChapters.add(chapter); + } + } + + for (let i = 1; i < targetChapter; i++) { + if (!markedChapters.has(i)) { + updates.push({ + ...variables, + mangaChapterNumber: i.toString(), + }); + } } } } diff --git a/crates/models/media/src/lib.rs b/crates/models/media/src/lib.rs index daa2b3b05c..2bb0df0637 100644 --- a/crates/models/media/src/lib.rs +++ b/crates/models/media/src/lib.rs @@ -365,7 +365,7 @@ pub struct AnimeSpecifics { )] #[graphql(input_name = "MangaSpecificsInput")] pub struct MangaSpecifics { - pub chapters: Option, + pub chapters: Option, pub volumes: Option, pub url: Option, } @@ -401,7 +401,7 @@ pub struct PostReviewInput { pub show_episode_number: Option, pub podcast_episode_number: Option, pub anime_episode_number: Option, - pub manga_chapter_number: Option, + pub manga_chapter_number: Option, pub manga_volume_number: Option, } @@ -414,7 +414,7 @@ pub struct ProgressUpdateInput { pub show_episode_number: Option, pub podcast_episode_number: Option, pub anime_episode_number: Option, - pub manga_chapter_number: Option, + pub manga_chapter_number: Option, pub manga_volume_number: Option, pub change_state: Option, pub provider_watched_on: Option, @@ -562,7 +562,7 @@ pub struct ImportOrExportMediaItemSeen { /// If for an anime, the episode which was seen. pub anime_episode_number: Option, /// If for a manga, the chapter which was seen. - pub manga_chapter_number: Option, + pub manga_chapter_number: Option, /// If for a manga, the volume which was seen. pub manga_volume_number: Option, /// The provider this item was watched on. @@ -602,7 +602,7 @@ pub struct ImportOrExportItemRating { /// If for an anime, the episode for which this review was for. pub anime_episode_number: Option, /// If for a manga, the chapter for which this review was for. - pub manga_chapter_number: Option, + pub manga_chapter_number: Option, /// The comments attached to this review. pub comments: Option>, } @@ -811,7 +811,7 @@ pub struct SeenAnimeExtraInformation { Debug, PartialEq, Eq, Serialize, Deserialize, Clone, SimpleObject, FromJsonQueryResult, )] pub struct SeenMangaExtraInformation { - pub chapter: Option, + pub chapter: Option, pub volume: Option, } @@ -1054,7 +1054,7 @@ pub struct IntegrationMediaSeen { pub show_episode_number: Option, pub podcast_episode_number: Option, pub anime_episode_number: Option, - pub manga_chapter_number: Option, + pub manga_chapter_number: Option, pub manga_volume_number: Option, pub provider_watched_on: Option, } @@ -1463,7 +1463,7 @@ pub struct UserMetadataDetailsShowSeasonProgress { pub struct UserMediaNextEntry { pub season: Option, pub volume: Option, - pub chapter: Option, + pub chapter: Option, pub episode: Option, } diff --git a/crates/providers/src/mal.rs b/crates/providers/src/mal.rs index 9177465f69..7fe2114627 100644 --- a/crates/providers/src/mal.rs +++ b/crates/providers/src/mal.rs @@ -222,7 +222,7 @@ async fn details(client: &Client, media_type: &str, id: &str) -> Result ImportOrExportMediaItem { .map(|i| { let (anime_episode, manga_chapter) = match lot { MediaLot::Anime => (Some(i), None), - MediaLot::Manga => (None, Some(i)), + MediaLot::Manga => (None, Some(Decimal::new(i as i64, 0))), _ => unreachable!(), }; ImportOrExportMediaItemSeen { diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs index cf66a087cd..565d63553a 100644 --- a/crates/services/miscellaneous/src/lib.rs +++ b/crates/services/miscellaneous/src/lib.rs @@ -90,6 +90,7 @@ use providers::{ tmdb::{NonMediaTmdbService, TmdbMovieService, TmdbService, TmdbShowService}, vndb::VndbService, }; +use rust_decimal::prelude::{One, ToPrimitive}; use rust_decimal::Decimal; use rust_decimal_macros::dec; use sea_orm::{ @@ -130,7 +131,7 @@ struct ProgressUpdateCache { show_episode_number: Option, podcast_episode_number: Option, anime_episode_number: Option, - manga_chapter_number: Option, + manga_chapter_number: Option, manga_volume_number: Option, } @@ -545,7 +546,7 @@ impl MiscellaneousService { h.manga_extra_information.as_ref().and_then(|hist| { hist.chapter .map(|e| UserMediaNextEntry { - chapter: Some(e + 1), + chapter: Some(e.floor() + dec!(1)), ..Default::default() }) .or(hist.volume.map(|e| UserMediaNextEntry { @@ -3394,7 +3395,11 @@ impl MiscellaneousService { } else if let Some(e) = metadata.model.anime_specifics.and_then(|a| a.episodes) { (1..e + 1).map(|e| format!("{}", e)).collect_vec() } else if let Some(c) = metadata.model.manga_specifics.and_then(|m| m.chapters) { - (1..c + 1).map(|e| format!("{}", e)).collect_vec() + let one = Decimal::one(); + (0..c.to_u32().unwrap_or(0)) + .map(|i| Decimal::from(i) + one) + .map(|d| d.to_string()) + .collect_vec() } else { vec![] }; diff --git a/crates/utils/database/src/lib.rs b/crates/utils/database/src/lib.rs index ee6a9fa33b..01d6a42536 100644 --- a/crates/utils/database/src/lib.rs +++ b/crates/utils/database/src/lib.rs @@ -38,6 +38,7 @@ use media_models::{ }; use migrations::AliasedCollectionToEntity; use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; use rust_decimal_macros::dec; use sea_orm::{ prelude::{Date, DateTimeUtc, Expr}, @@ -489,7 +490,7 @@ pub async fn calculate_user_activities_and_summary( shows: HashSet, show_seasons: HashSet, anime_episodes: HashSet, - manga_chapters: HashSet, + manga_chapters: HashSet, manga_volumes: HashSet, } type Tracker = HashMap; diff --git a/docs/includes/export-schema.ts b/docs/includes/export-schema.ts index f02ffeff4b..e7bf44d01e 100644 --- a/docs/includes/export-schema.ts +++ b/docs/includes/export-schema.ts @@ -42,7 +42,7 @@ export interface ImportOrExportItemRating { /** The comments attached to this review. */ comments: ImportOrExportItemReviewComment[] | null; /** If for a manga, the chapter for which this review was for. */ - manga_chapter_number: number | null; + manga_chapter_number: string | null; /** If for a podcast, the episode for which this review was for. */ podcast_episode_number: number | null; /** The score of the review. */ @@ -115,7 +115,7 @@ export interface ImportOrExportMediaItemSeen { /** The timestamp when finished watching. */ ended_on: string | null; /** If for a manga, the chapter which was seen. */ - manga_chapter_number: number | null; + manga_chapter_number: string | null; /** If for a manga, the volume which was seen. */ manga_volume_number: number | null; /** If for a podcast, the episode which was seen. */ diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts index bacaf00c8c..42e63d5f81 100644 --- a/libs/generated/src/graphql/backend/graphql.ts +++ b/libs/generated/src/graphql/backend/graphql.ts @@ -824,13 +824,13 @@ export type LoginResponse = { export type LoginResult = LoginError | LoginResponse; export type MangaSpecifics = { - chapters?: Maybe; + chapters?: Maybe; url?: Maybe; volumes?: Maybe; }; export type MangaSpecificsInput = { - chapters?: InputMaybe; + chapters?: InputMaybe; url?: InputMaybe; volumes?: InputMaybe; }; @@ -1473,7 +1473,7 @@ export type PostReviewInput = { entityId: Scalars['String']['input']; entityLot: EntityLot; isSpoiler?: InputMaybe; - mangaChapterNumber?: InputMaybe; + mangaChapterNumber?: InputMaybe; mangaVolumeNumber?: InputMaybe; podcastEpisodeNumber?: InputMaybe; rating?: InputMaybe; @@ -1512,7 +1512,7 @@ export type ProgressUpdateInput = { animeEpisodeNumber?: InputMaybe; changeState?: InputMaybe; date?: InputMaybe; - mangaChapterNumber?: InputMaybe; + mangaChapterNumber?: InputMaybe; mangaVolumeNumber?: InputMaybe; metadataId: Scalars['String']['input']; podcastEpisodeNumber?: InputMaybe; @@ -1828,7 +1828,7 @@ export type SeenAnimeExtraInformation = { }; export type SeenMangaExtraInformation = { - chapter?: Maybe; + chapter?: Maybe; volume?: Maybe; }; @@ -2217,7 +2217,7 @@ export type UserMediaFeaturesEnabledPreferences = { }; export type UserMediaNextEntry = { - chapter?: Maybe; + chapter?: Maybe; episode?: Maybe; season?: Maybe; volume?: Maybe; @@ -2779,7 +2779,7 @@ export type CollectionContentsQueryVariables = Exact<{ }>; -export type CollectionContentsQuery = { collectionContents: { user: { id: string, name: string }, reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: number | null, volume?: number | null } | null }>, results: { details: { total: number, nextPage?: number | null }, items: Array<{ entityId: string, entityLot: EntityLot }> }, details: { name: string, description?: string | null, createdOn: string } } }; +export type CollectionContentsQuery = { collectionContents: { user: { id: string, name: string }, reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null }>, results: { details: { total: number, nextPage?: number | null }, items: Array<{ entityId: string, entityLot: EntityLot }> }, details: { name: string, description?: string | null, createdOn: string } } }; export type CoreDetailsQueryVariables = Exact<{ [key: string]: never; }>; @@ -2834,7 +2834,7 @@ export type MetadataDetailsQueryVariables = Exact<{ }>; -export type MetadataDetailsQuery = { metadataDetails: { id: string, lot: MediaLot, title: string, source: MediaSource, isNsfw?: boolean | null, isPartial?: boolean | null, sourceUrl?: string | null, identifier: string, description?: string | null, suggestions: Array, publishYear?: number | null, publishDate?: string | null, providerRating?: string | null, productionStatus?: string | null, originalLanguage?: string | null, genres: Array<{ id: string, name: string }>, group?: { id: string, name: string, part: number } | null, assets: { images: Array, videos: Array<{ videoId: string, source: MetadataVideoSource }> }, creators: Array<{ name: string, items: Array<{ id?: string | null, name: string, image?: string | null, character?: string | null }> }>, watchProviders: Array<{ name: string, image?: string | null, languages: Array }>, animeSpecifics?: { episodes?: number | null } | null, audioBookSpecifics?: { runtime?: number | null } | null, bookSpecifics?: { pages?: number | null } | null, movieSpecifics?: { runtime?: number | null } | null, mangaSpecifics?: { volumes?: number | null, chapters?: number | null } | null, podcastSpecifics?: { totalEpisodes: number, episodes: Array<{ id: string, title: string, overview?: string | null, thumbnail?: string | null, number: number, runtime?: number | null, publishDate: string }> } | null, showSpecifics?: { totalSeasons?: number | null, totalEpisodes?: number | null, runtime?: number | null, seasons: Array<{ id: number, seasonNumber: number, name: string, overview?: string | null, backdropImages: Array, posterImages: Array, episodes: Array<{ id: number, name: string, posterImages: Array, episodeNumber: number, publishDate?: string | null, overview?: string | null, runtime?: number | null }> }> } | null, visualNovelSpecifics?: { length?: number | null } | null, videoGameSpecifics?: { platforms: Array } | null } }; +export type MetadataDetailsQuery = { metadataDetails: { id: string, lot: MediaLot, title: string, source: MediaSource, isNsfw?: boolean | null, isPartial?: boolean | null, sourceUrl?: string | null, identifier: string, description?: string | null, suggestions: Array, publishYear?: number | null, publishDate?: string | null, providerRating?: string | null, productionStatus?: string | null, originalLanguage?: string | null, genres: Array<{ id: string, name: string }>, group?: { id: string, name: string, part: number } | null, assets: { images: Array, videos: Array<{ videoId: string, source: MetadataVideoSource }> }, creators: Array<{ name: string, items: Array<{ id?: string | null, name: string, image?: string | null, character?: string | null }> }>, watchProviders: Array<{ name: string, image?: string | null, languages: Array }>, animeSpecifics?: { episodes?: number | null } | null, audioBookSpecifics?: { runtime?: number | null } | null, bookSpecifics?: { pages?: number | null } | null, movieSpecifics?: { runtime?: number | null } | null, mangaSpecifics?: { volumes?: number | null, chapters?: string | null } | null, podcastSpecifics?: { totalEpisodes: number, episodes: Array<{ id: string, title: string, overview?: string | null, thumbnail?: string | null, number: number, runtime?: number | null, publishDate: string }> } | null, showSpecifics?: { totalSeasons?: number | null, totalEpisodes?: number | null, runtime?: number | null, seasons: Array<{ id: number, seasonNumber: number, name: string, overview?: string | null, backdropImages: Array, posterImages: Array, episodes: Array<{ id: number, name: string, posterImages: Array, episodeNumber: number, publishDate?: string | null, overview?: string | null, runtime?: number | null }> }> } | null, visualNovelSpecifics?: { length?: number | null } | null, videoGameSpecifics?: { platforms: Array } | null } }; export type MetadataGroupDetailsQueryVariables = Exact<{ metadataGroupId: Scalars['String']['input']; @@ -2888,7 +2888,7 @@ export type UserExerciseDetailsQueryVariables = Exact<{ }>; -export type UserExerciseDetailsQuery = { userExerciseDetails: { collections: Array<{ id: string, name: string, userId: string }>, reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: number | null, volume?: number | null } | null }>, history?: Array<{ idx: number, workoutId: string, workoutEndOn: string, bestSet?: { lot: SetLot, personalBests?: Array | null, statistic: { duration?: string | null, distance?: string | null, reps?: string | null, weight?: string | null, oneRm?: string | null, pace?: string | null, volume?: string | null } } | null }> | null, details?: { exerciseId?: string | null, createdOn: string, lastUpdatedOn: string, exerciseNumTimesInteracted?: number | null, exerciseExtraInformation?: { lifetimeStats: { weight: string, reps: string, distance: string, duration: string, personalBestsAchieved: number }, personalBests: Array<{ lot: WorkoutSetPersonalBest, sets: Array<{ workoutId: string, exerciseIdx: number, setIdx: number }> }> } | null } | null } }; +export type UserExerciseDetailsQuery = { userExerciseDetails: { collections: Array<{ id: string, name: string, userId: string }>, reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null }>, history?: Array<{ idx: number, workoutId: string, workoutEndOn: string, bestSet?: { lot: SetLot, personalBests?: Array | null, statistic: { duration?: string | null, distance?: string | null, reps?: string | null, weight?: string | null, oneRm?: string | null, pace?: string | null, volume?: string | null } } | null }> | null, details?: { exerciseId?: string | null, createdOn: string, lastUpdatedOn: string, exerciseNumTimesInteracted?: number | null, exerciseExtraInformation?: { lifetimeStats: { weight: string, reps: string, distance: string, duration: string, personalBestsAchieved: number }, personalBests: Array<{ lot: WorkoutSetPersonalBest, sets: Array<{ workoutId: string, exerciseIdx: number, setIdx: number }> }> } | null } | null } }; export type UserMeasurementsListQueryVariables = Exact<{ input: UserMeasurementsListInput; @@ -2902,21 +2902,21 @@ export type UserMetadataDetailsQueryVariables = Exact<{ }>; -export type UserMetadataDetailsQuery = { userMetadataDetails: { mediaReason?: Array | null, hasInteracted: boolean, averageRating?: string | null, seenByAllCount: number, seenByUserCount: number, collections: Array<{ id: string, name: string, userId: string }>, inProgress?: { id: string, progress: string, providerWatchedOn?: string | null, state: SeenState, startedOn?: string | null, finishedOn?: string | null, lastUpdatedOn: string, numTimesUpdated: number, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: number | null, volume?: number | null } | null } | null, history: Array<{ id: string, progress: string, providerWatchedOn?: string | null, state: SeenState, startedOn?: string | null, finishedOn?: string | null, lastUpdatedOn: string, numTimesUpdated: number, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: number | null, volume?: number | null } | null }>, reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: number | null, volume?: number | null } | null }>, nextEntry?: { season?: number | null, volume?: number | null, episode?: number | null, chapter?: number | null } | null, showProgress?: Array<{ timesSeen: number, seasonNumber: number, episodes: Array<{ episodeNumber: number, timesSeen: number }> }> | null, podcastProgress?: Array<{ episodeNumber: number, timesSeen: number }> | null } }; +export type UserMetadataDetailsQuery = { userMetadataDetails: { mediaReason?: Array | null, hasInteracted: boolean, averageRating?: string | null, seenByAllCount: number, seenByUserCount: number, collections: Array<{ id: string, name: string, userId: string }>, inProgress?: { id: string, progress: string, providerWatchedOn?: string | null, state: SeenState, startedOn?: string | null, finishedOn?: string | null, lastUpdatedOn: string, numTimesUpdated: number, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null } | null, history: Array<{ id: string, progress: string, providerWatchedOn?: string | null, state: SeenState, startedOn?: string | null, finishedOn?: string | null, lastUpdatedOn: string, numTimesUpdated: number, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null }>, reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null }>, nextEntry?: { season?: number | null, volume?: number | null, episode?: number | null, chapter?: string | null } | null, showProgress?: Array<{ timesSeen: number, seasonNumber: number, episodes: Array<{ episodeNumber: number, timesSeen: number }> }> | null, podcastProgress?: Array<{ episodeNumber: number, timesSeen: number }> | null } }; export type UserMetadataGroupDetailsQueryVariables = Exact<{ metadataGroupId: Scalars['String']['input']; }>; -export type UserMetadataGroupDetailsQuery = { userMetadataGroupDetails: { reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: number | null, volume?: number | null } | null }>, collections: Array<{ id: string, name: string, userId: string }> } }; +export type UserMetadataGroupDetailsQuery = { userMetadataGroupDetails: { reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null }>, collections: Array<{ id: string, name: string, userId: string }> } }; export type UserPersonDetailsQueryVariables = Exact<{ personId: Scalars['String']['input']; }>; -export type UserPersonDetailsQuery = { userPersonDetails: { collections: Array<{ id: string, name: string, userId: string }>, reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: number | null, volume?: number | null } | null }> } }; +export type UserPersonDetailsQuery = { userPersonDetails: { collections: Array<{ id: string, name: string, userId: string }>, reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null }> } }; export type UserPreferencesQueryVariables = Exact<{ [key: string]: never; }>; @@ -3045,11 +3045,11 @@ export type SeenShowExtraInformationPartFragment = { episode: number, season: nu export type SeenAnimeExtraInformationPartFragment = { episode?: number | null }; -export type SeenMangaExtraInformationPartFragment = { chapter?: number | null, volume?: number | null }; +export type SeenMangaExtraInformationPartFragment = { chapter?: string | null, volume?: number | null }; export type CalendarEventPartFragment = { date: string, metadataId: string, metadataLot: MediaLot, episodeName?: string | null, metadataTitle: string, metadataImage?: string | null, calendarEventId: string, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null }; -export type SeenPartFragment = { id: string, progress: string, providerWatchedOn?: string | null, state: SeenState, startedOn?: string | null, finishedOn?: string | null, lastUpdatedOn: string, numTimesUpdated: number, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: number | null, volume?: number | null } | null }; +export type SeenPartFragment = { id: string, progress: string, providerWatchedOn?: string | null, state: SeenState, startedOn?: string | null, finishedOn?: string | null, lastUpdatedOn: string, numTimesUpdated: number, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null }; export type MetadataSearchItemPartFragment = { identifier: string, title: string, image?: string | null, publishYear?: number | null }; @@ -3065,7 +3065,7 @@ export type WorkoutSummaryPartFragment = { total?: { personalBestsAchieved: numb export type CollectionPartFragment = { id: string, name: string, userId: string }; -export type ReviewItemPartFragment = { id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: number | null, volume?: number | null } | null }; +export type ReviewItemPartFragment = { id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null }; export type WorkoutInformationPartFragment = { comment?: string | null, assets?: { images: Array, videos: Array } | null, exercises: Array<{ name: string, lot: ExerciseLot, notes: Array, restTime?: number | null, supersetWith: Array, total?: { personalBestsAchieved: number, weight: string, reps: string, distance: string, duration: string, restTime: number } | null, assets?: { images: Array, videos: Array } | null, sets: Array<{ note?: string | null, lot: SetLot, personalBests?: Array | null, confirmedAt?: string | null, statistic: { duration?: string | null, distance?: string | null, reps?: string | null, weight?: string | null, oneRm?: string | null, pace?: string | null, volume?: string | null } }> }> }; diff --git a/libs/ts-utils/src/index.ts b/libs/ts-utils/src/index.ts index 77b5eb7b91..b0a2cf9c82 100644 --- a/libs/ts-utils/src/index.ts +++ b/libs/ts-utils/src/index.ts @@ -11,6 +11,7 @@ import groupBy from "lodash/groupBy"; import isBoolean from "lodash/isBoolean"; import isEmpty from "lodash/isEmpty"; import isEqual from "lodash/isEqual"; +import isInteger from "lodash/isInteger"; import isNumber from "lodash/isNumber"; import isString from "lodash/isString"; import mapValues from "lodash/mapValues"; @@ -98,6 +99,7 @@ export { isBoolean, isEmpty, isEqual, + isInteger, isNumber, isString, mapValues,