From 61d7c40c0c17cdc0fbc416b0ab575d72cb6eda9e Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Sun, 24 Nov 2024 19:37:48 +0530 Subject: [PATCH 001/233] feat(migrations): create new columns for daily user activities --- crates/migrations/src/lib.rs | 2 ++ .../m20240827_create_daily_user_activity.rs | 21 +++++++++++++ .../src/m20241124_changes_for_issue_1113.rs | 31 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 crates/migrations/src/m20241124_changes_for_issue_1113.rs diff --git a/crates/migrations/src/lib.rs b/crates/migrations/src/lib.rs index 3d1a63ced7..1c7fa990ef 100644 --- a/crates/migrations/src/lib.rs +++ b/crates/migrations/src/lib.rs @@ -49,6 +49,7 @@ mod m20241019_changes_for_issue_964; mod m20241025_changes_for_issue_1084; mod m20241110_changes_for_issue_1103; mod m20241121_changes_for_issue_445; +mod m20241124_changes_for_issue_1113; pub use m20230410_create_metadata::Metadata as AliasedMetadata; pub use m20230413_create_person::Person as AliasedPerson; @@ -119,6 +120,7 @@ impl MigratorTrait for Migrator { Box::new(m20241025_changes_for_issue_1084::Migration), Box::new(m20241110_changes_for_issue_1103::Migration), Box::new(m20241121_changes_for_issue_445::Migration), + Box::new(m20241124_changes_for_issue_1113::Migration), ] } } diff --git a/crates/migrations/src/m20240827_create_daily_user_activity.rs b/crates/migrations/src/m20240827_create_daily_user_activity.rs index 48c8f58b2a..0f41c9c300 100644 --- a/crates/migrations/src/m20240827_create_daily_user_activity.rs +++ b/crates/migrations/src/m20240827_create_daily_user_activity.rs @@ -52,6 +52,9 @@ pub enum DailyUserActivity { TotalCount, TotalDuration, HourRecords, + WorkoutMuscles, + WorkoutExercises, + WorkoutEquipments, } #[async_trait::async_trait] @@ -116,6 +119,24 @@ impl MigrationTrait for Migration { .not_null() .default(Expr::cust("'[]'")), ) + .col( + ColumnDef::new(DailyUserActivity::WorkoutMuscles) + .array(ColumnType::Text) + .not_null() + .default(Expr::cust("'{}'")), + ) + .col( + ColumnDef::new(DailyUserActivity::WorkoutExercises) + .array(ColumnType::Text) + .not_null() + .default(Expr::cust("'{}'")), + ) + .col( + ColumnDef::new(DailyUserActivity::WorkoutEquipments) + .array(ColumnType::Text) + .not_null() + .default(Expr::cust("'{}'")), + ) .foreign_key( ForeignKey::create() .name("daily_user_activity_to_user_foreign_key") diff --git a/crates/migrations/src/m20241124_changes_for_issue_1113.rs b/crates/migrations/src/m20241124_changes_for_issue_1113.rs new file mode 100644 index 0000000000..fd445283bf --- /dev/null +++ b/crates/migrations/src/m20241124_changes_for_issue_1113.rs @@ -0,0 +1,31 @@ +use sea_orm_migration::prelude::*; + +const NEW_DUA_COLUMNS: [&str; 3] = ["workout_muscles", "workout_exercises", "workout_equipments"]; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + db.execute_unprepared("TRUNCATE daily_user_activity CASCADE") + .await?; + for col in NEW_DUA_COLUMNS { + if !manager.has_column("daily_user_activity", col).await? { + db.execute_unprepared(&format!( + r#" +ALTER TABLE "daily_user_activity" ADD COLUMN "{}" TEXT[] NOT NULL DEFAULT '{{}}'; +"#, + col, + )) + .await?; + } + } + Ok(()) + } + + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + Ok(()) + } +} From 614729e7bd5e2dace858f3b9c32876d7a2c52bf4 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Sun, 24 Nov 2024 19:38:25 +0530 Subject: [PATCH 002/233] chore(migrations): remove newlines --- crates/migrations/src/m20241124_changes_for_issue_1113.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/migrations/src/m20241124_changes_for_issue_1113.rs b/crates/migrations/src/m20241124_changes_for_issue_1113.rs index fd445283bf..b9701e8fbb 100644 --- a/crates/migrations/src/m20241124_changes_for_issue_1113.rs +++ b/crates/migrations/src/m20241124_changes_for_issue_1113.rs @@ -9,14 +9,12 @@ pub struct Migration; impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { let db = manager.get_connection(); - db.execute_unprepared("TRUNCATE daily_user_activity CASCADE") + db.execute_unprepared("TRUNCATE daily_user_activity") .await?; for col in NEW_DUA_COLUMNS { if !manager.has_column("daily_user_activity", col).await? { db.execute_unprepared(&format!( - r#" -ALTER TABLE "daily_user_activity" ADD COLUMN "{}" TEXT[] NOT NULL DEFAULT '{{}}'; -"#, + r#"ALTER TABLE "daily_user_activity" ADD COLUMN "{}" TEXT[] NOT NULL DEFAULT '{{}}'"#, col, )) .await?; From 24f3a73903196f172ccdb3f325d302959425d71b Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Sun, 24 Nov 2024 19:50:00 +0530 Subject: [PATCH 003/233] feat(backend): store data in new dua columns --- crates/models/database/src/daily_user_activity.rs | 4 ++++ crates/utils/database/src/lib.rs | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/models/database/src/daily_user_activity.rs b/crates/models/database/src/daily_user_activity.rs index dd0916442a..790b668648 100644 --- a/crates/models/database/src/daily_user_activity.rs +++ b/crates/models/database/src/daily_user_activity.rs @@ -1,6 +1,7 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 use common_models::DailyUserActivityHourRecord; +use enums::{ExerciseEquipment, ExerciseMuscle}; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; @@ -47,6 +48,9 @@ pub struct Model { pub total_duration: i32, #[sea_orm(column_type = "Json")] pub hour_records: Vec, + pub workout_muscles: Vec, + pub workout_exercises: Vec, + pub workout_equipments: Vec, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/utils/database/src/lib.rs b/crates/utils/database/src/lib.rs index 6177846295..591361f27b 100644 --- a/crates/utils/database/src/lib.rs +++ b/crates/utils/database/src/lib.rs @@ -16,8 +16,8 @@ use database_models::{ functions::associate_user_with_entity, metadata, prelude::{ - AccessLink, Collection, CollectionToEntity, DailyUserActivity, Metadata, Review, Seen, - User, UserMeasurement, UserToEntity, Workout, WorkoutTemplate, + AccessLink, Collection, CollectionToEntity, DailyUserActivity, Exercise, Metadata, Review, + Seen, User, UserMeasurement, UserToEntity, Workout, WorkoutTemplate, }, review, seen, user, user_measurement, user_to_entity, workout, }; @@ -765,6 +765,15 @@ pub async fn calculate_user_activities_and_summary( activity.workout_reps += workout_total.reps.to_i32().unwrap_or_default(); activity.workout_distance += workout_total.distance.to_i32().unwrap_or_default(); activity.workout_rest_time += workout_total.rest_time as i32; + for exercise in workout.information.exercises { + let db_exercise = Exercise::find_by_id(exercise.name.clone()) + .one(db) + .await? + .unwrap(); + activity.workout_exercises.push(db_exercise.id); + activity.workout_muscles.extend(db_exercise.muscles); + activity.workout_equipments.extend(db_exercise.equipment); + } } let mut measurement_stream = UserMeasurement::find() From 91f9577a89ba9955a0b260245d42a8b3cc2a7d4e Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Sun, 24 Nov 2024 22:11:35 +0530 Subject: [PATCH 004/233] chore(backend): change data type of muscles stored in db --- crates/enums/src/lib.rs | 13 +++++++----- .../src/m20230822_create_exercise.rs | 6 +++++- .../src/m20241124_changes_for_issue_1113.rs | 21 +++++++++++++++++++ .../database/src/daily_user_activity.rs | 2 +- crates/models/database/src/exercise.rs | 1 - 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/crates/enums/src/lib.rs b/crates/enums/src/lib.rs index a11021f802..a588832a7d 100644 --- a/crates/enums/src/lib.rs +++ b/crates/enums/src/lib.rs @@ -1,6 +1,6 @@ use async_graphql::Enum; use schematic::ConfigEnum; -use sea_orm::{DeriveActiveEnum, EnumIter, FromJsonQueryResult}; +use sea_orm::{DeriveActiveEnum, EnumIter}; use sea_orm_migration::prelude::*; use serde::{Deserialize, Serialize}; use strum::Display; @@ -209,16 +209,19 @@ pub enum ImportSource { Enum, Copy, Deserialize, - FromJsonQueryResult, + DeriveActiveEnum, + EnumIter, Eq, PartialEq, - EnumIter, - PartialOrd, - Ord, Default, ConfigEnum, Hash, )] +#[sea_orm( + rs_type = "String", + db_type = "String(StringLen::None)", + rename_all = "snake_case" +)] #[serde(rename_all = "snake_case")] pub enum ExerciseMuscle { #[default] diff --git a/crates/migrations/src/m20230822_create_exercise.rs b/crates/migrations/src/m20230822_create_exercise.rs index 7b1d582f74..b125597780 100644 --- a/crates/migrations/src/m20230822_create_exercise.rs +++ b/crates/migrations/src/m20230822_create_exercise.rs @@ -30,7 +30,6 @@ impl MigrationTrait for Migration { Table::create() .table(Exercise::Table) .col(ColumnDef::new(Exercise::Id).primary_key().text().not_null()) - .col(ColumnDef::new(Exercise::Muscles).json_binary().not_null()) .col(ColumnDef::new(Exercise::Lot).text().not_null()) .col(ColumnDef::new(Exercise::Level).text().not_null()) .col(ColumnDef::new(Exercise::Force).text()) @@ -44,6 +43,11 @@ impl MigrationTrait for Migration { ) .col(ColumnDef::new(Exercise::Source).text().not_null()) .col(ColumnDef::new(Exercise::CreatedByUserId).text()) + .col( + ColumnDef::new(Exercise::Muscles) + .array(ColumnType::Text) + .not_null(), + ) .foreign_key( ForeignKey::create() .name("workout_to_user_foreign_key") diff --git a/crates/migrations/src/m20241124_changes_for_issue_1113.rs b/crates/migrations/src/m20241124_changes_for_issue_1113.rs index b9701e8fbb..72e7ee4a1e 100644 --- a/crates/migrations/src/m20241124_changes_for_issue_1113.rs +++ b/crates/migrations/src/m20241124_changes_for_issue_1113.rs @@ -20,6 +20,27 @@ impl MigrationTrait for Migration { .await?; } } + db.execute_unprepared( + r#" +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'exercise' + AND column_name = 'muscles' + AND data_type = 'jsonb' + ) THEN + ALTER TABLE exercise ADD COLUMN muscles_text_array text[]; + UPDATE exercise + SET muscles_text_array = ARRAY(SELECT jsonb_array_elements_text(muscles)); + ALTER TABLE exercise DROP COLUMN muscles; + ALTER TABLE exercise RENAME COLUMN muscles_text_array TO muscles; + END IF; +END $$; + "#, + ) + .await?; Ok(()) } diff --git a/crates/models/database/src/daily_user_activity.rs b/crates/models/database/src/daily_user_activity.rs index 790b668648..ab448d9907 100644 --- a/crates/models/database/src/daily_user_activity.rs +++ b/crates/models/database/src/daily_user_activity.rs @@ -48,8 +48,8 @@ pub struct Model { pub total_duration: i32, #[sea_orm(column_type = "Json")] pub hour_records: Vec, - pub workout_muscles: Vec, pub workout_exercises: Vec, + pub workout_muscles: Vec, pub workout_equipments: Vec, } diff --git a/crates/models/database/src/exercise.rs b/crates/models/database/src/exercise.rs index c05b085c34..9a6b103075 100644 --- a/crates/models/database/src/exercise.rs +++ b/crates/models/database/src/exercise.rs @@ -38,7 +38,6 @@ pub struct Model { pub equipment: Option, #[graphql(skip_input)] pub source: ExerciseSource, - #[sea_orm(column_type = "Json")] pub muscles: Vec, pub attributes: ExerciseAttributes, #[graphql(skip_input)] From 7293becc0a321e7a221da788b4d9b8688019f79c Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 05:40:07 +0530 Subject: [PATCH 005/233] chore: make start time less than end time --- .../app/routes/_dashboard.fitness.measurements.list.tsx | 6 +++--- crates/utils/database/src/lib.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx index 61b2360f7d..defa931fb5 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx @@ -69,15 +69,15 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { await redirectUsingEnhancedCookieSearchParams(request, cookieName); const query = zx.parseQuery(request, searchParamsSchema); const now = dayjsLib(); - const endTime = getDateFromTimeSpan(query.timeSpan || defaultTimeSpan); + const startTime = getDateFromTimeSpan(query.timeSpan || defaultTimeSpan); const [{ userMeasurementsList }] = await Promise.all([ serverGqlService.authenticatedRequest( request, UserMeasurementsListDocument, { input: { - startTime: now.toISOString(), - endTime: endTime?.toISOString(), + endTime: now.toISOString(), + startTime: startTime?.toISOString(), }, }, ), diff --git a/crates/utils/database/src/lib.rs b/crates/utils/database/src/lib.rs index 591361f27b..aa6601209e 100644 --- a/crates/utils/database/src/lib.rs +++ b/crates/utils/database/src/lib.rs @@ -112,10 +112,10 @@ pub async fn user_measurements_list( ) -> Result> { let resp = UserMeasurement::find() .apply_if(input.start_time, |query, v| { - query.filter(user_measurement::Column::Timestamp.lte(v)) + query.filter(user_measurement::Column::Timestamp.gte(v)) }) .apply_if(input.end_time, |query, v| { - query.filter(user_measurement::Column::Timestamp.gte(v)) + query.filter(user_measurement::Column::Timestamp.lte(v)) }) .filter(user_measurement::Column::UserId.eq(user_id)) .order_by_asc(user_measurement::Column::Timestamp) From d9b8837eedd63d2b71b0a5d3d67eea99c661867b Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 05:45:23 +0530 Subject: [PATCH 006/233] chore: make start date less than end date --- crates/services/miscellaneous/src/lib.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs index 21749f1609..6a86d8f24c 100644 --- a/crates/services/miscellaneous/src/lib.rs +++ b/crates/services/miscellaneous/src/lib.rs @@ -856,10 +856,10 @@ ORDER BY RANDOM() LIMIT 10; .into(), ) .order_by_asc(calendar_event::Column::Date) - .apply_if(end_date, |q, v| { + .apply_if(start_date, |q, v| { q.filter(calendar_event::Column::Date.gte(v)) }) - .apply_if(start_date, |q, v| { + .apply_if(end_date, |q, v| { q.filter(calendar_event::Column::Date.lte(v)) }) .limit(media_limit) @@ -920,7 +920,7 @@ ORDER BY RANDOM() LIMIT 10; user_id: String, input: UserCalendarEventInput, ) -> Result> { - let (end_date, start_date) = get_first_and_last_day_of_month(input.year, input.month); + let (start_date, end_date) = get_first_and_last_day_of_month(input.year, input.month); let events = self .get_calendar_events(user_id, false, Some(start_date), Some(end_date), None, None) .await?; @@ -941,11 +941,11 @@ ORDER BY RANDOM() LIMIT 10; user_id: String, input: UserUpcomingCalendarEventInput, ) -> Result> { - let from_date = Utc::now().date_naive(); - let (media_limit, to_date) = match input { + let start_date = Utc::now().date_naive(); + let (media_limit, end_date) = match input { UserUpcomingCalendarEventInput::NextMedia(l) => (Some(l), None), UserUpcomingCalendarEventInput::NextDays(d) => { - (None, from_date.checked_add_days(Days::new(d))) + (None, start_date.checked_add_days(Days::new(d))) } }; let preferences = user_by_id(&user_id, &self.0).await?.preferences.general; @@ -957,8 +957,8 @@ ORDER BY RANDOM() LIMIT 10; .get_calendar_events( user_id, true, - to_date, - Some(from_date), + Some(start_date), + end_date, media_limit, element.and_then(|e| e.deduplicate_media), ) From 01587f6e29af99f009bd631b509a9a94b6e1ac75 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 06:10:47 +0530 Subject: [PATCH 007/233] chore(backend): common input struct for date range --- Cargo.lock | 1 + crates/models/common/Cargo.toml | 1 + crates/models/common/src/lib.rs | 7 +++++++ crates/models/media/src/lib.rs | 6 +++--- crates/services/statistics/src/lib.rs | 4 ++-- libs/generated/src/graphql/backend/graphql.ts | 8 ++++++-- libs/generated/src/graphql/backend/types.generated.ts | 8 ++++++-- 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4bb7974ab..50f46d37d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1327,6 +1327,7 @@ name = "common-models" version = "0.1.0" dependencies = [ "async-graphql", + "chrono", "educe", "enum_meta", "enums", diff --git a/crates/models/common/Cargo.toml b/crates/models/common/Cargo.toml index 780195cb14..5c8ca4646c 100644 --- a/crates/models/common/Cargo.toml +++ b/crates/models/common/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] async-graphql = { workspace = true } +chrono = { workspace = true } educe = { workspace = true } enums = { path = "../../enums" } enum_meta = { workspace = true } diff --git a/crates/models/common/src/lib.rs b/crates/models/common/src/lib.rs index 02043c8fb3..8f1e45068a 100644 --- a/crates/models/common/src/lib.rs +++ b/crates/models/common/src/lib.rs @@ -1,4 +1,5 @@ use async_graphql::{Enum, InputObject, SimpleObject}; +use chrono::NaiveDate; use educe::Educe; use enum_meta::{meta, Meta}; use enums::{EntityLot, MediaLot}; @@ -233,3 +234,9 @@ pub struct DailyUserActivityHourRecord { pub hour: u32, pub entities: Vec, } + +#[derive(Debug, Default, Serialize, Deserialize, InputObject, Clone)] +pub struct DateRangeInput { + pub end_date: Option, + pub start_date: Option, +} diff --git a/crates/models/media/src/lib.rs b/crates/models/media/src/lib.rs index 5619d537c7..259cf4e4fd 100644 --- a/crates/models/media/src/lib.rs +++ b/crates/models/media/src/lib.rs @@ -4,7 +4,8 @@ use async_graphql::{Enum, InputObject, InputType, OneofObject, SimpleObject, Uni use boilermates::boilermates; use chrono::{NaiveDate, NaiveDateTime}; use common_models::{ - CollectionExtraInformation, IdAndNamedObject, SearchInput, StoredUrl, StringIdObject, + CollectionExtraInformation, DateRangeInput, IdAndNamedObject, SearchInput, StoredUrl, + StringIdObject, }; use common_utils::deserialize_date; use enums::{ @@ -1506,8 +1507,7 @@ pub enum DailyUserActivitiesResponseGroupedBy { #[derive(Debug, Default, Serialize, Deserialize, InputObject, Clone)] pub struct DailyUserActivitiesInput { - pub end_date: Option, - pub start_date: Option, + pub date_range: DateRangeInput, pub group_by: Option, } diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index d854035275..c0748e5bca 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -31,10 +31,10 @@ impl StatisticsService { } let precondition = DailyUserActivity::find() .filter(daily_user_activity::Column::UserId.eq(user_id)) - .apply_if(input.end_date, |query, v| { + .apply_if(input.date_range.end_date, |query, v| { query.filter(daily_user_activity::Column::Date.lte(v)) }) - .apply_if(input.start_date, |query, v| { + .apply_if(input.date_range.start_date, |query, v| { query.filter(daily_user_activity::Column::Date.gte(v)) }) .select_only(); diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts index dad07bfb7a..4c02b2da45 100644 --- a/libs/generated/src/graphql/backend/graphql.ts +++ b/libs/generated/src/graphql/backend/graphql.ts @@ -318,9 +318,8 @@ export type CreateUserNotificationPlatformInput = { }; export type DailyUserActivitiesInput = { - endDate?: InputMaybe; + dateRange: DateRangeInput; groupBy?: InputMaybe; - startDate?: InputMaybe; }; export type DailyUserActivitiesResponse = { @@ -382,6 +381,11 @@ export enum DashboardElementLot { Upcoming = 'UPCOMING' } +export type DateRangeInput = { + endDate?: InputMaybe; + startDate?: InputMaybe; +}; + export type DeployGenericCsvImportInput = { csvPath: Scalars['String']['input']; }; diff --git a/libs/generated/src/graphql/backend/types.generated.ts b/libs/generated/src/graphql/backend/types.generated.ts index 848c9d0291..aed1457fb3 100644 --- a/libs/generated/src/graphql/backend/types.generated.ts +++ b/libs/generated/src/graphql/backend/types.generated.ts @@ -303,9 +303,8 @@ export type CreateUserNotificationPlatformInput = { }; export type DailyUserActivitiesInput = { - endDate?: InputMaybe; + dateRange: DateRangeInput; groupBy?: InputMaybe; - startDate?: InputMaybe; }; export type DailyUserActivitiesResponse = { @@ -369,6 +368,11 @@ export enum DashboardElementLot { Upcoming = 'UPCOMING' } +export type DateRangeInput = { + endDate?: InputMaybe; + startDate?: InputMaybe; +}; + export type DeployGenericCsvImportInput = { csvPath: Scalars['String']['input']; }; From c45defbf7cdde586b5648b962c05f315b1e9cced Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 06:12:20 +0530 Subject: [PATCH 008/233] chore(backend): add comment to input struct --- crates/models/common/src/lib.rs | 1 + libs/generated/src/graphql/backend/graphql.ts | 1 + libs/generated/src/graphql/backend/types.generated.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/crates/models/common/src/lib.rs b/crates/models/common/src/lib.rs index 8f1e45068a..a7261006b0 100644 --- a/crates/models/common/src/lib.rs +++ b/crates/models/common/src/lib.rs @@ -235,6 +235,7 @@ pub struct DailyUserActivityHourRecord { pub entities: Vec, } +/// The start date must be before the end date. #[derive(Debug, Default, Serialize, Deserialize, InputObject, Clone)] pub struct DateRangeInput { pub end_date: Option, diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts index 4c02b2da45..ceacd14baf 100644 --- a/libs/generated/src/graphql/backend/graphql.ts +++ b/libs/generated/src/graphql/backend/graphql.ts @@ -381,6 +381,7 @@ export enum DashboardElementLot { Upcoming = 'UPCOMING' } +/** The start date must be before the end date. */ export type DateRangeInput = { endDate?: InputMaybe; startDate?: InputMaybe; diff --git a/libs/generated/src/graphql/backend/types.generated.ts b/libs/generated/src/graphql/backend/types.generated.ts index aed1457fb3..a8f1c1db8c 100644 --- a/libs/generated/src/graphql/backend/types.generated.ts +++ b/libs/generated/src/graphql/backend/types.generated.ts @@ -368,6 +368,7 @@ export enum DashboardElementLot { Upcoming = 'UPCOMING' } +/** The start date must be before the end date. */ export type DateRangeInput = { endDate?: InputMaybe; startDate?: InputMaybe; From 9633ee7a34bfe4e2a9d79ee5c89e4ac6ccee57e4 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 06:13:00 +0530 Subject: [PATCH 009/233] chore(frontend): adapt to new gql schema --- apps/frontend/app/routes/_dashboard._index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/app/routes/_dashboard._index.tsx b/apps/frontend/app/routes/_dashboard._index.tsx index e655a0b6e6..09403b94c5 100644 --- a/apps/frontend/app/routes/_dashboard._index.tsx +++ b/apps/frontend/app/routes/_dashboard._index.tsx @@ -662,7 +662,7 @@ const ActivitySection = () => { queryFn: async () => { const { dailyUserActivities } = await clientGqlService.request( DailyUserActivitiesDocument, - { input: { startDate, endDate } }, + { input: { dateRange: { startDate, endDate } } }, ); const trackSeries = mapValues(MediaColors, () => false); const data = dailyUserActivities.items.map((d) => { From b0c1d90bfc4b5a8e8b13fbb1c220d5b032c1934a Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 06:15:55 +0530 Subject: [PATCH 010/233] chore(utils): add debug logs for user activity calculation --- crates/utils/database/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/utils/database/src/lib.rs b/crates/utils/database/src/lib.rs index aa6601209e..1a97420e75 100644 --- a/crates/utils/database/src/lib.rs +++ b/crates/utils/database/src/lib.rs @@ -828,8 +828,10 @@ pub async fn calculate_user_activities_and_summary( .one(db) .await? { + ryot_log!(debug, "Deleting activity = {:#?}", activity.date); entity.delete(db).await?; } + ryot_log!(debug, "Inserting activity = {:#?}", activity.date); let total_review_count = activity.metadata_review_count + activity.collection_review_count + activity.metadata_group_review_count From dac8fd76ed9dd1add4d3e1d641645ee87110aa78 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 06:20:46 +0530 Subject: [PATCH 011/233] feat(models): add new struct to return response for analytics --- crates/models/dependent/src/lib.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/models/dependent/src/lib.rs b/crates/models/dependent/src/lib.rs index 0658ceebe1..069892acb8 100644 --- a/crates/models/dependent/src/lib.rs +++ b/crates/models/dependent/src/lib.rs @@ -1,11 +1,11 @@ use async_graphql::{InputObject, OutputType, SimpleObject, Union}; -use common_models::{BackendError, SearchDetails}; +use common_models::{BackendError, DailyUserActivityHourRecord, SearchDetails}; use config::FrontendConfig; use database_models::{ - collection, exercise, metadata, metadata_group, person, seen, user, user_measurement, - user_to_entity, workout, workout_template, + collection, exercise, metadata, metadata_group, person, prelude::DailyUserActivity, seen, user, + user_measurement, user_to_entity, workout, workout_template, }; -use enums::UserToMediaReason; +use enums::{ExerciseEquipment, ExerciseMuscle, UserToMediaReason}; use fitness_models::{UserToExerciseHistoryExtraInformation, UserWorkoutInput}; use importer_models::ImportFailedItem; use media_models::{ @@ -17,6 +17,7 @@ use media_models::{ }; use rust_decimal::Decimal; use schematic::Schematic; +use sea_orm::{DerivePartialModel, FromQueryResult}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -232,3 +233,17 @@ pub struct UserWorkoutTemplateDetails { pub details: workout_template::Model, pub collections: Vec, } + +#[derive(Debug, SimpleObject, Serialize, Deserialize, DerivePartialModel, FromQueryResult)] +#[sea_orm(entity = "DailyUserActivity")] +pub struct FitnessAnalytics { + pub workout_reps: i32, + pub workout_weight: i32, + pub workout_distance: i32, + pub workout_rest_time: i32, + pub workout_personal_bests: i32, + pub workout_exercises: Vec, + pub workout_muscles: Vec, + pub workout_equipments: Vec, + pub hour_records: Vec, +} From 6160b2047ad0cc52638e837ed162c2c49b151736 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 06:21:00 +0530 Subject: [PATCH 012/233] build(models/dependent): add new deps --- Cargo.lock | 1 + crates/models/dependent/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 50f46d37d5..f933159ea3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1733,6 +1733,7 @@ dependencies = [ "media-models", "rust_decimal", "schematic", + "sea-orm", "serde", "serde_with 3.11.0", ] diff --git a/crates/models/dependent/Cargo.toml b/crates/models/dependent/Cargo.toml index 21a3c07004..b2b26730eb 100644 --- a/crates/models/dependent/Cargo.toml +++ b/crates/models/dependent/Cargo.toml @@ -14,5 +14,6 @@ media-models = { path = "../media" } fitness-models = { path = "../fitness" } rust_decimal = { workspace = true } schematic = { workspace = true } +sea-orm = { workspace = true } serde = { workspace = true } serde_with = { workspace = true } From 89b93b18285eb8fe8ff99893f8896c4171228204 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 06:23:44 +0530 Subject: [PATCH 013/233] build(backend): add new required deps --- Cargo.lock | 2 ++ crates/resolvers/statistics/Cargo.toml | 1 + crates/services/statistics/Cargo.toml | 1 + 3 files changed, 4 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index f933159ea3..fc133d94f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6182,6 +6182,7 @@ name = "statistics-resolver" version = "0.1.0" dependencies = [ "async-graphql", + "common-models", "dependent-models", "media-models", "statistics-service", @@ -6193,6 +6194,7 @@ name = "statistics-service" version = "0.1.0" dependencies = [ "async-graphql", + "common-models", "database-models", "database-utils", "dependent-models", diff --git a/crates/resolvers/statistics/Cargo.toml b/crates/resolvers/statistics/Cargo.toml index 47d918a7fd..159d6ddfd3 100644 --- a/crates/resolvers/statistics/Cargo.toml +++ b/crates/resolvers/statistics/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] async-graphql = { workspace = true } +common-models = { path = "../../models/common" } dependent-models = { path = "../../models/dependent" } media-models = { path = "../../models/media" } statistics-service = { path = "../../services/statistics" } diff --git a/crates/services/statistics/Cargo.toml b/crates/services/statistics/Cargo.toml index e3ffc1f337..cb468b166e 100644 --- a/crates/services/statistics/Cargo.toml +++ b/crates/services/statistics/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] async-graphql = { workspace = true } +common-models = { path = "../../models/common" } database-models = { path = "../../models/database" } database-utils = { path = "../../utils/database" } dependent-models = { path = "../../models/dependent" } From cac10a481b24506512b7e5a1ca2877b1fe147f29 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 06:26:13 +0530 Subject: [PATCH 014/233] feat(backend): add new query for getting fitness analytics --- crates/resolvers/statistics/src/lib.rs | 14 +++++++++++++- crates/services/statistics/src/lib.rs | 11 ++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/resolvers/statistics/src/lib.rs b/crates/resolvers/statistics/src/lib.rs index f3d2a94386..00ec4ea056 100644 --- a/crates/resolvers/statistics/src/lib.rs +++ b/crates/resolvers/statistics/src/lib.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use async_graphql::{Context, Object, Result}; -use dependent_models::DailyUserActivitiesResponse; +use common_models::DateRangeInput; +use dependent_models::{DailyUserActivitiesResponse, FitnessAnalytics}; use media_models::{DailyUserActivitiesInput, DailyUserActivityItem}; use statistics_service::StatisticsService; use traits::AuthProvider; @@ -30,4 +31,15 @@ impl StatisticsQuery { let user_id = self.user_id_from_ctx(gql_ctx).await?; service.latest_user_summary(&user_id).await } + + /// Get the fitness analytics for the currently logged in user. + async fn fitness_analytics( + &self, + gql_ctx: &Context<'_>, + input: DateRangeInput, + ) -> Result { + let service = gql_ctx.data_unchecked::>(); + let user_id = self.user_id_from_ctx(gql_ctx).await?; + service.fitness_analytics(&user_id, input).await + } } diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index c0748e5bca..0cf495e305 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -1,9 +1,10 @@ use std::{fmt::Write, sync::Arc}; use async_graphql::Result; +use common_models::DateRangeInput; use database_models::{daily_user_activity, prelude::DailyUserActivity}; use database_utils::calculate_user_activities_and_summary; -use dependent_models::DailyUserActivitiesResponse; +use dependent_models::{DailyUserActivitiesResponse, FitnessAnalytics}; use media_models::{ DailyUserActivitiesInput, DailyUserActivitiesResponseGroupedBy, DailyUserActivityItem, }; @@ -224,4 +225,12 @@ impl StatisticsService { ) -> Result<()> { calculate_user_activities_and_summary(&self.0.db, user_id, calculate_from_beginning).await } + + pub async fn fitness_analytics( + &self, + user_id: &String, + input: DateRangeInput, + ) -> Result { + todo!() + } } From dd715e7e5f742c461519d02e753cafd1eb1ed591 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 06:37:59 +0530 Subject: [PATCH 015/233] feat(backend): add query fragment for fitness analytics --- crates/models/dependent/src/lib.rs | 15 +++++++- crates/services/statistics/src/lib.rs | 14 ++++++- libs/generated/src/graphql/backend/gql.ts | 4 +- libs/generated/src/graphql/backend/graphql.ts | 38 ++++++++++++++++++- .../src/graphql/backend/types.generated.ts | 31 +++++++++++++++ libs/graphql/src/backend/queries/combined.gql | 19 ++++++++++ 6 files changed, 116 insertions(+), 5 deletions(-) diff --git a/crates/models/dependent/src/lib.rs b/crates/models/dependent/src/lib.rs index 069892acb8..8db4774b7f 100644 --- a/crates/models/dependent/src/lib.rs +++ b/crates/models/dependent/src/lib.rs @@ -236,7 +236,7 @@ pub struct UserWorkoutTemplateDetails { #[derive(Debug, SimpleObject, Serialize, Deserialize, DerivePartialModel, FromQueryResult)] #[sea_orm(entity = "DailyUserActivity")] -pub struct FitnessAnalytics { +pub struct CoreFitnessAnalytics { pub workout_reps: i32, pub workout_weight: i32, pub workout_distance: i32, @@ -245,5 +245,18 @@ pub struct FitnessAnalytics { pub workout_exercises: Vec, pub workout_muscles: Vec, pub workout_equipments: Vec, + #[graphql(skip)] pub hour_records: Vec, } + +#[derive(Debug, SimpleObject, Serialize, Deserialize, FromQueryResult)] +pub struct FitnessAnalyticsHour { + pub hour: u32, + pub count: u32, +} + +#[derive(Debug, SimpleObject, Serialize, Deserialize)] +pub struct FitnessAnalytics { + pub core: CoreFitnessAnalytics, + pub hours: Vec, +} diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index 0cf495e305..1ebef04779 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -4,7 +4,7 @@ use async_graphql::Result; use common_models::DateRangeInput; use database_models::{daily_user_activity, prelude::DailyUserActivity}; use database_utils::calculate_user_activities_and_summary; -use dependent_models::{DailyUserActivitiesResponse, FitnessAnalytics}; +use dependent_models::{CoreFitnessAnalytics, DailyUserActivitiesResponse, FitnessAnalytics}; use media_models::{ DailyUserActivitiesInput, DailyUserActivitiesResponseGroupedBy, DailyUserActivityItem, }; @@ -231,6 +231,18 @@ impl StatisticsService { user_id: &String, input: DateRangeInput, ) -> Result { + let items = DailyUserActivity::find() + .filter(daily_user_activity::Column::UserId.eq(user_id)) + .apply_if(input.start_date, |query, v| { + query.filter(daily_user_activity::Column::Date.gte(v)) + }) + .apply_if(input.end_date, |query, v| { + query.filter(daily_user_activity::Column::Date.lte(v)) + }) + .into_partial_model::() + .all(&self.0.db) + .await?; + dbg!(items); todo!() } } diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts index ab8a7fd785..9b51faf8e2 100644 --- a/libs/generated/src/graphql/backend/gql.ts +++ b/libs/generated/src/graphql/backend/gql.ts @@ -41,7 +41,7 @@ const documents = { "query UserWorkoutTemplateDetails($workoutTemplateId: String!) {\n userWorkoutTemplateDetails(workoutTemplateId: $workoutTemplateId) {\n collections {\n ...CollectionPart\n }\n details {\n id\n name\n createdOn\n visibility\n summary {\n ...WorkoutSummaryPart\n }\n information {\n ...WorkoutInformationPart\n }\n }\n }\n}": types.UserWorkoutTemplateDetailsDocument, "query UserWorkoutTemplatesList($input: SearchInput!) {\n userWorkoutTemplatesList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n id\n name\n createdOn\n visibility\n summary {\n ...WorkoutSummaryPart\n }\n }\n }\n}": types.UserWorkoutTemplatesListDocument, "query UserWorkoutsList($input: SearchInput!) {\n userWorkoutsList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n id\n name\n endTime\n duration\n startTime\n summary {\n ...WorkoutSummaryPart\n }\n }\n }\n}": types.UserWorkoutsListDocument, - "query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}": types.GetOidcRedirectUrlDocument, + "query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n hours {\n hour\n count\n }\n core {\n workoutReps\n workoutWeight\n workoutDistance\n workoutRestTime\n workoutPersonalBests\n workoutExercises\n workoutMuscles\n workoutEquipments\n }\n }\n}": types.GetOidcRedirectUrlDocument, "fragment SeenPodcastExtraInformationPart on SeenPodcastExtraInformation {\n episode\n}\n\nfragment SeenShowExtraInformationPart on SeenShowExtraInformation {\n episode\n season\n}\n\nfragment SeenAnimeExtraInformationPart on SeenAnimeExtraInformation {\n episode\n}\n\nfragment SeenMangaExtraInformationPart on SeenMangaExtraInformation {\n volume\n chapter\n}\n\nfragment CalendarEventPart on GraphqlCalendarEvent {\n date\n metadataId\n metadataLot\n episodeName\n metadataTitle\n metadataImage\n calendarEventId\n showExtraInformation {\n ...SeenShowExtraInformationPart\n }\n podcastExtraInformation {\n ...SeenPodcastExtraInformationPart\n }\n animeExtraInformation {\n ...SeenAnimeExtraInformationPart\n }\n}\n\nfragment SeenPart on Seen {\n id\n state\n progress\n reviewId\n startedOn\n finishedOn\n lastUpdatedOn\n manualTimeSpent\n numTimesUpdated\n providerWatchedOn\n showExtraInformation {\n ...SeenShowExtraInformationPart\n }\n podcastExtraInformation {\n ...SeenPodcastExtraInformationPart\n }\n animeExtraInformation {\n ...SeenAnimeExtraInformationPart\n }\n mangaExtraInformation {\n ...SeenMangaExtraInformationPart\n }\n}\n\nfragment MetadataSearchItemPart on MetadataSearchItem {\n title\n image\n identifier\n publishYear\n}\n\nfragment WorkoutOrExerciseTotalsPart on WorkoutOrExerciseTotals {\n reps\n weight\n distance\n duration\n restTime\n personalBestsAchieved\n}\n\nfragment EntityAssetsPart on EntityAssets {\n images\n videos\n}\n\nfragment WorkoutSetStatisticPart on WorkoutSetStatistic {\n reps\n pace\n oneRm\n weight\n volume\n duration\n distance\n}\n\nfragment WorkoutSetRecordPart on WorkoutSetRecord {\n lot\n personalBests\n statistic {\n ...WorkoutSetStatisticPart\n }\n}\n\nfragment WorkoutSummaryPart on WorkoutSummary {\n total {\n ...WorkoutOrExerciseTotalsPart\n }\n exercises {\n lot\n name\n numSets\n bestSet {\n ...WorkoutSetRecordPart\n }\n }\n focused {\n lots {\n lot\n exercises\n }\n levels {\n level\n exercises\n }\n forces {\n force\n exercises\n }\n muscles {\n muscle\n exercises\n }\n equipments {\n equipment\n exercises\n }\n }\n}\n\nfragment CollectionPart on Collection {\n id\n name\n userId\n}\n\nfragment ReviewItemPart on ReviewItem {\n id\n rating\n postedOn\n isSpoiler\n visibility\n textOriginal\n textRendered\n seenItemsAssociatedWith\n postedBy {\n id\n name\n }\n comments {\n id\n text\n likedBy\n createdOn\n user {\n id\n name\n }\n }\n showExtraInformation {\n ...SeenShowExtraInformationPart\n }\n podcastExtraInformation {\n ...SeenPodcastExtraInformationPart\n }\n animeExtraInformation {\n ...SeenAnimeExtraInformationPart\n }\n mangaExtraInformation {\n ...SeenMangaExtraInformationPart\n }\n}\n\nfragment WorkoutInformationPart on WorkoutInformation {\n comment\n assets {\n ...EntityAssetsPart\n }\n supersets {\n color\n exercises\n }\n exercises {\n lot\n name\n notes\n total {\n ...WorkoutOrExerciseTotalsPart\n }\n assets {\n ...EntityAssetsPart\n }\n sets {\n lot\n note\n restTime\n confirmedAt\n personalBests\n statistic {\n ...WorkoutSetStatisticPart\n }\n }\n }\n}\n\nfragment SetRestTimersPart on SetRestTimersSettings {\n drop\n warmup\n normal\n failure\n}": types.SeenPodcastExtraInformationPartFragmentDoc, }; @@ -170,7 +170,7 @@ export function graphql(source: "query UserWorkoutsList($input: SearchInput!) {\ /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}"): (typeof documents)["query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}"]; +export function graphql(source: "query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n hours {\n hour\n count\n }\n core {\n workoutReps\n workoutWeight\n workoutDistance\n workoutRestTime\n workoutPersonalBests\n workoutExercises\n workoutMuscles\n workoutEquipments\n }\n }\n}"): (typeof documents)["query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n hours {\n hour\n count\n }\n core {\n workoutReps\n workoutWeight\n workoutDistance\n workoutRestTime\n workoutPersonalBests\n workoutExercises\n workoutMuscles\n workoutEquipments\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts index ceacd14baf..67c8e0aff6 100644 --- a/libs/generated/src/graphql/backend/graphql.ts +++ b/libs/generated/src/graphql/backend/graphql.ts @@ -234,6 +234,17 @@ export type CoreDetails = { websiteUrl: Scalars['String']['output']; }; +export type CoreFitnessAnalytics = { + workoutDistance: Scalars['Int']['output']; + workoutEquipments: Array; + workoutExercises: Array; + workoutMuscles: Array; + workoutPersonalBests: Scalars['Int']['output']; + workoutReps: Scalars['Int']['output']; + workoutRestTime: Scalars['Int']['output']; + workoutWeight: Scalars['Int']['output']; +}; + export type CreateAccessLinkInput = { expiresOn?: InputMaybe; isAccountDefault?: InputMaybe; @@ -653,6 +664,16 @@ export type ExternalIdentifiers = { tvdbId?: Maybe; }; +export type FitnessAnalytics = { + core: CoreFitnessAnalytics; + hours: Array; +}; + +export type FitnessAnalyticsHour = { + count: Scalars['Int']['output']; + hour: Scalars['Int']['output']; +}; + export type FrontendConfig = { /** A message to be displayed on the dashboard. */ dashboardMessage: Scalars['String']['output']; @@ -1695,6 +1716,8 @@ export type QueryRoot = { exerciseParameters: ExerciseParameters; /** Get a paginated list of exercises in the database. */ exercisesList: ExerciseListResults; + /** Get the fitness analytics for the currently logged in user. */ + fitnessAnalytics: FitnessAnalytics; /** Get details about a genre present in the database. */ genreDetails: GenreDetails; /** Get paginated list of genres. */ @@ -1794,6 +1817,11 @@ export type QueryRootExercisesListArgs = { }; +export type QueryRootFitnessAnalyticsArgs = { + input: DateRangeInput; +}; + + export type QueryRootGenreDetailsArgs = { input: GenreDetailsInput; }; @@ -3389,6 +3417,13 @@ export type DailyUserActivitiesQueryVariables = Exact<{ export type DailyUserActivitiesQuery = { dailyUserActivities: { groupedBy: DailyUserActivitiesResponseGroupedBy, totalCount: number, totalDuration: number, items: Array<{ day: string, totalReviewCount: number, workoutCount: number, measurementCount: number, audioBookCount: number, animeCount: number, bookCount: number, podcastCount: number, mangaCount: number, showCount: number, movieCount: number, videoGameCount: number, visualNovelCount: number }> } }; +export type FitnessAnalyticsQueryVariables = Exact<{ + input: DateRangeInput; +}>; + + +export type FitnessAnalyticsQuery = { fitnessAnalytics: { hours: Array<{ hour: number, count: number }>, core: { workoutReps: number, workoutWeight: number, workoutDistance: number, workoutRestTime: number, workoutPersonalBests: number, workoutExercises: Array, workoutMuscles: Array, workoutEquipments: Array } } }; + export type SeenPodcastExtraInformationPartFragment = { episode: number }; export type SeenShowExtraInformationPartFragment = { episode: number, season: number }; @@ -3530,4 +3565,5 @@ export const MetadataPartialDetailsDocument = {"kind":"Document","definitions":[ export const MetadataGroupsListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataGroupsList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MetadataGroupsListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataGroupsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"}}]}}]}}]} as unknown as DocumentNode; export const PeopleListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PeopleList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PeopleListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"peopleList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"}}]}}]}}]} as unknown as DocumentNode; export const UserAccessLinksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserAccessLinks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userAccessLinks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isDemo"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"expiresOn"}},{"kind":"Field","name":{"kind":"Name","value":"timesUsed"}},{"kind":"Field","name":{"kind":"Name","value":"isRevoked"}},{"kind":"Field","name":{"kind":"Name","value":"maximumUses"}},{"kind":"Field","name":{"kind":"Name","value":"isAccountDefault"}},{"kind":"Field","name":{"kind":"Name","value":"isMutationAllowed"}}]}}]}}]} as unknown as DocumentNode; -export const DailyUserActivitiesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DailyUserActivities"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DailyUserActivitiesInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dailyUserActivities"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groupedBy"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalDuration"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"day"}},{"kind":"Field","name":{"kind":"Name","value":"totalReviewCount"}},{"kind":"Field","name":{"kind":"Name","value":"workoutCount"}},{"kind":"Field","name":{"kind":"Name","value":"measurementCount"}},{"kind":"Field","name":{"kind":"Name","value":"audioBookCount"}},{"kind":"Field","name":{"kind":"Name","value":"animeCount"}},{"kind":"Field","name":{"kind":"Name","value":"bookCount"}},{"kind":"Field","name":{"kind":"Name","value":"podcastCount"}},{"kind":"Field","name":{"kind":"Name","value":"mangaCount"}},{"kind":"Field","name":{"kind":"Name","value":"showCount"}},{"kind":"Field","name":{"kind":"Name","value":"movieCount"}},{"kind":"Field","name":{"kind":"Name","value":"videoGameCount"}},{"kind":"Field","name":{"kind":"Name","value":"visualNovelCount"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const DailyUserActivitiesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DailyUserActivities"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DailyUserActivitiesInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dailyUserActivities"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groupedBy"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalDuration"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"day"}},{"kind":"Field","name":{"kind":"Name","value":"totalReviewCount"}},{"kind":"Field","name":{"kind":"Name","value":"workoutCount"}},{"kind":"Field","name":{"kind":"Name","value":"measurementCount"}},{"kind":"Field","name":{"kind":"Name","value":"audioBookCount"}},{"kind":"Field","name":{"kind":"Name","value":"animeCount"}},{"kind":"Field","name":{"kind":"Name","value":"bookCount"}},{"kind":"Field","name":{"kind":"Name","value":"podcastCount"}},{"kind":"Field","name":{"kind":"Name","value":"mangaCount"}},{"kind":"Field","name":{"kind":"Name","value":"showCount"}},{"kind":"Field","name":{"kind":"Name","value":"movieCount"}},{"kind":"Field","name":{"kind":"Name","value":"videoGameCount"}},{"kind":"Field","name":{"kind":"Name","value":"visualNovelCount"}}]}}]}}]}}]} as unknown as DocumentNode; +export const FitnessAnalyticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FitnessAnalytics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateRangeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fitnessAnalytics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hours"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hour"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"core"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutReps"}},{"kind":"Field","name":{"kind":"Name","value":"workoutWeight"}},{"kind":"Field","name":{"kind":"Name","value":"workoutDistance"}},{"kind":"Field","name":{"kind":"Name","value":"workoutRestTime"}},{"kind":"Field","name":{"kind":"Name","value":"workoutPersonalBests"}},{"kind":"Field","name":{"kind":"Name","value":"workoutExercises"}},{"kind":"Field","name":{"kind":"Name","value":"workoutMuscles"}},{"kind":"Field","name":{"kind":"Name","value":"workoutEquipments"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/libs/generated/src/graphql/backend/types.generated.ts b/libs/generated/src/graphql/backend/types.generated.ts index a8f1c1db8c..c569206e59 100644 --- a/libs/generated/src/graphql/backend/types.generated.ts +++ b/libs/generated/src/graphql/backend/types.generated.ts @@ -219,6 +219,18 @@ export type CoreDetails = { websiteUrl: Scalars['String']['output']; }; +export type CoreFitnessAnalytics = { + __typename?: 'CoreFitnessAnalytics'; + workoutDistance: Scalars['Int']['output']; + workoutEquipments: Array; + workoutExercises: Array; + workoutMuscles: Array; + workoutPersonalBests: Scalars['Int']['output']; + workoutReps: Scalars['Int']['output']; + workoutRestTime: Scalars['Int']['output']; + workoutWeight: Scalars['Int']['output']; +}; + export type CreateAccessLinkInput = { expiresOn?: InputMaybe; isAccountDefault?: InputMaybe; @@ -652,6 +664,18 @@ export type ExternalIdentifiers = { tvdbId?: Maybe; }; +export type FitnessAnalytics = { + __typename?: 'FitnessAnalytics'; + core: CoreFitnessAnalytics; + hours: Array; +}; + +export type FitnessAnalyticsHour = { + __typename?: 'FitnessAnalyticsHour'; + count: Scalars['Int']['output']; + hour: Scalars['Int']['output']; +}; + export type FrontendConfig = { __typename?: 'FrontendConfig'; /** A message to be displayed on the dashboard. */ @@ -1745,6 +1769,8 @@ export type QueryRoot = { exerciseParameters: ExerciseParameters; /** Get a paginated list of exercises in the database. */ exercisesList: ExerciseListResults; + /** Get the fitness analytics for the currently logged in user. */ + fitnessAnalytics: FitnessAnalytics; /** Get details about a genre present in the database. */ genreDetails: GenreDetails; /** Get paginated list of genres. */ @@ -1844,6 +1870,11 @@ export type QueryRootExercisesListArgs = { }; +export type QueryRootFitnessAnalyticsArgs = { + input: DateRangeInput; +}; + + export type QueryRootGenreDetailsArgs = { input: GenreDetailsInput; }; diff --git a/libs/graphql/src/backend/queries/combined.gql b/libs/graphql/src/backend/queries/combined.gql index 79b55d6e35..cf63d9ea8e 100644 --- a/libs/graphql/src/backend/queries/combined.gql +++ b/libs/graphql/src/backend/queries/combined.gql @@ -178,3 +178,22 @@ query DailyUserActivities($input: DailyUserActivitiesInput!) { } } } + +query FitnessAnalytics($input: DateRangeInput!) { + fitnessAnalytics(input: $input) { + hours { + hour + count + } + core { + workoutReps + workoutWeight + workoutDistance + workoutRestTime + workoutPersonalBests + workoutExercises + workoutMuscles + workoutEquipments + } + } +} From d4a7e7536963afe070cb6e68a7520587d7452f34 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 06:51:55 +0530 Subject: [PATCH 016/233] build(services/statistics): add new deps to crate --- Cargo.lock | 3 +++ crates/services/statistics/Cargo.toml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index fc133d94f3..9d9cb14d04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6198,6 +6198,9 @@ dependencies = [ "database-models", "database-utils", "dependent-models", + "enums", + "hashbag", + "itertools 0.13.0", "media-models", "sea-orm", "supporting-service", diff --git a/crates/services/statistics/Cargo.toml b/crates/services/statistics/Cargo.toml index cb468b166e..c940e1c1a8 100644 --- a/crates/services/statistics/Cargo.toml +++ b/crates/services/statistics/Cargo.toml @@ -9,6 +9,9 @@ common-models = { path = "../../models/common" } database-models = { path = "../../models/database" } database-utils = { path = "../../utils/database" } dependent-models = { path = "../../models/dependent" } +enums = { path = "../../enums" } +hashbag = { workspace = true } +itertools = { workspace = true } media-models = { path = "../../models/media" } sea-orm = { workspace = true } supporting-service = { path = "../supporting" } From 4596668822680c2c6843ce217d828b9dd2f91eb3 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 06:52:11 +0530 Subject: [PATCH 017/233] feat(services/statistics): calculate hour counts --- crates/services/statistics/src/lib.rs | 28 ++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index 1ebef04779..375ba3be7d 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -1,10 +1,15 @@ -use std::{fmt::Write, sync::Arc}; +use std::{cmp::Reverse, fmt::Write, sync::Arc}; use async_graphql::Result; use common_models::DateRangeInput; use database_models::{daily_user_activity, prelude::DailyUserActivity}; use database_utils::calculate_user_activities_and_summary; -use dependent_models::{CoreFitnessAnalytics, DailyUserActivitiesResponse, FitnessAnalytics}; +use dependent_models::{ + CoreFitnessAnalytics, DailyUserActivitiesResponse, FitnessAnalytics, FitnessAnalyticsHour, +}; +use enums::EntityLot; +use hashbag::HashBag; +use itertools::Itertools; use media_models::{ DailyUserActivitiesInput, DailyUserActivitiesResponseGroupedBy, DailyUserActivityItem, }; @@ -242,7 +247,24 @@ impl StatisticsService { .into_partial_model::() .all(&self.0.db) .await?; - dbg!(items); + let mut hours_bag = HashBag::new(); + items.iter().for_each(|i| { + for record in i.hour_records.iter() { + for entity in record.entities.iter() { + if entity.entity_lot == EntityLot::Workout { + hours_bag.insert(record.hour); + } + } + } + }); + let hours = hours_bag + .into_iter() + .map(|(hour, count)| FitnessAnalyticsHour { + hour, + count: count.try_into().unwrap(), + }) + .sorted_by_key(|f| Reverse(f.count)) + .collect_vec(); todo!() } } From bb4b0634bdc4c5e98048d6374f5cbd2358ee5371 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 07:06:22 +0530 Subject: [PATCH 018/233] feat(backend): complete implementation of fitness statistics --- crates/models/dependent/src/lib.rs | 46 +++++++----- crates/services/statistics/src/lib.rs | 75 +++++++++++++++++-- libs/generated/src/graphql/backend/gql.ts | 4 +- libs/generated/src/graphql/backend/graphql.ts | 39 ++++++---- .../src/graphql/backend/types.generated.ts | 39 ++++++---- libs/graphql/src/backend/queries/combined.gql | 25 ++++--- 6 files changed, 166 insertions(+), 62 deletions(-) diff --git a/crates/models/dependent/src/lib.rs b/crates/models/dependent/src/lib.rs index 8db4774b7f..daecad111f 100644 --- a/crates/models/dependent/src/lib.rs +++ b/crates/models/dependent/src/lib.rs @@ -1,9 +1,9 @@ use async_graphql::{InputObject, OutputType, SimpleObject, Union}; -use common_models::{BackendError, DailyUserActivityHourRecord, SearchDetails}; +use common_models::{BackendError, SearchDetails}; use config::FrontendConfig; use database_models::{ - collection, exercise, metadata, metadata_group, person, prelude::DailyUserActivity, seen, user, - user_measurement, user_to_entity, workout, workout_template, + collection, exercise, metadata, metadata_group, person, seen, user, user_measurement, + user_to_entity, workout, workout_template, }; use enums::{ExerciseEquipment, ExerciseMuscle, UserToMediaReason}; use fitness_models::{UserToExerciseHistoryExtraInformation, UserWorkoutInput}; @@ -17,7 +17,7 @@ use media_models::{ }; use rust_decimal::Decimal; use schematic::Schematic; -use sea_orm::{DerivePartialModel, FromQueryResult}; +use sea_orm::FromQueryResult; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -234,19 +234,22 @@ pub struct UserWorkoutTemplateDetails { pub collections: Vec, } -#[derive(Debug, SimpleObject, Serialize, Deserialize, DerivePartialModel, FromQueryResult)] -#[sea_orm(entity = "DailyUserActivity")] -pub struct CoreFitnessAnalytics { - pub workout_reps: i32, - pub workout_weight: i32, - pub workout_distance: i32, - pub workout_rest_time: i32, - pub workout_personal_bests: i32, - pub workout_exercises: Vec, - pub workout_muscles: Vec, - pub workout_equipments: Vec, - #[graphql(skip)] - pub hour_records: Vec, +#[derive(Debug, SimpleObject, Serialize, Deserialize)] +pub struct FitnessAnalyticsExercise { + pub count: u32, + pub exercise: String, +} + +#[derive(Debug, SimpleObject, Serialize, Deserialize)] +pub struct FitnessAnalyticsMuscle { + pub count: u32, + pub muscle: ExerciseMuscle, +} + +#[derive(Debug, SimpleObject, Serialize, Deserialize)] +pub struct FitnessAnalyticsEquipment { + pub count: u32, + pub equipment: ExerciseEquipment, } #[derive(Debug, SimpleObject, Serialize, Deserialize, FromQueryResult)] @@ -257,6 +260,13 @@ pub struct FitnessAnalyticsHour { #[derive(Debug, SimpleObject, Serialize, Deserialize)] pub struct FitnessAnalytics { - pub core: CoreFitnessAnalytics, + pub workout_reps: i32, + pub workout_weight: i32, + pub workout_distance: i32, + pub workout_rest_time: i32, + pub workout_personal_bests: i32, pub hours: Vec, + pub workout_muscles: Vec, + pub workout_exercises: Vec, + pub workout_equipments: Vec, } diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index 375ba3be7d..e3cba0f38a 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -1,13 +1,14 @@ use std::{cmp::Reverse, fmt::Write, sync::Arc}; use async_graphql::Result; -use common_models::DateRangeInput; +use common_models::{DailyUserActivityHourRecord, DateRangeInput}; use database_models::{daily_user_activity, prelude::DailyUserActivity}; use database_utils::calculate_user_activities_and_summary; use dependent_models::{ - CoreFitnessAnalytics, DailyUserActivitiesResponse, FitnessAnalytics, FitnessAnalyticsHour, + DailyUserActivitiesResponse, FitnessAnalytics, FitnessAnalyticsEquipment, + FitnessAnalyticsExercise, FitnessAnalyticsHour, FitnessAnalyticsMuscle, }; -use enums::EntityLot; +use enums::{EntityLot, ExerciseEquipment, ExerciseMuscle}; use hashbag::HashBag; use itertools::Itertools; use media_models::{ @@ -16,7 +17,8 @@ use media_models::{ use sea_orm::{ prelude::Expr, sea_query::{Alias, Func}, - ColumnTrait, EntityTrait, Iden, QueryFilter, QueryOrder, QuerySelect, QueryTrait, + ColumnTrait, DerivePartialModel, EntityTrait, FromQueryResult, Iden, QueryFilter, QueryOrder, + QuerySelect, QueryTrait, }; use supporting_service::SupportingService; @@ -236,6 +238,19 @@ impl StatisticsService { user_id: &String, input: DateRangeInput, ) -> Result { + #[derive(Debug, DerivePartialModel, FromQueryResult)] + #[sea_orm(entity = "DailyUserActivity")] + pub struct CustomFitnessAnalytics { + pub workout_reps: i32, + pub workout_weight: i32, + pub workout_distance: i32, + pub workout_rest_time: i32, + pub workout_personal_bests: i32, + pub workout_exercises: Vec, + pub workout_muscles: Vec, + pub workout_equipments: Vec, + pub hour_records: Vec, + } let items = DailyUserActivity::find() .filter(daily_user_activity::Column::UserId.eq(user_id)) .apply_if(input.start_date, |query, v| { @@ -244,7 +259,7 @@ impl StatisticsService { .apply_if(input.end_date, |query, v| { query.filter(daily_user_activity::Column::Date.lte(v)) }) - .into_partial_model::() + .into_partial_model::() .all(&self.0.db) .await?; let mut hours_bag = HashBag::new(); @@ -265,6 +280,54 @@ impl StatisticsService { }) .sorted_by_key(|f| Reverse(f.count)) .collect_vec(); - todo!() + let workout_reps = items.iter().map(|i| i.workout_reps).sum(); + let workout_weight = items.iter().map(|i| i.workout_weight).sum(); + let workout_distance = items.iter().map(|i| i.workout_distance).sum(); + let workout_rest_time = items.iter().map(|i| i.workout_rest_time).sum(); + let workout_personal_bests = items.iter().map(|i| i.workout_personal_bests).sum(); + let workout_muscles = items + .iter() + .flat_map(|i| i.workout_muscles.clone()) + .collect::>() + .into_iter() + .map(|(muscle, count)| FitnessAnalyticsMuscle { + muscle, + count: count.try_into().unwrap(), + }) + .sorted_by_key(|f| Reverse(f.count)) + .collect_vec(); + let workout_exercises = items + .iter() + .flat_map(|i| i.workout_exercises.clone()) + .collect::>() + .into_iter() + .map(|(exercise_id, count)| FitnessAnalyticsExercise { + exercise: exercise_id, + count: count.try_into().unwrap(), + }) + .sorted_by_key(|f| Reverse(f.count)) + .collect_vec(); + let workout_equipments = items + .iter() + .flat_map(|i| i.workout_equipments.clone()) + .collect::>() + .into_iter() + .map(|(equipment, count)| FitnessAnalyticsEquipment { + equipment, + count: count.try_into().unwrap(), + }) + .sorted_by_key(|f| Reverse(f.count)) + .collect_vec(); + Ok(FitnessAnalytics { + hours, + workout_reps, + workout_weight, + workout_muscles, + workout_distance, + workout_rest_time, + workout_exercises, + workout_equipments, + workout_personal_bests, + }) } } diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts index 9b51faf8e2..d3b6a0a205 100644 --- a/libs/generated/src/graphql/backend/gql.ts +++ b/libs/generated/src/graphql/backend/gql.ts @@ -41,7 +41,7 @@ const documents = { "query UserWorkoutTemplateDetails($workoutTemplateId: String!) {\n userWorkoutTemplateDetails(workoutTemplateId: $workoutTemplateId) {\n collections {\n ...CollectionPart\n }\n details {\n id\n name\n createdOn\n visibility\n summary {\n ...WorkoutSummaryPart\n }\n information {\n ...WorkoutInformationPart\n }\n }\n }\n}": types.UserWorkoutTemplateDetailsDocument, "query UserWorkoutTemplatesList($input: SearchInput!) {\n userWorkoutTemplatesList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n id\n name\n createdOn\n visibility\n summary {\n ...WorkoutSummaryPart\n }\n }\n }\n}": types.UserWorkoutTemplatesListDocument, "query UserWorkoutsList($input: SearchInput!) {\n userWorkoutsList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n id\n name\n endTime\n duration\n startTime\n summary {\n ...WorkoutSummaryPart\n }\n }\n }\n}": types.UserWorkoutsListDocument, - "query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n hours {\n hour\n count\n }\n core {\n workoutReps\n workoutWeight\n workoutDistance\n workoutRestTime\n workoutPersonalBests\n workoutExercises\n workoutMuscles\n workoutEquipments\n }\n }\n}": types.GetOidcRedirectUrlDocument, + "query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n workoutReps\n workoutWeight\n workoutDistance\n workoutRestTime\n workoutPersonalBests\n hours {\n hour\n count\n }\n workoutExercises {\n count\n exercise\n }\n workoutMuscles {\n count\n muscle\n }\n workoutEquipments {\n count\n equipment\n }\n }\n}": types.GetOidcRedirectUrlDocument, "fragment SeenPodcastExtraInformationPart on SeenPodcastExtraInformation {\n episode\n}\n\nfragment SeenShowExtraInformationPart on SeenShowExtraInformation {\n episode\n season\n}\n\nfragment SeenAnimeExtraInformationPart on SeenAnimeExtraInformation {\n episode\n}\n\nfragment SeenMangaExtraInformationPart on SeenMangaExtraInformation {\n volume\n chapter\n}\n\nfragment CalendarEventPart on GraphqlCalendarEvent {\n date\n metadataId\n metadataLot\n episodeName\n metadataTitle\n metadataImage\n calendarEventId\n showExtraInformation {\n ...SeenShowExtraInformationPart\n }\n podcastExtraInformation {\n ...SeenPodcastExtraInformationPart\n }\n animeExtraInformation {\n ...SeenAnimeExtraInformationPart\n }\n}\n\nfragment SeenPart on Seen {\n id\n state\n progress\n reviewId\n startedOn\n finishedOn\n lastUpdatedOn\n manualTimeSpent\n numTimesUpdated\n providerWatchedOn\n showExtraInformation {\n ...SeenShowExtraInformationPart\n }\n podcastExtraInformation {\n ...SeenPodcastExtraInformationPart\n }\n animeExtraInformation {\n ...SeenAnimeExtraInformationPart\n }\n mangaExtraInformation {\n ...SeenMangaExtraInformationPart\n }\n}\n\nfragment MetadataSearchItemPart on MetadataSearchItem {\n title\n image\n identifier\n publishYear\n}\n\nfragment WorkoutOrExerciseTotalsPart on WorkoutOrExerciseTotals {\n reps\n weight\n distance\n duration\n restTime\n personalBestsAchieved\n}\n\nfragment EntityAssetsPart on EntityAssets {\n images\n videos\n}\n\nfragment WorkoutSetStatisticPart on WorkoutSetStatistic {\n reps\n pace\n oneRm\n weight\n volume\n duration\n distance\n}\n\nfragment WorkoutSetRecordPart on WorkoutSetRecord {\n lot\n personalBests\n statistic {\n ...WorkoutSetStatisticPart\n }\n}\n\nfragment WorkoutSummaryPart on WorkoutSummary {\n total {\n ...WorkoutOrExerciseTotalsPart\n }\n exercises {\n lot\n name\n numSets\n bestSet {\n ...WorkoutSetRecordPart\n }\n }\n focused {\n lots {\n lot\n exercises\n }\n levels {\n level\n exercises\n }\n forces {\n force\n exercises\n }\n muscles {\n muscle\n exercises\n }\n equipments {\n equipment\n exercises\n }\n }\n}\n\nfragment CollectionPart on Collection {\n id\n name\n userId\n}\n\nfragment ReviewItemPart on ReviewItem {\n id\n rating\n postedOn\n isSpoiler\n visibility\n textOriginal\n textRendered\n seenItemsAssociatedWith\n postedBy {\n id\n name\n }\n comments {\n id\n text\n likedBy\n createdOn\n user {\n id\n name\n }\n }\n showExtraInformation {\n ...SeenShowExtraInformationPart\n }\n podcastExtraInformation {\n ...SeenPodcastExtraInformationPart\n }\n animeExtraInformation {\n ...SeenAnimeExtraInformationPart\n }\n mangaExtraInformation {\n ...SeenMangaExtraInformationPart\n }\n}\n\nfragment WorkoutInformationPart on WorkoutInformation {\n comment\n assets {\n ...EntityAssetsPart\n }\n supersets {\n color\n exercises\n }\n exercises {\n lot\n name\n notes\n total {\n ...WorkoutOrExerciseTotalsPart\n }\n assets {\n ...EntityAssetsPart\n }\n sets {\n lot\n note\n restTime\n confirmedAt\n personalBests\n statistic {\n ...WorkoutSetStatisticPart\n }\n }\n }\n}\n\nfragment SetRestTimersPart on SetRestTimersSettings {\n drop\n warmup\n normal\n failure\n}": types.SeenPodcastExtraInformationPartFragmentDoc, }; @@ -170,7 +170,7 @@ export function graphql(source: "query UserWorkoutsList($input: SearchInput!) {\ /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n hours {\n hour\n count\n }\n core {\n workoutReps\n workoutWeight\n workoutDistance\n workoutRestTime\n workoutPersonalBests\n workoutExercises\n workoutMuscles\n workoutEquipments\n }\n }\n}"): (typeof documents)["query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n hours {\n hour\n count\n }\n core {\n workoutReps\n workoutWeight\n workoutDistance\n workoutRestTime\n workoutPersonalBests\n workoutExercises\n workoutMuscles\n workoutEquipments\n }\n }\n}"]; +export function graphql(source: "query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n workoutReps\n workoutWeight\n workoutDistance\n workoutRestTime\n workoutPersonalBests\n hours {\n hour\n count\n }\n workoutExercises {\n count\n exercise\n }\n workoutMuscles {\n count\n muscle\n }\n workoutEquipments {\n count\n equipment\n }\n }\n}"): (typeof documents)["query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n workoutReps\n workoutWeight\n workoutDistance\n workoutRestTime\n workoutPersonalBests\n hours {\n hour\n count\n }\n workoutExercises {\n count\n exercise\n }\n workoutMuscles {\n count\n muscle\n }\n workoutEquipments {\n count\n equipment\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts index 67c8e0aff6..8bbfd3cf77 100644 --- a/libs/generated/src/graphql/backend/graphql.ts +++ b/libs/generated/src/graphql/backend/graphql.ts @@ -234,17 +234,6 @@ export type CoreDetails = { websiteUrl: Scalars['String']['output']; }; -export type CoreFitnessAnalytics = { - workoutDistance: Scalars['Int']['output']; - workoutEquipments: Array; - workoutExercises: Array; - workoutMuscles: Array; - workoutPersonalBests: Scalars['Int']['output']; - workoutReps: Scalars['Int']['output']; - workoutRestTime: Scalars['Int']['output']; - workoutWeight: Scalars['Int']['output']; -}; - export type CreateAccessLinkInput = { expiresOn?: InputMaybe; isAccountDefault?: InputMaybe; @@ -665,8 +654,25 @@ export type ExternalIdentifiers = { }; export type FitnessAnalytics = { - core: CoreFitnessAnalytics; hours: Array; + workoutDistance: Scalars['Int']['output']; + workoutEquipments: Array; + workoutExercises: Array; + workoutMuscles: Array; + workoutPersonalBests: Scalars['Int']['output']; + workoutReps: Scalars['Int']['output']; + workoutRestTime: Scalars['Int']['output']; + workoutWeight: Scalars['Int']['output']; +}; + +export type FitnessAnalyticsEquipment = { + count: Scalars['Int']['output']; + equipment: ExerciseEquipment; +}; + +export type FitnessAnalyticsExercise = { + count: Scalars['Int']['output']; + exercise: Scalars['String']['output']; }; export type FitnessAnalyticsHour = { @@ -674,6 +680,11 @@ export type FitnessAnalyticsHour = { hour: Scalars['Int']['output']; }; +export type FitnessAnalyticsMuscle = { + count: Scalars['Int']['output']; + muscle: ExerciseMuscle; +}; + export type FrontendConfig = { /** A message to be displayed on the dashboard. */ dashboardMessage: Scalars['String']['output']; @@ -3422,7 +3433,7 @@ export type FitnessAnalyticsQueryVariables = Exact<{ }>; -export type FitnessAnalyticsQuery = { fitnessAnalytics: { hours: Array<{ hour: number, count: number }>, core: { workoutReps: number, workoutWeight: number, workoutDistance: number, workoutRestTime: number, workoutPersonalBests: number, workoutExercises: Array, workoutMuscles: Array, workoutEquipments: Array } } }; +export type FitnessAnalyticsQuery = { fitnessAnalytics: { workoutReps: number, workoutWeight: number, workoutDistance: number, workoutRestTime: number, workoutPersonalBests: number, hours: Array<{ hour: number, count: number }>, workoutExercises: Array<{ count: number, exercise: string }>, workoutMuscles: Array<{ count: number, muscle: ExerciseMuscle }>, workoutEquipments: Array<{ count: number, equipment: ExerciseEquipment }> } }; export type SeenPodcastExtraInformationPartFragment = { episode: number }; @@ -3566,4 +3577,4 @@ export const MetadataGroupsListDocument = {"kind":"Document","definitions":[{"ki export const PeopleListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PeopleList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PeopleListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"peopleList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"}}]}}]}}]} as unknown as DocumentNode; export const UserAccessLinksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserAccessLinks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userAccessLinks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isDemo"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"expiresOn"}},{"kind":"Field","name":{"kind":"Name","value":"timesUsed"}},{"kind":"Field","name":{"kind":"Name","value":"isRevoked"}},{"kind":"Field","name":{"kind":"Name","value":"maximumUses"}},{"kind":"Field","name":{"kind":"Name","value":"isAccountDefault"}},{"kind":"Field","name":{"kind":"Name","value":"isMutationAllowed"}}]}}]}}]} as unknown as DocumentNode; export const DailyUserActivitiesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DailyUserActivities"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DailyUserActivitiesInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dailyUserActivities"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groupedBy"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalDuration"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"day"}},{"kind":"Field","name":{"kind":"Name","value":"totalReviewCount"}},{"kind":"Field","name":{"kind":"Name","value":"workoutCount"}},{"kind":"Field","name":{"kind":"Name","value":"measurementCount"}},{"kind":"Field","name":{"kind":"Name","value":"audioBookCount"}},{"kind":"Field","name":{"kind":"Name","value":"animeCount"}},{"kind":"Field","name":{"kind":"Name","value":"bookCount"}},{"kind":"Field","name":{"kind":"Name","value":"podcastCount"}},{"kind":"Field","name":{"kind":"Name","value":"mangaCount"}},{"kind":"Field","name":{"kind":"Name","value":"showCount"}},{"kind":"Field","name":{"kind":"Name","value":"movieCount"}},{"kind":"Field","name":{"kind":"Name","value":"videoGameCount"}},{"kind":"Field","name":{"kind":"Name","value":"visualNovelCount"}}]}}]}}]}}]} as unknown as DocumentNode; -export const FitnessAnalyticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FitnessAnalytics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateRangeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fitnessAnalytics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hours"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hour"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"core"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutReps"}},{"kind":"Field","name":{"kind":"Name","value":"workoutWeight"}},{"kind":"Field","name":{"kind":"Name","value":"workoutDistance"}},{"kind":"Field","name":{"kind":"Name","value":"workoutRestTime"}},{"kind":"Field","name":{"kind":"Name","value":"workoutPersonalBests"}},{"kind":"Field","name":{"kind":"Name","value":"workoutExercises"}},{"kind":"Field","name":{"kind":"Name","value":"workoutMuscles"}},{"kind":"Field","name":{"kind":"Name","value":"workoutEquipments"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const FitnessAnalyticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FitnessAnalytics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateRangeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fitnessAnalytics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutReps"}},{"kind":"Field","name":{"kind":"Name","value":"workoutWeight"}},{"kind":"Field","name":{"kind":"Name","value":"workoutDistance"}},{"kind":"Field","name":{"kind":"Name","value":"workoutRestTime"}},{"kind":"Field","name":{"kind":"Name","value":"workoutPersonalBests"}},{"kind":"Field","name":{"kind":"Name","value":"hours"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hour"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workoutExercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"exercise"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workoutMuscles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workoutEquipments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"equipment"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/libs/generated/src/graphql/backend/types.generated.ts b/libs/generated/src/graphql/backend/types.generated.ts index c569206e59..3cc14b23dd 100644 --- a/libs/generated/src/graphql/backend/types.generated.ts +++ b/libs/generated/src/graphql/backend/types.generated.ts @@ -219,18 +219,6 @@ export type CoreDetails = { websiteUrl: Scalars['String']['output']; }; -export type CoreFitnessAnalytics = { - __typename?: 'CoreFitnessAnalytics'; - workoutDistance: Scalars['Int']['output']; - workoutEquipments: Array; - workoutExercises: Array; - workoutMuscles: Array; - workoutPersonalBests: Scalars['Int']['output']; - workoutReps: Scalars['Int']['output']; - workoutRestTime: Scalars['Int']['output']; - workoutWeight: Scalars['Int']['output']; -}; - export type CreateAccessLinkInput = { expiresOn?: InputMaybe; isAccountDefault?: InputMaybe; @@ -666,8 +654,27 @@ export type ExternalIdentifiers = { export type FitnessAnalytics = { __typename?: 'FitnessAnalytics'; - core: CoreFitnessAnalytics; hours: Array; + workoutDistance: Scalars['Int']['output']; + workoutEquipments: Array; + workoutExercises: Array; + workoutMuscles: Array; + workoutPersonalBests: Scalars['Int']['output']; + workoutReps: Scalars['Int']['output']; + workoutRestTime: Scalars['Int']['output']; + workoutWeight: Scalars['Int']['output']; +}; + +export type FitnessAnalyticsEquipment = { + __typename?: 'FitnessAnalyticsEquipment'; + count: Scalars['Int']['output']; + equipment: ExerciseEquipment; +}; + +export type FitnessAnalyticsExercise = { + __typename?: 'FitnessAnalyticsExercise'; + count: Scalars['Int']['output']; + exercise: Scalars['String']['output']; }; export type FitnessAnalyticsHour = { @@ -676,6 +683,12 @@ export type FitnessAnalyticsHour = { hour: Scalars['Int']['output']; }; +export type FitnessAnalyticsMuscle = { + __typename?: 'FitnessAnalyticsMuscle'; + count: Scalars['Int']['output']; + muscle: ExerciseMuscle; +}; + export type FrontendConfig = { __typename?: 'FrontendConfig'; /** A message to be displayed on the dashboard. */ diff --git a/libs/graphql/src/backend/queries/combined.gql b/libs/graphql/src/backend/queries/combined.gql index cf63d9ea8e..7a2c1711df 100644 --- a/libs/graphql/src/backend/queries/combined.gql +++ b/libs/graphql/src/backend/queries/combined.gql @@ -181,19 +181,26 @@ query DailyUserActivities($input: DailyUserActivitiesInput!) { query FitnessAnalytics($input: DateRangeInput!) { fitnessAnalytics(input: $input) { + workoutReps + workoutWeight + workoutDistance + workoutRestTime + workoutPersonalBests hours { hour count } - core { - workoutReps - workoutWeight - workoutDistance - workoutRestTime - workoutPersonalBests - workoutExercises - workoutMuscles - workoutEquipments + workoutExercises { + count + exercise + } + workoutMuscles { + count + muscle + } + workoutEquipments { + count + equipment } } } From e3c05ba3227718ead03fb4da22a4519eb09d5b59 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 07:10:18 +0530 Subject: [PATCH 019/233] feat(backend): compute additional fields --- crates/models/dependent/src/lib.rs | 2 ++ crates/services/statistics/src/lib.rs | 6 ++++++ libs/generated/src/graphql/backend/gql.ts | 4 ++-- libs/generated/src/graphql/backend/graphql.ts | 6 ++++-- libs/generated/src/graphql/backend/types.generated.ts | 2 ++ libs/graphql/src/backend/queries/combined.gql | 2 ++ 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/models/dependent/src/lib.rs b/crates/models/dependent/src/lib.rs index daecad111f..04e96216c9 100644 --- a/crates/models/dependent/src/lib.rs +++ b/crates/models/dependent/src/lib.rs @@ -262,8 +262,10 @@ pub struct FitnessAnalyticsHour { pub struct FitnessAnalytics { pub workout_reps: i32, pub workout_weight: i32, + pub workout_count: i32, pub workout_distance: i32, pub workout_rest_time: i32, + pub measurement_count: i32, pub workout_personal_bests: i32, pub hours: Vec, pub workout_muscles: Vec, diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index e3cba0f38a..711b6f8450 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -242,9 +242,11 @@ impl StatisticsService { #[sea_orm(entity = "DailyUserActivity")] pub struct CustomFitnessAnalytics { pub workout_reps: i32, + pub workout_count: i32, pub workout_weight: i32, pub workout_distance: i32, pub workout_rest_time: i32, + pub measurement_count: i32, pub workout_personal_bests: i32, pub workout_exercises: Vec, pub workout_muscles: Vec, @@ -281,9 +283,11 @@ impl StatisticsService { .sorted_by_key(|f| Reverse(f.count)) .collect_vec(); let workout_reps = items.iter().map(|i| i.workout_reps).sum(); + let workout_count = items.iter().map(|i| i.workout_count).sum(); let workout_weight = items.iter().map(|i| i.workout_weight).sum(); let workout_distance = items.iter().map(|i| i.workout_distance).sum(); let workout_rest_time = items.iter().map(|i| i.workout_rest_time).sum(); + let measurement_count = items.iter().map(|i| i.measurement_count).sum(); let workout_personal_bests = items.iter().map(|i| i.workout_personal_bests).sum(); let workout_muscles = items .iter() @@ -321,11 +325,13 @@ impl StatisticsService { Ok(FitnessAnalytics { hours, workout_reps, + workout_count, workout_weight, workout_muscles, workout_distance, workout_rest_time, workout_exercises, + measurement_count, workout_equipments, workout_personal_bests, }) diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts index d3b6a0a205..e99898cf26 100644 --- a/libs/generated/src/graphql/backend/gql.ts +++ b/libs/generated/src/graphql/backend/gql.ts @@ -41,7 +41,7 @@ const documents = { "query UserWorkoutTemplateDetails($workoutTemplateId: String!) {\n userWorkoutTemplateDetails(workoutTemplateId: $workoutTemplateId) {\n collections {\n ...CollectionPart\n }\n details {\n id\n name\n createdOn\n visibility\n summary {\n ...WorkoutSummaryPart\n }\n information {\n ...WorkoutInformationPart\n }\n }\n }\n}": types.UserWorkoutTemplateDetailsDocument, "query UserWorkoutTemplatesList($input: SearchInput!) {\n userWorkoutTemplatesList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n id\n name\n createdOn\n visibility\n summary {\n ...WorkoutSummaryPart\n }\n }\n }\n}": types.UserWorkoutTemplatesListDocument, "query UserWorkoutsList($input: SearchInput!) {\n userWorkoutsList(input: $input) {\n details {\n total\n nextPage\n }\n items {\n id\n name\n endTime\n duration\n startTime\n summary {\n ...WorkoutSummaryPart\n }\n }\n }\n}": types.UserWorkoutsListDocument, - "query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n workoutReps\n workoutWeight\n workoutDistance\n workoutRestTime\n workoutPersonalBests\n hours {\n hour\n count\n }\n workoutExercises {\n count\n exercise\n }\n workoutMuscles {\n count\n muscle\n }\n workoutEquipments {\n count\n equipment\n }\n }\n}": types.GetOidcRedirectUrlDocument, + "query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n workoutReps\n workoutCount\n workoutWeight\n workoutDistance\n workoutRestTime\n measurementCount\n workoutPersonalBests\n hours {\n hour\n count\n }\n workoutExercises {\n count\n exercise\n }\n workoutMuscles {\n count\n muscle\n }\n workoutEquipments {\n count\n equipment\n }\n }\n}": types.GetOidcRedirectUrlDocument, "fragment SeenPodcastExtraInformationPart on SeenPodcastExtraInformation {\n episode\n}\n\nfragment SeenShowExtraInformationPart on SeenShowExtraInformation {\n episode\n season\n}\n\nfragment SeenAnimeExtraInformationPart on SeenAnimeExtraInformation {\n episode\n}\n\nfragment SeenMangaExtraInformationPart on SeenMangaExtraInformation {\n volume\n chapter\n}\n\nfragment CalendarEventPart on GraphqlCalendarEvent {\n date\n metadataId\n metadataLot\n episodeName\n metadataTitle\n metadataImage\n calendarEventId\n showExtraInformation {\n ...SeenShowExtraInformationPart\n }\n podcastExtraInformation {\n ...SeenPodcastExtraInformationPart\n }\n animeExtraInformation {\n ...SeenAnimeExtraInformationPart\n }\n}\n\nfragment SeenPart on Seen {\n id\n state\n progress\n reviewId\n startedOn\n finishedOn\n lastUpdatedOn\n manualTimeSpent\n numTimesUpdated\n providerWatchedOn\n showExtraInformation {\n ...SeenShowExtraInformationPart\n }\n podcastExtraInformation {\n ...SeenPodcastExtraInformationPart\n }\n animeExtraInformation {\n ...SeenAnimeExtraInformationPart\n }\n mangaExtraInformation {\n ...SeenMangaExtraInformationPart\n }\n}\n\nfragment MetadataSearchItemPart on MetadataSearchItem {\n title\n image\n identifier\n publishYear\n}\n\nfragment WorkoutOrExerciseTotalsPart on WorkoutOrExerciseTotals {\n reps\n weight\n distance\n duration\n restTime\n personalBestsAchieved\n}\n\nfragment EntityAssetsPart on EntityAssets {\n images\n videos\n}\n\nfragment WorkoutSetStatisticPart on WorkoutSetStatistic {\n reps\n pace\n oneRm\n weight\n volume\n duration\n distance\n}\n\nfragment WorkoutSetRecordPart on WorkoutSetRecord {\n lot\n personalBests\n statistic {\n ...WorkoutSetStatisticPart\n }\n}\n\nfragment WorkoutSummaryPart on WorkoutSummary {\n total {\n ...WorkoutOrExerciseTotalsPart\n }\n exercises {\n lot\n name\n numSets\n bestSet {\n ...WorkoutSetRecordPart\n }\n }\n focused {\n lots {\n lot\n exercises\n }\n levels {\n level\n exercises\n }\n forces {\n force\n exercises\n }\n muscles {\n muscle\n exercises\n }\n equipments {\n equipment\n exercises\n }\n }\n}\n\nfragment CollectionPart on Collection {\n id\n name\n userId\n}\n\nfragment ReviewItemPart on ReviewItem {\n id\n rating\n postedOn\n isSpoiler\n visibility\n textOriginal\n textRendered\n seenItemsAssociatedWith\n postedBy {\n id\n name\n }\n comments {\n id\n text\n likedBy\n createdOn\n user {\n id\n name\n }\n }\n showExtraInformation {\n ...SeenShowExtraInformationPart\n }\n podcastExtraInformation {\n ...SeenPodcastExtraInformationPart\n }\n animeExtraInformation {\n ...SeenAnimeExtraInformationPart\n }\n mangaExtraInformation {\n ...SeenMangaExtraInformationPart\n }\n}\n\nfragment WorkoutInformationPart on WorkoutInformation {\n comment\n assets {\n ...EntityAssetsPart\n }\n supersets {\n color\n exercises\n }\n exercises {\n lot\n name\n notes\n total {\n ...WorkoutOrExerciseTotalsPart\n }\n assets {\n ...EntityAssetsPart\n }\n sets {\n lot\n note\n restTime\n confirmedAt\n personalBests\n statistic {\n ...WorkoutSetStatisticPart\n }\n }\n }\n}\n\nfragment SetRestTimersPart on SetRestTimersSettings {\n drop\n warmup\n normal\n failure\n}": types.SeenPodcastExtraInformationPartFragmentDoc, }; @@ -170,7 +170,7 @@ export function graphql(source: "query UserWorkoutsList($input: SearchInput!) {\ /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n workoutReps\n workoutWeight\n workoutDistance\n workoutRestTime\n workoutPersonalBests\n hours {\n hour\n count\n }\n workoutExercises {\n count\n exercise\n }\n workoutMuscles {\n count\n muscle\n }\n workoutEquipments {\n count\n equipment\n }\n }\n}"): (typeof documents)["query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n workoutReps\n workoutWeight\n workoutDistance\n workoutRestTime\n workoutPersonalBests\n hours {\n hour\n count\n }\n workoutExercises {\n count\n exercise\n }\n workoutMuscles {\n count\n muscle\n }\n workoutEquipments {\n count\n equipment\n }\n }\n}"]; +export function graphql(source: "query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n workoutReps\n workoutCount\n workoutWeight\n workoutDistance\n workoutRestTime\n measurementCount\n workoutPersonalBests\n hours {\n hour\n count\n }\n workoutExercises {\n count\n exercise\n }\n workoutMuscles {\n count\n muscle\n }\n workoutEquipments {\n count\n equipment\n }\n }\n}"): (typeof documents)["query GetOidcRedirectUrl {\n getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n getOidcToken(code: $code) {\n subject\n email\n }\n}\n\nquery GetPresignedS3Url($key: String!) {\n getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n providersLanguageInformation {\n supported\n default\n source\n }\n}\n\nquery UserExports {\n userExports {\n url\n size\n endedAt\n startedAt\n }\n}\n\nquery UserCollectionsList($name: String) {\n userCollectionsList(name: $name) {\n id\n name\n count\n isDefault\n description\n creator {\n id\n name\n }\n collaborators {\n id\n name\n }\n informationTemplate {\n lot\n name\n required\n description\n defaultValue\n }\n }\n}\n\nquery UserIntegrations {\n userIntegrations {\n id\n lot\n provider\n createdOn\n isDisabled\n maximumProgress\n minimumProgress\n lastTriggeredOn\n syncToOwnedCollection\n }\n}\n\nquery UserNotificationPlatforms {\n userNotificationPlatforms {\n id\n lot\n createdOn\n isDisabled\n description\n }\n}\n\nquery UsersList($query: String) {\n usersList(query: $query) {\n id\n lot\n name\n isDisabled\n }\n}\n\nquery UserRecommendations {\n userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n userUpcomingCalendarEvents(input: $input) {\n ...CalendarEventPart\n }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n userCalendarEvents(input: $input) {\n date\n events {\n ...CalendarEventPart\n }\n }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n metadataPartialDetails(metadataId: $metadataId) {\n id\n lot\n title\n image\n publishYear\n }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n metadataGroupsList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n peopleList(input: $input) {\n details {\n total\n nextPage\n }\n items\n }\n}\n\nquery UserAccessLinks {\n userAccessLinks {\n id\n name\n isDemo\n createdOn\n expiresOn\n timesUsed\n isRevoked\n maximumUses\n isAccountDefault\n isMutationAllowed\n }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n dailyUserActivities(input: $input) {\n groupedBy\n totalCount\n totalDuration\n items {\n day\n totalReviewCount\n workoutCount\n measurementCount\n audioBookCount\n animeCount\n bookCount\n podcastCount\n mangaCount\n showCount\n movieCount\n videoGameCount\n visualNovelCount\n }\n }\n}\n\nquery FitnessAnalytics($input: DateRangeInput!) {\n fitnessAnalytics(input: $input) {\n workoutReps\n workoutCount\n workoutWeight\n workoutDistance\n workoutRestTime\n measurementCount\n workoutPersonalBests\n hours {\n hour\n count\n }\n workoutExercises {\n count\n exercise\n }\n workoutMuscles {\n count\n muscle\n }\n workoutEquipments {\n count\n equipment\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts index 8bbfd3cf77..8c03b0e73b 100644 --- a/libs/generated/src/graphql/backend/graphql.ts +++ b/libs/generated/src/graphql/backend/graphql.ts @@ -655,6 +655,8 @@ export type ExternalIdentifiers = { export type FitnessAnalytics = { hours: Array; + measurementCount: Scalars['Int']['output']; + workoutCount: Scalars['Int']['output']; workoutDistance: Scalars['Int']['output']; workoutEquipments: Array; workoutExercises: Array; @@ -3433,7 +3435,7 @@ export type FitnessAnalyticsQueryVariables = Exact<{ }>; -export type FitnessAnalyticsQuery = { fitnessAnalytics: { workoutReps: number, workoutWeight: number, workoutDistance: number, workoutRestTime: number, workoutPersonalBests: number, hours: Array<{ hour: number, count: number }>, workoutExercises: Array<{ count: number, exercise: string }>, workoutMuscles: Array<{ count: number, muscle: ExerciseMuscle }>, workoutEquipments: Array<{ count: number, equipment: ExerciseEquipment }> } }; +export type FitnessAnalyticsQuery = { fitnessAnalytics: { workoutReps: number, workoutCount: number, workoutWeight: number, workoutDistance: number, workoutRestTime: number, measurementCount: number, workoutPersonalBests: number, hours: Array<{ hour: number, count: number }>, workoutExercises: Array<{ count: number, exercise: string }>, workoutMuscles: Array<{ count: number, muscle: ExerciseMuscle }>, workoutEquipments: Array<{ count: number, equipment: ExerciseEquipment }> } }; export type SeenPodcastExtraInformationPartFragment = { episode: number }; @@ -3577,4 +3579,4 @@ export const MetadataGroupsListDocument = {"kind":"Document","definitions":[{"ki export const PeopleListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PeopleList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PeopleListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"peopleList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"}}]}}]}}]} as unknown as DocumentNode; export const UserAccessLinksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserAccessLinks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userAccessLinks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isDemo"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"expiresOn"}},{"kind":"Field","name":{"kind":"Name","value":"timesUsed"}},{"kind":"Field","name":{"kind":"Name","value":"isRevoked"}},{"kind":"Field","name":{"kind":"Name","value":"maximumUses"}},{"kind":"Field","name":{"kind":"Name","value":"isAccountDefault"}},{"kind":"Field","name":{"kind":"Name","value":"isMutationAllowed"}}]}}]}}]} as unknown as DocumentNode; export const DailyUserActivitiesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DailyUserActivities"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DailyUserActivitiesInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dailyUserActivities"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groupedBy"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalDuration"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"day"}},{"kind":"Field","name":{"kind":"Name","value":"totalReviewCount"}},{"kind":"Field","name":{"kind":"Name","value":"workoutCount"}},{"kind":"Field","name":{"kind":"Name","value":"measurementCount"}},{"kind":"Field","name":{"kind":"Name","value":"audioBookCount"}},{"kind":"Field","name":{"kind":"Name","value":"animeCount"}},{"kind":"Field","name":{"kind":"Name","value":"bookCount"}},{"kind":"Field","name":{"kind":"Name","value":"podcastCount"}},{"kind":"Field","name":{"kind":"Name","value":"mangaCount"}},{"kind":"Field","name":{"kind":"Name","value":"showCount"}},{"kind":"Field","name":{"kind":"Name","value":"movieCount"}},{"kind":"Field","name":{"kind":"Name","value":"videoGameCount"}},{"kind":"Field","name":{"kind":"Name","value":"visualNovelCount"}}]}}]}}]}}]} as unknown as DocumentNode; -export const FitnessAnalyticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FitnessAnalytics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateRangeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fitnessAnalytics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutReps"}},{"kind":"Field","name":{"kind":"Name","value":"workoutWeight"}},{"kind":"Field","name":{"kind":"Name","value":"workoutDistance"}},{"kind":"Field","name":{"kind":"Name","value":"workoutRestTime"}},{"kind":"Field","name":{"kind":"Name","value":"workoutPersonalBests"}},{"kind":"Field","name":{"kind":"Name","value":"hours"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hour"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workoutExercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"exercise"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workoutMuscles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workoutEquipments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"equipment"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const FitnessAnalyticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FitnessAnalytics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateRangeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fitnessAnalytics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutReps"}},{"kind":"Field","name":{"kind":"Name","value":"workoutCount"}},{"kind":"Field","name":{"kind":"Name","value":"workoutWeight"}},{"kind":"Field","name":{"kind":"Name","value":"workoutDistance"}},{"kind":"Field","name":{"kind":"Name","value":"workoutRestTime"}},{"kind":"Field","name":{"kind":"Name","value":"measurementCount"}},{"kind":"Field","name":{"kind":"Name","value":"workoutPersonalBests"}},{"kind":"Field","name":{"kind":"Name","value":"hours"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hour"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workoutExercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"exercise"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workoutMuscles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workoutEquipments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"equipment"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/libs/generated/src/graphql/backend/types.generated.ts b/libs/generated/src/graphql/backend/types.generated.ts index 3cc14b23dd..3653c40783 100644 --- a/libs/generated/src/graphql/backend/types.generated.ts +++ b/libs/generated/src/graphql/backend/types.generated.ts @@ -655,6 +655,8 @@ export type ExternalIdentifiers = { export type FitnessAnalytics = { __typename?: 'FitnessAnalytics'; hours: Array; + measurementCount: Scalars['Int']['output']; + workoutCount: Scalars['Int']['output']; workoutDistance: Scalars['Int']['output']; workoutEquipments: Array; workoutExercises: Array; diff --git a/libs/graphql/src/backend/queries/combined.gql b/libs/graphql/src/backend/queries/combined.gql index 7a2c1711df..e7319d5c67 100644 --- a/libs/graphql/src/backend/queries/combined.gql +++ b/libs/graphql/src/backend/queries/combined.gql @@ -182,9 +182,11 @@ query DailyUserActivities($input: DailyUserActivitiesInput!) { query FitnessAnalytics($input: DateRangeInput!) { fitnessAnalytics(input: $input) { workoutReps + workoutCount workoutWeight workoutDistance workoutRestTime + measurementCount workoutPersonalBests hours { hour From 7885b14d277459042fa955e0ee338fe29674a7ec Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 07:35:53 +0530 Subject: [PATCH 020/233] feat(migrations): add new column to store value --- crates/migrations/src/m20241004_create_application_cache.rs | 2 ++ crates/migrations/src/m20241124_changes_for_issue_1113.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/crates/migrations/src/m20241004_create_application_cache.rs b/crates/migrations/src/m20241004_create_application_cache.rs index 1fb661360e..3f21da02b4 100644 --- a/crates/migrations/src/m20241004_create_application_cache.rs +++ b/crates/migrations/src/m20241004_create_application_cache.rs @@ -8,6 +8,7 @@ pub enum ApplicationCache { Table, Id, Key, + Value, ExpiresAt, CreatedAt, } @@ -39,6 +40,7 @@ impl MigrationTrait for Migration { .unique_key(), ) .col(ColumnDef::new(ApplicationCache::ExpiresAt).timestamp_with_time_zone()) + .col(ColumnDef::new(ApplicationCache::Value).json_binary()) .to_owned(), ) .await?; diff --git a/crates/migrations/src/m20241124_changes_for_issue_1113.rs b/crates/migrations/src/m20241124_changes_for_issue_1113.rs index 72e7ee4a1e..07841b4ad4 100644 --- a/crates/migrations/src/m20241124_changes_for_issue_1113.rs +++ b/crates/migrations/src/m20241124_changes_for_issue_1113.rs @@ -41,6 +41,10 @@ END $$; "#, ) .await?; + if !manager.has_column("application_cache", "value").await? { + db.execute_unprepared(r#"ALTER TABLE "application_cache" ADD COLUMN "value" JSONB;"#) + .await?; + } Ok(()) } From d41fa6471a35f1ae09ba40878c4101552a7313a6 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 07:38:03 +0530 Subject: [PATCH 021/233] feat(backend): allow saving value in application cache --- Cargo.lock | 1 - crates/models/common/src/lib.rs | 57 ++++++++++++++++++- .../models/database/src/application_cache.rs | 4 +- crates/models/dependent/Cargo.toml | 1 - crates/models/dependent/src/lib.rs | 42 +------------- crates/resolvers/statistics/src/lib.rs | 4 +- crates/services/cache/src/lib.rs | 11 ++-- crates/services/statistics/src/lib.rs | 10 ++-- crates/utils/dependent/src/lib.rs | 2 +- 9 files changed, 75 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d9cb14d04..820a4a4081 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1733,7 +1733,6 @@ dependencies = [ "media-models", "rust_decimal", "schematic", - "sea-orm", "serde", "serde_with 3.11.0", ] diff --git a/crates/models/common/src/lib.rs b/crates/models/common/src/lib.rs index a7261006b0..bd70db579e 100644 --- a/crates/models/common/src/lib.rs +++ b/crates/models/common/src/lib.rs @@ -2,7 +2,7 @@ use async_graphql::{Enum, InputObject, SimpleObject}; use chrono::NaiveDate; use educe::Educe; use enum_meta::{meta, Meta}; -use enums::{EntityLot, MediaLot}; +use enums::{EntityLot, ExerciseEquipment, ExerciseMuscle, MediaLot}; use rust_decimal::Decimal; use schematic::{ConfigEnum, Schematic}; use sea_orm::{prelude::DateTimeUtc, FromJsonQueryResult}; @@ -241,3 +241,58 @@ pub struct DateRangeInput { pub end_date: Option, pub start_date: Option, } + +#[derive( + Debug, SimpleObject, Serialize, Deserialize, FromJsonQueryResult, Clone, Eq, PartialEq, +)] +pub struct FitnessAnalyticsExercise { + pub count: u32, + pub exercise: String, +} + +#[derive( + Debug, SimpleObject, Serialize, Deserialize, FromJsonQueryResult, Clone, Eq, PartialEq, +)] +pub struct FitnessAnalyticsMuscle { + pub count: u32, + pub muscle: ExerciseMuscle, +} + +#[derive( + Debug, SimpleObject, Serialize, Deserialize, FromJsonQueryResult, Clone, Eq, PartialEq, +)] +pub struct FitnessAnalyticsEquipment { + pub count: u32, + pub equipment: ExerciseEquipment, +} + +#[derive( + Debug, SimpleObject, Serialize, Deserialize, FromJsonQueryResult, Clone, Eq, PartialEq, +)] +pub struct FitnessAnalyticsHour { + pub hour: u32, + pub count: u32, +} + +#[derive( + Debug, SimpleObject, Serialize, Deserialize, FromJsonQueryResult, Clone, Eq, PartialEq, +)] +pub struct FitnessAnalytics { + pub workout_reps: i32, + pub workout_weight: i32, + pub workout_count: i32, + pub workout_distance: i32, + pub workout_rest_time: i32, + pub measurement_count: i32, + pub workout_personal_bests: i32, + pub hours: Vec, + pub workout_muscles: Vec, + pub workout_exercises: Vec, + pub workout_equipments: Vec, +} + +#[skip_serializing_none] +#[derive(Clone, Debug, PartialEq, FromJsonQueryResult, Serialize, Deserialize, Eq)] +pub enum ApplicationCacheValue { + FitnessAnalytics(FitnessAnalytics), +} diff --git a/crates/models/database/src/application_cache.rs b/crates/models/database/src/application_cache.rs index a3d1249a59..30742081f1 100644 --- a/crates/models/database/src/application_cache.rs +++ b/crates/models/database/src/application_cache.rs @@ -1,6 +1,6 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1 -use common_models::ApplicationCacheKey; +use common_models::{ApplicationCacheKey, ApplicationCacheValue}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -11,6 +11,8 @@ pub struct Model { pub created_at: DateTimeUtc, #[sea_orm(column_type = "Json")] pub key: ApplicationCacheKey, + #[sea_orm(column_type = "Json")] + pub value: Option, pub expires_at: Option, } diff --git a/crates/models/dependent/Cargo.toml b/crates/models/dependent/Cargo.toml index b2b26730eb..21a3c07004 100644 --- a/crates/models/dependent/Cargo.toml +++ b/crates/models/dependent/Cargo.toml @@ -14,6 +14,5 @@ media-models = { path = "../media" } fitness-models = { path = "../fitness" } rust_decimal = { workspace = true } schematic = { workspace = true } -sea-orm = { workspace = true } serde = { workspace = true } serde_with = { workspace = true } diff --git a/crates/models/dependent/src/lib.rs b/crates/models/dependent/src/lib.rs index 04e96216c9..0658ceebe1 100644 --- a/crates/models/dependent/src/lib.rs +++ b/crates/models/dependent/src/lib.rs @@ -5,7 +5,7 @@ use database_models::{ collection, exercise, metadata, metadata_group, person, seen, user, user_measurement, user_to_entity, workout, workout_template, }; -use enums::{ExerciseEquipment, ExerciseMuscle, UserToMediaReason}; +use enums::UserToMediaReason; use fitness_models::{UserToExerciseHistoryExtraInformation, UserWorkoutInput}; use importer_models::ImportFailedItem; use media_models::{ @@ -17,7 +17,6 @@ use media_models::{ }; use rust_decimal::Decimal; use schematic::Schematic; -use sea_orm::FromQueryResult; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -233,42 +232,3 @@ pub struct UserWorkoutTemplateDetails { pub details: workout_template::Model, pub collections: Vec, } - -#[derive(Debug, SimpleObject, Serialize, Deserialize)] -pub struct FitnessAnalyticsExercise { - pub count: u32, - pub exercise: String, -} - -#[derive(Debug, SimpleObject, Serialize, Deserialize)] -pub struct FitnessAnalyticsMuscle { - pub count: u32, - pub muscle: ExerciseMuscle, -} - -#[derive(Debug, SimpleObject, Serialize, Deserialize)] -pub struct FitnessAnalyticsEquipment { - pub count: u32, - pub equipment: ExerciseEquipment, -} - -#[derive(Debug, SimpleObject, Serialize, Deserialize, FromQueryResult)] -pub struct FitnessAnalyticsHour { - pub hour: u32, - pub count: u32, -} - -#[derive(Debug, SimpleObject, Serialize, Deserialize)] -pub struct FitnessAnalytics { - pub workout_reps: i32, - pub workout_weight: i32, - pub workout_count: i32, - pub workout_distance: i32, - pub workout_rest_time: i32, - pub measurement_count: i32, - pub workout_personal_bests: i32, - pub hours: Vec, - pub workout_muscles: Vec, - pub workout_exercises: Vec, - pub workout_equipments: Vec, -} diff --git a/crates/resolvers/statistics/src/lib.rs b/crates/resolvers/statistics/src/lib.rs index 00ec4ea056..05db23c8ad 100644 --- a/crates/resolvers/statistics/src/lib.rs +++ b/crates/resolvers/statistics/src/lib.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use async_graphql::{Context, Object, Result}; -use common_models::DateRangeInput; -use dependent_models::{DailyUserActivitiesResponse, FitnessAnalytics}; +use common_models::{DateRangeInput, FitnessAnalytics}; +use dependent_models::DailyUserActivitiesResponse; use media_models::{DailyUserActivitiesInput, DailyUserActivityItem}; use statistics_service::StatisticsService; use traits::AuthProvider; diff --git a/crates/services/cache/src/lib.rs b/crates/services/cache/src/lib.rs index 1614fc12e3..460c2ddd52 100644 --- a/crates/services/cache/src/lib.rs +++ b/crates/services/cache/src/lib.rs @@ -1,6 +1,6 @@ use async_graphql::Result; use chrono::{Duration, Utc}; -use common_models::ApplicationCacheKey; +use common_models::{ApplicationCacheKey, ApplicationCacheValue}; use common_utils::ryot_log; use database_models::{application_cache, prelude::ApplicationCache}; use sea_orm::{ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; @@ -22,18 +22,21 @@ impl CacheService { &self, key: ApplicationCacheKey, expiry_hours: i64, + value: Option, ) -> Result { let now = Utc::now(); let to_insert = application_cache::ActiveModel { key: ActiveValue::Set(key), - expires_at: ActiveValue::Set(Some(now + Duration::hours(expiry_hours))), + value: ActiveValue::Set(value), created_at: ActiveValue::Set(now), + expires_at: ActiveValue::Set(Some(now + Duration::hours(expiry_hours))), ..Default::default() }; let inserted = ApplicationCache::insert(to_insert) .on_conflict( OnConflict::column(application_cache::Column::Key) .update_columns([ + application_cache::Column::Value, application_cache::Column::ExpiresAt, application_cache::Column::CreatedAt, ]) @@ -46,7 +49,7 @@ impl CacheService { Ok(insert_id) } - pub async fn get(&self, key: ApplicationCacheKey) -> Result> { + pub async fn get(&self, key: ApplicationCacheKey) -> Result> { let cache = ApplicationCache::find() .filter(application_cache::Column::Key.eq(key)) .one(&self.db) @@ -57,7 +60,7 @@ impl CacheService { .expires_at .map_or(false, |expires_at| expires_at > Utc::now()) }) - .map(|_| ())) + .and_then(|m| m.value)) } pub async fn delete(&self, key: ApplicationCacheKey) -> Result { diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index 711b6f8450..f004e6eb3c 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -1,13 +1,13 @@ use std::{cmp::Reverse, fmt::Write, sync::Arc}; use async_graphql::Result; -use common_models::{DailyUserActivityHourRecord, DateRangeInput}; -use database_models::{daily_user_activity, prelude::DailyUserActivity}; -use database_utils::calculate_user_activities_and_summary; -use dependent_models::{ - DailyUserActivitiesResponse, FitnessAnalytics, FitnessAnalyticsEquipment, +use common_models::{ + DailyUserActivityHourRecord, DateRangeInput, FitnessAnalytics, FitnessAnalyticsEquipment, FitnessAnalyticsExercise, FitnessAnalyticsHour, FitnessAnalyticsMuscle, }; +use database_models::{daily_user_activity, prelude::DailyUserActivity}; +use database_utils::calculate_user_activities_and_summary; +use dependent_models::DailyUserActivitiesResponse; use enums::{EntityLot, ExerciseEquipment, ExerciseMuscle}; use hashbag::HashBag; use itertools::Itertools; diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs index 9e9a01ccb5..7ffc753293 100644 --- a/crates/utils/dependent/src/lib.rs +++ b/crates/utils/dependent/src/lib.rs @@ -1479,7 +1479,7 @@ pub async fn progress_update( let id = seen.id.clone(); if seen.state == SeenState::Completed && respect_cache { ss.cache_service - .set_with_expiry(cache, ss.config.server.progress_update_threshold) + .set_with_expiry(cache, ss.config.server.progress_update_threshold, None) .await?; } if seen.state == SeenState::Completed { From 6bd9e6967728473902ce23a9fd5abb59510e10fd Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 07:45:07 +0530 Subject: [PATCH 022/233] feat(backend): cache fitness analytics for 2 hours --- crates/models/common/src/lib.rs | 6 +++++- crates/services/statistics/src/lib.rs | 27 +++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/crates/models/common/src/lib.rs b/crates/models/common/src/lib.rs index bd70db579e..e4a2061ffc 100644 --- a/crates/models/common/src/lib.rs +++ b/crates/models/common/src/lib.rs @@ -215,6 +215,10 @@ pub enum ApplicationCacheKey { manga_chapter_number: Option, manga_volume_number: Option, }, + FitnessAnalytics { + user_id: String, + date_range: DateRangeInput, + }, } #[derive( @@ -236,7 +240,7 @@ pub struct DailyUserActivityHourRecord { } /// The start date must be before the end date. -#[derive(Debug, Default, Serialize, Deserialize, InputObject, Clone)] +#[derive(Debug, Default, Serialize, Deserialize, InputObject, Clone, Eq, PartialEq)] pub struct DateRangeInput { pub end_date: Option, pub start_date: Option, diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index f004e6eb3c..d903c36321 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -2,8 +2,9 @@ use std::{cmp::Reverse, fmt::Write, sync::Arc}; use async_graphql::Result; use common_models::{ - DailyUserActivityHourRecord, DateRangeInput, FitnessAnalytics, FitnessAnalyticsEquipment, - FitnessAnalyticsExercise, FitnessAnalyticsHour, FitnessAnalyticsMuscle, + ApplicationCacheKey, ApplicationCacheValue, DailyUserActivityHourRecord, DateRangeInput, + FitnessAnalytics, FitnessAnalyticsEquipment, FitnessAnalyticsExercise, FitnessAnalyticsHour, + FitnessAnalyticsMuscle, }; use database_models::{daily_user_activity, prelude::DailyUserActivity}; use database_utils::calculate_user_activities_and_summary; @@ -238,6 +239,15 @@ impl StatisticsService { user_id: &String, input: DateRangeInput, ) -> Result { + let cache_key = ApplicationCacheKey::FitnessAnalytics { + date_range: input.clone(), + user_id: user_id.to_owned(), + }; + if let Some(ApplicationCacheValue::FitnessAnalytics(cached)) = + self.0.cache_service.get(cache_key.clone()).await? + { + return Ok(cached); + } #[derive(Debug, DerivePartialModel, FromQueryResult)] #[sea_orm(entity = "DailyUserActivity")] pub struct CustomFitnessAnalytics { @@ -322,7 +332,7 @@ impl StatisticsService { }) .sorted_by_key(|f| Reverse(f.count)) .collect_vec(); - Ok(FitnessAnalytics { + let response = FitnessAnalytics { hours, workout_reps, workout_count, @@ -334,6 +344,15 @@ impl StatisticsService { measurement_count, workout_equipments, workout_personal_bests, - }) + }; + self.0 + .cache_service + .set_with_expiry( + cache_key, + 2, + Some(ApplicationCacheValue::FitnessAnalytics(response.clone())), + ) + .await?; + Ok(response) } } From 84429bf422e4732bf836aed34a6eb2b00339f064 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 12:16:31 +0530 Subject: [PATCH 023/233] feat(frontend): add basic fitness analytics page --- .../routes/_dashboard.fitness.analytics.tsx | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 apps/frontend/app/routes/_dashboard.fitness.analytics.tsx diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx new file mode 100644 index 0000000000..0bc515a755 --- /dev/null +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -0,0 +1,39 @@ +import { Box, Container, Stack } from "@mantine/core"; +import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { z } from "zod"; +import { zx } from "zodix"; +import { + getEnhancedCookieName, + redirectUsingEnhancedCookieSearchParams, +} from "~/lib/utilities.server"; + +const searchParamsSchema = z.object({ + startDate: z.string().optional(), + endDate: z.string().optional(), +}); + +export type SearchParams = z.infer; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const query = zx.parseQuery(request, {}); + const cookieName = await getEnhancedCookieName("fitness.analytics", request); + await redirectUsingEnhancedCookieSearchParams(request, cookieName); + return { query }; +}; + +export const meta = (_args: MetaArgs) => { + return [{ title: "Fitness Analytics | Ryot" }]; +}; + +export default function Page() { + const loaderData = useLoaderData(); + + return ( + + + {JSON.stringify(loaderData.query)} + + + ); +} From 76cedd5e16382d683f7139a94f49a295d2e8fee3 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 12:17:07 +0530 Subject: [PATCH 024/233] fix(frontend): pass correct schema to parser --- apps/frontend/app/routes/_dashboard.fitness.analytics.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 0bc515a755..4c5d02baff 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -16,7 +16,7 @@ const searchParamsSchema = z.object({ export type SearchParams = z.infer; export const loader = async ({ request }: LoaderFunctionArgs) => { - const query = zx.parseQuery(request, {}); + const query = zx.parseQuery(request, searchParamsSchema); const cookieName = await getEnhancedCookieName("fitness.analytics", request); await redirectUsingEnhancedCookieSearchParams(request, cookieName); return { query }; From e50e905a72214adb6641d5b1a40d558fc2a477d2 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 12:21:55 +0530 Subject: [PATCH 025/233] feat(backend): add new preference for fitness analytics --- crates/migrations/src/m20241124_changes_for_issue_1113.rs | 4 ++++ crates/models/user/src/lib.rs | 2 ++ crates/services/user/src/lib.rs | 4 ++++ libs/generated/src/graphql/backend/gql.ts | 4 ++-- libs/generated/src/graphql/backend/graphql.ts | 5 +++-- libs/generated/src/graphql/backend/types.generated.ts | 1 + libs/graphql/src/backend/queries/UserDetails.gql | 1 + 7 files changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/migrations/src/m20241124_changes_for_issue_1113.rs b/crates/migrations/src/m20241124_changes_for_issue_1113.rs index 07841b4ad4..db54a94ec1 100644 --- a/crates/migrations/src/m20241124_changes_for_issue_1113.rs +++ b/crates/migrations/src/m20241124_changes_for_issue_1113.rs @@ -45,6 +45,10 @@ END $$; db.execute_unprepared(r#"ALTER TABLE "application_cache" ADD COLUMN "value" JSONB;"#) .await?; } + db.execute_unprepared(r#" +UPDATE "user" SET "preferences" = jsonb_set("preferences", '{features_enabled,fitness,analytics}', 'true'); + "#) + .await?; Ok(()) } diff --git a/crates/models/user/src/lib.rs b/crates/models/user/src/lib.rs index 836774aa89..557d51e518 100644 --- a/crates/models/user/src/lib.rs +++ b/crates/models/user/src/lib.rs @@ -87,6 +87,8 @@ pub struct UserFitnessFeaturesEnabledPreferences { pub workouts: bool, #[educe(Default = true)] pub templates: bool, + #[educe(Default = true)] + pub analytics: bool, } #[derive( diff --git a/crates/services/user/src/lib.rs b/crates/services/user/src/lib.rs index 7df4aa2b7d..9d0daa19f7 100644 --- a/crates/services/user/src/lib.rs +++ b/crates/services/user/src/lib.rs @@ -632,6 +632,10 @@ impl UserService { preferences.features_enabled.fitness.templates = value_bool.unwrap() } + "analytics" => { + preferences.features_enabled.fitness.analytics = + value_bool.unwrap() + } _ => return Err(err()), }, "media" => { diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts index e99898cf26..7ad49aeece 100644 --- a/libs/generated/src/graphql/backend/gql.ts +++ b/libs/generated/src/graphql/backend/gql.ts @@ -31,7 +31,7 @@ const documents = { "query MetadataSearch($input: MetadataSearchInput!) {\n metadataSearch(input: $input) {\n details {\n total\n nextPage\n }\n items {\n databaseId\n hasInteracted\n item {\n identifier\n title\n image\n publishYear\n }\n }\n }\n}": types.MetadataSearchDocument, "query PeopleSearch($input: PeopleSearchInput!) {\n peopleSearch(input: $input) {\n details {\n total\n nextPage\n }\n items {\n identifier\n name\n image\n birthYear\n }\n }\n}": types.PeopleSearchDocument, "query PersonDetails($personId: String!) {\n personDetails(personId: $personId) {\n sourceUrl\n details {\n id\n name\n source\n identifier\n isPartial\n description\n birthDate\n deathDate\n place\n website\n gender\n displayImages\n }\n contents {\n name\n count\n items {\n character\n metadataId\n }\n }\n }\n}": types.PersonDetailsDocument, - "query UserDetails {\n userDetails {\n __typename\n ... on User {\n id\n lot\n name\n isDisabled\n oidcIssuerId\n preferences {\n general {\n reviewScale\n gridPacking\n displayNsfw\n disableVideos\n persistQueries\n disableReviews\n disableIntegrations\n disableWatchProviders\n disableNavigationAnimation\n dashboard {\n hidden\n section\n numElements\n deduplicateMedia\n }\n watchProviders {\n lot\n values\n }\n }\n fitness {\n logging {\n showDetailsWhileEditing\n }\n exercises {\n unitSystem\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n measurements {\n custom {\n name\n dataType\n }\n inbuilt {\n weight\n bodyMassIndex\n totalBodyWater\n muscle\n leanBodyMass\n bodyFat\n boneMass\n visceralFat\n waistCircumference\n waistToHeightRatio\n hipCircumference\n waistToHipRatio\n chestCircumference\n thighCircumference\n bicepsCircumference\n neckCircumference\n bodyFatCaliper\n chestSkinfold\n abdominalSkinfold\n thighSkinfold\n basalMetabolicRate\n totalDailyEnergyExpenditure\n calories\n }\n }\n }\n notifications {\n toSend\n enabled\n }\n featuresEnabled {\n others {\n calendar\n collections\n }\n fitness {\n enabled\n workouts\n templates\n measurements\n }\n media {\n enabled\n anime\n audioBook\n book\n manga\n movie\n podcast\n show\n videoGame\n visualNovel\n people\n groups\n genres\n }\n }\n }\n }\n }\n}": types.UserDetailsDocument, + "query UserDetails {\n userDetails {\n __typename\n ... on User {\n id\n lot\n name\n isDisabled\n oidcIssuerId\n preferences {\n general {\n reviewScale\n gridPacking\n displayNsfw\n disableVideos\n persistQueries\n disableReviews\n disableIntegrations\n disableWatchProviders\n disableNavigationAnimation\n dashboard {\n hidden\n section\n numElements\n deduplicateMedia\n }\n watchProviders {\n lot\n values\n }\n }\n fitness {\n logging {\n showDetailsWhileEditing\n }\n exercises {\n unitSystem\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n measurements {\n custom {\n name\n dataType\n }\n inbuilt {\n weight\n bodyMassIndex\n totalBodyWater\n muscle\n leanBodyMass\n bodyFat\n boneMass\n visceralFat\n waistCircumference\n waistToHeightRatio\n hipCircumference\n waistToHipRatio\n chestCircumference\n thighCircumference\n bicepsCircumference\n neckCircumference\n bodyFatCaliper\n chestSkinfold\n abdominalSkinfold\n thighSkinfold\n basalMetabolicRate\n totalDailyEnergyExpenditure\n calories\n }\n }\n }\n notifications {\n toSend\n enabled\n }\n featuresEnabled {\n others {\n calendar\n collections\n }\n fitness {\n enabled\n workouts\n templates\n analytics\n measurements\n }\n media {\n enabled\n anime\n audioBook\n book\n manga\n movie\n podcast\n show\n videoGame\n visualNovel\n people\n groups\n genres\n }\n }\n }\n }\n }\n}": types.UserDetailsDocument, "query UserExerciseDetails($exerciseId: String!) {\n userExerciseDetails(exerciseId: $exerciseId) {\n collections {\n ...CollectionPart\n }\n reviews {\n ...ReviewItemPart\n }\n history {\n idx\n workoutId\n workoutEndOn\n bestSet {\n ...WorkoutSetRecordPart\n }\n }\n details {\n exerciseId\n createdOn\n lastUpdatedOn\n exerciseNumTimesInteracted\n exerciseExtraInformation {\n settings {\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n lifetimeStats {\n weight\n reps\n distance\n duration\n personalBestsAchieved\n }\n personalBests {\n lot\n sets {\n workoutId\n exerciseIdx\n setIdx\n }\n }\n }\n }\n }\n}": types.UserExerciseDetailsDocument, "query UserMeasurementsList($input: UserMeasurementsListInput!) {\n userMeasurementsList(input: $input) {\n timestamp\n name\n comment\n stats {\n weight\n bodyMassIndex\n totalBodyWater\n muscle\n leanBodyMass\n bodyFat\n boneMass\n visceralFat\n waistCircumference\n waistToHeightRatio\n hipCircumference\n waistToHipRatio\n chestCircumference\n thighCircumference\n bicepsCircumference\n neckCircumference\n bodyFatCaliper\n chestSkinfold\n abdominalSkinfold\n thighSkinfold\n basalMetabolicRate\n totalDailyEnergyExpenditure\n calories\n custom\n }\n }\n}": types.UserMeasurementsListDocument, "query UserMetadataDetails($metadataId: String!) {\n userMetadataDetails(metadataId: $metadataId) {\n mediaReason\n hasInteracted\n collections {\n ...CollectionPart\n }\n inProgress {\n ...SeenPart\n }\n history {\n ...SeenPart\n }\n averageRating\n reviews {\n ...ReviewItemPart\n }\n seenByAllCount\n seenByUserCount\n nextEntry {\n season\n volume\n episode\n chapter\n }\n showProgress {\n timesSeen\n seasonNumber\n episodes {\n episodeNumber\n timesSeen\n }\n }\n podcastProgress {\n episodeNumber\n timesSeen\n }\n }\n}": types.UserMetadataDetailsDocument, @@ -130,7 +130,7 @@ export function graphql(source: "query PersonDetails($personId: String!) {\n pe /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query UserDetails {\n userDetails {\n __typename\n ... on User {\n id\n lot\n name\n isDisabled\n oidcIssuerId\n preferences {\n general {\n reviewScale\n gridPacking\n displayNsfw\n disableVideos\n persistQueries\n disableReviews\n disableIntegrations\n disableWatchProviders\n disableNavigationAnimation\n dashboard {\n hidden\n section\n numElements\n deduplicateMedia\n }\n watchProviders {\n lot\n values\n }\n }\n fitness {\n logging {\n showDetailsWhileEditing\n }\n exercises {\n unitSystem\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n measurements {\n custom {\n name\n dataType\n }\n inbuilt {\n weight\n bodyMassIndex\n totalBodyWater\n muscle\n leanBodyMass\n bodyFat\n boneMass\n visceralFat\n waistCircumference\n waistToHeightRatio\n hipCircumference\n waistToHipRatio\n chestCircumference\n thighCircumference\n bicepsCircumference\n neckCircumference\n bodyFatCaliper\n chestSkinfold\n abdominalSkinfold\n thighSkinfold\n basalMetabolicRate\n totalDailyEnergyExpenditure\n calories\n }\n }\n }\n notifications {\n toSend\n enabled\n }\n featuresEnabled {\n others {\n calendar\n collections\n }\n fitness {\n enabled\n workouts\n templates\n measurements\n }\n media {\n enabled\n anime\n audioBook\n book\n manga\n movie\n podcast\n show\n videoGame\n visualNovel\n people\n groups\n genres\n }\n }\n }\n }\n }\n}"): (typeof documents)["query UserDetails {\n userDetails {\n __typename\n ... on User {\n id\n lot\n name\n isDisabled\n oidcIssuerId\n preferences {\n general {\n reviewScale\n gridPacking\n displayNsfw\n disableVideos\n persistQueries\n disableReviews\n disableIntegrations\n disableWatchProviders\n disableNavigationAnimation\n dashboard {\n hidden\n section\n numElements\n deduplicateMedia\n }\n watchProviders {\n lot\n values\n }\n }\n fitness {\n logging {\n showDetailsWhileEditing\n }\n exercises {\n unitSystem\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n measurements {\n custom {\n name\n dataType\n }\n inbuilt {\n weight\n bodyMassIndex\n totalBodyWater\n muscle\n leanBodyMass\n bodyFat\n boneMass\n visceralFat\n waistCircumference\n waistToHeightRatio\n hipCircumference\n waistToHipRatio\n chestCircumference\n thighCircumference\n bicepsCircumference\n neckCircumference\n bodyFatCaliper\n chestSkinfold\n abdominalSkinfold\n thighSkinfold\n basalMetabolicRate\n totalDailyEnergyExpenditure\n calories\n }\n }\n }\n notifications {\n toSend\n enabled\n }\n featuresEnabled {\n others {\n calendar\n collections\n }\n fitness {\n enabled\n workouts\n templates\n measurements\n }\n media {\n enabled\n anime\n audioBook\n book\n manga\n movie\n podcast\n show\n videoGame\n visualNovel\n people\n groups\n genres\n }\n }\n }\n }\n }\n}"]; +export function graphql(source: "query UserDetails {\n userDetails {\n __typename\n ... on User {\n id\n lot\n name\n isDisabled\n oidcIssuerId\n preferences {\n general {\n reviewScale\n gridPacking\n displayNsfw\n disableVideos\n persistQueries\n disableReviews\n disableIntegrations\n disableWatchProviders\n disableNavigationAnimation\n dashboard {\n hidden\n section\n numElements\n deduplicateMedia\n }\n watchProviders {\n lot\n values\n }\n }\n fitness {\n logging {\n showDetailsWhileEditing\n }\n exercises {\n unitSystem\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n measurements {\n custom {\n name\n dataType\n }\n inbuilt {\n weight\n bodyMassIndex\n totalBodyWater\n muscle\n leanBodyMass\n bodyFat\n boneMass\n visceralFat\n waistCircumference\n waistToHeightRatio\n hipCircumference\n waistToHipRatio\n chestCircumference\n thighCircumference\n bicepsCircumference\n neckCircumference\n bodyFatCaliper\n chestSkinfold\n abdominalSkinfold\n thighSkinfold\n basalMetabolicRate\n totalDailyEnergyExpenditure\n calories\n }\n }\n }\n notifications {\n toSend\n enabled\n }\n featuresEnabled {\n others {\n calendar\n collections\n }\n fitness {\n enabled\n workouts\n templates\n analytics\n measurements\n }\n media {\n enabled\n anime\n audioBook\n book\n manga\n movie\n podcast\n show\n videoGame\n visualNovel\n people\n groups\n genres\n }\n }\n }\n }\n }\n}"): (typeof documents)["query UserDetails {\n userDetails {\n __typename\n ... on User {\n id\n lot\n name\n isDisabled\n oidcIssuerId\n preferences {\n general {\n reviewScale\n gridPacking\n displayNsfw\n disableVideos\n persistQueries\n disableReviews\n disableIntegrations\n disableWatchProviders\n disableNavigationAnimation\n dashboard {\n hidden\n section\n numElements\n deduplicateMedia\n }\n watchProviders {\n lot\n values\n }\n }\n fitness {\n logging {\n showDetailsWhileEditing\n }\n exercises {\n unitSystem\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n measurements {\n custom {\n name\n dataType\n }\n inbuilt {\n weight\n bodyMassIndex\n totalBodyWater\n muscle\n leanBodyMass\n bodyFat\n boneMass\n visceralFat\n waistCircumference\n waistToHeightRatio\n hipCircumference\n waistToHipRatio\n chestCircumference\n thighCircumference\n bicepsCircumference\n neckCircumference\n bodyFatCaliper\n chestSkinfold\n abdominalSkinfold\n thighSkinfold\n basalMetabolicRate\n totalDailyEnergyExpenditure\n calories\n }\n }\n }\n notifications {\n toSend\n enabled\n }\n featuresEnabled {\n others {\n calendar\n collections\n }\n fitness {\n enabled\n workouts\n templates\n analytics\n measurements\n }\n media {\n enabled\n anime\n audioBook\n book\n manga\n movie\n podcast\n show\n videoGame\n visualNovel\n people\n groups\n genres\n }\n }\n }\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts index 8c03b0e73b..95331b0285 100644 --- a/libs/generated/src/graphql/backend/graphql.ts +++ b/libs/generated/src/graphql/backend/graphql.ts @@ -2276,6 +2276,7 @@ export type UserFitnessExercisesPreferences = { }; export type UserFitnessFeaturesEnabledPreferences = { + analytics: Scalars['Boolean']['output']; enabled: Scalars['Boolean']['output']; measurements: Scalars['Boolean']['output']; templates: Scalars['Boolean']['output']; @@ -3253,7 +3254,7 @@ export type PersonDetailsQuery = { personDetails: { sourceUrl?: string | null, d export type UserDetailsQueryVariables = Exact<{ [key: string]: never; }>; -export type UserDetailsQuery = { userDetails: { __typename: 'User', id: string, lot: UserLot, name: string, isDisabled?: boolean | null, oidcIssuerId?: string | null, preferences: { general: { reviewScale: UserReviewScale, gridPacking: GridPacking, displayNsfw: boolean, disableVideos: boolean, persistQueries: boolean, disableReviews: boolean, disableIntegrations: boolean, disableWatchProviders: boolean, disableNavigationAnimation: boolean, dashboard: Array<{ hidden: boolean, section: DashboardElementLot, numElements?: number | null, deduplicateMedia?: boolean | null }>, watchProviders: Array<{ lot: MediaLot, values: Array }> }, fitness: { logging: { showDetailsWhileEditing: boolean }, exercises: { unitSystem: UserUnitSystem, setRestTimers: { drop?: number | null, warmup?: number | null, normal?: number | null, failure?: number | null } }, measurements: { custom: Array<{ name: string, dataType: UserCustomMeasurementDataType }>, inbuilt: { weight: boolean, bodyMassIndex: boolean, totalBodyWater: boolean, muscle: boolean, leanBodyMass: boolean, bodyFat: boolean, boneMass: boolean, visceralFat: boolean, waistCircumference: boolean, waistToHeightRatio: boolean, hipCircumference: boolean, waistToHipRatio: boolean, chestCircumference: boolean, thighCircumference: boolean, bicepsCircumference: boolean, neckCircumference: boolean, bodyFatCaliper: boolean, chestSkinfold: boolean, abdominalSkinfold: boolean, thighSkinfold: boolean, basalMetabolicRate: boolean, totalDailyEnergyExpenditure: boolean, calories: boolean } } }, notifications: { toSend: Array, enabled: boolean }, featuresEnabled: { others: { calendar: boolean, collections: boolean }, fitness: { enabled: boolean, workouts: boolean, templates: boolean, measurements: boolean }, media: { enabled: boolean, anime: boolean, audioBook: boolean, book: boolean, manga: boolean, movie: boolean, podcast: boolean, show: boolean, videoGame: boolean, visualNovel: boolean, people: boolean, groups: boolean, genres: boolean } } } } | { __typename: 'UserDetailsError' } }; +export type UserDetailsQuery = { userDetails: { __typename: 'User', id: string, lot: UserLot, name: string, isDisabled?: boolean | null, oidcIssuerId?: string | null, preferences: { general: { reviewScale: UserReviewScale, gridPacking: GridPacking, displayNsfw: boolean, disableVideos: boolean, persistQueries: boolean, disableReviews: boolean, disableIntegrations: boolean, disableWatchProviders: boolean, disableNavigationAnimation: boolean, dashboard: Array<{ hidden: boolean, section: DashboardElementLot, numElements?: number | null, deduplicateMedia?: boolean | null }>, watchProviders: Array<{ lot: MediaLot, values: Array }> }, fitness: { logging: { showDetailsWhileEditing: boolean }, exercises: { unitSystem: UserUnitSystem, setRestTimers: { drop?: number | null, warmup?: number | null, normal?: number | null, failure?: number | null } }, measurements: { custom: Array<{ name: string, dataType: UserCustomMeasurementDataType }>, inbuilt: { weight: boolean, bodyMassIndex: boolean, totalBodyWater: boolean, muscle: boolean, leanBodyMass: boolean, bodyFat: boolean, boneMass: boolean, visceralFat: boolean, waistCircumference: boolean, waistToHeightRatio: boolean, hipCircumference: boolean, waistToHipRatio: boolean, chestCircumference: boolean, thighCircumference: boolean, bicepsCircumference: boolean, neckCircumference: boolean, bodyFatCaliper: boolean, chestSkinfold: boolean, abdominalSkinfold: boolean, thighSkinfold: boolean, basalMetabolicRate: boolean, totalDailyEnergyExpenditure: boolean, calories: boolean } } }, notifications: { toSend: Array, enabled: boolean }, featuresEnabled: { others: { calendar: boolean, collections: boolean }, fitness: { enabled: boolean, workouts: boolean, templates: boolean, analytics: boolean, measurements: boolean }, media: { enabled: boolean, anime: boolean, audioBook: boolean, book: boolean, manga: boolean, movie: boolean, podcast: boolean, show: boolean, videoGame: boolean, visualNovel: boolean, people: boolean, groups: boolean, genres: boolean } } } } | { __typename: 'UserDetailsError' } }; export type UserExerciseDetailsQueryVariables = Exact<{ exerciseId: Scalars['String']['input']; @@ -3551,7 +3552,7 @@ export const MetadataListDocument = {"kind":"Document","definitions":[{"kind":"O export const MetadataSearchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataSearch"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MetadataSearchInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataSearch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"databaseId"}},{"kind":"Field","name":{"kind":"Name","value":"hasInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"item"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"publishYear"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const PeopleSearchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PeopleSearch"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PeopleSearchInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"peopleSearch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"birthYear"}}]}}]}}]}}]} as unknown as DocumentNode; export const PersonDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PersonDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"personId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"personId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"personId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"isPartial"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"birthDate"}},{"kind":"Field","name":{"kind":"Name","value":"deathDate"}},{"kind":"Field","name":{"kind":"Name","value":"place"}},{"kind":"Field","name":{"kind":"Name","value":"website"}},{"kind":"Field","name":{"kind":"Name","value":"gender"}},{"kind":"Field","name":{"kind":"Name","value":"displayImages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"contents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"character"}},{"kind":"Field","name":{"kind":"Name","value":"metadataId"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const UserDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isDisabled"}},{"kind":"Field","name":{"kind":"Name","value":"oidcIssuerId"}},{"kind":"Field","name":{"kind":"Name","value":"preferences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"general"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reviewScale"}},{"kind":"Field","name":{"kind":"Name","value":"gridPacking"}},{"kind":"Field","name":{"kind":"Name","value":"displayNsfw"}},{"kind":"Field","name":{"kind":"Name","value":"disableVideos"}},{"kind":"Field","name":{"kind":"Name","value":"persistQueries"}},{"kind":"Field","name":{"kind":"Name","value":"disableReviews"}},{"kind":"Field","name":{"kind":"Name","value":"disableIntegrations"}},{"kind":"Field","name":{"kind":"Name","value":"disableWatchProviders"}},{"kind":"Field","name":{"kind":"Name","value":"disableNavigationAnimation"}},{"kind":"Field","name":{"kind":"Name","value":"dashboard"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hidden"}},{"kind":"Field","name":{"kind":"Name","value":"section"}},{"kind":"Field","name":{"kind":"Name","value":"numElements"}},{"kind":"Field","name":{"kind":"Name","value":"deduplicateMedia"}}]}},{"kind":"Field","name":{"kind":"Name","value":"watchProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"values"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logging"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"showDetailsWhileEditing"}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unitSystem"}},{"kind":"Field","name":{"kind":"Name","value":"setRestTimers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SetRestTimersPart"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"measurements"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"custom"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dataType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inbuilt"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"bodyMassIndex"}},{"kind":"Field","name":{"kind":"Name","value":"totalBodyWater"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}},{"kind":"Field","name":{"kind":"Name","value":"leanBodyMass"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFat"}},{"kind":"Field","name":{"kind":"Name","value":"boneMass"}},{"kind":"Field","name":{"kind":"Name","value":"visceralFat"}},{"kind":"Field","name":{"kind":"Name","value":"waistCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHeightRatio"}},{"kind":"Field","name":{"kind":"Name","value":"hipCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHipRatio"}},{"kind":"Field","name":{"kind":"Name","value":"chestCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"thighCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bicepsCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"neckCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFatCaliper"}},{"kind":"Field","name":{"kind":"Name","value":"chestSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"abdominalSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"thighSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"basalMetabolicRate"}},{"kind":"Field","name":{"kind":"Name","value":"totalDailyEnergyExpenditure"}},{"kind":"Field","name":{"kind":"Name","value":"calories"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"toSend"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featuresEnabled"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"others"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"calendar"}},{"kind":"Field","name":{"kind":"Name","value":"collections"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"workouts"}},{"kind":"Field","name":{"kind":"Name","value":"templates"}},{"kind":"Field","name":{"kind":"Name","value":"measurements"}}]}},{"kind":"Field","name":{"kind":"Name","value":"media"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"anime"}},{"kind":"Field","name":{"kind":"Name","value":"audioBook"}},{"kind":"Field","name":{"kind":"Name","value":"book"}},{"kind":"Field","name":{"kind":"Name","value":"manga"}},{"kind":"Field","name":{"kind":"Name","value":"movie"}},{"kind":"Field","name":{"kind":"Name","value":"podcast"}},{"kind":"Field","name":{"kind":"Name","value":"show"}},{"kind":"Field","name":{"kind":"Name","value":"videoGame"}},{"kind":"Field","name":{"kind":"Name","value":"visualNovel"}},{"kind":"Field","name":{"kind":"Name","value":"people"}},{"kind":"Field","name":{"kind":"Name","value":"groups"}},{"kind":"Field","name":{"kind":"Name","value":"genres"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SetRestTimersPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SetRestTimersSettings"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"drop"}},{"kind":"Field","name":{"kind":"Name","value":"warmup"}},{"kind":"Field","name":{"kind":"Name","value":"normal"}},{"kind":"Field","name":{"kind":"Name","value":"failure"}}]}}]} as unknown as DocumentNode; +export const UserDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isDisabled"}},{"kind":"Field","name":{"kind":"Name","value":"oidcIssuerId"}},{"kind":"Field","name":{"kind":"Name","value":"preferences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"general"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reviewScale"}},{"kind":"Field","name":{"kind":"Name","value":"gridPacking"}},{"kind":"Field","name":{"kind":"Name","value":"displayNsfw"}},{"kind":"Field","name":{"kind":"Name","value":"disableVideos"}},{"kind":"Field","name":{"kind":"Name","value":"persistQueries"}},{"kind":"Field","name":{"kind":"Name","value":"disableReviews"}},{"kind":"Field","name":{"kind":"Name","value":"disableIntegrations"}},{"kind":"Field","name":{"kind":"Name","value":"disableWatchProviders"}},{"kind":"Field","name":{"kind":"Name","value":"disableNavigationAnimation"}},{"kind":"Field","name":{"kind":"Name","value":"dashboard"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hidden"}},{"kind":"Field","name":{"kind":"Name","value":"section"}},{"kind":"Field","name":{"kind":"Name","value":"numElements"}},{"kind":"Field","name":{"kind":"Name","value":"deduplicateMedia"}}]}},{"kind":"Field","name":{"kind":"Name","value":"watchProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"values"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logging"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"showDetailsWhileEditing"}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unitSystem"}},{"kind":"Field","name":{"kind":"Name","value":"setRestTimers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SetRestTimersPart"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"measurements"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"custom"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dataType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inbuilt"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"bodyMassIndex"}},{"kind":"Field","name":{"kind":"Name","value":"totalBodyWater"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}},{"kind":"Field","name":{"kind":"Name","value":"leanBodyMass"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFat"}},{"kind":"Field","name":{"kind":"Name","value":"boneMass"}},{"kind":"Field","name":{"kind":"Name","value":"visceralFat"}},{"kind":"Field","name":{"kind":"Name","value":"waistCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHeightRatio"}},{"kind":"Field","name":{"kind":"Name","value":"hipCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHipRatio"}},{"kind":"Field","name":{"kind":"Name","value":"chestCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"thighCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bicepsCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"neckCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFatCaliper"}},{"kind":"Field","name":{"kind":"Name","value":"chestSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"abdominalSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"thighSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"basalMetabolicRate"}},{"kind":"Field","name":{"kind":"Name","value":"totalDailyEnergyExpenditure"}},{"kind":"Field","name":{"kind":"Name","value":"calories"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"toSend"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featuresEnabled"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"others"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"calendar"}},{"kind":"Field","name":{"kind":"Name","value":"collections"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"workouts"}},{"kind":"Field","name":{"kind":"Name","value":"templates"}},{"kind":"Field","name":{"kind":"Name","value":"analytics"}},{"kind":"Field","name":{"kind":"Name","value":"measurements"}}]}},{"kind":"Field","name":{"kind":"Name","value":"media"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"anime"}},{"kind":"Field","name":{"kind":"Name","value":"audioBook"}},{"kind":"Field","name":{"kind":"Name","value":"book"}},{"kind":"Field","name":{"kind":"Name","value":"manga"}},{"kind":"Field","name":{"kind":"Name","value":"movie"}},{"kind":"Field","name":{"kind":"Name","value":"podcast"}},{"kind":"Field","name":{"kind":"Name","value":"show"}},{"kind":"Field","name":{"kind":"Name","value":"videoGame"}},{"kind":"Field","name":{"kind":"Name","value":"visualNovel"}},{"kind":"Field","name":{"kind":"Name","value":"people"}},{"kind":"Field","name":{"kind":"Name","value":"groups"}},{"kind":"Field","name":{"kind":"Name","value":"genres"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SetRestTimersPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SetRestTimersSettings"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"drop"}},{"kind":"Field","name":{"kind":"Name","value":"warmup"}},{"kind":"Field","name":{"kind":"Name","value":"normal"}},{"kind":"Field","name":{"kind":"Name","value":"failure"}}]}}]} as unknown as DocumentNode; export const UserExerciseDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserExerciseDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userExerciseDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"exerciseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"idx"}},{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"workoutEndOn"}},{"kind":"Field","name":{"kind":"Name","value":"bestSet"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetRecordPart"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"exerciseId"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseNumTimesInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setRestTimers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SetRestTimersPart"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lifetimeStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}}]}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseIdx"}},{"kind":"Field","name":{"kind":"Name","value":"setIdx"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"chapter"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"seenItemsAssociatedWith"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetRecordPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetRecord"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}},{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SetRestTimersPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SetRestTimersSettings"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"drop"}},{"kind":"Field","name":{"kind":"Name","value":"warmup"}},{"kind":"Field","name":{"kind":"Name","value":"normal"}},{"kind":"Field","name":{"kind":"Name","value":"failure"}}]}}]} as unknown as DocumentNode; export const UserMeasurementsListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMeasurementsList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserMeasurementsListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMeasurementsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"stats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"bodyMassIndex"}},{"kind":"Field","name":{"kind":"Name","value":"totalBodyWater"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}},{"kind":"Field","name":{"kind":"Name","value":"leanBodyMass"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFat"}},{"kind":"Field","name":{"kind":"Name","value":"boneMass"}},{"kind":"Field","name":{"kind":"Name","value":"visceralFat"}},{"kind":"Field","name":{"kind":"Name","value":"waistCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHeightRatio"}},{"kind":"Field","name":{"kind":"Name","value":"hipCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHipRatio"}},{"kind":"Field","name":{"kind":"Name","value":"chestCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"thighCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bicepsCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"neckCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFatCaliper"}},{"kind":"Field","name":{"kind":"Name","value":"chestSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"abdominalSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"thighSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"basalMetabolicRate"}},{"kind":"Field","name":{"kind":"Name","value":"totalDailyEnergyExpenditure"}},{"kind":"Field","name":{"kind":"Name","value":"calories"}},{"kind":"Field","name":{"kind":"Name","value":"custom"}}]}}]}}]}}]} as unknown as DocumentNode; export const UserMetadataDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMetadataDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMetadataDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"mediaReason"}},{"kind":"Field","name":{"kind":"Name","value":"hasInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"averageRating"}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"seenByAllCount"}},{"kind":"Field","name":{"kind":"Name","value":"seenByUserCount"}},{"kind":"Field","name":{"kind":"Name","value":"nextEntry"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"chapter"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}},{"kind":"Field","name":{"kind":"Name","value":"seasonNumber"}},{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"chapter"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Seen"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"progress"}},{"kind":"Field","name":{"kind":"Name","value":"reviewId"}},{"kind":"Field","name":{"kind":"Name","value":"startedOn"}},{"kind":"Field","name":{"kind":"Name","value":"finishedOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"manualTimeSpent"}},{"kind":"Field","name":{"kind":"Name","value":"numTimesUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"providerWatchedOn"}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"seenItemsAssociatedWith"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}}]} as unknown as DocumentNode; diff --git a/libs/generated/src/graphql/backend/types.generated.ts b/libs/generated/src/graphql/backend/types.generated.ts index 3653c40783..2752b8d568 100644 --- a/libs/generated/src/graphql/backend/types.generated.ts +++ b/libs/generated/src/graphql/backend/types.generated.ts @@ -2351,6 +2351,7 @@ export type UserFitnessExercisesPreferences = { export type UserFitnessFeaturesEnabledPreferences = { __typename?: 'UserFitnessFeaturesEnabledPreferences'; + analytics: Scalars['Boolean']['output']; enabled: Scalars['Boolean']['output']; measurements: Scalars['Boolean']['output']; templates: Scalars['Boolean']['output']; diff --git a/libs/graphql/src/backend/queries/UserDetails.gql b/libs/graphql/src/backend/queries/UserDetails.gql index 5bfa0aafe1..908abaf7c9 100644 --- a/libs/graphql/src/backend/queries/UserDetails.gql +++ b/libs/graphql/src/backend/queries/UserDetails.gql @@ -84,6 +84,7 @@ query UserDetails { enabled workouts templates + analytics measurements } media { From 3f349d852040a38912a0755af3fed1b7a14217dc Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 12:25:32 +0530 Subject: [PATCH 026/233] feat(frontend): add fitness analytics to sidebar --- apps/frontend/app/routes/_dashboard.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.tsx b/apps/frontend/app/routes/_dashboard.tsx index 508e3a3e43..f3b5d39393 100644 --- a/apps/frontend/app/routes/_dashboard.tsx +++ b/apps/frontend/app/routes/_dashboard.tsx @@ -215,7 +215,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const fitnessLinks = [ ...(Object.entries(userPreferences.featuresEnabled.fitness || {}) - .filter(([v, _]) => v !== "enabled") + .filter(([v, _]) => !["enabled", "analytics"].includes(v)) .map(([name, enabled]) => ({ name, enabled })) ?.filter((f) => f.enabled) .map((f) => ({ @@ -223,10 +223,12 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { href: joinURL("/fitness", f.name, "list"), })) || []), { label: "Exercises", href: $path("/fitness/exercises/list") }, - ].map((link) => ({ - label: link.label, - link: link.href, - })); + userPreferences.featuresEnabled.fitness.analytics + ? { label: "Analytics", href: $path("/fitness/analytics") } + : undefined, + ] + .filter((link) => link !== undefined) + .map((link) => ({ label: link.label, link: link.href })); const settingsLinks = [ { label: "Preferences", link: $path("/settings/preferences") }, From 935ad4dd09d0b29739fa371f53513c41a49114e8 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 13:52:56 +0530 Subject: [PATCH 027/233] feat(frontend): design basic webpage --- .../routes/_dashboard.fitness.analytics.tsx | 108 +++++++++++++++++- 1 file changed, 102 insertions(+), 6 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 4c5d02baff..b271fc87d7 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -1,25 +1,50 @@ -import { Box, Container, Stack } from "@mantine/core"; +import { + Button, + Container, + Menu, + SimpleGrid, + Stack, + Text, +} from "@mantine/core"; +import { DatePickerInput } from "@mantine/dates"; import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; +import { formatDateToNaiveDate } from "@ryot/ts-utils"; +import { IconCalendar } from "@tabler/icons-react"; +import { match } from "ts-pattern"; import { z } from "zod"; import { zx } from "zodix"; +import { dayjsLib } from "~/lib/generals"; +import { useAppSearchParam } from "~/lib/hooks"; import { getEnhancedCookieName, redirectUsingEnhancedCookieSearchParams, } from "~/lib/utilities.server"; +const TIME_RANGES = [ + "Yesterday", + "This Week", + "This Month", + "This Year", + "Past 7 Days", + "Past 30 Days", + "Past 6 Months", + "Past 12 Months", +] as const; + const searchParamsSchema = z.object({ - startDate: z.string().optional(), - endDate: z.string().optional(), + range: z.enum(TIME_RANGES).optional(), }); export type SearchParams = z.infer; export const loader = async ({ request }: LoaderFunctionArgs) => { - const query = zx.parseQuery(request, searchParamsSchema); const cookieName = await getEnhancedCookieName("fitness.analytics", request); + let { range } = zx.parseQuery(request, searchParamsSchema); await redirectUsingEnhancedCookieSearchParams(request, cookieName); - return { query }; + const startDate = formatDateToNaiveDate(); + const endDate = formatDateToNaiveDate(dayjsLib()); + return { range, startDate, endDate, cookieName }; }; export const meta = (_args: MetaArgs) => { @@ -28,11 +53,82 @@ export const meta = (_args: MetaArgs) => { export default function Page() { const loaderData = useLoaderData(); + const [_, { setP }] = useAppSearchParam(loaderData.cookieName); return ( - {JSON.stringify(loaderData.query)} + + + Fitness Analytics + + + + + + + {TIME_RANGES.map((range) => ( + { + const startDate = match(range) + .with("Yesterday", () => dayjsLib().subtract(1, "day")) + .with("This Week", () => dayjsLib().startOf("week")) + .with("This Month", () => dayjsLib().startOf("month")) + .with("This Year", () => dayjsLib().startOf("year")) + .with("Past 7 Days", () => dayjsLib().subtract(7, "day")) + .with("Past 30 Days", () => + dayjsLib().subtract(30, "day"), + ) + .with("Past 6 Months", () => + dayjsLib().subtract(6, "month"), + ) + .with("Past 12 Months", () => + dayjsLib().subtract(12, "month"), + ) + .exhaustive(); + setP("range", range); + if (startDate) { + setP("startDate", formatDateToNaiveDate(startDate)); + setP("endDate", formatDateToNaiveDate(dayjsLib())); + } + }} + > + {range} + + ))} + + + + { + setP( + "startDate", + start ? formatDateToNaiveDate(start) : loaderData.startDate, + ); + setP( + "endDate", + end ? formatDateToNaiveDate(end) : loaderData.endDate, + ); + }} + /> ); From 6315b210089b9a0eec2c2481205275f246dc15ff Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 16:16:11 +0530 Subject: [PATCH 028/233] feat(frontend): do most calculations on the server loader --- .../routes/_dashboard.fitness.analytics.tsx | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index b271fc87d7..cc01713067 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -38,11 +38,24 @@ const searchParamsSchema = z.object({ export type SearchParams = z.infer; +const getStartTime = (range: (typeof TIME_RANGES)[number]) => + match(range) + .with("Yesterday", () => dayjsLib().subtract(1, "day")) + .with("This Week", () => dayjsLib().startOf("week")) + .with("This Month", () => dayjsLib().startOf("month")) + .with("This Year", () => dayjsLib().startOf("year")) + .with("Past 7 Days", () => dayjsLib().subtract(7, "day")) + .with("Past 30 Days", () => dayjsLib().subtract(30, "day")) + .with("Past 6 Months", () => dayjsLib().subtract(6, "month")) + .with("Past 12 Months", () => dayjsLib().subtract(12, "month")) + .exhaustive(); + export const loader = async ({ request }: LoaderFunctionArgs) => { const cookieName = await getEnhancedCookieName("fitness.analytics", request); let { range } = zx.parseQuery(request, searchParamsSchema); + range = range ?? "Past 30 Days"; await redirectUsingEnhancedCookieSearchParams(request, cookieName); - const startDate = formatDateToNaiveDate(); + const startDate = formatDateToNaiveDate(getStartTime(range)); const endDate = formatDateToNaiveDate(dayjsLib()); return { range, startDate, endDate, cookieName }; }; @@ -58,14 +71,14 @@ export default function Page() { return ( - - + + Fitness Analytics + + + {TIME_RANGES.map((range) => ( + { + if (range === "Custom") { + setCustomRangeOpened(true); + return; + } + setP("range", range); + delP("startDate"); + delP("endDate"); + }} + color={loaderData.range === range ? "blue" : undefined} + > + {range} + + ))} + + + + + + + ); +} + +const CustomDateSelectModal = (props: { + opened: boolean; + onClose: () => void; +}) => { const loaderData = useLoaderData(); const [_, { setP }] = useAppSearchParam(loaderData.cookieName); + const [value, setValue] = useState<[Date | null, Date | null]>([ + new Date(loaderData.startDate), + new Date(loaderData.endDate), + ]); return ( - + - - - Fitness Analytics - - - - - - - {TIME_RANGES.map((range) => ( - setP("range", range)} - color={loaderData.range === range ? "blue" : undefined} - > - {range} - - ))} - - - - { - setP( - "startDate", - start ? formatDateToNaiveDate(start) : loaderData.startDate, - ); - setP( - "endDate", - end ? formatDateToNaiveDate(end) : loaderData.endDate, - ); - }} + value={value} + w="fit-content" + onChange={setValue} /> + - + ); -} +}; From 34b57508380ed08e975fe2d8d81be39791825824 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 17:10:51 +0530 Subject: [PATCH 031/233] feat(frontend): fetch data for fitness analytics --- .../app/routes/_dashboard.fitness.analytics.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index c89d6bd131..7017ad4aa8 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -10,6 +10,7 @@ import { import { DatePicker } from "@mantine/dates"; import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; +import { FitnessAnalyticsDocument } from "@ryot/generated/graphql/backend/graphql"; import { formatDateToNaiveDate } from "@ryot/ts-utils"; import { IconCalendar, IconDeviceFloppy } from "@tabler/icons-react"; import { useState } from "react"; @@ -21,6 +22,7 @@ import { useAppSearchParam } from "~/lib/hooks"; import { getEnhancedCookieName, redirectUsingEnhancedCookieSearchParams, + serverGqlService, } from "~/lib/utilities.server"; const TIME_RANGES = [ @@ -64,7 +66,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const startDate = query.startDate || formatDateToNaiveDate(getStartTime(range) || new Date()); const endDate = query.endDate || formatDateToNaiveDate(dayjsLib()); - return { range, startDate, endDate, cookieName }; + console.log(startDate, endDate); + const { fitnessAnalytics } = await serverGqlService.authenticatedRequest( + request, + FitnessAnalyticsDocument, + { input: { startDate, endDate } }, + ); + return { range, startDate, endDate, cookieName, fitnessAnalytics }; }; export const meta = (_args: MetaArgs) => { @@ -121,6 +129,7 @@ export default function Page() { + {JSON.stringify(loaderData.fitnessAnalytics, null, 4)} From ca8e59dffc8abd54cd0f4092a8b07e8eec18b2ee Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 17:13:21 +0530 Subject: [PATCH 032/233] chore(services/cache): log when cache key found --- crates/services/cache/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/services/cache/src/lib.rs b/crates/services/cache/src/lib.rs index 460c2ddd52..43ae8df61d 100644 --- a/crates/services/cache/src/lib.rs +++ b/crates/services/cache/src/lib.rs @@ -60,7 +60,8 @@ impl CacheService { .expires_at .map_or(false, |expires_at| expires_at > Utc::now()) }) - .and_then(|m| m.value)) + .and_then(|m| m.value) + .inspect(|key| ryot_log!(debug, "Found application cache with key = {key:?}"))) } pub async fn delete(&self, key: ApplicationCacheKey) -> Result { From e6e50688b99e2105097bc9a265aa46e342e2e046 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 17:13:50 +0530 Subject: [PATCH 033/233] Revert "chore(services/cache): log when cache key found" This reverts commit ca8e59dffc8abd54cd0f4092a8b07e8eec18b2ee. --- crates/services/cache/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/services/cache/src/lib.rs b/crates/services/cache/src/lib.rs index 43ae8df61d..460c2ddd52 100644 --- a/crates/services/cache/src/lib.rs +++ b/crates/services/cache/src/lib.rs @@ -60,8 +60,7 @@ impl CacheService { .expires_at .map_or(false, |expires_at| expires_at > Utc::now()) }) - .and_then(|m| m.value) - .inspect(|key| ryot_log!(debug, "Found application cache with key = {key:?}"))) + .and_then(|m| m.value)) } pub async fn delete(&self, key: ApplicationCacheKey) -> Result { From f02330a8831e48c8f4583fade8eadd909ce9c029 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 17:44:40 +0530 Subject: [PATCH 034/233] feat(frontend): display basic pie chart --- .../routes/_dashboard.fitness.analytics.tsx | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 7017ad4aa8..b14e287c48 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -1,23 +1,27 @@ +import { PieChart } from "@mantine/charts"; import { Button, Container, + Flex, Menu, Modal, + Paper, SimpleGrid, Stack, Text, + Title, } from "@mantine/core"; import { DatePicker } from "@mantine/dates"; import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { FitnessAnalyticsDocument } from "@ryot/generated/graphql/backend/graphql"; -import { formatDateToNaiveDate } from "@ryot/ts-utils"; +import { changeCase, formatDateToNaiveDate } from "@ryot/ts-utils"; import { IconCalendar, IconDeviceFloppy } from "@tabler/icons-react"; import { useState } from "react"; import { match } from "ts-pattern"; import { z } from "zod"; import { zx } from "zodix"; -import { dayjsLib } from "~/lib/generals"; +import { dayjsLib, generateColor, getStringAsciiValue } from "~/lib/generals"; import { useAppSearchParam } from "~/lib/hooks"; import { getEnhancedCookieName, @@ -27,13 +31,13 @@ import { const TIME_RANGES = [ "Yesterday", - "This Week", - "This Month", - "This Year", "Past 7 Days", "Past 30 Days", "Past 6 Months", "Past 12 Months", + "This Week", + "This Month", + "This Year", "Custom", ] as const; @@ -129,7 +133,27 @@ export default function Page() { - {JSON.stringify(loaderData.fitnessAnalytics, null, 4)} + + + + Muscles used + ({ + value: item.count, + name: changeCase(item.muscle), + color: generateColor(getStringAsciiValue(item.muscle)), + }), + )} + /> + + + From 11fcaad52612dc142b8d78d91a26ace5799cc545 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 21:51:37 +0530 Subject: [PATCH 035/233] refactor(frontend): extract component to display chart container --- .../routes/_dashboard.fitness.analytics.tsx | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index b14e287c48..d2bb45f625 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -17,7 +17,7 @@ import { useLoaderData } from "@remix-run/react"; import { FitnessAnalyticsDocument } from "@ryot/generated/graphql/backend/graphql"; import { changeCase, formatDateToNaiveDate } from "@ryot/ts-utils"; import { IconCalendar, IconDeviceFloppy } from "@tabler/icons-react"; -import { useState } from "react"; +import { type ReactNode, useState } from "react"; import { match } from "ts-pattern"; import { z } from "zod"; import { zx } from "zodix"; @@ -134,25 +134,23 @@ export default function Page() { - - - Muscles used - ({ - value: item.count, - name: changeCase(item.muscle), - color: generateColor(getStringAsciiValue(item.muscle)), - }), - )} - /> - - + + ({ + value: item.count, + name: changeCase(item.muscle), + color: generateColor(getStringAsciiValue(item.muscle)), + }), + )} + /> + @@ -202,3 +200,17 @@ const CustomDateSelectModal = (props: { ); }; + +const ChartContainer = (props: { + title: string; + children: ReactNode; +}) => { + return ( + + + {props.title} + {props.children} + + + ); +}; From e7995f410574249dd4a75c04371a3ca77863ba59 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 22:10:40 +0530 Subject: [PATCH 036/233] feat(frontend): allow changing how many muscles to display --- .../routes/_dashboard.fitness.analytics.tsx | 89 ++++++++++++------- 1 file changed, 57 insertions(+), 32 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index d2bb45f625..0096a9bea0 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -3,13 +3,14 @@ import { Button, Container, Flex, + Group, Menu, Modal, + NumberInput, Paper, SimpleGrid, Stack, Text, - Title, } from "@mantine/core"; import { DatePicker } from "@mantine/dates"; import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/node"; @@ -134,23 +135,7 @@ export default function Page() { - - ({ - value: item.count, - name: changeCase(item.muscle), - color: generateColor(getStringAsciiValue(item.muscle)), - }), - )} - /> - + @@ -158,6 +143,60 @@ export default function Page() { ); } +const MusclesChart = () => { + const loaderData = useLoaderData(); + const data = loaderData.fitnessAnalytics.workoutMuscles; + const [count, setCount] = useState(data.length > 7 ? 7 : data.length); + + return ( + + ({ + value: item.count, + name: changeCase(item.muscle), + color: generateColor(getStringAsciiValue(item.muscle)), + }))} + /> + + ); +}; + +const ChartContainer = (props: { + title: string; + count: number; + totalItems: number; + children: ReactNode; + setCount: (count: number) => void; +}) => ( + + + + {props.title} + props.setCount(Number(v))} + /> + + {props.children} + + +); + const CustomDateSelectModal = (props: { opened: boolean; onClose: () => void; @@ -200,17 +239,3 @@ const CustomDateSelectModal = (props: { ); }; - -const ChartContainer = (props: { - title: string; - children: ReactNode; -}) => { - return ( - - - {props.title} - {props.children} - - - ); -}; From 1f7c55e19a01d40475470e6323b91a2433d39479 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 22:14:02 +0530 Subject: [PATCH 037/233] feat(frontend): save count of muscles shown in local storage --- .../routes/_dashboard.fitness.analytics.tsx | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 0096a9bea0..7f7fafb994 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -19,7 +19,9 @@ import { FitnessAnalyticsDocument } from "@ryot/generated/graphql/backend/graphq import { changeCase, formatDateToNaiveDate } from "@ryot/ts-utils"; import { IconCalendar, IconDeviceFloppy } from "@tabler/icons-react"; import { type ReactNode, useState } from "react"; +import { ClientOnly } from "remix-utils/client-only"; import { match } from "ts-pattern"; +import { useLocalStorage } from "usehooks-ts"; import { z } from "zod"; import { zx } from "zodix"; import { dayjsLib, generateColor, getStringAsciiValue } from "~/lib/generals"; @@ -146,7 +148,10 @@ export default function Page() { const MusclesChart = () => { const loaderData = useLoaderData(); const data = loaderData.fitnessAnalytics.workoutMuscles; - const [count, setCount] = useState(data.length > 7 ? 7 : data.length); + const [count, setCount] = useLocalStorage( + "FitnessAnalyticsMusclesChartCount", + data.length > 7 ? 7 : data.length, + ); return ( void; }) => ( - - - - {props.title} - props.setCount(Number(v))} - /> - - {props.children} - - + + {() => ( + + + + {props.title} + props.setCount(Number(v))} + /> + + {props.children} + + + )} + ); const CustomDateSelectModal = (props: { From 1200f9eab6c351b7aea5e06ab395b9b6bca3f7a2 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 22:18:10 +0530 Subject: [PATCH 038/233] feat(frontend): also allow setting analytics timespan to "All Time" --- apps/frontend/app/routes/_dashboard.fitness.analytics.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 7f7fafb994..565f2db4c7 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -41,6 +41,7 @@ const TIME_RANGES = [ "This Week", "This Month", "This Year", + "All Time", "Custom", ] as const; @@ -62,6 +63,7 @@ const getStartTime = (range: (typeof TIME_RANGES)[number]) => .with("Past 30 Days", () => dayjsLib().subtract(30, "day")) .with("Past 6 Months", () => dayjsLib().subtract(6, "month")) .with("Past 12 Months", () => dayjsLib().subtract(12, "month")) + .with("All Time", () => dayjsLib().subtract(2000, "year")) .with("Custom", () => undefined) .exhaustive(); From 628d082588de8fc03c289a46abf91c9de1ab72a5 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 22:25:08 +0530 Subject: [PATCH 039/233] chore(docs): remove fragment from link --- docs/content/guides/books.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/guides/books.md b/docs/content/guides/books.md index 627242dd2a..0533e76835 100644 --- a/docs/content/guides/books.md +++ b/docs/content/guides/books.md @@ -29,4 +29,4 @@ Google Books. 6. Click on "Create" and copy the API key. 7. Set the `BOOKS_GOOGLE_BOOKS_API_KEY` environment variable as described in the - [configuration](../configuration.md#important-parameters) docs. + [configuration](../configuration.md) docs. From 037202d25debd1fe4432e773d46aa3ef4ad40b20 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 25 Nov 2024 22:54:59 +0530 Subject: [PATCH 040/233] feat(frontend): add bar chart for exercises done --- .../routes/_dashboard.fitness.analytics.tsx | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 565f2db4c7..5bed1dc538 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -1,4 +1,4 @@ -import { PieChart } from "@mantine/charts"; +import { BarChart, PieChart } from "@mantine/charts"; import { Button, Container, @@ -140,6 +140,7 @@ export default function Page() { + @@ -179,6 +180,38 @@ const MusclesChart = () => { ); }; +const ExercisesChart = () => { + const loaderData = useLoaderData(); + const data = loaderData.fitnessAnalytics.workoutExercises; + const [count, setCount] = useLocalStorage( + "FitnessAnalyticsExercisesChartCount", + data.length > 7 ? 7 : data.length, + ); + + return ( + + ({ + value: item.count, + name: changeCase(item.exercise), + }))} + /> + + ); +}; + const ChartContainer = (props: { title: string; count: number; @@ -189,7 +222,7 @@ const ChartContainer = (props: { {() => ( - + {props.title} Date: Mon, 25 Nov 2024 23:03:38 +0530 Subject: [PATCH 041/233] chore(frontend): minor enhancements to chart --- apps/frontend/app/routes/_dashboard.fitness.analytics.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 5bed1dc538..da36719fcf 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -198,9 +198,9 @@ const ExercisesChart = () => { ({ From 47794fa590ea3c46c8ca1833480e253c261a0764 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 06:43:50 +0530 Subject: [PATCH 042/233] feat(frontend): handle cases when no analytics present --- apps/frontend/app/routes/_dashboard.fitness.analytics.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index da36719fcf..70cd5fdfb8 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -221,7 +221,7 @@ const ChartContainer = (props: { }) => ( {() => ( - + {props.title} @@ -234,7 +234,11 @@ const ChartContainer = (props: { onChange={(v) => props.setCount(Number(v))} /> - {props.children} + {props.totalItems > 0 ? ( + props.children + ) : ( + No data found + )} )} From 2faaf1f4a13bd7fa63225b6f1b9ab6f33accef82 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 13:04:11 +0530 Subject: [PATCH 043/233] feat(backend): save igdb settings in application cache --- Cargo.lock | 1 + crates/models/common/src/lib.rs | 2 + crates/providers/Cargo.toml | 1 + crates/providers/src/igdb.rs | 72 +++++++++++++----------- crates/services/miscellaneous/src/lib.rs | 4 +- crates/utils/dependent/src/lib.rs | 2 +- 6 files changed, 46 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 820a4a4081..9062418f82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4654,6 +4654,7 @@ dependencies = [ "serde_json", "serde_with 3.11.0", "strum", + "supporting-service", "tracing", "traits", ] diff --git a/crates/models/common/src/lib.rs b/crates/models/common/src/lib.rs index e4a2061ffc..d0cec93ea9 100644 --- a/crates/models/common/src/lib.rs +++ b/crates/models/common/src/lib.rs @@ -219,6 +219,7 @@ pub enum ApplicationCacheKey { user_id: String, date_range: DateRangeInput, }, + IgdbSettings, } #[derive( @@ -299,4 +300,5 @@ pub struct FitnessAnalytics { #[derive(Clone, Debug, PartialEq, FromJsonQueryResult, Serialize, Deserialize, Eq)] pub enum ApplicationCacheValue { FitnessAnalytics(FitnessAnalytics), + IgdbSettings { access_token: String }, } diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index 71a9a3f205..e1388538fe 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -35,6 +35,7 @@ serde = { workspace = true } serde_json = { workspace = true } serde_with = { workspace = true } strum = { workspace = true } +supporting-service = { path = "../services/supporting" } tracing = { workspace = true } traits = { path = "../traits" } diff --git a/crates/providers/src/igdb.rs b/crates/providers/src/igdb.rs index f13a2e4800..f182c749cd 100644 --- a/crates/providers/src/igdb.rs +++ b/crates/providers/src/igdb.rs @@ -1,11 +1,13 @@ -use std::{collections::HashMap, fs, path::PathBuf}; +use std::{collections::HashMap, sync::Arc}; use anyhow::{anyhow, Result}; use application_utils::get_base_http_client; use async_trait::async_trait; use chrono::Datelike; -use common_models::{IdObject, NamedObject, SearchDetails, StoredUrl}; -use common_utils::{ryot_log, PAGE_SIZE, TEMP_DIR}; +use common_models::{ + ApplicationCacheKey, ApplicationCacheValue, IdObject, NamedObject, SearchDetails, StoredUrl, +}; +use common_utils::{ryot_log, PAGE_SIZE}; use database_models::metadata_group::MetadataGroupWithoutId; use dependent_models::SearchResults; use enums::{MediaLot, MediaSource}; @@ -26,17 +28,12 @@ use sea_orm::prelude::DateTimeUtc; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use serde_with::{formats::Flexible, serde_as, TimestampSeconds}; +use supporting_service::SupportingService; use traits::{MediaProvider, MediaProviderLanguages}; static URL: &str = "https://api.igdb.com/v4"; static IMAGE_URL: &str = "https://images.igdb.com/igdb/image/upload"; static AUTH_URL: &str = "https://id.twitch.tv/oauth2/token"; -static FILE: &str = "igdb.json"; - -#[derive(Debug, Serialize, Deserialize, Clone)] -struct Settings { - access_token: String, -} static GAME_FIELDS: &str = " fields @@ -157,11 +154,12 @@ struct IgdbItemResponse { rest_data: Option>, } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct IgdbService { image_url: String, image_size: String, config: config::VideoGameConfig, + supporting_service: Arc, } impl MediaProviderLanguages for IgdbService { @@ -175,11 +173,12 @@ impl MediaProviderLanguages for IgdbService { } impl IgdbService { - pub async fn new(config: &config::VideoGameConfig) -> Self { + pub async fn new(config: &config::VideoGameConfig, ss: Arc) -> Self { Self { + config: config.clone(), + supporting_service: ss, image_url: IMAGE_URL.to_owned(), image_size: config.igdb.image_size.to_string(), - config: config.clone(), } } } @@ -193,7 +192,7 @@ impl MediaProvider for IgdbService { page: Option, _display_nsfw: bool, ) -> Result> { - let client = self.get_client().await; + let client = self.get_client().await?; let req_body = format!( r#" {fields} @@ -235,7 +234,7 @@ offset: {offset}; &self, identifier: &str, ) -> Result<(MetadataGroupWithoutId, Vec)> { - let client = self.get_client().await; + let client = self.get_client().await?; let req_body = format!( r" {fields} @@ -296,7 +295,7 @@ where id = {id}; _source_specifics: &Option, _display_nsfw: bool, ) -> Result> { - let client = self.get_client().await; + let client = self.get_client().await?; let req_body = format!( r#" {fields} @@ -342,7 +341,7 @@ offset: {offset}; identity: &str, _source_specifics: &Option, ) -> Result { - let client = self.get_client().await; + let client = self.get_client().await?; let req_body = format!( r#" {fields} @@ -423,7 +422,7 @@ where id = {id}; } async fn metadata_details(&self, identifier: &str) -> Result { - let client = self.get_client().await; + let client = self.get_client().await?; let req_body = format!( r#"{field} where id = {id};"#, field = GAME_FIELDS, @@ -455,7 +454,7 @@ where id = {id}; _display_nsfw: bool, ) -> Result> { let page = page.unwrap_or(1); - let client = self.get_client().await; + let client = self.get_client().await?; let count_req_body = format!(r#"fields id; where version_parent = null; search "{query}"; limit: 500;"#); let rsp = client @@ -538,28 +537,33 @@ impl IgdbService { format!("{} {}", access.token_type, access.access_token) } - async fn get_client(&self) -> Client { - let path = PathBuf::new().join(TEMP_DIR).join(FILE); - let settings = if !path.exists() { - let tok = self.get_access_token().await; - let settings = Settings { access_token: tok }; - let data_to_write = serde_json::to_string(&settings).unwrap(); - fs::write(path, data_to_write).unwrap(); - settings + async fn get_client(&self) -> Result { + let cc = &self.supporting_service.cache_service; + let maybe_settings = cc.get(ApplicationCacheKey::IgdbSettings).await.ok(); + let access_token = if let Some(Some(ApplicationCacheValue::IgdbSettings { access_token })) = + maybe_settings + { + access_token } else { - let data = fs::read_to_string(path).unwrap(); - serde_json::from_str(&data).unwrap() + let access_token = self.get_access_token().await; + cc.set_with_expiry( + ApplicationCacheKey::IgdbSettings, + 4, + Some(ApplicationCacheValue::IgdbSettings { + access_token: access_token.clone(), + }), + ) + .await + .ok(); + access_token }; - get_base_http_client(Some(vec![ + Ok(get_base_http_client(Some(vec![ ( HeaderName::from_static("client-id"), HeaderValue::from_str(&self.config.twitch.client_id).unwrap(), ), - ( - AUTHORIZATION, - HeaderValue::from_str(&settings.access_token).unwrap(), - ), - ])) + (AUTHORIZATION, HeaderValue::from_str(&access_token).unwrap()), + ]))) } fn igdb_response_to_search_response(&self, item: IgdbItemResponse) -> MetadataDetails { diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs index 6a86d8f24c..1b7739a614 100644 --- a/crates/services/miscellaneous/src/lib.rs +++ b/crates/services/miscellaneous/src/lib.rs @@ -1695,7 +1695,9 @@ ORDER BY RANDOM() LIMIT 10; MediaSource::Listennotes => { Box::new(ListennotesService::new(&self.0.config.podcasts).await) } - MediaSource::Igdb => Box::new(IgdbService::new(&self.0.config.video_games).await), + MediaSource::Igdb => { + Box::new(IgdbService::new(&self.0.config.video_games, self.0.clone()).await) + } MediaSource::MangaUpdates => Box::new( MangaUpdatesService::new(&self.0.config.anime_and_manga.manga_updates).await, ), diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs index b411ed8d4d..eb389078bc 100644 --- a/crates/utils/dependent/src/lib.rs +++ b/crates/utils/dependent/src/lib.rs @@ -164,7 +164,7 @@ pub async fn get_metadata_provider( MediaLot::Manga => Box::new(MalMangaService::new(&ss.config.anime_and_manga.mal).await), _ => return err(), }, - MediaSource::Igdb => Box::new(IgdbService::new(&ss.config.video_games).await), + MediaSource::Igdb => Box::new(IgdbService::new(&ss.config.video_games, ss.clone()).await), MediaSource::MangaUpdates => { Box::new(MangaUpdatesService::new(&ss.config.anime_and_manga.manga_updates).await) } From 1fe5871e658e3b014980011def1c5c00e3ebc47f Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 17:21:20 +0530 Subject: [PATCH 044/233] feat(backend): save listennotes settings in application cache --- crates/models/common/src/lib.rs | 4 + crates/providers/src/listennotes.rs | 112 ++++++++++++----------- crates/services/miscellaneous/src/lib.rs | 2 +- crates/utils/dependent/src/lib.rs | 4 +- 4 files changed, 68 insertions(+), 54 deletions(-) diff --git a/crates/models/common/src/lib.rs b/crates/models/common/src/lib.rs index d0cec93ea9..b1bbbc0306 100644 --- a/crates/models/common/src/lib.rs +++ b/crates/models/common/src/lib.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use async_graphql::{Enum, InputObject, SimpleObject}; use chrono::NaiveDate; use educe::Educe; @@ -220,6 +222,7 @@ pub enum ApplicationCacheKey { date_range: DateRangeInput, }, IgdbSettings, + ListennotesSettings, } #[derive( @@ -301,4 +304,5 @@ pub struct FitnessAnalytics { pub enum ApplicationCacheValue { FitnessAnalytics(FitnessAnalytics), IgdbSettings { access_token: String }, + ListennotesSettings { genres: HashMap }, } diff --git a/crates/providers/src/listennotes.rs b/crates/providers/src/listennotes.rs index 29faef33a9..9d71c41064 100644 --- a/crates/providers/src/listennotes.rs +++ b/crates/providers/src/listennotes.rs @@ -1,11 +1,11 @@ -use std::{collections::HashMap, env, fs, path::PathBuf}; +use std::{collections::HashMap, env, sync::Arc}; use anyhow::{anyhow, Result}; use application_utils::get_base_http_client; use async_trait::async_trait; use chrono::Datelike; -use common_models::SearchDetails; -use common_utils::{convert_naive_to_utc, PAGE_SIZE, TEMP_DIR}; +use common_models::{ApplicationCacheKey, ApplicationCacheValue, SearchDetails}; +use common_utils::{convert_naive_to_utc, PAGE_SIZE}; use dependent_models::SearchResults; use enums::{MediaLot, MediaSource}; use itertools::Itertools; @@ -22,21 +22,15 @@ use sea_orm::prelude::DateTimeUtc; use serde::{Deserialize, Serialize}; use serde_json::json; use serde_with::{formats::Flexible, serde_as, TimestampMilliSeconds}; +use supporting_service::SupportingService; use traits::{MediaProvider, MediaProviderLanguages}; static URL: &str = "https://listen-api.listennotes.com/api/v2"; -static FILE: &str = "listennotes.json"; -#[derive(Debug, Serialize, Deserialize, Clone)] -struct Settings { - genres: HashMap, -} - -#[derive(Debug, Clone)] pub struct ListennotesService { url: String, client: Client, - settings: Settings, + supporting_service: Arc, } impl MediaProviderLanguages for ListennotesService { @@ -50,16 +44,19 @@ impl MediaProviderLanguages for ListennotesService { } impl ListennotesService { - pub async fn new(config: &config::PodcastConfig) -> Self { + pub async fn new(config: &config::PodcastConfig, ss: Arc) -> Self { let url = env::var("LISTENNOTES_API_URL") .unwrap_or_else(|_| URL.to_owned()) .as_str() .to_owned(); - let (client, settings) = get_client_config(&config.listennotes.api_token).await; + let client = get_base_http_client(Some(vec![( + HeaderName::from_static("x-listenapi-key"), + HeaderValue::from_str(&config.listennotes.api_token).unwrap(), + )])); Self { url, client, - settings, + supporting_service: ss, } } } @@ -152,13 +149,14 @@ impl MediaProvider for ListennotesService { results: Vec, next_offset: Option, } + let rsp = self .client .get(format!("{}/search", self.url)) .query(&json!({ + "type": "podcast", "q": query.to_owned(), "offset": (page - 1) * PAGE_SIZE, - "type": "podcast" })) .send() .await @@ -186,6 +184,49 @@ impl MediaProvider for ListennotesService { } impl ListennotesService { + async fn get_genres(&self) -> Result> { + let cc = &self.supporting_service.cache_service; + let maybe_settings = cc.get(ApplicationCacheKey::ListennotesSettings).await.ok(); + let genres = if let Some(Some(ApplicationCacheValue::ListennotesSettings { genres })) = + maybe_settings + { + genres + } else { + #[derive(Debug, Serialize, Deserialize, Default)] + #[serde(rename_all = "snake_case")] + pub struct ListennotesIdAndNamedObject { + pub id: i32, + pub name: String, + } + #[derive(Debug, Serialize, Deserialize, Default)] + struct GenreResponse { + genres: Vec, + } + let rsp = self + .client + .get(format!("{}/genres", self.url)) + .send() + .await + .unwrap(); + let data: GenreResponse = rsp.json().await.unwrap_or_default(); + let mut genres = HashMap::new(); + for genre in data.genres { + genres.insert(genre.id, genre.name); + } + cc.set_with_expiry( + ApplicationCacheKey::ListennotesSettings, + 4, + Some(ApplicationCacheValue::ListennotesSettings { + genres: genres.clone(), + }), + ) + .await + .ok(); + genres + }; + Ok(genres) + } + // The API does not return all the episodes for a podcast, and instead needs to be // paginated through. It also does not return the episode number. So we have to // handle those manually. @@ -212,7 +253,7 @@ impl ListennotesService { genre_ids: Vec, total_episodes: usize, } - let rsp = self + let resp = self .client .get(format!("{}/podcasts/{}", self.url, identifier)) .query(&json!({ @@ -222,7 +263,8 @@ impl ListennotesService { .send() .await .map_err(|e| anyhow!(e))?; - let podcast_data: Podcast = rsp.json().await.map_err(|e| anyhow!(e))?; + let podcast_data: Podcast = resp.json().await.map_err(|e| anyhow!(e))?; + let genres = self.get_genres().await?; Ok(MetadataDetails { identifier: podcast_data.id, title: podcast_data.title, @@ -238,7 +280,7 @@ impl ListennotesService { genres: podcast_data .genre_ids .into_iter() - .filter_map(|g| self.settings.genres.get(&g).cloned()) + .filter_map(|g| genres.get(&g).cloned()) .unique() .collect(), url_images: Vec::from_iter( @@ -266,37 +308,3 @@ impl ListennotesService { }) } } - -async fn get_client_config(api_token: &str) -> (Client, Settings) { - let client = get_base_http_client(Some(vec![( - HeaderName::from_static("x-listenapi-key"), - HeaderValue::from_str(api_token).unwrap(), - )])); - let path = PathBuf::new().join(TEMP_DIR).join(FILE); - let settings = if !path.exists() { - #[derive(Debug, Serialize, Deserialize, Default)] - #[serde(rename_all = "snake_case")] - pub struct ListennotesIdAndNamedObject { - pub id: i32, - pub name: String, - } - #[derive(Debug, Serialize, Deserialize, Default)] - struct GenreResponse { - genres: Vec, - } - let rsp = client.get(format!("{}/genres", URL)).send().await.unwrap(); - let data: GenreResponse = rsp.json().await.unwrap_or_default(); - let mut genres = HashMap::new(); - for genre in data.genres { - genres.insert(genre.id, genre.name); - } - let settings = Settings { genres }; - let data_to_write = serde_json::to_string(&settings); - fs::write(path, data_to_write.unwrap()).unwrap(); - settings - } else { - let data = fs::read_to_string(path).unwrap(); - serde_json::from_str(&data).unwrap() - }; - (client, settings) -} diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs index 1b7739a614..416c619c62 100644 --- a/crates/services/miscellaneous/src/lib.rs +++ b/crates/services/miscellaneous/src/lib.rs @@ -1693,7 +1693,7 @@ ORDER BY RANDOM() LIMIT 10; Box::new(AudibleService::new(&self.0.config.audio_books.audible).await) } MediaSource::Listennotes => { - Box::new(ListennotesService::new(&self.0.config.podcasts).await) + Box::new(ListennotesService::new(&self.0.config.podcasts, self.0.clone()).await) } MediaSource::Igdb => { Box::new(IgdbService::new(&self.0.config.video_games, self.0.clone()).await) diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs index eb389078bc..b65bf77095 100644 --- a/crates/utils/dependent/src/lib.rs +++ b/crates/utils/dependent/src/lib.rs @@ -139,7 +139,9 @@ pub async fn get_metadata_provider( MediaSource::Itunes => Box::new(ITunesService::new(&ss.config.podcasts.itunes).await), MediaSource::GoogleBooks => Box::new(get_google_books_service(&ss.config).await?), MediaSource::Audible => Box::new(AudibleService::new(&ss.config.audio_books.audible).await), - MediaSource::Listennotes => Box::new(ListennotesService::new(&ss.config.podcasts).await), + MediaSource::Listennotes => { + Box::new(ListennotesService::new(&ss.config.podcasts, ss.clone()).await) + } MediaSource::Tmdb => match lot { MediaLot::Show => Box::new( TmdbShowService::new(&ss.config.movies_and_shows.tmdb, Arc::new(ss.timezone)).await, From 6d010b0c58b3a746677eb23c2edf863b8e2826ad Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 17:30:26 +0530 Subject: [PATCH 045/233] perf(backend): remove extra parameter --- crates/providers/src/igdb.rs | 3 ++- crates/providers/src/listennotes.rs | 4 ++-- crates/services/miscellaneous/src/lib.rs | 8 ++------ crates/utils/dependent/src/lib.rs | 6 ++---- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/crates/providers/src/igdb.rs b/crates/providers/src/igdb.rs index f182c749cd..8915c2ae9b 100644 --- a/crates/providers/src/igdb.rs +++ b/crates/providers/src/igdb.rs @@ -173,7 +173,8 @@ impl MediaProviderLanguages for IgdbService { } impl IgdbService { - pub async fn new(config: &config::VideoGameConfig, ss: Arc) -> Self { + pub async fn new(ss: Arc) -> Self { + let config = ss.config.video_games.clone(); Self { config: config.clone(), supporting_service: ss, diff --git a/crates/providers/src/listennotes.rs b/crates/providers/src/listennotes.rs index 9d71c41064..36b224f0ba 100644 --- a/crates/providers/src/listennotes.rs +++ b/crates/providers/src/listennotes.rs @@ -44,14 +44,14 @@ impl MediaProviderLanguages for ListennotesService { } impl ListennotesService { - pub async fn new(config: &config::PodcastConfig, ss: Arc) -> Self { + pub async fn new(ss: Arc) -> Self { let url = env::var("LISTENNOTES_API_URL") .unwrap_or_else(|_| URL.to_owned()) .as_str() .to_owned(); let client = get_base_http_client(Some(vec![( HeaderName::from_static("x-listenapi-key"), - HeaderValue::from_str(&config.listennotes.api_token).unwrap(), + HeaderValue::from_str(&ss.config.podcasts.listennotes.api_token).unwrap(), )])); Self { url, diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs index 416c619c62..abe48c044d 100644 --- a/crates/services/miscellaneous/src/lib.rs +++ b/crates/services/miscellaneous/src/lib.rs @@ -1692,12 +1692,8 @@ ORDER BY RANDOM() LIMIT 10; MediaSource::Audible => { Box::new(AudibleService::new(&self.0.config.audio_books.audible).await) } - MediaSource::Listennotes => { - Box::new(ListennotesService::new(&self.0.config.podcasts, self.0.clone()).await) - } - MediaSource::Igdb => { - Box::new(IgdbService::new(&self.0.config.video_games, self.0.clone()).await) - } + MediaSource::Listennotes => Box::new(ListennotesService::new(self.0.clone()).await), + MediaSource::Igdb => Box::new(IgdbService::new(self.0.clone()).await), MediaSource::MangaUpdates => Box::new( MangaUpdatesService::new(&self.0.config.anime_and_manga.manga_updates).await, ), diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs index b65bf77095..79290c3950 100644 --- a/crates/utils/dependent/src/lib.rs +++ b/crates/utils/dependent/src/lib.rs @@ -139,9 +139,7 @@ pub async fn get_metadata_provider( MediaSource::Itunes => Box::new(ITunesService::new(&ss.config.podcasts.itunes).await), MediaSource::GoogleBooks => Box::new(get_google_books_service(&ss.config).await?), MediaSource::Audible => Box::new(AudibleService::new(&ss.config.audio_books.audible).await), - MediaSource::Listennotes => { - Box::new(ListennotesService::new(&ss.config.podcasts, ss.clone()).await) - } + MediaSource::Listennotes => Box::new(ListennotesService::new(ss.clone()).await), MediaSource::Tmdb => match lot { MediaLot::Show => Box::new( TmdbShowService::new(&ss.config.movies_and_shows.tmdb, Arc::new(ss.timezone)).await, @@ -166,7 +164,7 @@ pub async fn get_metadata_provider( MediaLot::Manga => Box::new(MalMangaService::new(&ss.config.anime_and_manga.mal).await), _ => return err(), }, - MediaSource::Igdb => Box::new(IgdbService::new(&ss.config.video_games, ss.clone()).await), + MediaSource::Igdb => Box::new(IgdbService::new(ss.clone()).await), MediaSource::MangaUpdates => { Box::new(MangaUpdatesService::new(&ss.config.anime_and_manga.manga_updates).await) } From cf7575455de9468889772dab01bdf658be58d3e8 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 17:59:13 +0530 Subject: [PATCH 046/233] refactor(backend): change function name --- crates/providers/src/igdb.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/providers/src/igdb.rs b/crates/providers/src/igdb.rs index 8915c2ae9b..72130ea163 100644 --- a/crates/providers/src/igdb.rs +++ b/crates/providers/src/igdb.rs @@ -193,7 +193,7 @@ impl MediaProvider for IgdbService { page: Option, _display_nsfw: bool, ) -> Result> { - let client = self.get_client().await?; + let client = self.get_client_config().await?; let req_body = format!( r#" {fields} @@ -235,7 +235,7 @@ offset: {offset}; &self, identifier: &str, ) -> Result<(MetadataGroupWithoutId, Vec)> { - let client = self.get_client().await?; + let client = self.get_client_config().await?; let req_body = format!( r" {fields} @@ -296,7 +296,7 @@ where id = {id}; _source_specifics: &Option, _display_nsfw: bool, ) -> Result> { - let client = self.get_client().await?; + let client = self.get_client_config().await?; let req_body = format!( r#" {fields} @@ -342,7 +342,7 @@ offset: {offset}; identity: &str, _source_specifics: &Option, ) -> Result { - let client = self.get_client().await?; + let client = self.get_client_config().await?; let req_body = format!( r#" {fields} @@ -423,7 +423,7 @@ where id = {id}; } async fn metadata_details(&self, identifier: &str) -> Result { - let client = self.get_client().await?; + let client = self.get_client_config().await?; let req_body = format!( r#"{field} where id = {id};"#, field = GAME_FIELDS, @@ -455,7 +455,7 @@ where id = {id}; _display_nsfw: bool, ) -> Result> { let page = page.unwrap_or(1); - let client = self.get_client().await?; + let client = self.get_client_config().await?; let count_req_body = format!(r#"fields id; where version_parent = null; search "{query}"; limit: 500;"#); let rsp = client @@ -538,7 +538,7 @@ impl IgdbService { format!("{} {}", access.token_type, access.access_token) } - async fn get_client(&self) -> Result { + async fn get_client_config(&self) -> Result { let cc = &self.supporting_service.cache_service; let maybe_settings = cc.get(ApplicationCacheKey::IgdbSettings).await.ok(); let access_token = if let Some(Some(ApplicationCacheValue::IgdbSettings { access_token })) = From f2fec30b9476e8162ecc6d3092af651cc5522754 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 18:04:43 +0530 Subject: [PATCH 047/233] perf(backend): do not make a useless clone --- crates/providers/src/igdb.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/providers/src/igdb.rs b/crates/providers/src/igdb.rs index 72130ea163..004806fd1d 100644 --- a/crates/providers/src/igdb.rs +++ b/crates/providers/src/igdb.rs @@ -158,7 +158,6 @@ struct IgdbItemResponse { pub struct IgdbService { image_url: String, image_size: String, - config: config::VideoGameConfig, supporting_service: Arc, } @@ -176,7 +175,6 @@ impl IgdbService { pub async fn new(ss: Arc) -> Self { let config = ss.config.video_games.clone(); Self { - config: config.clone(), supporting_service: ss, image_url: IMAGE_URL.to_owned(), image_size: config.igdb.image_size.to_string(), @@ -524,9 +522,9 @@ impl IgdbService { let access_res = client .post(AUTH_URL) .query(&json!({ - "client_id": self.config.twitch.client_id.to_owned(), - "client_secret": self.config.twitch.client_secret.to_owned(), "grant_type": "client_credentials".to_owned(), + "client_id": self.supporting_service.config.video_games.twitch.client_id.to_owned(), + "client_secret": self.supporting_service.config.video_games.twitch.client_secret.to_owned(), })) .send() .await @@ -561,7 +559,8 @@ impl IgdbService { Ok(get_base_http_client(Some(vec![ ( HeaderName::from_static("client-id"), - HeaderValue::from_str(&self.config.twitch.client_id).unwrap(), + HeaderValue::from_str(&self.supporting_service.config.video_games.twitch.client_id) + .unwrap(), ), (AUTHORIZATION, HeaderValue::from_str(&access_token).unwrap()), ]))) From 79ff3c865f9e810de5ba2ab239a38825ed7ba29a Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 21:07:01 +0530 Subject: [PATCH 048/233] feat(backend): save tmdb settings in application cache --- crates/models/common/src/lib.rs | 14 +++ crates/providers/src/tmdb.rs | 165 +++++++++++++----------------- crates/utils/dependent/src/lib.rs | 16 +-- 3 files changed, 90 insertions(+), 105 deletions(-) diff --git a/crates/models/common/src/lib.rs b/crates/models/common/src/lib.rs index b1bbbc0306..389daf53ab 100644 --- a/crates/models/common/src/lib.rs +++ b/crates/models/common/src/lib.rs @@ -223,6 +223,7 @@ pub enum ApplicationCacheKey { }, IgdbSettings, ListennotesSettings, + TmdbSettings, } #[derive( @@ -299,9 +300,22 @@ pub struct FitnessAnalytics { pub workout_equipments: Vec, } +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] +pub struct TmdbLanguage { + pub iso_639_1: String, + pub english_name: String, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] +pub struct TmdbSettings { + pub image_url: String, + pub languages: Vec, +} + #[skip_serializing_none] #[derive(Clone, Debug, PartialEq, FromJsonQueryResult, Serialize, Deserialize, Eq)] pub enum ApplicationCacheValue { + TmdbSettings(TmdbSettings), FitnessAnalytics(FitnessAnalytics), IgdbSettings { access_token: String }, ListennotesSettings { genres: HashMap }, diff --git a/crates/providers/src/tmdb.rs b/crates/providers/src/tmdb.rs index 3f24ffd8fa..13d9f0eb78 100644 --- a/crates/providers/src/tmdb.rs +++ b/crates/providers/src/tmdb.rs @@ -1,7 +1,5 @@ use std::{ collections::{HashMap, HashSet}, - fs, - path::PathBuf, sync::Arc, }; @@ -9,10 +7,11 @@ use anyhow::{anyhow, Result}; use application_utils::{get_base_http_client, get_current_date}; use async_trait::async_trait; use chrono::NaiveDate; -use common_models::{IdObject, NamedObject, SearchDetails, StoredUrl}; -use common_utils::{ - convert_date_to_year, convert_string_to_date, SHOW_SPECIAL_SEASON_NAMES, TEMP_DIR, +use common_models::{ + ApplicationCacheKey, ApplicationCacheValue, IdObject, NamedObject, SearchDetails, StoredUrl, + TmdbLanguage, TmdbSettings, }; +use common_utils::{convert_date_to_year, convert_string_to_date, SHOW_SPECIAL_SEASON_NAMES}; use database_models::metadata_group::MetadataGroupWithoutId; use dependent_models::SearchResults; use enums::{MediaLot, MediaSource}; @@ -34,22 +33,10 @@ use rust_decimal_macros::dec; use sea_orm::prelude::DateTimeUtc; use serde::{Deserialize, Serialize}; use serde_json::json; +use supporting_service::SupportingService; use traits::{MediaProvider, MediaProviderLanguages}; static URL: &str = "https://api.themoviedb.org/3"; -static FILE: &str = "tmdb.json"; - -#[derive(Debug, Serialize, Deserialize, Clone)] -struct Settings { - image_url: String, - languages: Vec, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -struct TmdbLanguage { - iso_639_1: String, - english_name: String, -} #[derive(Debug, Serialize, Deserialize, Clone)] struct TmdbCredit { @@ -179,12 +166,28 @@ struct TmdbNonMediaEntity { place_of_birth: Option, } -#[derive(Debug, Clone)] pub struct TmdbService { client: Client, language: String, - settings: Settings, - timezone: Arc, + settings: TmdbSettings, + supporting_service: Arc, +} + +impl TmdbService { + pub async fn new(ss: Arc) -> Self { + let access_token = &ss.config.movies_and_shows.tmdb.access_token; + let client: Client = get_base_http_client(Some(vec![( + AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {access_token}")).unwrap(), + )])); + let settings = get_settings(&client, &ss).await.unwrap(); + Self { + client, + settings, + language: ss.config.movies_and_shows.tmdb.locale.clone(), + supporting_service: ss, + } + } } impl TmdbService { @@ -351,7 +354,7 @@ impl TmdbService { struct TmdbChangesResponse { changes: Vec, } - let end_date = get_current_date(&self.timezone); + let end_date = get_current_date(&self.supporting_service.timezone); let start_date = since.date_naive(); let changes = self .client @@ -396,21 +399,14 @@ impl MediaProviderLanguages for TmdbService { } } -#[derive(Debug, Clone)] pub struct NonMediaTmdbService { base: TmdbService, } impl NonMediaTmdbService { - pub async fn new(access_token: &str, language: String, timezone: Arc) -> Self { - let (client, settings) = get_client_config(access_token).await; + pub async fn new(ss: Arc) -> Self { Self { - base: TmdbService { - client, - language, - settings, - timezone, - }, + base: TmdbService::new(ss).await, } } } @@ -424,6 +420,13 @@ impl MediaProvider for NonMediaTmdbService { source_specifics: &Option, display_nsfw: bool, ) -> Result> { + let language = &self + .base + .supporting_service + .config + .movies_and_shows + .tmdb + .locale; let type_ = match source_specifics { Some(PersonSourceSpecifics { is_tmdb_company: Some(true), @@ -437,9 +440,9 @@ impl MediaProvider for NonMediaTmdbService { .client .get(format!("{}/search/{}", URL, type_)) .query(&json!({ - "query": query.to_owned(), "page": page, - "language": self.base.language, + "language": language, + "query": query.to_owned(), "include_adult": display_nsfw, })) .send() @@ -623,21 +626,14 @@ impl NonMediaTmdbService { } } -#[derive(Debug, Clone)] pub struct TmdbMovieService { base: TmdbService, } impl TmdbMovieService { - pub async fn new(config: &config::TmdbConfig, timezone: Arc) -> Self { - let (client, settings) = get_client_config(&config.access_token).await; + pub async fn new(ss: Arc) -> Self { Self { - base: TmdbService { - client, - language: config.locale.clone(), - settings, - timezone, - }, + base: TmdbService::new(ss).await, } } } @@ -973,21 +969,14 @@ impl MediaProvider for TmdbMovieService { } } -#[derive(Debug, Clone)] pub struct TmdbShowService { base: TmdbService, } impl TmdbShowService { - pub async fn new(config: &config::TmdbConfig, timezone: Arc) -> Self { - let (client, settings) = get_client_config(&config.access_token).await; + pub async fn new(ss: Arc) -> Self { Self { - base: TmdbService { - client, - language: config.locale.clone(), - settings, - timezone, - }, + base: TmdbService::new(ss).await, } } } @@ -1315,47 +1304,6 @@ impl MediaProvider for TmdbShowService { } } -async fn get_client_config(access_token: &str) -> (Client, Settings) { - let client: Client = get_base_http_client(Some(vec![( - AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {access_token}")).unwrap(), - )])); - let path = PathBuf::new().join(TEMP_DIR).join(FILE); - let tmdb_settings = if !path.exists() { - #[derive(Debug, Serialize, Deserialize, Clone)] - struct TmdbImageConfiguration { - secure_base_url: String, - } - #[derive(Debug, Serialize, Deserialize, Clone)] - struct TmdbConfiguration { - images: TmdbImageConfiguration, - } - let rsp = client - .get(format!("{}/configuration", URL)) - .send() - .await - .unwrap(); - let data_1: TmdbConfiguration = rsp.json().await.unwrap(); - let rsp = client - .get(format!("{}/configuration/languages", URL)) - .send() - .await - .unwrap(); - let data_2: Vec = rsp.json().await.unwrap(); - let tmdb_settings = Settings { - image_url: data_1.images.secure_base_url, - languages: data_2, - }; - let data_to_write = serde_json::to_string(&tmdb_settings); - fs::write(path, data_to_write.unwrap()).unwrap(); - tmdb_settings - } else { - let data = fs::read_to_string(path).unwrap(); - serde_json::from_str(&data).unwrap() - }; - (client, tmdb_settings) -} - fn replace_from_end(input_string: String, search_string: &str, replace_string: &str) -> String { if let Some(last_index) = input_string.rfind(search_string) { let mut modified_string = input_string.clone(); @@ -1365,3 +1313,36 @@ fn replace_from_end(input_string: String, search_string: &str, replace_string: & } input_string } + +async fn get_settings( + client: &Client, + supporting_service: &Arc, +) -> Result { + let cc = &supporting_service.cache_service; + let maybe_settings = cc.get(ApplicationCacheKey::TmdbSettings).await.ok(); + let tmdb_settings = + if let Some(Some(ApplicationCacheValue::TmdbSettings(setting))) = maybe_settings { + setting + } else { + #[derive(Debug, Serialize, Deserialize, Clone)] + struct TmdbImageConfiguration { + secure_base_url: String, + } + #[derive(Debug, Serialize, Deserialize, Clone)] + struct TmdbConfiguration { + images: TmdbImageConfiguration, + } + let rsp = client.get(format!("{}/configuration", URL)).send().await?; + let data_1: TmdbConfiguration = rsp.json().await?; + let rsp = client + .get(format!("{}/configuration/languages", URL)) + .send() + .await?; + let data_2: Vec = rsp.json().await?; + TmdbSettings { + image_url: data_1.images.secure_base_url, + languages: data_2, + } + }; + Ok(tmdb_settings) +} diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs index 79290c3950..0cd77a11b4 100644 --- a/crates/utils/dependent/src/lib.rs +++ b/crates/utils/dependent/src/lib.rs @@ -119,12 +119,7 @@ pub async fn get_google_books_service(config: &config::AppConfig) -> Result, ) -> Result { - Ok(NonMediaTmdbService::new( - &ss.config.movies_and_shows.tmdb.access_token, - ss.config.movies_and_shows.tmdb.locale.clone(), - Arc::new(ss.timezone), - ) - .await) + Ok(NonMediaTmdbService::new(ss.clone()).await) } pub async fn get_metadata_provider( @@ -141,13 +136,8 @@ pub async fn get_metadata_provider( MediaSource::Audible => Box::new(AudibleService::new(&ss.config.audio_books.audible).await), MediaSource::Listennotes => Box::new(ListennotesService::new(ss.clone()).await), MediaSource::Tmdb => match lot { - MediaLot::Show => Box::new( - TmdbShowService::new(&ss.config.movies_and_shows.tmdb, Arc::new(ss.timezone)).await, - ), - MediaLot::Movie => Box::new( - TmdbMovieService::new(&ss.config.movies_and_shows.tmdb, Arc::new(ss.timezone)) - .await, - ), + MediaLot::Show => Box::new(TmdbShowService::new(ss.clone()).await), + MediaLot::Movie => Box::new(TmdbMovieService::new(ss.clone()).await), _ => return err(), }, MediaSource::Anilist => match lot { From db0947e3e67b51e7ec0480eb06ac870c1d0e2737 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 21:09:41 +0530 Subject: [PATCH 049/233] fix(providers): cache tmdb settings in the database --- crates/providers/src/tmdb.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/providers/src/tmdb.rs b/crates/providers/src/tmdb.rs index 13d9f0eb78..e693b109c5 100644 --- a/crates/providers/src/tmdb.rs +++ b/crates/providers/src/tmdb.rs @@ -1339,10 +1339,18 @@ async fn get_settings( .send() .await?; let data_2: Vec = rsp.json().await?; - TmdbSettings { + let settings = TmdbSettings { image_url: data_1.images.secure_base_url, languages: data_2, - } + }; + cc.set_with_expiry( + ApplicationCacheKey::TmdbSettings, + 4, + Some(ApplicationCacheValue::TmdbSettings(settings.clone())), + ) + .await + .ok(); + settings }; Ok(tmdb_settings) } From 6e79e6704fe3b8c0ffe88177c14888f5e2e8f60c Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 21:13:13 +0530 Subject: [PATCH 050/233] chore(migrations): make column non nullable --- .../migrations/src/m20241004_create_application_cache.rs | 6 +++++- crates/migrations/src/m20241124_changes_for_issue_1113.rs | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/migrations/src/m20241004_create_application_cache.rs b/crates/migrations/src/m20241004_create_application_cache.rs index 3f21da02b4..f06b948b99 100644 --- a/crates/migrations/src/m20241004_create_application_cache.rs +++ b/crates/migrations/src/m20241004_create_application_cache.rs @@ -39,7 +39,11 @@ impl MigrationTrait for Migration { .not_null() .unique_key(), ) - .col(ColumnDef::new(ApplicationCache::ExpiresAt).timestamp_with_time_zone()) + .col( + ColumnDef::new(ApplicationCache::ExpiresAt) + .not_null() + .timestamp_with_time_zone(), + ) .col(ColumnDef::new(ApplicationCache::Value).json_binary()) .to_owned(), ) diff --git a/crates/migrations/src/m20241124_changes_for_issue_1113.rs b/crates/migrations/src/m20241124_changes_for_issue_1113.rs index db54a94ec1..fd9dcb6d58 100644 --- a/crates/migrations/src/m20241124_changes_for_issue_1113.rs +++ b/crates/migrations/src/m20241124_changes_for_issue_1113.rs @@ -47,8 +47,12 @@ END $$; } db.execute_unprepared(r#" UPDATE "user" SET "preferences" = jsonb_set("preferences", '{features_enabled,fitness,analytics}', 'true'); - "#) - .await?; + "#) + .await?; + db.execute_unprepared( + r#"ALTER TABLE application_cache ALTER COLUMN expires_at SET NOT NULL;"#, + ) + .await?; Ok(()) } From dce86b1189a4373df5a0dc6474aa9769ec0f48a1 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 21:14:34 +0530 Subject: [PATCH 051/233] chore(backend): adapt to new database schema --- crates/models/database/src/application_cache.rs | 2 +- crates/services/cache/src/lib.rs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/models/database/src/application_cache.rs b/crates/models/database/src/application_cache.rs index 30742081f1..8558f0ba8a 100644 --- a/crates/models/database/src/application_cache.rs +++ b/crates/models/database/src/application_cache.rs @@ -13,7 +13,7 @@ pub struct Model { pub key: ApplicationCacheKey, #[sea_orm(column_type = "Json")] pub value: Option, - pub expires_at: Option, + pub expires_at: DateTimeUtc, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/services/cache/src/lib.rs b/crates/services/cache/src/lib.rs index 460c2ddd52..4aaffc5df1 100644 --- a/crates/services/cache/src/lib.rs +++ b/crates/services/cache/src/lib.rs @@ -29,7 +29,7 @@ impl CacheService { key: ActiveValue::Set(key), value: ActiveValue::Set(value), created_at: ActiveValue::Set(now), - expires_at: ActiveValue::Set(Some(now + Duration::hours(expiry_hours))), + expires_at: ActiveValue::Set(now + Duration::hours(expiry_hours)), ..Default::default() }; let inserted = ApplicationCache::insert(to_insert) @@ -55,11 +55,7 @@ impl CacheService { .one(&self.db) .await?; Ok(cache - .filter(|cache| { - cache - .expires_at - .map_or(false, |expires_at| expires_at > Utc::now()) - }) + .filter(|cache| cache.expires_at > Utc::now()) .and_then(|m| m.value)) } From c158727c29fde75a65f8fb78bd8686265a148206 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 21:17:42 +0530 Subject: [PATCH 052/233] refactor(backend): change function names --- crates/providers/src/igdb.rs | 2 +- crates/providers/src/listennotes.rs | 5 ++++- crates/providers/src/tmdb.rs | 2 +- crates/services/cache/src/lib.rs | 10 +++++++--- crates/services/miscellaneous/src/lib.rs | 2 +- crates/services/statistics/src/lib.rs | 2 +- crates/utils/dependent/src/lib.rs | 2 +- 7 files changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/providers/src/igdb.rs b/crates/providers/src/igdb.rs index 004806fd1d..3a648388bf 100644 --- a/crates/providers/src/igdb.rs +++ b/crates/providers/src/igdb.rs @@ -538,7 +538,7 @@ impl IgdbService { async fn get_client_config(&self) -> Result { let cc = &self.supporting_service.cache_service; - let maybe_settings = cc.get(ApplicationCacheKey::IgdbSettings).await.ok(); + let maybe_settings = cc.get_key(ApplicationCacheKey::IgdbSettings).await.ok(); let access_token = if let Some(Some(ApplicationCacheValue::IgdbSettings { access_token })) = maybe_settings { diff --git a/crates/providers/src/listennotes.rs b/crates/providers/src/listennotes.rs index 36b224f0ba..9b5c18b45e 100644 --- a/crates/providers/src/listennotes.rs +++ b/crates/providers/src/listennotes.rs @@ -186,7 +186,10 @@ impl MediaProvider for ListennotesService { impl ListennotesService { async fn get_genres(&self) -> Result> { let cc = &self.supporting_service.cache_service; - let maybe_settings = cc.get(ApplicationCacheKey::ListennotesSettings).await.ok(); + let maybe_settings = cc + .get_key(ApplicationCacheKey::ListennotesSettings) + .await + .ok(); let genres = if let Some(Some(ApplicationCacheValue::ListennotesSettings { genres })) = maybe_settings { diff --git a/crates/providers/src/tmdb.rs b/crates/providers/src/tmdb.rs index e693b109c5..571527211a 100644 --- a/crates/providers/src/tmdb.rs +++ b/crates/providers/src/tmdb.rs @@ -1319,7 +1319,7 @@ async fn get_settings( supporting_service: &Arc, ) -> Result { let cc = &supporting_service.cache_service; - let maybe_settings = cc.get(ApplicationCacheKey::TmdbSettings).await.ok(); + let maybe_settings = cc.get_key(ApplicationCacheKey::TmdbSettings).await.ok(); let tmdb_settings = if let Some(Some(ApplicationCacheValue::TmdbSettings(setting))) = maybe_settings { setting diff --git a/crates/services/cache/src/lib.rs b/crates/services/cache/src/lib.rs index 4aaffc5df1..db61e45314 100644 --- a/crates/services/cache/src/lib.rs +++ b/crates/services/cache/src/lib.rs @@ -49,7 +49,7 @@ impl CacheService { Ok(insert_id) } - pub async fn get(&self, key: ApplicationCacheKey) -> Result> { + pub async fn get_key(&self, key: ApplicationCacheKey) -> Result> { let cache = ApplicationCache::find() .filter(application_cache::Column::Key.eq(key)) .one(&self.db) @@ -59,9 +59,13 @@ impl CacheService { .and_then(|m| m.value)) } - pub async fn delete(&self, key: ApplicationCacheKey) -> Result { - let deleted = ApplicationCache::delete_many() + pub async fn expire_key(&self, key: ApplicationCacheKey) -> Result { + let deleted = ApplicationCache::update_many() .filter(application_cache::Column::Key.eq(key)) + .set(application_cache::ActiveModel { + expires_at: ActiveValue::Set(Utc::now()), + ..Default::default() + }) .exec(&self.db) .await?; Ok(deleted.rows_affected > 0) diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs index abe48c044d..6b0e668328 100644 --- a/crates/services/miscellaneous/src/lib.rs +++ b/crates/services/miscellaneous/src/lib.rs @@ -1795,7 +1795,7 @@ ORDER BY RANDOM() LIMIT 10; user_id: user_id.to_owned(), metadata_id: si.metadata_id.clone(), }; - self.0.cache_service.delete(cache).await?; + self.0.cache_service.expire_key(cache).await?; let seen_id = si.id.clone(); let metadata_id = si.metadata_id.clone(); if &si.user_id != user_id { diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index d903c36321..15c2306711 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -244,7 +244,7 @@ impl StatisticsService { user_id: user_id.to_owned(), }; if let Some(ApplicationCacheValue::FitnessAnalytics(cached)) = - self.0.cache_service.get(cache_key.clone()).await? + self.0.cache_service.get_key(cache_key.clone()).await? { return Ok(cached); } diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs index 0cd77a11b4..89cfc1e9d2 100644 --- a/crates/utils/dependent/src/lib.rs +++ b/crates/utils/dependent/src/lib.rs @@ -1261,7 +1261,7 @@ pub async fn progress_update( anime_episode_number: input.anime_episode_number, podcast_episode_number: input.podcast_episode_number, }; - let in_cache = ss.cache_service.get(cache.clone()).await?; + let in_cache = ss.cache_service.get_key(cache.clone()).await?; if respect_cache && in_cache.is_some() { ryot_log!(debug, "Seen is already in cache"); return Ok(ProgressUpdateResultUnion::Error(ProgressUpdateError { From 581c1f3c17f45a841742a411bee3245bbd0c9055 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 21:59:07 +0530 Subject: [PATCH 053/233] chore(frontend): minor changes to graphs --- apps/frontend/app/routes/_dashboard.fitness.analytics.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 70cd5fdfb8..c58e75c137 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -198,8 +198,8 @@ const ExercisesChart = () => { ( {() => ( - + {props.title} From 825ed521b12e692d01e1388ebe3e980e8f41f460 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 22:11:11 +0530 Subject: [PATCH 054/233] chore(gql): fix formatting of query --- libs/graphql/src/backend/queries/combined.gql | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/libs/graphql/src/backend/queries/combined.gql b/libs/graphql/src/backend/queries/combined.gql index e7319d5c67..20128c5adf 100644 --- a/libs/graphql/src/backend/queries/combined.gql +++ b/libs/graphql/src/backend/queries/combined.gql @@ -180,29 +180,29 @@ query DailyUserActivities($input: DailyUserActivitiesInput!) { } query FitnessAnalytics($input: DateRangeInput!) { - fitnessAnalytics(input: $input) { - workoutReps + fitnessAnalytics(input: $input) { + workoutReps workoutCount - workoutWeight - workoutDistance - workoutRestTime + workoutWeight + workoutDistance + workoutRestTime measurementCount - workoutPersonalBests - hours { - hour - count - } - workoutExercises { - count - exercise - } - workoutMuscles { - count - muscle - } - workoutEquipments { - count - equipment - } - } + workoutPersonalBests + hours { + hour + count + } + workoutExercises { + count + exercise + } + workoutMuscles { + count + muscle + } + workoutEquipments { + count + equipment + } + } } From fcbebc733f12970ccfbe5ed824f7b8aec9507a23 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 22:58:28 +0530 Subject: [PATCH 055/233] feat(frontend): use better colors for graphs --- apps/frontend/app/lib/generals.ts | 5 +++++ apps/frontend/app/lib/hooks.ts | 6 ++---- .../app/routes/_dashboard.fitness.analytics.tsx | 11 +++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/frontend/app/lib/generals.ts b/apps/frontend/app/lib/generals.ts index 7592debe33..ba797a17d2 100644 --- a/apps/frontend/app/lib/generals.ts +++ b/apps/frontend/app/lib/generals.ts @@ -296,6 +296,11 @@ export const getStringAsciiValue = (input: string) => { return total; }; +export const selectRandomElement = (array: T[], input: string): T => { + // taken from https://stackoverflow.com/questions/44975435/using-mod-operator-in-javascript-to-wrap-around#comment76926119_44975435 + return array[(getStringAsciiValue(input) + array.length) % array.length]; +}; + export const getMetadataIcon = (lot: MediaLot) => match(lot) .with(MediaLot.Book, () => IconBook) diff --git a/apps/frontend/app/lib/hooks.ts b/apps/frontend/app/lib/hooks.ts index b2cec590da..c5dfef462b 100644 --- a/apps/frontend/app/lib/hooks.ts +++ b/apps/frontend/app/lib/hooks.ts @@ -20,8 +20,8 @@ import { FitnessAction, dayjsLib, getMetadataDetailsQuery, - getStringAsciiValue, getUserMetadataDetailsQuery, + selectRandomElement, } from "~/lib/generals"; import { type InProgressWorkout, useCurrentWorkout } from "~/lib/state/fitness"; import type { loader as dashboardLoader } from "~/routes/_dashboard"; @@ -34,9 +34,7 @@ export const useGetMantineColors = () => { export const useGetRandomMantineColor = (input: string) => { const colors = useGetMantineColors(); - - // taken from https://stackoverflow.com/questions/44975435/using-mod-operator-in-javascript-to-wrap-around#comment76926119_44975435 - return colors[(getStringAsciiValue(input) + colors.length) % colors.length]; + return selectRandomElement(colors, input); }; export const useFallbackImageUrl = (text = "No Image") => { diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index c58e75c137..1439e64e55 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -24,8 +24,8 @@ import { match } from "ts-pattern"; import { useLocalStorage } from "usehooks-ts"; import { z } from "zod"; import { zx } from "zodix"; -import { dayjsLib, generateColor, getStringAsciiValue } from "~/lib/generals"; -import { useAppSearchParam } from "~/lib/hooks"; +import { dayjsLib, selectRandomElement } from "~/lib/generals"; +import { useAppSearchParam, useGetMantineColors } from "~/lib/hooks"; import { getEnhancedCookieName, redirectUsingEnhancedCookieSearchParams, @@ -150,6 +150,7 @@ export default function Page() { const MusclesChart = () => { const loaderData = useLoaderData(); + const colors = useGetMantineColors(); const data = loaderData.fitnessAnalytics.workoutMuscles; const [count, setCount] = useLocalStorage( "FitnessAnalyticsMusclesChartCount", @@ -173,7 +174,7 @@ const MusclesChart = () => { data={data.slice(0, count).map((item) => ({ value: item.count, name: changeCase(item.muscle), - color: generateColor(getStringAsciiValue(item.muscle)), + color: selectRandomElement(colors, item.muscle), }))} /> @@ -182,6 +183,7 @@ const MusclesChart = () => { const ExercisesChart = () => { const loaderData = useLoaderData(); + const colors = useGetMantineColors(); const data = loaderData.fitnessAnalytics.workoutExercises; const [count, setCount] = useLocalStorage( "FitnessAnalyticsExercisesChartCount", @@ -202,10 +204,11 @@ const ExercisesChart = () => { gridAxis="none" tickLine="none" tooltipAnimationDuration={500} - series={[{ name: "value", label: "Times done", color: "teal" }]} + series={[{ name: "value", label: "Times done" }]} data={data.slice(0, count).map((item) => ({ value: item.count, name: changeCase(item.exercise), + color: selectRandomElement(colors, item.exercise), }))} /> From 8f943d1bec2400336bf663219f1086dd2c81dbae Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 23:44:28 +0530 Subject: [PATCH 056/233] fix(frontend): remove text align from page heading --- apps/frontend/app/routes/_dashboard.fitness.analytics.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 1439e64e55..5eedef4d08 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -102,7 +102,7 @@ export default function Page() { - + Fitness Analytics From 9cf4e74ab91b30403333adea9b8d7079ba3ec05e Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 27 Nov 2024 23:48:51 +0530 Subject: [PATCH 057/233] chore(frontend): select entire text when focusing on input --- apps/frontend/app/routes/_dashboard.fitness.analytics.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 5eedef4d08..461cd7dec9 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -234,6 +234,7 @@ const ChartContainer = (props: { size="xs" value={props.count} max={props.totalItems} + onFocus={(e) => e.target.select()} onChange={(v) => props.setCount(Number(v))} /> From a6c08377c71f4aa3fc1061c40afae1aa64ae2d79 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 06:50:46 +0530 Subject: [PATCH 058/233] chore(frontend): change how props are structured --- .../routes/_dashboard.fitness.analytics.tsx | 74 ++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 461cd7dec9..04980520d6 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -159,10 +159,9 @@ const MusclesChart = () => { return ( { return ( { const ChartContainer = (props: { title: string; - count: number; totalItems: number; children: ReactNode; - setCount: (count: number) => void; -}) => ( - - {() => ( - - - - {props.title} - e.target.select()} - onChange={(v) => props.setCount(Number(v))} - /> - - {props.totalItems > 0 ? ( - props.children - ) : ( - No data found - )} - - - )} - -); + counter: + | { + count: number; + setCount: (count: number) => void; + } + | false; +}) => { + const counter = props.counter; + + return ( + + {() => ( + + + + {props.title} + {counter ? ( + e.target.select()} + onChange={(v) => counter.setCount(Number(v))} + /> + ) : null} + + {props.totalItems > 0 ? ( + props.children + ) : ( + No data found + )} + + + )} + + ); +}; const CustomDateSelectModal = (props: { opened: boolean; From 14af0b1226fd3f7f4933a2e9180298c1db4d31ba Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 07:06:50 +0530 Subject: [PATCH 059/233] feat(frontend): function to convert utc hour to local hour --- apps/frontend/app/lib/generals.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/frontend/app/lib/generals.ts b/apps/frontend/app/lib/generals.ts index ba797a17d2..abad894bc8 100644 --- a/apps/frontend/app/lib/generals.ts +++ b/apps/frontend/app/lib/generals.ts @@ -457,3 +457,13 @@ export const refreshUserMetadataDetails = (metadataId: string) => queryKey: queryFactory.media.userMetadataDetails(metadataId).queryKey, }); }, 1500); + +export const convertUtcHourToLocalHour = ( + utcHour: number, + userTimezone?: string, +) => { + const targetTimezone = userTimezone || dayjs.tz.guess(); + const utcDate = dayjs.utc().hour(utcHour).minute(0).second(0); + const localDate = utcDate.tz(targetTimezone); + return localDate.hour(); +}; From 160a9543e49094bc39d5de53d11e75917b23661c Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 07:08:32 +0530 Subject: [PATCH 060/233] feat(frontend): start section to display time of day --- .../routes/_dashboard.fitness.analytics.tsx | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 04980520d6..5a42be6544 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -18,13 +18,18 @@ import { useLoaderData } from "@remix-run/react"; import { FitnessAnalyticsDocument } from "@ryot/generated/graphql/backend/graphql"; import { changeCase, formatDateToNaiveDate } from "@ryot/ts-utils"; import { IconCalendar, IconDeviceFloppy } from "@tabler/icons-react"; +import { produce } from "immer"; import { type ReactNode, useState } from "react"; import { ClientOnly } from "remix-utils/client-only"; import { match } from "ts-pattern"; import { useLocalStorage } from "usehooks-ts"; import { z } from "zod"; import { zx } from "zodix"; -import { dayjsLib, selectRandomElement } from "~/lib/generals"; +import { + convertUtcHourToLocalHour, + dayjsLib, + selectRandomElement, +} from "~/lib/generals"; import { useAppSearchParam, useGetMantineColors } from "~/lib/hooks"; import { getEnhancedCookieName, @@ -141,6 +146,7 @@ export default function Page() { + @@ -213,6 +219,25 @@ const ExercisesChart = () => { ); }; +const TimeOfDayChart = () => { + const loaderData = useLoaderData(); + const hours = loaderData.fitnessAnalytics.hours.map((h) => + produce(h, (draft) => { + draft.hour2 = convertUtcHourToLocalHour(draft.hour); + }), + ); + + return ( + +
{JSON.stringify(hours, null, 4)}
+
+ ); +}; + const ChartContainer = (props: { title: string; totalItems: number; From 993bc3ee05c0823a996c7eb695e30d3d26282913 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 07:11:03 +0530 Subject: [PATCH 061/233] fix(utils/database): calculate hour correctly --- crates/utils/database/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/utils/database/src/lib.rs b/crates/utils/database/src/lib.rs index 1a97420e75..82aec09343 100644 --- a/crates/utils/database/src/lib.rs +++ b/crates/utils/database/src/lib.rs @@ -620,7 +620,11 @@ pub async fn calculate_user_activities_and_summary( ..Default::default() }); existing.entity_ids.push(entity_id.clone()); - let hour = timestamp.hour(); + let hour = if timestamp.minute() < 30 { + timestamp.hour() + } else { + timestamp.hour() + 1 + }; let maybe_idx = existing.hour_records.iter().position(|hr| hr.hour == hour); if let Some(idx) = maybe_idx { existing.hour_records.get_mut(idx).unwrap().entities.push( From d1401b695e5ce33d77fa05dfbb3929f5408b536b Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 08:00:11 +0530 Subject: [PATCH 062/233] feat(frontend): generate correct data for time of day chart --- .../routes/_dashboard.fitness.analytics.tsx | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 5a42be6544..04189ace2a 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -16,9 +16,8 @@ import { DatePicker } from "@mantine/dates"; import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { FitnessAnalyticsDocument } from "@ryot/generated/graphql/backend/graphql"; -import { changeCase, formatDateToNaiveDate } from "@ryot/ts-utils"; +import { changeCase, formatDateToNaiveDate, groupBy } from "@ryot/ts-utils"; import { IconCalendar, IconDeviceFloppy } from "@tabler/icons-react"; -import { produce } from "immer"; import { type ReactNode, useState } from "react"; import { ClientOnly } from "remix-utils/client-only"; import { match } from "ts-pattern"; @@ -219,13 +218,28 @@ const ExercisesChart = () => { ); }; +const hourTuples = Array.from({ length: 12 }, (_, i) => [i * 2, i * 2 + 1]); + const TimeOfDayChart = () => { const loaderData = useLoaderData(); - const hours = loaderData.fitnessAnalytics.hours.map((h) => - produce(h, (draft) => { - draft.hour2 = convertUtcHourToLocalHour(draft.hour); - }), - ); + const hours = Object.entries( + groupBy( + loaderData.fitnessAnalytics.hours.map((h) => ({ + ...h, + hour: convertUtcHourToLocalHour(h.hour), + })), + (item) => + hourTuples.find( + ([start, end]) => item.hour >= start && item.hour <= end, + ), + ), + ).map(([hour, values]) => { + const grouped = hour.split(",").map(Number); + return { + hour: { from: grouped[0], to: grouped[1] + 1 }, + count: values.reduce((acc, val) => acc + val.count, 0), + }; + }); return ( {() => ( - + {props.title} From 29d244a2ff7d8fb4687bf23fcde32b25572a115c Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 08:07:25 +0530 Subject: [PATCH 063/233] fix(frontend): generate correct arrays --- apps/frontend/app/routes/_dashboard.fitness.analytics.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 04189ace2a..cec1f828a3 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -218,7 +218,7 @@ const ExercisesChart = () => { ); }; -const hourTuples = Array.from({ length: 12 }, (_, i) => [i * 2, i * 2 + 1]); +const hourTuples = Array.from({ length: 12 }, (_, i) => [i * 2, i * 2 + 2]); const TimeOfDayChart = () => { const loaderData = useLoaderData(); @@ -230,13 +230,13 @@ const TimeOfDayChart = () => { })), (item) => hourTuples.find( - ([start, end]) => item.hour >= start && item.hour <= end, + ([start, end]) => item.hour >= start && item.hour < end, ), ), ).map(([hour, values]) => { - const grouped = hour.split(",").map(Number); + const unGrouped = hour.split(",").map(Number); return { - hour: { from: grouped[0], to: grouped[1] + 1 }, + hour: { from: unGrouped[0], to: unGrouped[1] }, count: values.reduce((acc, val) => acc + val.count, 0), }; }); From 8f99589d64c12ec424ea4ffefc21f9529aa23e83 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 08:26:07 +0530 Subject: [PATCH 064/233] feat(frontend): display bubble chart for time of day --- .../routes/_dashboard.fitness.analytics.tsx | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index cec1f828a3..48a7e168bd 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -1,4 +1,4 @@ -import { BarChart, PieChart } from "@mantine/charts"; +import { BarChart, BubbleChart, PieChart } from "@mantine/charts"; import { Button, Container, @@ -218,7 +218,15 @@ const ExercisesChart = () => { ); }; -const hourTuples = Array.from({ length: 12 }, (_, i) => [i * 2, i * 2 + 2]); +const hourTuples = Array.from({ length: 8 }, (_, i) => [i * 3, i * 3 + 3]); + +const formattedHour = (hour: number) => + dayjsLib().hour(hour).minute(0).format("ha"); + +const formattedHourLabel = (hour: string) => { + const unGrouped = hour.split(",").map(Number); + return `${formattedHour(unGrouped[0])}-${formattedHour(unGrouped[1])}`; +}; const TimeOfDayChart = () => { const loaderData = useLoaderData(); @@ -233,13 +241,11 @@ const TimeOfDayChart = () => { ([start, end]) => item.hour >= start && item.hour < end, ), ), - ).map(([hour, values]) => { - const unGrouped = hour.split(",").map(Number); - return { - hour: { from: unGrouped[0], to: unGrouped[1] }, - count: values.reduce((acc, val) => acc + val.count, 0), - }; - }); + ).map(([hour, values]) => ({ + index: 1, + hour: formattedHourLabel(hour), + count: values.reduce((acc, val) => acc + val.count, 0), + })); return ( { title="Time of day" totalItems={hours.length} > -
{JSON.stringify(hours, null, 4)}
+
); }; @@ -268,8 +280,14 @@ const ChartContainer = (props: { return ( {() => ( - - + + 0 ? "space-between" : undefined} + > {props.title} {counter ? ( @@ -287,7 +305,9 @@ const ChartContainer = (props: { {props.totalItems > 0 ? ( props.children ) : ( - No data found + + No data found + )} @@ -328,7 +348,7 @@ const CustomDateSelectModal = (props: { onClick={() => { setP("startDate", formatDateToNaiveDate(value[0] || new Date())); setP("endDate", formatDateToNaiveDate(value[1] || new Date())); - setP("range", TIME_RANGES[8]); + setP("range", TIME_RANGES[9]); props.onClose(); }} > From abd967068e44e08250113f9b51ae630fcf058705 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 08:26:54 +0530 Subject: [PATCH 065/233] chore(frontend): better types of stuff --- .../app/routes/_dashboard.fitness.analytics.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx index 48a7e168bd..f8a16c976d 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.analytics.tsx @@ -248,11 +248,7 @@ const TimeOfDayChart = () => { })); return ( - + void; - } - | false; + counter?: { + count: number; + setCount: (count: number) => void; + }; }) => { const counter = props.counter; From 35019ef55e9dff1ebc75cfc04f8ea526c54a39f1 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 08:30:11 +0530 Subject: [PATCH 066/233] docs: better wording for authentication caveat --- docs/content/guides/authentication.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/content/guides/authentication.md b/docs/content/guides/authentication.md index 712de2074c..5e5e39a74e 100644 --- a/docs/content/guides/authentication.md +++ b/docs/content/guides/authentication.md @@ -27,7 +27,8 @@ username set to their email address. This can be changed later in the profile se !!! warning - A user can authenticate using only one provider at a time. + A user can either have a username/password or it can use your OIDC provider to + authenticate but not both. You can set `USERS_DISABLE_LOCAL_AUTH=true` to disable local authentication and only allow users to authenticate using OIDC. From 9ce1d04ad97ae8ed540118aa90ab15c4fc5a9b82 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 08:41:20 +0530 Subject: [PATCH 067/233] feat(backend): setting to exclude exercise from analytics --- crates/migrations/src/m20241126_changes_for_issue_1113.rs | 7 +++++++ crates/models/fitness/src/lib.rs | 1 + crates/services/fitness/src/lib.rs | 4 ++++ libs/generated/src/graphql/backend/gql.ts | 4 ++-- libs/generated/src/graphql/backend/graphql.ts | 5 +++-- libs/generated/src/graphql/backend/types.generated.ts | 1 + libs/graphql/src/backend/queries/UserExerciseDetails.gql | 3 ++- 7 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/migrations/src/m20241126_changes_for_issue_1113.rs b/crates/migrations/src/m20241126_changes_for_issue_1113.rs index fd9dcb6d58..8337e166b4 100644 --- a/crates/migrations/src/m20241126_changes_for_issue_1113.rs +++ b/crates/migrations/src/m20241126_changes_for_issue_1113.rs @@ -53,6 +53,13 @@ UPDATE "user" SET "preferences" = jsonb_set("preferences", '{features_enabled,fi r#"ALTER TABLE application_cache ALTER COLUMN expires_at SET NOT NULL;"#, ) .await?; + db.execute_unprepared( + r#" +UPDATE "user_to_entity" SET "exercise_extra_information" = jsonb_set("exercise_extra_information", '{settings,exclude_from_analytics}', 'false') +WHERE "exercise_extra_information" IS NOT NULL; + "#, + ) + .await?; Ok(()) } diff --git a/crates/models/fitness/src/lib.rs b/crates/models/fitness/src/lib.rs index 5f3136d69f..0bd7d786ff 100644 --- a/crates/models/fitness/src/lib.rs +++ b/crates/models/fitness/src/lib.rs @@ -333,6 +333,7 @@ pub struct SetRestTimersSettings { Debug, Clone, Serialize, Deserialize, FromJsonQueryResult, Eq, PartialEq, SimpleObject, Default, )] pub struct UserToExerciseSettingsExtraInformation { + pub exclude_from_analytics: bool, pub set_rest_timers: SetRestTimersSettings, } diff --git a/crates/services/fitness/src/lib.rs b/crates/services/fitness/src/lib.rs index b3ad129730..4859f6bd58 100644 --- a/crates/services/fitness/src/lib.rs +++ b/crates/services/fitness/src/lib.rs @@ -768,6 +768,10 @@ impl ExerciseService { let mut exercise_extra_information = ute.clone().exercise_extra_information.unwrap(); let (left, right) = input.change.property.split_once('.').ok_or_else(err)?; match left { + "exclude_from_analytics" => { + exercise_extra_information.settings.exclude_from_analytics = + input.change.value.parse().unwrap(); + } "set_rest_timers" => { let value = input.change.value.parse().unwrap(); let set_rest_timers = &mut exercise_extra_information.settings.set_rest_timers; diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts index 0c299b68a4..69fee3dd84 100644 --- a/libs/generated/src/graphql/backend/gql.ts +++ b/libs/generated/src/graphql/backend/gql.ts @@ -32,7 +32,7 @@ const documents = { "query PeopleSearch($input: PeopleSearchInput!) {\n peopleSearch(input: $input) {\n details {\n total\n nextPage\n }\n items {\n identifier\n name\n image\n birthYear\n }\n }\n}": types.PeopleSearchDocument, "query PersonDetails($personId: String!) {\n personDetails(personId: $personId) {\n sourceUrl\n details {\n id\n name\n source\n identifier\n isPartial\n description\n birthDate\n deathDate\n place\n website\n gender\n displayImages\n }\n contents {\n name\n count\n items {\n character\n metadataId\n }\n }\n }\n}": types.PersonDetailsDocument, "query UserDetails {\n userDetails {\n __typename\n ... on User {\n id\n lot\n name\n isDisabled\n oidcIssuerId\n preferences {\n general {\n reviewScale\n gridPacking\n displayNsfw\n disableVideos\n persistQueries\n disableReviews\n disableIntegrations\n disableWatchProviders\n disableNavigationAnimation\n dashboard {\n hidden\n section\n numElements\n deduplicateMedia\n }\n watchProviders {\n lot\n values\n }\n }\n fitness {\n logging {\n muteSounds\n showDetailsWhileEditing\n }\n exercises {\n unitSystem\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n measurements {\n custom {\n name\n dataType\n }\n inbuilt {\n weight\n bodyMassIndex\n totalBodyWater\n muscle\n leanBodyMass\n bodyFat\n boneMass\n visceralFat\n waistCircumference\n waistToHeightRatio\n hipCircumference\n waistToHipRatio\n chestCircumference\n thighCircumference\n bicepsCircumference\n neckCircumference\n bodyFatCaliper\n chestSkinfold\n abdominalSkinfold\n thighSkinfold\n basalMetabolicRate\n totalDailyEnergyExpenditure\n calories\n }\n }\n }\n notifications {\n toSend\n enabled\n }\n featuresEnabled {\n others {\n calendar\n collections\n }\n fitness {\n enabled\n workouts\n templates\n analytics\n measurements\n }\n media {\n enabled\n anime\n audioBook\n book\n manga\n movie\n podcast\n show\n videoGame\n visualNovel\n people\n groups\n genres\n }\n }\n }\n }\n }\n}": types.UserDetailsDocument, - "query UserExerciseDetails($exerciseId: String!) {\n userExerciseDetails(exerciseId: $exerciseId) {\n collections {\n ...CollectionPart\n }\n reviews {\n ...ReviewItemPart\n }\n history {\n idx\n workoutId\n workoutEndOn\n bestSet {\n ...WorkoutSetRecordPart\n }\n }\n details {\n exerciseId\n createdOn\n lastUpdatedOn\n exerciseNumTimesInteracted\n exerciseExtraInformation {\n settings {\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n lifetimeStats {\n weight\n reps\n distance\n duration\n personalBestsAchieved\n }\n personalBests {\n lot\n sets {\n workoutId\n exerciseIdx\n setIdx\n }\n }\n }\n }\n }\n}": types.UserExerciseDetailsDocument, + "query UserExerciseDetails($exerciseId: String!) {\n userExerciseDetails(exerciseId: $exerciseId) {\n collections {\n ...CollectionPart\n }\n reviews {\n ...ReviewItemPart\n }\n history {\n idx\n workoutId\n workoutEndOn\n bestSet {\n ...WorkoutSetRecordPart\n }\n }\n details {\n exerciseId\n createdOn\n lastUpdatedOn\n exerciseNumTimesInteracted\n exerciseExtraInformation {\n settings {\n excludeFromAnalytics\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n lifetimeStats {\n weight\n reps\n distance\n duration\n personalBestsAchieved\n }\n personalBests {\n lot\n sets {\n setIdx\n workoutId\n exerciseIdx\n }\n }\n }\n }\n }\n}": types.UserExerciseDetailsDocument, "query UserMeasurementsList($input: UserMeasurementsListInput!) {\n userMeasurementsList(input: $input) {\n timestamp\n name\n comment\n stats {\n weight\n bodyMassIndex\n totalBodyWater\n muscle\n leanBodyMass\n bodyFat\n boneMass\n visceralFat\n waistCircumference\n waistToHeightRatio\n hipCircumference\n waistToHipRatio\n chestCircumference\n thighCircumference\n bicepsCircumference\n neckCircumference\n bodyFatCaliper\n chestSkinfold\n abdominalSkinfold\n thighSkinfold\n basalMetabolicRate\n totalDailyEnergyExpenditure\n calories\n custom\n }\n }\n}": types.UserMeasurementsListDocument, "query UserMetadataDetails($metadataId: String!) {\n userMetadataDetails(metadataId: $metadataId) {\n mediaReason\n hasInteracted\n collections {\n ...CollectionPart\n }\n inProgress {\n ...SeenPart\n }\n history {\n ...SeenPart\n }\n averageRating\n reviews {\n ...ReviewItemPart\n }\n seenByAllCount\n seenByUserCount\n nextEntry {\n season\n volume\n episode\n chapter\n }\n showProgress {\n timesSeen\n seasonNumber\n episodes {\n episodeNumber\n timesSeen\n }\n }\n podcastProgress {\n episodeNumber\n timesSeen\n }\n }\n}": types.UserMetadataDetailsDocument, "query UserMetadataGroupDetails($metadataGroupId: String!) {\n userMetadataGroupDetails(metadataGroupId: $metadataGroupId) {\n reviews {\n ...ReviewItemPart\n }\n collections {\n ...CollectionPart\n }\n }\n}": types.UserMetadataGroupDetailsDocument, @@ -134,7 +134,7 @@ export function graphql(source: "query UserDetails {\n userDetails {\n __typ /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query UserExerciseDetails($exerciseId: String!) {\n userExerciseDetails(exerciseId: $exerciseId) {\n collections {\n ...CollectionPart\n }\n reviews {\n ...ReviewItemPart\n }\n history {\n idx\n workoutId\n workoutEndOn\n bestSet {\n ...WorkoutSetRecordPart\n }\n }\n details {\n exerciseId\n createdOn\n lastUpdatedOn\n exerciseNumTimesInteracted\n exerciseExtraInformation {\n settings {\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n lifetimeStats {\n weight\n reps\n distance\n duration\n personalBestsAchieved\n }\n personalBests {\n lot\n sets {\n workoutId\n exerciseIdx\n setIdx\n }\n }\n }\n }\n }\n}"): (typeof documents)["query UserExerciseDetails($exerciseId: String!) {\n userExerciseDetails(exerciseId: $exerciseId) {\n collections {\n ...CollectionPart\n }\n reviews {\n ...ReviewItemPart\n }\n history {\n idx\n workoutId\n workoutEndOn\n bestSet {\n ...WorkoutSetRecordPart\n }\n }\n details {\n exerciseId\n createdOn\n lastUpdatedOn\n exerciseNumTimesInteracted\n exerciseExtraInformation {\n settings {\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n lifetimeStats {\n weight\n reps\n distance\n duration\n personalBestsAchieved\n }\n personalBests {\n lot\n sets {\n workoutId\n exerciseIdx\n setIdx\n }\n }\n }\n }\n }\n}"]; +export function graphql(source: "query UserExerciseDetails($exerciseId: String!) {\n userExerciseDetails(exerciseId: $exerciseId) {\n collections {\n ...CollectionPart\n }\n reviews {\n ...ReviewItemPart\n }\n history {\n idx\n workoutId\n workoutEndOn\n bestSet {\n ...WorkoutSetRecordPart\n }\n }\n details {\n exerciseId\n createdOn\n lastUpdatedOn\n exerciseNumTimesInteracted\n exerciseExtraInformation {\n settings {\n excludeFromAnalytics\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n lifetimeStats {\n weight\n reps\n distance\n duration\n personalBestsAchieved\n }\n personalBests {\n lot\n sets {\n setIdx\n workoutId\n exerciseIdx\n }\n }\n }\n }\n }\n}"): (typeof documents)["query UserExerciseDetails($exerciseId: String!) {\n userExerciseDetails(exerciseId: $exerciseId) {\n collections {\n ...CollectionPart\n }\n reviews {\n ...ReviewItemPart\n }\n history {\n idx\n workoutId\n workoutEndOn\n bestSet {\n ...WorkoutSetRecordPart\n }\n }\n details {\n exerciseId\n createdOn\n lastUpdatedOn\n exerciseNumTimesInteracted\n exerciseExtraInformation {\n settings {\n excludeFromAnalytics\n setRestTimers {\n ...SetRestTimersPart\n }\n }\n lifetimeStats {\n weight\n reps\n distance\n duration\n personalBestsAchieved\n }\n personalBests {\n lot\n sets {\n setIdx\n workoutId\n exerciseIdx\n }\n }\n }\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts index f41d58819f..5e4b6e9d34 100644 --- a/libs/generated/src/graphql/backend/graphql.ts +++ b/libs/generated/src/graphql/backend/graphql.ts @@ -2568,6 +2568,7 @@ export type UserToExerciseHistoryExtraInformation = { }; export type UserToExerciseSettingsExtraInformation = { + excludeFromAnalytics: Scalars['Boolean']['output']; setRestTimers: SetRestTimersSettings; }; @@ -3262,7 +3263,7 @@ export type UserExerciseDetailsQueryVariables = Exact<{ }>; -export type UserExerciseDetailsQuery = { userExerciseDetails: { collections: Array<{ id: string, name: string, userId: string }>, reviews: Array<{ id: string, rating?: string | null, postedOn: string, isSpoiler: boolean, visibility: Visibility, textOriginal?: string | null, textRendered?: string | null, seenItemsAssociatedWith: Array, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, likedBy: Array, createdOn: string, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { volume?: number | null, chapter?: string | null } | null }>, history?: Array<{ idx: number, workoutId: string, workoutEndOn: string, bestSet?: { lot: SetLot, personalBests?: Array | null, statistic: { reps?: string | null, pace?: string | null, oneRm?: string | null, weight?: string | null, volume?: string | null, duration?: string | null, distance?: string | null } } | null }> | null, details?: { exerciseId?: string | null, createdOn: string, lastUpdatedOn: string, exerciseNumTimesInteracted?: number | null, exerciseExtraInformation?: { settings: { setRestTimers: { drop?: number | null, warmup?: number | null, normal?: number | null, failure?: number | null } }, 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, postedOn: string, isSpoiler: boolean, visibility: Visibility, textOriginal?: string | null, textRendered?: string | null, seenItemsAssociatedWith: Array, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, likedBy: Array, createdOn: string, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { volume?: number | null, chapter?: string | null } | null }>, history?: Array<{ idx: number, workoutId: string, workoutEndOn: string, bestSet?: { lot: SetLot, personalBests?: Array | null, statistic: { reps?: string | null, pace?: string | null, oneRm?: string | null, weight?: string | null, volume?: string | null, duration?: string | null, distance?: string | null } } | null }> | null, details?: { exerciseId?: string | null, createdOn: string, lastUpdatedOn: string, exerciseNumTimesInteracted?: number | null, exerciseExtraInformation?: { settings: { excludeFromAnalytics: boolean, setRestTimers: { drop?: number | null, warmup?: number | null, normal?: number | null, failure?: number | null } }, lifetimeStats: { weight: string, reps: string, distance: string, duration: string, personalBestsAchieved: number }, personalBests: Array<{ lot: WorkoutSetPersonalBest, sets: Array<{ setIdx: number, workoutId: string, exerciseIdx: number }> }> } | null } | null } }; export type UserMeasurementsListQueryVariables = Exact<{ input: UserMeasurementsListInput; @@ -3554,7 +3555,7 @@ export const MetadataSearchDocument = {"kind":"Document","definitions":[{"kind": export const PeopleSearchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PeopleSearch"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PeopleSearchInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"peopleSearch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"birthYear"}}]}}]}}]}}]} as unknown as DocumentNode; export const PersonDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PersonDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"personId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"personId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"personId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"isPartial"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"birthDate"}},{"kind":"Field","name":{"kind":"Name","value":"deathDate"}},{"kind":"Field","name":{"kind":"Name","value":"place"}},{"kind":"Field","name":{"kind":"Name","value":"website"}},{"kind":"Field","name":{"kind":"Name","value":"gender"}},{"kind":"Field","name":{"kind":"Name","value":"displayImages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"contents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"character"}},{"kind":"Field","name":{"kind":"Name","value":"metadataId"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const UserDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isDisabled"}},{"kind":"Field","name":{"kind":"Name","value":"oidcIssuerId"}},{"kind":"Field","name":{"kind":"Name","value":"preferences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"general"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reviewScale"}},{"kind":"Field","name":{"kind":"Name","value":"gridPacking"}},{"kind":"Field","name":{"kind":"Name","value":"displayNsfw"}},{"kind":"Field","name":{"kind":"Name","value":"disableVideos"}},{"kind":"Field","name":{"kind":"Name","value":"persistQueries"}},{"kind":"Field","name":{"kind":"Name","value":"disableReviews"}},{"kind":"Field","name":{"kind":"Name","value":"disableIntegrations"}},{"kind":"Field","name":{"kind":"Name","value":"disableWatchProviders"}},{"kind":"Field","name":{"kind":"Name","value":"disableNavigationAnimation"}},{"kind":"Field","name":{"kind":"Name","value":"dashboard"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hidden"}},{"kind":"Field","name":{"kind":"Name","value":"section"}},{"kind":"Field","name":{"kind":"Name","value":"numElements"}},{"kind":"Field","name":{"kind":"Name","value":"deduplicateMedia"}}]}},{"kind":"Field","name":{"kind":"Name","value":"watchProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"values"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logging"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"muteSounds"}},{"kind":"Field","name":{"kind":"Name","value":"showDetailsWhileEditing"}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unitSystem"}},{"kind":"Field","name":{"kind":"Name","value":"setRestTimers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SetRestTimersPart"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"measurements"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"custom"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dataType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inbuilt"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"bodyMassIndex"}},{"kind":"Field","name":{"kind":"Name","value":"totalBodyWater"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}},{"kind":"Field","name":{"kind":"Name","value":"leanBodyMass"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFat"}},{"kind":"Field","name":{"kind":"Name","value":"boneMass"}},{"kind":"Field","name":{"kind":"Name","value":"visceralFat"}},{"kind":"Field","name":{"kind":"Name","value":"waistCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHeightRatio"}},{"kind":"Field","name":{"kind":"Name","value":"hipCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHipRatio"}},{"kind":"Field","name":{"kind":"Name","value":"chestCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"thighCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bicepsCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"neckCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFatCaliper"}},{"kind":"Field","name":{"kind":"Name","value":"chestSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"abdominalSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"thighSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"basalMetabolicRate"}},{"kind":"Field","name":{"kind":"Name","value":"totalDailyEnergyExpenditure"}},{"kind":"Field","name":{"kind":"Name","value":"calories"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"toSend"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featuresEnabled"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"others"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"calendar"}},{"kind":"Field","name":{"kind":"Name","value":"collections"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"workouts"}},{"kind":"Field","name":{"kind":"Name","value":"templates"}},{"kind":"Field","name":{"kind":"Name","value":"analytics"}},{"kind":"Field","name":{"kind":"Name","value":"measurements"}}]}},{"kind":"Field","name":{"kind":"Name","value":"media"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"anime"}},{"kind":"Field","name":{"kind":"Name","value":"audioBook"}},{"kind":"Field","name":{"kind":"Name","value":"book"}},{"kind":"Field","name":{"kind":"Name","value":"manga"}},{"kind":"Field","name":{"kind":"Name","value":"movie"}},{"kind":"Field","name":{"kind":"Name","value":"podcast"}},{"kind":"Field","name":{"kind":"Name","value":"show"}},{"kind":"Field","name":{"kind":"Name","value":"videoGame"}},{"kind":"Field","name":{"kind":"Name","value":"visualNovel"}},{"kind":"Field","name":{"kind":"Name","value":"people"}},{"kind":"Field","name":{"kind":"Name","value":"groups"}},{"kind":"Field","name":{"kind":"Name","value":"genres"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SetRestTimersPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SetRestTimersSettings"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"drop"}},{"kind":"Field","name":{"kind":"Name","value":"warmup"}},{"kind":"Field","name":{"kind":"Name","value":"normal"}},{"kind":"Field","name":{"kind":"Name","value":"failure"}}]}}]} as unknown as DocumentNode; -export const UserExerciseDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserExerciseDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userExerciseDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"exerciseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"idx"}},{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"workoutEndOn"}},{"kind":"Field","name":{"kind":"Name","value":"bestSet"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetRecordPart"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"exerciseId"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseNumTimesInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setRestTimers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SetRestTimersPart"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lifetimeStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}}]}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseIdx"}},{"kind":"Field","name":{"kind":"Name","value":"setIdx"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"chapter"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"seenItemsAssociatedWith"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetRecordPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetRecord"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}},{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SetRestTimersPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SetRestTimersSettings"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"drop"}},{"kind":"Field","name":{"kind":"Name","value":"warmup"}},{"kind":"Field","name":{"kind":"Name","value":"normal"}},{"kind":"Field","name":{"kind":"Name","value":"failure"}}]}}]} as unknown as DocumentNode; +export const UserExerciseDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserExerciseDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userExerciseDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"exerciseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"idx"}},{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"workoutEndOn"}},{"kind":"Field","name":{"kind":"Name","value":"bestSet"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetRecordPart"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"exerciseId"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseNumTimesInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"excludeFromAnalytics"}},{"kind":"Field","name":{"kind":"Name","value":"setRestTimers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SetRestTimersPart"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lifetimeStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}}]}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setIdx"}},{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseIdx"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"chapter"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"seenItemsAssociatedWith"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetRecordPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetRecord"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}},{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SetRestTimersPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SetRestTimersSettings"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"drop"}},{"kind":"Field","name":{"kind":"Name","value":"warmup"}},{"kind":"Field","name":{"kind":"Name","value":"normal"}},{"kind":"Field","name":{"kind":"Name","value":"failure"}}]}}]} as unknown as DocumentNode; export const UserMeasurementsListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMeasurementsList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserMeasurementsListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMeasurementsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"stats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"bodyMassIndex"}},{"kind":"Field","name":{"kind":"Name","value":"totalBodyWater"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}},{"kind":"Field","name":{"kind":"Name","value":"leanBodyMass"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFat"}},{"kind":"Field","name":{"kind":"Name","value":"boneMass"}},{"kind":"Field","name":{"kind":"Name","value":"visceralFat"}},{"kind":"Field","name":{"kind":"Name","value":"waistCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHeightRatio"}},{"kind":"Field","name":{"kind":"Name","value":"hipCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHipRatio"}},{"kind":"Field","name":{"kind":"Name","value":"chestCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"thighCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bicepsCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"neckCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFatCaliper"}},{"kind":"Field","name":{"kind":"Name","value":"chestSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"abdominalSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"thighSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"basalMetabolicRate"}},{"kind":"Field","name":{"kind":"Name","value":"totalDailyEnergyExpenditure"}},{"kind":"Field","name":{"kind":"Name","value":"calories"}},{"kind":"Field","name":{"kind":"Name","value":"custom"}}]}}]}}]}}]} as unknown as DocumentNode; export const UserMetadataDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMetadataDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMetadataDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"mediaReason"}},{"kind":"Field","name":{"kind":"Name","value":"hasInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"averageRating"}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"seenByAllCount"}},{"kind":"Field","name":{"kind":"Name","value":"seenByUserCount"}},{"kind":"Field","name":{"kind":"Name","value":"nextEntry"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"chapter"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}},{"kind":"Field","name":{"kind":"Name","value":"seasonNumber"}},{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"chapter"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Seen"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"progress"}},{"kind":"Field","name":{"kind":"Name","value":"reviewId"}},{"kind":"Field","name":{"kind":"Name","value":"startedOn"}},{"kind":"Field","name":{"kind":"Name","value":"finishedOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"manualTimeSpent"}},{"kind":"Field","name":{"kind":"Name","value":"numTimesUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"providerWatchedOn"}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"seenItemsAssociatedWith"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}}]} as unknown as DocumentNode; export const UserMetadataGroupDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMetadataGroupDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataGroupId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMetadataGroupDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataGroupId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataGroupId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"chapter"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"seenItemsAssociatedWith"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}}]} as unknown as DocumentNode; diff --git a/libs/generated/src/graphql/backend/types.generated.ts b/libs/generated/src/graphql/backend/types.generated.ts index ef9e94b4fa..72bfd4d6e6 100644 --- a/libs/generated/src/graphql/backend/types.generated.ts +++ b/libs/generated/src/graphql/backend/types.generated.ts @@ -2667,6 +2667,7 @@ export type UserToExerciseHistoryExtraInformation = { export type UserToExerciseSettingsExtraInformation = { __typename?: 'UserToExerciseSettingsExtraInformation'; + excludeFromAnalytics: Scalars['Boolean']['output']; setRestTimers: SetRestTimersSettings; }; diff --git a/libs/graphql/src/backend/queries/UserExerciseDetails.gql b/libs/graphql/src/backend/queries/UserExerciseDetails.gql index 2604e85489..1ef9fa24aa 100644 --- a/libs/graphql/src/backend/queries/UserExerciseDetails.gql +++ b/libs/graphql/src/backend/queries/UserExerciseDetails.gql @@ -21,6 +21,7 @@ query UserExerciseDetails($exerciseId: String!) { exerciseNumTimesInteracted exerciseExtraInformation { settings { + excludeFromAnalytics setRestTimers { ...SetRestTimersPart } @@ -35,9 +36,9 @@ query UserExerciseDetails($exerciseId: String!) { personalBests { lot sets { + setIdx workoutId exerciseIdx - setIdx } } } From 831425c925c886245c7a2bb8db63acf19bf4f5d9 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 08:43:48 +0530 Subject: [PATCH 068/233] feat(utils/database): respect new exercise setting --- crates/utils/database/src/lib.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/utils/database/src/lib.rs b/crates/utils/database/src/lib.rs index 82aec09343..693d30887e 100644 --- a/crates/utils/database/src/lib.rs +++ b/crates/utils/database/src/lib.rs @@ -774,6 +774,19 @@ pub async fn calculate_user_activities_and_summary( .one(db) .await? .unwrap(); + let user_exercise = UserToEntity::find() + .filter(user_to_entity::Column::UserId.eq(user_id)) + .filter(user_to_entity::Column::ExerciseId.eq(exercise.name.clone())) + .one(db) + .await? + .unwrap(); + if user_exercise + .exercise_extra_information + .map(|d| d.settings.exclude_from_analytics) + .unwrap_or_default() + { + continue; + } activity.workout_exercises.push(db_exercise.id); activity.workout_muscles.extend(db_exercise.muscles); activity.workout_equipments.extend(db_exercise.equipment); From aca3fda1f4957416548b2134372702c2c42660b5 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 08:54:34 +0530 Subject: [PATCH 069/233] fix(services/fitness): handle edge case for updating exercise settings --- crates/services/fitness/src/lib.rs | 40 +++++++++++++++++------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/crates/services/fitness/src/lib.rs b/crates/services/fitness/src/lib.rs index 4859f6bd58..3977517e53 100644 --- a/crates/services/fitness/src/lib.rs +++ b/crates/services/fitness/src/lib.rs @@ -766,25 +766,31 @@ impl ExerciseService { } }; let mut exercise_extra_information = ute.clone().exercise_extra_information.unwrap(); - let (left, right) = input.change.property.split_once('.').ok_or_else(err)?; - match left { - "exclude_from_analytics" => { - exercise_extra_information.settings.exclude_from_analytics = - input.change.value.parse().unwrap(); - } - "set_rest_timers" => { - let value = input.change.value.parse().unwrap(); - let set_rest_timers = &mut exercise_extra_information.settings.set_rest_timers; - match right { - "drop" => set_rest_timers.drop = Some(value), - "normal" => set_rest_timers.normal = Some(value), - "warmup" => set_rest_timers.warmup = Some(value), - "failure" => set_rest_timers.failure = Some(value), - _ => return Err(err()), + if input.change.property.contains('.') { + let (left, right) = input.change.property.split_once('.').ok_or_else(err)?; + match left { + "set_rest_timers" => { + let value = input.change.value.parse().unwrap(); + let set_rest_timers = &mut exercise_extra_information.settings.set_rest_timers; + match right { + "drop" => set_rest_timers.drop = Some(value), + "normal" => set_rest_timers.normal = Some(value), + "warmup" => set_rest_timers.warmup = Some(value), + "failure" => set_rest_timers.failure = Some(value), + _ => return Err(err()), + } + } + _ => return Err(err()), + }; + } else { + match input.change.property.as_str() { + "exclude_from_analytics" => { + exercise_extra_information.settings.exclude_from_analytics = + input.change.value.parse().unwrap(); } + _ => return Err(err()), } - _ => return Err(err()), - }; + } let mut ute: user_to_entity::ActiveModel = ute.into(); ute.exercise_extra_information = ActiveValue::Set(Some(exercise_extra_information)); ute.update(&self.0.db).await?; From b757fb2f297feb8d873b2d601923b769c4014d70 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 08:55:03 +0530 Subject: [PATCH 070/233] feat(frontend): allow excluding exercise from analytics --- ...oard.fitness.exercises.item.$id._index.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx index 414c0f5461..5af2d04615 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx @@ -18,6 +18,7 @@ import { Select, SimpleGrid, Stack, + Switch, Tabs, Text, Title, @@ -138,11 +139,10 @@ export const action = async ({ params, request }: ActionFunctionArgs) => { const entries = Object.entries(Object.fromEntries(await request.formData())); const submission = []; for (const [property, value] of entries) { - if (property.includes(".")) - submission.push({ - property, - value: value.toString(), - }); + submission.push({ + property, + value: value.toString(), + }); } for (const change of submission) { await serverGqlService.authenticatedRequest( @@ -217,7 +217,19 @@ export default function Page() { value={pref[1]} /> ))} - Rest timers + { + appendPref( + "exclude_from_analytics", + String(ev.currentTarget.checked), + ); + }} + /> When a new set is added, rest timers will be added automatically according to the settings below. From e759bbb5b05fdbe4b47b70f46f876f80e10e930c Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 09:26:31 +0530 Subject: [PATCH 071/233] feat(frontend): display the active episode for shows and podcasts --- .../routes/_dashboard.media.item.$id._index.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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 3a4dadbe00..489f5e08c4 100644 --- a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx +++ b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx @@ -266,6 +266,7 @@ export default function Page() { const [_m, setMetadataToUpdate] = useMetadataProgressUpdate(); const [_r, setEntityToReview] = useReviewEntity(); const [_a, setAddEntityToCollectionData] = useAddEntityToCollection(); + const inProgress = loaderData.userMetadataDetails.inProgress; const nextEntry = loaderData.userMetadataDetails.nextEntry; const onSubmitProgressUpdate = (e: React.FormEvent) => { @@ -508,14 +509,17 @@ export default function Page() { : null}
) : null} - {loaderData.userMetadataDetails?.inProgress ? ( + {inProgress ? ( } variant="outline"> You are currently{" "} {getVerb(Verb.Read, loaderData.metadataDetails.lot)} - ing this ( - {Number(loaderData.userMetadataDetails.inProgress.progress).toFixed( - 2, - )} + ing{" "} + {inProgress.podcastExtraInformation + ? `EP-${inProgress.podcastExtraInformation.episode}` + : inProgress.showExtraInformation + ? `S${inProgress.showExtraInformation.season}-E${inProgress.showExtraInformation.episode}` + : "this"}{" "} + ({Number(inProgress.progress).toFixed(2)} %) ) : null} From ebb714593eccaaced5f70ee78028e5920f5465b2 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 28 Nov 2024 22:46:28 +0530 Subject: [PATCH 072/233] feat(frontend): remove scroll margin when it is no longer first exercise --- apps/frontend/app/lib/state/fitness.ts | 1 + apps/frontend/app/routes/_dashboard.fitness.$action.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/frontend/app/lib/state/fitness.ts b/apps/frontend/app/lib/state/fitness.ts index d4f9965ab2..372d8b52d7 100644 --- a/apps/frontend/app/lib/state/fitness.ts +++ b/apps/frontend/app/lib/state/fitness.ts @@ -57,6 +57,7 @@ export type Exercise = { isCollapsed?: boolean; sets: Array; isShowDetailsOpen: boolean; + scrollMarginRemoved?: true; openedDetailsTab?: "images" | "history"; alreadyDoneSets: Array; }; diff --git a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx index 4b774c9e4b..7c794d4d61 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx @@ -1101,7 +1101,7 @@ const ExerciseDisplay = (props: { Date: Sat, 30 Nov 2024 16:52:33 +0530 Subject: [PATCH 073/233] fix(providers): do not unwrap directly --- crates/providers/src/igdb.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/providers/src/igdb.rs b/crates/providers/src/igdb.rs index 3a648388bf..b5612e5e11 100644 --- a/crates/providers/src/igdb.rs +++ b/crates/providers/src/igdb.rs @@ -436,7 +436,7 @@ where id = {id}; .map_err(|e| anyhow!(e))?; ryot_log!(debug, "Response = {:?}", rsp); let mut details: Vec = rsp.json().await.map_err(|e| anyhow!(e))?; - let detail = details.pop().unwrap(); + let detail = details.pop().ok_or_else(|| anyhow!("No details found"))?; let groups = match detail.collection.as_ref() { Some(c) => vec![c.id.to_string()], None => vec![], From c623456fc026268ba1b1b96ae2800a9cfaa3a462 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 2 Dec 2024 11:30:40 +0530 Subject: [PATCH 074/233] perf(services/fitness): do not cast json to text when filtering on muscles --- crates/services/fitness/src/lib.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/services/fitness/src/lib.rs b/crates/services/fitness/src/lib.rs index 3977517e53..d152b268ea 100644 --- a/crates/services/fitness/src/lib.rs +++ b/crates/services/fitness/src/lib.rs @@ -46,7 +46,9 @@ use sea_orm::{ prelude::DateTimeUtc, ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, Iterable, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait, RelationTrait, }; -use sea_query::{extension::postgres::PgExpr, Alias, Condition, Expr, Func, JoinType, OnConflict}; +use sea_query::{ + extension::postgres::PgExpr, Alias, Condition, Expr, Func, JoinType, OnConflict, PgFunc, +}; use slug::slugify; use supporting_service::SupportingService; @@ -348,13 +350,7 @@ impl ExerciseService { query .apply_if(q.lot, |q, v| q.filter(exercise::Column::Lot.eq(v))) .apply_if(q.muscle, |q, v| { - q.filter( - Expr::expr(Func::cast_as( - Expr::col(exercise::Column::Muscles), - Alias::new("text"), - )) - .ilike(ilike_sql(&v.to_string())), - ) + q.filter(Expr::val(v).eq(PgFunc::any(Expr::col(exercise::Column::Muscles)))) }) .apply_if(q.level, |q, v| q.filter(exercise::Column::Level.eq(v))) .apply_if(q.force, |q, v| q.filter(exercise::Column::Force.eq(v))) From 664b36ab6e0ba472a6f70678b7f69f793970e71f Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 4 Dec 2024 07:36:04 +0530 Subject: [PATCH 075/233] chore(enums): some other changes --- crates/enums/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/enums/src/lib.rs b/crates/enums/src/lib.rs index 68880b9d3e..f2cfffdad0 100644 --- a/crates/enums/src/lib.rs +++ b/crates/enums/src/lib.rs @@ -1,6 +1,6 @@ use async_graphql::Enum; use schematic::ConfigEnum; -use sea_orm::{DeriveActiveEnum, EnumIter}; +use sea_orm::{DeriveActiveEnum, EnumIter, FromJsonQueryResult}; use sea_orm_migration::prelude::*; use serde::{Deserialize, Serialize}; use strum::Display; @@ -438,17 +438,17 @@ pub enum ExerciseSource { /// The different types of personal bests that can be achieved on a set. #[derive( - Clone, - Debug, - Deserialize, - Serialize, - FromJsonQueryResult, Eq, - PartialEq, Enum, Copy, + Clone, + Debug, Default, + PartialEq, + Serialize, ConfigEnum, + Deserialize, + FromJsonQueryResult, )] #[serde(rename_all = "snake_case")] pub enum WorkoutSetPersonalBest { From 6fc9141383a649a320d30d42cb2a374a60204564 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 4 Dec 2024 08:04:48 +0530 Subject: [PATCH 076/233] feat(migrations): new columns for daily_user_activity --- .../m20240827_create_daily_user_activity.rs | 29 ++++++++++++++----- .../src/m20241126_changes_for_issue_1113.rs | 22 ++++++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/crates/migrations/src/m20240827_create_daily_user_activity.rs b/crates/migrations/src/m20240827_create_daily_user_activity.rs index 0f41c9c300..88dea47ad7 100644 --- a/crates/migrations/src/m20240827_create_daily_user_activity.rs +++ b/crates/migrations/src/m20240827_create_daily_user_activity.rs @@ -6,7 +6,7 @@ fn integer_not_null(col: T) -> ColumnDef { ColumnDef::new(col).integer().not_null().default(0).take() } -pub static DAILY_USER_ACTIVITY_PRIMARY_KEY: &str = "pk-daily_user_activity"; +pub static DAILY_USER_ACTIVITY_COMPOSITE_UNIQUE_KEY: &str = "daily_user_activity_uqi1"; #[derive(DeriveMigrationName)] pub struct Migration; @@ -15,6 +15,7 @@ pub struct Migration; pub enum DailyUserActivity { Table, UserId, + Id, Date, EntityIds, MetadataReviewCount, @@ -64,7 +65,7 @@ impl MigrationTrait for Migration { .create_table( Table::create() .table(DailyUserActivity::Table) - .col(ColumnDef::new(DailyUserActivity::Date).date().not_null()) + .col(ColumnDef::new(DailyUserActivity::Date).date()) .col(ColumnDef::new(DailyUserActivity::UserId).text().not_null()) .col( ColumnDef::new(DailyUserActivity::EntityIds) @@ -72,12 +73,6 @@ impl MigrationTrait for Migration { .not_null() .default(Expr::cust("'{}'")), ) - .primary_key( - Index::create() - .name(DAILY_USER_ACTIVITY_PRIMARY_KEY) - .col(DailyUserActivity::Date) - .col(DailyUserActivity::UserId), - ) .col(integer_not_null(DailyUserActivity::MetadataReviewCount)) .col(integer_not_null(DailyUserActivity::CollectionReviewCount)) .col(integer_not_null( @@ -119,6 +114,13 @@ impl MigrationTrait for Migration { .not_null() .default(Expr::cust("'[]'")), ) + .col( + ColumnDef::new(DailyUserActivity::Id) + .uuid() + .not_null() + .default(PgFunc::gen_random_uuid()) + .primary_key(), + ) .col( ColumnDef::new(DailyUserActivity::WorkoutMuscles) .array(ColumnType::Text) @@ -148,6 +150,17 @@ impl MigrationTrait for Migration { .to_owned(), ) .await?; + manager + .create_index( + Index::create() + .name(DAILY_USER_ACTIVITY_COMPOSITE_UNIQUE_KEY) + .unique() + .table(DailyUserActivity::Table) + .col(DailyUserActivity::UserId) + .col(DailyUserActivity::Date) + .to_owned(), + ) + .await?; manager .create_index( Index::create() diff --git a/crates/migrations/src/m20241126_changes_for_issue_1113.rs b/crates/migrations/src/m20241126_changes_for_issue_1113.rs index 8337e166b4..0059d4e22a 100644 --- a/crates/migrations/src/m20241126_changes_for_issue_1113.rs +++ b/crates/migrations/src/m20241126_changes_for_issue_1113.rs @@ -1,5 +1,7 @@ use sea_orm_migration::prelude::*; +use crate::m20240827_create_daily_user_activity::DAILY_USER_ACTIVITY_COMPOSITE_UNIQUE_KEY; + const NEW_DUA_COLUMNS: [&str; 3] = ["workout_muscles", "workout_exercises", "workout_equipments"]; #[derive(DeriveMigrationName)] @@ -11,6 +13,26 @@ impl MigrationTrait for Migration { let db = manager.get_connection(); db.execute_unprepared("TRUNCATE daily_user_activity") .await?; + if !manager.has_column("daily_user_activity", "id").await? { + db.execute_unprepared(&format!( + r#" +ALTER TABLE daily_user_activity +DROP CONSTRAINT "pk-daily_user_activity"; + +ALTER TABLE daily_user_activity ADD COLUMN "id" UUID NOT NULL DEFAULT gen_random_uuid(); + +ALTER TABLE daily_user_activity +ALTER COLUMN "date" DROP NOT NULL; + +ALTER TABLE daily_user_activity +ADD CONSTRAINT "daily_user_activity_pkey" PRIMARY KEY (id); + +CREATE UNIQUE INDEX "{}" ON daily_user_activity (user_id, date); + "#, + DAILY_USER_ACTIVITY_COMPOSITE_UNIQUE_KEY + )) + .await?; + } for col in NEW_DUA_COLUMNS { if !manager.has_column("daily_user_activity", col).await? { db.execute_unprepared(&format!( From 5cf23c95134cdb82a0b8b4f0a8d92fc70b102004 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 4 Dec 2024 08:20:20 +0530 Subject: [PATCH 077/233] feat(migrations): drop entire table and recreate it --- .../m20240827_create_daily_user_activity.rs | 223 +++++++++--------- .../src/m20241126_changes_for_issue_1113.rs | 34 +-- 2 files changed, 117 insertions(+), 140 deletions(-) diff --git a/crates/migrations/src/m20240827_create_daily_user_activity.rs b/crates/migrations/src/m20240827_create_daily_user_activity.rs index 88dea47ad7..67e5c54417 100644 --- a/crates/migrations/src/m20240827_create_daily_user_activity.rs +++ b/crates/migrations/src/m20240827_create_daily_user_activity.rs @@ -58,118 +58,123 @@ pub enum DailyUserActivity { WorkoutEquipments, } +pub async fn create_daily_user_activity_table(manager: &SchemaManager<'_>) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(DailyUserActivity::Table) + .col( + ColumnDef::new(DailyUserActivity::Id) + .uuid() + .not_null() + .default(PgFunc::gen_random_uuid()) + .primary_key(), + ) + .col(ColumnDef::new(DailyUserActivity::Date).date()) + .col(ColumnDef::new(DailyUserActivity::UserId).text().not_null()) + .col( + ColumnDef::new(DailyUserActivity::EntityIds) + .array(ColumnType::Text) + .not_null() + .default(Expr::cust("'{}'")), + ) + .col(integer_not_null(DailyUserActivity::MetadataReviewCount)) + .col(integer_not_null(DailyUserActivity::CollectionReviewCount)) + .col(integer_not_null( + DailyUserActivity::MetadataGroupReviewCount, + )) + .col(integer_not_null(DailyUserActivity::PersonReviewCount)) + .col(integer_not_null(DailyUserActivity::ExerciseReviewCount)) + .col(integer_not_null(DailyUserActivity::WorkoutCount)) + .col(integer_not_null(DailyUserActivity::WorkoutDuration)) + .col(integer_not_null(DailyUserActivity::MeasurementCount)) + .col(integer_not_null(DailyUserActivity::AudioBookCount)) + .col(integer_not_null(DailyUserActivity::AudioBookDuration)) + .col(integer_not_null(DailyUserActivity::AnimeCount)) + .col(integer_not_null(DailyUserActivity::BookCount)) + .col(integer_not_null(DailyUserActivity::BookPages)) + .col(integer_not_null(DailyUserActivity::PodcastCount)) + .col(integer_not_null(DailyUserActivity::PodcastDuration)) + .col(integer_not_null(DailyUserActivity::MangaCount)) + .col(integer_not_null(DailyUserActivity::MovieCount)) + .col(integer_not_null(DailyUserActivity::MovieDuration)) + .col(integer_not_null(DailyUserActivity::ShowCount)) + .col(integer_not_null(DailyUserActivity::ShowDuration)) + .col(integer_not_null(DailyUserActivity::VideoGameCount)) + .col(integer_not_null(DailyUserActivity::VideoGameDuration)) + .col(integer_not_null(DailyUserActivity::VisualNovelCount)) + .col(integer_not_null(DailyUserActivity::VisualNovelDuration)) + .col(integer_not_null(DailyUserActivity::WorkoutPersonalBests)) + .col(integer_not_null(DailyUserActivity::WorkoutWeight)) + .col(integer_not_null(DailyUserActivity::WorkoutReps)) + .col(integer_not_null(DailyUserActivity::WorkoutDistance)) + .col(integer_not_null(DailyUserActivity::WorkoutRestTime)) + .col(integer_not_null(DailyUserActivity::TotalMetadataCount)) + .col(integer_not_null(DailyUserActivity::TotalReviewCount)) + .col(integer_not_null(DailyUserActivity::TotalCount)) + .col(integer_not_null(DailyUserActivity::TotalDuration)) + .col( + ColumnDef::new(DailyUserActivity::HourRecords) + .json_binary() + .not_null() + .default(Expr::cust("'[]'")), + ) + .col( + ColumnDef::new(DailyUserActivity::WorkoutMuscles) + .array(ColumnType::Text) + .not_null() + .default(Expr::cust("'{}'")), + ) + .col( + ColumnDef::new(DailyUserActivity::WorkoutExercises) + .array(ColumnType::Text) + .not_null() + .default(Expr::cust("'{}'")), + ) + .col( + ColumnDef::new(DailyUserActivity::WorkoutEquipments) + .array(ColumnType::Text) + .not_null() + .default(Expr::cust("'{}'")), + ) + .foreign_key( + ForeignKey::create() + .name("daily_user_activity_to_user_foreign_key") + .from(DailyUserActivity::Table, DailyUserActivity::UserId) + .to(User::Table, User::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .name(DAILY_USER_ACTIVITY_COMPOSITE_UNIQUE_KEY) + .unique() + .table(DailyUserActivity::Table) + .col(DailyUserActivity::UserId) + .col(DailyUserActivity::Date) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .name("daily_user_activity-user_id__index") + .table(DailyUserActivity::Table) + .col(DailyUserActivity::UserId) + .to_owned(), + ) + .await?; + Ok(()) +} + #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(DailyUserActivity::Table) - .col(ColumnDef::new(DailyUserActivity::Date).date()) - .col(ColumnDef::new(DailyUserActivity::UserId).text().not_null()) - .col( - ColumnDef::new(DailyUserActivity::EntityIds) - .array(ColumnType::Text) - .not_null() - .default(Expr::cust("'{}'")), - ) - .col(integer_not_null(DailyUserActivity::MetadataReviewCount)) - .col(integer_not_null(DailyUserActivity::CollectionReviewCount)) - .col(integer_not_null( - DailyUserActivity::MetadataGroupReviewCount, - )) - .col(integer_not_null(DailyUserActivity::PersonReviewCount)) - .col(integer_not_null(DailyUserActivity::ExerciseReviewCount)) - .col(integer_not_null(DailyUserActivity::WorkoutCount)) - .col(integer_not_null(DailyUserActivity::WorkoutDuration)) - .col(integer_not_null(DailyUserActivity::MeasurementCount)) - .col(integer_not_null(DailyUserActivity::AudioBookCount)) - .col(integer_not_null(DailyUserActivity::AudioBookDuration)) - .col(integer_not_null(DailyUserActivity::AnimeCount)) - .col(integer_not_null(DailyUserActivity::BookCount)) - .col(integer_not_null(DailyUserActivity::BookPages)) - .col(integer_not_null(DailyUserActivity::PodcastCount)) - .col(integer_not_null(DailyUserActivity::PodcastDuration)) - .col(integer_not_null(DailyUserActivity::MangaCount)) - .col(integer_not_null(DailyUserActivity::MovieCount)) - .col(integer_not_null(DailyUserActivity::MovieDuration)) - .col(integer_not_null(DailyUserActivity::ShowCount)) - .col(integer_not_null(DailyUserActivity::ShowDuration)) - .col(integer_not_null(DailyUserActivity::VideoGameCount)) - .col(integer_not_null(DailyUserActivity::VideoGameDuration)) - .col(integer_not_null(DailyUserActivity::VisualNovelCount)) - .col(integer_not_null(DailyUserActivity::VisualNovelDuration)) - .col(integer_not_null(DailyUserActivity::WorkoutPersonalBests)) - .col(integer_not_null(DailyUserActivity::WorkoutWeight)) - .col(integer_not_null(DailyUserActivity::WorkoutReps)) - .col(integer_not_null(DailyUserActivity::WorkoutDistance)) - .col(integer_not_null(DailyUserActivity::WorkoutRestTime)) - .col(integer_not_null(DailyUserActivity::TotalMetadataCount)) - .col(integer_not_null(DailyUserActivity::TotalReviewCount)) - .col(integer_not_null(DailyUserActivity::TotalCount)) - .col(integer_not_null(DailyUserActivity::TotalDuration)) - .col( - ColumnDef::new(DailyUserActivity::HourRecords) - .json_binary() - .not_null() - .default(Expr::cust("'[]'")), - ) - .col( - ColumnDef::new(DailyUserActivity::Id) - .uuid() - .not_null() - .default(PgFunc::gen_random_uuid()) - .primary_key(), - ) - .col( - ColumnDef::new(DailyUserActivity::WorkoutMuscles) - .array(ColumnType::Text) - .not_null() - .default(Expr::cust("'{}'")), - ) - .col( - ColumnDef::new(DailyUserActivity::WorkoutExercises) - .array(ColumnType::Text) - .not_null() - .default(Expr::cust("'{}'")), - ) - .col( - ColumnDef::new(DailyUserActivity::WorkoutEquipments) - .array(ColumnType::Text) - .not_null() - .default(Expr::cust("'{}'")), - ) - .foreign_key( - ForeignKey::create() - .name("daily_user_activity_to_user_foreign_key") - .from(DailyUserActivity::Table, DailyUserActivity::UserId) - .to(User::Table, User::Id) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ) - .to_owned(), - ) - .await?; - manager - .create_index( - Index::create() - .name(DAILY_USER_ACTIVITY_COMPOSITE_UNIQUE_KEY) - .unique() - .table(DailyUserActivity::Table) - .col(DailyUserActivity::UserId) - .col(DailyUserActivity::Date) - .to_owned(), - ) - .await?; - manager - .create_index( - Index::create() - .name("daily_user_activity-user_id__index") - .table(DailyUserActivity::Table) - .col(DailyUserActivity::UserId) - .to_owned(), - ) - .await?; + create_daily_user_activity_table(manager).await?; Ok(()) } diff --git a/crates/migrations/src/m20241126_changes_for_issue_1113.rs b/crates/migrations/src/m20241126_changes_for_issue_1113.rs index 0059d4e22a..d1704a71f7 100644 --- a/crates/migrations/src/m20241126_changes_for_issue_1113.rs +++ b/crates/migrations/src/m20241126_changes_for_issue_1113.rs @@ -1,8 +1,6 @@ use sea_orm_migration::prelude::*; -use crate::m20240827_create_daily_user_activity::DAILY_USER_ACTIVITY_COMPOSITE_UNIQUE_KEY; - -const NEW_DUA_COLUMNS: [&str; 3] = ["workout_muscles", "workout_exercises", "workout_equipments"]; +use crate::m20240827_create_daily_user_activity::create_daily_user_activity_table; #[derive(DeriveMigrationName)] pub struct Migration; @@ -11,36 +9,10 @@ pub struct Migration; impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { let db = manager.get_connection(); - db.execute_unprepared("TRUNCATE daily_user_activity") - .await?; if !manager.has_column("daily_user_activity", "id").await? { - db.execute_unprepared(&format!( - r#" -ALTER TABLE daily_user_activity -DROP CONSTRAINT "pk-daily_user_activity"; - -ALTER TABLE daily_user_activity ADD COLUMN "id" UUID NOT NULL DEFAULT gen_random_uuid(); - -ALTER TABLE daily_user_activity -ALTER COLUMN "date" DROP NOT NULL; - -ALTER TABLE daily_user_activity -ADD CONSTRAINT "daily_user_activity_pkey" PRIMARY KEY (id); - -CREATE UNIQUE INDEX "{}" ON daily_user_activity (user_id, date); - "#, - DAILY_USER_ACTIVITY_COMPOSITE_UNIQUE_KEY - )) - .await?; - } - for col in NEW_DUA_COLUMNS { - if !manager.has_column("daily_user_activity", col).await? { - db.execute_unprepared(&format!( - r#"ALTER TABLE "daily_user_activity" ADD COLUMN "{}" TEXT[] NOT NULL DEFAULT '{{}}'"#, - col, - )) + db.execute_unprepared("DROP TABLE daily_user_activity") .await?; - } + create_daily_user_activity_table(manager).await?; } db.execute_unprepared( r#" From c97da8ac7a309153a85ac1475e346b8c8e227654 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 4 Dec 2024 08:27:12 +0530 Subject: [PATCH 078/233] feat(backend): adjust daily user activity calculation to new database schema --- .../database/src/daily_user_activity.rs | 6 +++--- crates/utils/database/src/lib.rs | 21 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/crates/models/database/src/daily_user_activity.rs b/crates/models/database/src/daily_user_activity.rs index ab448d9907..ca16d3abe7 100644 --- a/crates/models/database/src/daily_user_activity.rs +++ b/crates/models/database/src/daily_user_activity.rs @@ -8,10 +8,10 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Default, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "daily_user_activity")] pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] + #[sea_orm(primary_key)] + pub id: Uuid, pub user_id: String, - #[sea_orm(primary_key, auto_increment = false)] - pub date: Date, + pub date: Option, pub entity_ids: Vec, pub metadata_review_count: i32, pub collection_review_count: i32, diff --git a/crates/utils/database/src/lib.rs b/crates/utils/database/src/lib.rs index 693d30887e..6bd8ee7c86 100644 --- a/crates/utils/database/src/lib.rs +++ b/crates/utils/database/src/lib.rs @@ -597,15 +597,15 @@ pub async fn calculate_user_activities_and_summary( .order_by_desc(daily_user_activity::Column::Date) .one(db) .await? - .map(|i| i.date) + .and_then(|i| i.date) .unwrap_or_default(), }; let mut activities = HashMap::new(); fn get_activity_count<'a>( - activities: &'a mut HashMap, + activities: &'a mut HashMap, daily_user_activity::Model>, user_id: &'a String, - date: Date, + date: Option, entity_id: String, entity_lot: EntityLot, metadata_lot: Option, @@ -681,12 +681,10 @@ pub async fn calculate_user_activities_and_summary( .await?; while let Some(seen) = seen_stream.try_next().await? { - let default_date = Date::from_ymd_opt(2023, 4, 3).unwrap(); // DEV: The first commit of Ryot - let date = seen.finished_on.unwrap_or(default_date); let activity = get_activity_count( &mut activities, user_id, - date, + seen.finished_on, seen.seen_id, EntityLot::Metadata, Some(seen.metadata_lot), @@ -755,7 +753,7 @@ pub async fn calculate_user_activities_and_summary( let activity = get_activity_count( &mut activities, user_id, - date, + Some(date), workout.id, EntityLot::Workout, None, @@ -803,7 +801,7 @@ pub async fn calculate_user_activities_and_summary( let activity = get_activity_count( &mut activities, user_id, - date, + Some(date), measurement.timestamp.to_string(), EntityLot::UserMeasurement, None, @@ -822,7 +820,7 @@ pub async fn calculate_user_activities_and_summary( let activity = get_activity_count( &mut activities, user_id, - date, + Some(date), review.id, EntityLot::Review, None, @@ -845,10 +843,10 @@ pub async fn calculate_user_activities_and_summary( .one(db) .await? { - ryot_log!(debug, "Deleting activity = {:#?}", activity.date); + ryot_log!(debug, "Deleting activity = {:?}", activity.date); entity.delete(db).await?; } - ryot_log!(debug, "Inserting activity = {:#?}", activity.date); + ryot_log!(debug, "Inserting activity = {:?}", activity.date); let total_review_count = activity.metadata_review_count + activity.collection_review_count + activity.metadata_group_review_count @@ -876,6 +874,7 @@ pub async fn calculate_user_activities_and_summary( + activity.video_game_duration; activity.hour_records.sort_by_key(|hr| hr.hour); let mut model: daily_user_activity::ActiveModel = activity.clone().into(); + model.id = ActiveValue::NotSet; model.total_review_count = ActiveValue::Set(total_review_count); model.total_metadata_count = ActiveValue::Set(total_metadata_count); model.total_count = ActiveValue::Set(total_count); From 673e24ef9d3b93f61776b3d391c7a4d7b1738645 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 4 Dec 2024 08:39:00 +0530 Subject: [PATCH 079/233] fix(services/statistics): adjust daily user activity calculation to new database schema --- crates/services/statistics/src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index 15c2306711..67b12a5e34 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -75,13 +75,17 @@ impl StatisticsService { } }; let day_alias = Expr::col(Alias::new("day")); + let date_type = Alias::new("DATE"); let items = precondition .column_as( Expr::expr(Func::cast_as( Func::cust(DateTrunc) .arg(Expr::val(grouped_by.to_string())) - .arg(daily_user_activity::Column::Date.into_expr()), - Alias::new("DATE"), + .arg(Func::coalesce([ + Expr::col(daily_user_activity::Column::Date).into(), + Func::cast_as(Expr::val("2000-01-01"), date_type.clone()).into(), + ])), + date_type, )), "day", ) From cfbf161b6faaf9f56f7d01e520aafb12a8999a19 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 4 Dec 2024 08:42:37 +0530 Subject: [PATCH 080/233] fix(services/statistics): change fallback to include in same millenium --- crates/services/statistics/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index 67b12a5e34..f9fbffe1c7 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -83,7 +83,7 @@ impl StatisticsService { .arg(Expr::val(grouped_by.to_string())) .arg(Func::coalesce([ Expr::col(daily_user_activity::Column::Date).into(), - Func::cast_as(Expr::val("2000-01-01"), date_type.clone()).into(), + Func::cast_as(Expr::val("2001-01-01"), date_type.clone()).into(), ])), date_type, )), From 7b32251a542ee448ac6e03f21ff146aa6eabff1f Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 4 Dec 2024 09:42:43 +0530 Subject: [PATCH 081/233] chore(backend): make the expiry of application cache nullable --- .../src/m20241004_create_application_cache.rs | 6 +----- .../src/m20241126_changes_for_issue_1113.rs | 4 ---- crates/models/database/src/application_cache.rs | 2 +- crates/providers/src/igdb.rs | 2 +- crates/providers/src/listennotes.rs | 2 +- crates/providers/src/tmdb.rs | 2 +- crates/services/cache/src/lib.rs | 12 ++++++++---- crates/services/statistics/src/lib.rs | 2 +- crates/utils/dependent/src/lib.rs | 6 +++++- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/migrations/src/m20241004_create_application_cache.rs b/crates/migrations/src/m20241004_create_application_cache.rs index f06b948b99..3f21da02b4 100644 --- a/crates/migrations/src/m20241004_create_application_cache.rs +++ b/crates/migrations/src/m20241004_create_application_cache.rs @@ -39,11 +39,7 @@ impl MigrationTrait for Migration { .not_null() .unique_key(), ) - .col( - ColumnDef::new(ApplicationCache::ExpiresAt) - .not_null() - .timestamp_with_time_zone(), - ) + .col(ColumnDef::new(ApplicationCache::ExpiresAt).timestamp_with_time_zone()) .col(ColumnDef::new(ApplicationCache::Value).json_binary()) .to_owned(), ) diff --git a/crates/migrations/src/m20241126_changes_for_issue_1113.rs b/crates/migrations/src/m20241126_changes_for_issue_1113.rs index d1704a71f7..4db95a59c4 100644 --- a/crates/migrations/src/m20241126_changes_for_issue_1113.rs +++ b/crates/migrations/src/m20241126_changes_for_issue_1113.rs @@ -43,10 +43,6 @@ END $$; UPDATE "user" SET "preferences" = jsonb_set("preferences", '{features_enabled,fitness,analytics}', 'true'); "#) .await?; - db.execute_unprepared( - r#"ALTER TABLE application_cache ALTER COLUMN expires_at SET NOT NULL;"#, - ) - .await?; db.execute_unprepared( r#" UPDATE "user_to_entity" SET "exercise_extra_information" = jsonb_set("exercise_extra_information", '{settings,exclude_from_analytics}', 'false') diff --git a/crates/models/database/src/application_cache.rs b/crates/models/database/src/application_cache.rs index 8558f0ba8a..30742081f1 100644 --- a/crates/models/database/src/application_cache.rs +++ b/crates/models/database/src/application_cache.rs @@ -13,7 +13,7 @@ pub struct Model { pub key: ApplicationCacheKey, #[sea_orm(column_type = "Json")] pub value: Option, - pub expires_at: DateTimeUtc, + pub expires_at: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/providers/src/igdb.rs b/crates/providers/src/igdb.rs index b5612e5e11..2c9195451d 100644 --- a/crates/providers/src/igdb.rs +++ b/crates/providers/src/igdb.rs @@ -547,7 +547,7 @@ impl IgdbService { let access_token = self.get_access_token().await; cc.set_with_expiry( ApplicationCacheKey::IgdbSettings, - 4, + None, Some(ApplicationCacheValue::IgdbSettings { access_token: access_token.clone(), }), diff --git a/crates/providers/src/listennotes.rs b/crates/providers/src/listennotes.rs index 9b5c18b45e..6d48062634 100644 --- a/crates/providers/src/listennotes.rs +++ b/crates/providers/src/listennotes.rs @@ -218,7 +218,7 @@ impl ListennotesService { } cc.set_with_expiry( ApplicationCacheKey::ListennotesSettings, - 4, + None, Some(ApplicationCacheValue::ListennotesSettings { genres: genres.clone(), }), diff --git a/crates/providers/src/tmdb.rs b/crates/providers/src/tmdb.rs index 571527211a..e119c3a4bf 100644 --- a/crates/providers/src/tmdb.rs +++ b/crates/providers/src/tmdb.rs @@ -1345,7 +1345,7 @@ async fn get_settings( }; cc.set_with_expiry( ApplicationCacheKey::TmdbSettings, - 4, + None, Some(ApplicationCacheValue::TmdbSettings(settings.clone())), ) .await diff --git a/crates/services/cache/src/lib.rs b/crates/services/cache/src/lib.rs index db61e45314..54ed2123c2 100644 --- a/crates/services/cache/src/lib.rs +++ b/crates/services/cache/src/lib.rs @@ -21,7 +21,7 @@ impl CacheService { pub async fn set_with_expiry( &self, key: ApplicationCacheKey, - expiry_hours: i64, + expiry_hours: Option, value: Option, ) -> Result { let now = Utc::now(); @@ -29,7 +29,7 @@ impl CacheService { key: ActiveValue::Set(key), value: ActiveValue::Set(value), created_at: ActiveValue::Set(now), - expires_at: ActiveValue::Set(now + Duration::hours(expiry_hours)), + expires_at: ActiveValue::Set(expiry_hours.map(|hours| now + Duration::hours(hours))), ..Default::default() }; let inserted = ApplicationCache::insert(to_insert) @@ -55,7 +55,11 @@ impl CacheService { .one(&self.db) .await?; Ok(cache - .filter(|cache| cache.expires_at > Utc::now()) + .filter(|cache| { + cache + .expires_at + .map_or(false, |expires_at| expires_at > Utc::now()) + }) .and_then(|m| m.value)) } @@ -63,7 +67,7 @@ impl CacheService { let deleted = ApplicationCache::update_many() .filter(application_cache::Column::Key.eq(key)) .set(application_cache::ActiveModel { - expires_at: ActiveValue::Set(Utc::now()), + expires_at: ActiveValue::Set(Some(Utc::now())), ..Default::default() }) .exec(&self.db) diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index f9fbffe1c7..0947f9c70b 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -353,7 +353,7 @@ impl StatisticsService { .cache_service .set_with_expiry( cache_key, - 2, + Some(2), Some(ApplicationCacheValue::FitnessAnalytics(response.clone())), ) .await?; diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs index 5e4bbbaa03..1e4bb46518 100644 --- a/crates/utils/dependent/src/lib.rs +++ b/crates/utils/dependent/src/lib.rs @@ -1474,7 +1474,11 @@ pub async fn progress_update( let id = seen.id.clone(); if seen.state == SeenState::Completed && respect_cache { ss.cache_service - .set_with_expiry(cache, ss.config.server.progress_update_threshold, None) + .set_with_expiry( + cache, + Some(ss.config.server.progress_update_threshold), + None, + ) .await?; } if seen.state == SeenState::Completed { From 6d63ab96cb214271ff7106ccd48133a970d79902 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 4 Dec 2024 13:42:40 +0530 Subject: [PATCH 082/233] chore(backend): change names of jobs --- apps/backend/src/job.rs | 4 ++-- apps/backend/src/main.rs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/backend/src/job.rs b/apps/backend/src/job.rs index 194f2bedf1..89fe2f48fa 100644 --- a/apps/backend/src/job.rs +++ b/apps/backend/src/job.rs @@ -11,7 +11,7 @@ use media_models::CommitMediaInput; use miscellaneous_service::MiscellaneousService; use statistics_service::StatisticsService; -pub async fn background_jobs( +pub async fn run_background_jobs( information: ScheduledJob, misc_service: Data>, ) -> Result<(), Error> { @@ -20,7 +20,7 @@ pub async fn background_jobs( Ok(()) } -pub async fn sync_integrations_data( +pub async fn run_frequent_jobs( _information: ScheduledJob, integration_service: Data>, ) -> Result<(), Error> { diff --git a/apps/backend/src/main.rs b/apps/backend/src/main.rs index 0f4862e651..ac1d869806 100644 --- a/apps/backend/src/main.rs +++ b/apps/backend/src/main.rs @@ -40,8 +40,8 @@ use unkey::{models::VerifyKeyRequest, Client}; use crate::{ common::create_app_services, job::{ - background_jobs, perform_application_job, perform_core_application_job, - sync_integrations_data, + perform_application_job, perform_core_application_job, run_background_jobs, + run_frequent_jobs, }, }; @@ -210,7 +210,7 @@ async fn main() -> Result<()> { let monitor = Monitor::::new() .register_with_count( 1, - WorkerBuilder::new("background_jobs") + WorkerBuilder::new("daily_background_jobs") .stream( // every day CronStream::new_with_timezone(Schedule::from_str("0 0 0 * * *").unwrap(), tz) @@ -218,11 +218,11 @@ async fn main() -> Result<()> { ) .layer(ApalisTraceLayer::new()) .data(miscellaneous_service_1.clone()) - .build_fn(background_jobs), + .build_fn(run_background_jobs), ) .register_with_count( 1, - WorkerBuilder::new("sync_integrations_data") + WorkerBuilder::new("frequent_jobs") .stream( CronStream::new_with_timezone( Schedule::from_str(&format!("0 */{} * * * *", sync_every_minutes)).unwrap(), @@ -232,7 +232,7 @@ async fn main() -> Result<()> { ) .layer(ApalisTraceLayer::new()) .data(integration_service_1.clone()) - .build_fn(sync_integrations_data), + .build_fn(run_frequent_jobs), ) // application jobs .register_with_count( From 97e8804d870394cd0b66957c28f73f0fda56f4cf Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 4 Dec 2024 16:11:36 +0530 Subject: [PATCH 083/233] feat: more resilient logic for validating pro keys --- Cargo.lock | 9 +-- apps/backend/Cargo.toml | 5 +- apps/backend/src/common.rs | 2 - apps/backend/src/job.rs | 16 +++- apps/backend/src/main.rs | 71 +++-------------- apps/frontend/app/components/common.tsx | 2 +- apps/frontend/app/lib/hooks.ts | 4 +- apps/frontend/app/lib/state/fitness.ts | 2 +- apps/frontend/app/lib/utilities.server.ts | 1 + .../frontend/app/routes/_dashboard._index.tsx | 2 +- .../routes/_dashboard.collections.list.tsx | 13 +-- .../app/routes/_dashboard.fitness.$action.tsx | 8 +- .../_dashboard.fitness.$entity.$id._index.tsx | 2 +- .../_dashboard.fitness.$entity.list.tsx | 2 +- .../routes/_dashboard.media.genre.list.tsx | 2 +- .../_dashboard.media.item.$id._index.tsx | 16 ++-- .../_dashboard.settings.integrations.tsx | 12 +-- apps/frontend/app/routes/_dashboard.tsx | 2 +- crates/background/src/lib.rs | 1 + .../src/m20241004_create_application_cache.rs | 6 +- .../src/m20241126_changes_for_issue_1113.rs | 10 ++- crates/models/common/src/lib.rs | 2 + .../models/database/src/application_cache.rs | 2 +- crates/models/dependent/src/lib.rs | 2 +- crates/providers/Cargo.toml | 1 - crates/providers/src/igdb.rs | 4 +- crates/providers/src/listennotes.rs | 4 +- crates/providers/src/tmdb.rs | 2 +- crates/services/cache/src/lib.rs | 6 +- crates/services/fitness/src/lib.rs | 6 +- crates/services/importer/src/strong_app.rs | 6 +- crates/services/miscellaneous/Cargo.toml | 2 + crates/services/miscellaneous/src/lib.rs | 79 +++++++++++++++++-- crates/services/statistics/src/lib.rs | 2 +- crates/services/supporting/Cargo.toml | 1 + crates/services/supporting/src/lib.rs | 11 ++- crates/services/user/src/lib.rs | 8 +- crates/utils/database/src/lib.rs | 4 +- crates/utils/dependent/src/lib.rs | 6 +- libs/generated/src/graphql/backend/gql.ts | 4 +- libs/generated/src/graphql/backend/graphql.ts | 6 +- .../src/graphql/backend/types.generated.ts | 2 +- libs/graphql/src/backend/queries/combined.gql | 2 +- 43 files changed, 198 insertions(+), 152 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68447a6458..d72a2c7cf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -837,7 +837,6 @@ dependencies = [ "axum", "background", "cache-service", - "chrono", "chrono-tz", "collection-resolver", "collection-service", @@ -869,9 +868,7 @@ dependencies = [ "schematic", "sea-orm", "sea-orm-migration", - "serde", "serde_json", - "serde_with 3.11.0", "statistics-resolver", "statistics-service", "supporting-service", @@ -880,7 +877,7 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", - "unkey", + "traits", "user-resolver", "user-service", ] @@ -3841,11 +3838,13 @@ dependencies = [ "sea-query", "serde", "serde_json", + "serde_with 3.11.0", "slug", "supporting-service", "tokio", "tracing", "traits", + "unkey", "user-models", "uuid", ] @@ -4578,7 +4577,6 @@ dependencies = [ "application-utils", "async-trait", "chrono", - "chrono-tz", "common-models", "common-utils", "config", @@ -6250,6 +6248,7 @@ dependencies = [ "background", "cache-service", "chrono-tz", + "common-models", "config", "file-storage-service", "openidconnect", diff --git a/apps/backend/Cargo.toml b/apps/backend/Cargo.toml index 8c9074914e..033295c0fb 100644 --- a/apps/backend/Cargo.toml +++ b/apps/backend/Cargo.toml @@ -15,7 +15,6 @@ axum = { workspace = true } aws-sdk-s3 = { workspace = true } background = { path = "../../crates/background" } cache-service = { path = "../../crates/services/cache" } -chrono = { workspace = true } chrono-tz = { workspace = true } collection-resolver = { path = "../../crates/resolvers/collection" } collection-service = { path = "../../crates/services/collection" } @@ -46,9 +45,7 @@ reqwest = { workspace = true } router-resolver = { path = "../../crates/resolvers/router" } sea-orm = { workspace = true } sea-orm-migration = { workspace = true } -serde = { workspace = true } serde_json = { workspace = true } -serde_with = { workspace = true } schematic = { workspace = true } statistics-resolver = { path = "../../crates/resolvers/statistics" } statistics-service = { path = "../../crates/services/statistics" } @@ -58,6 +55,6 @@ tower = { workspace = true } tower-http = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } -unkey = { workspace = true } +traits = { path = "../../crates/traits" } user-resolver = { path = "../../crates/resolvers/user" } user-service = { path = "../../crates/services/user" } diff --git a/apps/backend/src/common.rs b/apps/backend/src/common.rs index b9693896c8..a42da27e9d 100644 --- a/apps/backend/src/common.rs +++ b/apps/backend/src/common.rs @@ -57,7 +57,6 @@ pub struct AppServices { #[allow(clippy::too_many_arguments)] pub async fn create_app_services( - is_pro: bool, db: DatabaseConnection, timezone: chrono_tz::Tz, s3_client: aws_sdk_s3::Client, @@ -73,7 +72,6 @@ pub async fn create_app_services( let cache_service = CacheService::new(&db); let supporting_service = Arc::new( SupportingService::new( - is_pro, &db, timezone, cache_service, diff --git a/apps/backend/src/job.rs b/apps/backend/src/job.rs index 89fe2f48fa..5a522e17b1 100644 --- a/apps/backend/src/job.rs +++ b/apps/backend/src/job.rs @@ -10,21 +10,30 @@ use integration_service::IntegrationService; use media_models::CommitMediaInput; use miscellaneous_service::MiscellaneousService; use statistics_service::StatisticsService; +use traits::TraceOk; pub async fn run_background_jobs( information: ScheduledJob, misc_service: Data>, ) -> Result<(), Error> { ryot_log!(debug, "Running job at {:#?}", information.0); - misc_service.perform_background_jobs().await.unwrap(); + misc_service.perform_background_jobs().await.trace_ok(); Ok(()) } pub async fn run_frequent_jobs( _information: ScheduledJob, + misc_service: Data>, integration_service: Data>, ) -> Result<(), Error> { - integration_service.yank_integrations_data().await.unwrap(); + misc_service + .perform_server_key_validation() + .await + .trace_ok(); + integration_service + .yank_integrations_data() + .await + .trace_ok(); Ok(()) } @@ -133,6 +142,9 @@ pub async fn perform_application_job( ApplicationJob::SyncIntegrationsData => { integration_service.yank_integrations_data().await.is_ok() } + ApplicationJob::PerformServerKeyValidation => { + misc_service.perform_server_key_validation().await.is_ok() + } ApplicationJob::HandleEntityAddedToCollectionEvent(collection_to_entity_id) => { integration_service .handle_entity_added_to_collection_event(collection_to_entity_id) diff --git a/apps/backend/src/main.rs b/apps/backend/src/main.rs index ac1d869806..6dae910752 100644 --- a/apps/backend/src/main.rs +++ b/apps/backend/src/main.rs @@ -18,16 +18,13 @@ use apalis::{ }; use aws_sdk_s3::config::Region; use background::ApplicationJob; -use chrono::{DateTime, NaiveDate, TimeZone, Utc}; -use common_utils::{convert_naive_to_utc, ryot_log, COMPILATION_TIMESTAMP, PROJECT_NAME, TEMP_DIR}; +use common_utils::{ryot_log, PROJECT_NAME, TEMP_DIR}; use database_models::prelude::Exercise; -use env_utils::{APP_VERSION, UNKEY_API_ID}; +use env_utils::APP_VERSION; use logs_wheel::LogFileInitializer; use migrations::Migrator; use sea_orm::{ConnectionTrait, Database, DatabaseConnection, EntityTrait, PaginatorTrait}; use sea_orm_migration::MigratorTrait; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; use tokio::{ join, net::TcpListener, @@ -35,7 +32,6 @@ use tokio::{ }; use tower::buffer::BufferLayer; use tracing_subscriber::{fmt, layer::SubscriberExt}; -use unkey::{models::VerifyKeyRequest, Client}; use crate::{ common::create_app_services, @@ -78,14 +74,8 @@ async fn main() -> Result<()> { let sync_every_minutes = config.integration.sync_every_minutes; let disable_background_jobs = config.server.disable_background_jobs; - let compile_timestamp = Utc.timestamp_opt(COMPILATION_TIMESTAMP, 0).unwrap(); - ryot_log!(debug, "Compiled at: {}", compile_timestamp); - let config_dump_path = PathBuf::new().join(TEMP_DIR).join("config.json"); fs::write(config_dump_path, serde_json::to_string_pretty(&config)?)?; - let is_pro = get_is_pro(&config.server.pro_key, &compile_timestamp).await; - - ryot_log!(debug, "Is pro: {:#?}", is_pro); let mut aws_conf = aws_sdk_s3::Config::builder() .region(Region::new(config.file_storage.s3_region.clone())) @@ -129,10 +119,12 @@ async fn main() -> Result<()> { .unwrap_or_else(|_| chrono_tz::Etc::GMT); ryot_log!(info, "Timezone: {}", tz); - perform_application_job_storage - .enqueue(ApplicationJob::SyncIntegrationsData) - .await - .unwrap(); + for job in [ + ApplicationJob::PerformServerKeyValidation, + ApplicationJob::SyncIntegrationsData, + ] { + perform_application_job_storage.enqueue(job).await.ok(); + } if Exercise::find().count(&db).await? == 0 { ryot_log!( @@ -146,7 +138,6 @@ async fn main() -> Result<()> { } let app_services = create_app_services( - is_pro, db, tz, s3_client, @@ -206,6 +197,7 @@ async fn main() -> Result<()> { let miscellaneous_service_1 = app_services.miscellaneous_service.clone(); let miscellaneous_service_2 = app_services.miscellaneous_service.clone(); let miscellaneous_service_3 = app_services.miscellaneous_service.clone(); + let miscellaneous_service_4 = app_services.miscellaneous_service.clone(); let monitor = Monitor::::new() .register_with_count( @@ -232,6 +224,7 @@ async fn main() -> Result<()> { ) .layer(ApalisTraceLayer::new()) .data(integration_service_1.clone()) + .data(miscellaneous_service_4.clone()) .build_fn(run_frequent_jobs), ) // application jobs @@ -354,47 +347,3 @@ END $$; .await?; Ok(()) } - -async fn get_is_pro(pro_key: &str, compilation_time: &DateTime) -> bool { - if pro_key.is_empty() { - return false; - } - - ryot_log!(debug, "Verifying pro key for API ID: {:#?}", UNKEY_API_ID); - - #[skip_serializing_none] - #[derive(Debug, Serialize, Clone, Deserialize)] - struct Meta { - expiry: Option, - } - - let unkey_client = Client::new("public"); - let verify_request = VerifyKeyRequest::new(pro_key, UNKEY_API_ID); - let validated_key = match unkey_client.verify_key(verify_request).await { - Ok(verify_response) => { - if !verify_response.valid { - ryot_log!(debug, "Pro key is no longer valid."); - return false; - } - verify_response - } - Err(verify_error) => { - ryot_log!(debug, "Pro key verification error: {:?}", verify_error); - return false; - } - }; - let key_meta = validated_key - .meta - .map(|meta| serde_json::from_value::(meta).unwrap()); - ryot_log!(debug, "Expiry: {:?}", key_meta.clone().map(|m| m.expiry)); - if let Some(meta) = key_meta { - if let Some(expiry) = meta.expiry { - if compilation_time > &convert_naive_to_utc(expiry) { - ryot_log!(warn, "Pro key has expired. Please renew your subscription."); - return false; - } - } - } - ryot_log!(info, "Pro key verified successfully"); - true -} diff --git a/apps/frontend/app/components/common.tsx b/apps/frontend/app/components/common.tsx index f39b0f8218..09434e90bf 100644 --- a/apps/frontend/app/components/common.tsx +++ b/apps/frontend/app/components/common.tsx @@ -233,7 +233,7 @@ export const DebouncedSearchInput = (props: { export const ProRequiredAlert = (props: { tooltipLabel?: string }) => { const coreDetails = useCoreDetails(); - return !coreDetails.isPro ? ( + return !coreDetails.isServerKeyValidated ? ( {PRO_REQUIRED_MESSAGE} diff --git a/apps/frontend/app/lib/hooks.ts b/apps/frontend/app/lib/hooks.ts index c5dfef462b..851c20f0e3 100644 --- a/apps/frontend/app/lib/hooks.ts +++ b/apps/frontend/app/lib/hooks.ts @@ -136,10 +136,10 @@ export const useUserUnitSystem = () => useUserPreferences().fitness.exercises.unitSystem; export const useApplicationEvents = () => { - const { version, isPro } = useCoreDetails(); + const { version, isServerKeyValidated } = useCoreDetails(); const sendEvent = (eventName: string, data: Record) => { - window.umami?.track(eventName, { isPro, version, ...data }); + window.umami?.track(eventName, { isServerKeyValidated, version, ...data }); }; const updateProgress = (title: string) => { diff --git a/apps/frontend/app/lib/state/fitness.ts b/apps/frontend/app/lib/state/fitness.ts index bb4ebce9a0..50b3e6c91f 100644 --- a/apps/frontend/app/lib/state/fitness.ts +++ b/apps/frontend/app/lib/state/fitness.ts @@ -306,7 +306,7 @@ export const duplicateOldWorkout = async ( lot: ex.lot, notes: ex.notes, sets: sets, - openedDetailsTab: !coreDetails.isPro + openedDetailsTab: !coreDetails.isServerKeyValidated ? "images" : (exerciseDetails.userDetails.history?.length || 0) > 0 ? "history" diff --git a/apps/frontend/app/lib/utilities.server.ts b/apps/frontend/app/lib/utilities.server.ts index 024ae6bacf..b23f134d56 100644 --- a/apps/frontend/app/lib/utilities.server.ts +++ b/apps/frontend/app/lib/utilities.server.ts @@ -177,6 +177,7 @@ export const getDecodedJwt = (request: Request) => { export const getCachedCoreDetails = async () => { return await queryClient.ensureQueryData({ queryKey: queryFactory.miscellaneous.coreDetails().queryKey, + staleTime: dayjsLib.duration({ minutes: 5 }).asMilliseconds(), queryFn: () => serverGqlService.request(CoreDetailsDocument).then((d) => d.coreDetails), }); diff --git a/apps/frontend/app/routes/_dashboard._index.tsx b/apps/frontend/app/routes/_dashboard._index.tsx index 09403b94c5..53dd651993 100644 --- a/apps/frontend/app/routes/_dashboard._index.tsx +++ b/apps/frontend/app/routes/_dashboard._index.tsx @@ -235,7 +235,7 @@ export default function Page() { .with([DashboardElementLot.Recommendations, false], ([v, _]) => (
Recommendations - {coreDetails.isPro ? ( + {coreDetails.isServerKeyValidated ? ( {loaderData.userRecommendations.map((lm) => ( diff --git a/apps/frontend/app/routes/_dashboard.collections.list.tsx b/apps/frontend/app/routes/_dashboard.collections.list.tsx index 326c32ed50..d800b7770a 100644 --- a/apps/frontend/app/routes/_dashboard.collections.list.tsx +++ b/apps/frontend/app/routes/_dashboard.collections.list.tsx @@ -352,7 +352,7 @@ const DisplayCollection = (props: { pos="relative" style={{ overflow: "hidden" }} > - {coreDetails.isPro ? ( + {coreDetails.isServerKeyValidated ? ( collectionImages && collectionImages.length > 0 ? ( collectionImages.map((image, index) => { const shouldCollapse = index < currentlyHovered; @@ -535,11 +535,15 @@ const CreateOrUpdateModal = (props: { label="Description" defaultValue={props.toUpdateCollection?.description} /> - + c.id, )} @@ -548,7 +552,6 @@ const CreateOrUpdateModal = (props: { label: u.name, disabled: u.id === userDetails.id, }))} - disabled={!coreDetails.isPro} /> { - if (!coreDetails.isPro) { + if (!coreDetails.isServerKeyValidated) { notifications.show({ color: "red", message: PRO_REQUIRED_MESSAGE, diff --git a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx index eaa174a03a..2be206e730 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx @@ -1183,7 +1183,7 @@ const ExerciseDisplay = (props: { } onClick={() => { - if (!coreDetails.isPro) { + if (!coreDetails.isServerKeyValidated) { notifications.show({ message: PRO_REQUIRED_MESSAGE, color: "red", @@ -1311,7 +1311,7 @@ const ExerciseDisplay = (props: { entityId={history.workoutId} entityType={FitnessEntity.Workouts} onCopyButtonClick={async () => { - if (!coreDetails.isPro) { + if (!coreDetails.isServerKeyValidated) { notifications.show({ color: "red", message: @@ -1359,7 +1359,7 @@ const ExerciseDisplay = (props: { pos="absolute" p={2} onClick={() => { - if (!coreDetails.isPro) { + if (!coreDetails.isServerKeyValidated) { notifications.show({ color: "red", message: PRO_REQUIRED_MESSAGE, @@ -1659,7 +1659,7 @@ const SetDisplay = (props: { fz="xs" leftSection={} onClick={() => { - if (!coreDetails.isPro) { + if (!coreDetails.isServerKeyValidated) { notifications.show({ color: "red", message: PRO_REQUIRED_MESSAGE, diff --git a/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx index 44409e5840..e7c8d34824 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx @@ -368,7 +368,7 @@ export default function Page() { { - if (!coreDetails.isPro) { + if (!coreDetails.isServerKeyValidated) { notifications.show({ color: "red", message: PRO_REQUIRED_MESSAGE, diff --git a/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx index 8eca379250..bc751783e3 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx @@ -156,7 +156,7 @@ export default function Page() { variant="outline" onClick={() => { if ( - !coreDetails.isPro && + !coreDetails.isServerKeyValidated && loaderData.entity === FitnessEntity.Templates ) { notifications.show({ diff --git a/apps/frontend/app/routes/_dashboard.media.genre.list.tsx b/apps/frontend/app/routes/_dashboard.media.genre.list.tsx index 6f76c6d722..0dffd685b1 100644 --- a/apps/frontend/app/routes/_dashboard.media.genre.list.tsx +++ b/apps/frontend/app/routes/_dashboard.media.genre.list.tsx @@ -159,7 +159,7 @@ const DisplayGenre = (props: { genre: Genre }) => { > - {coreDetails.isPro ? ( + {coreDetails.isServerKeyValidated ? ( {genreImages?.map((image) => ( 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 489f5e08c4..71061f0612 100644 --- a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx +++ b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx @@ -1343,13 +1343,16 @@ const EditHistoryItemModal = (props: { defaultValue={providerWatchedOn} nothingFoundMessage="No watch providers configured. Please add them in your general preferences." /> - + - ) : null} - - ))} + watchTime !== WATCH_TIMES[3] + ? ["metadataLot", metadataDetails.lot] + : undefined, + watchTime === WATCH_TIMES[3] ? ["progress", "0"] : undefined, + ] + .filter((v) => typeof v !== "undefined") + .map(([k, v]) => ( + + {typeof v !== "undefined" ? ( + + ) : null} + + ))} {metadataDetails.lot === MediaLot.Anime ? ( <> @@ -1291,15 +1303,26 @@ const NewProgressUpdateForm = ({ ) : null} + {watchTime !== WATCH_TIMES[3] ? ( + Date: Thu, 5 Dec 2024 08:52:47 +0530 Subject: [PATCH 128/233] refactor(frontend): remove duplicated code --- apps/frontend/app/routes/_dashboard.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.tsx b/apps/frontend/app/routes/_dashboard.tsx index c743f1610a..2f461f71d2 100644 --- a/apps/frontend/app/routes/_dashboard.tsx +++ b/apps/frontend/app/routes/_dashboard.tsx @@ -1116,6 +1116,7 @@ const MetadataNewProgressUpdateForm = ({ metadataDetails: MetadataDetailsQuery["metadataDetails"]; history: History; }) => { + const [parent] = useAutoAnimate(); const [_, setMetadataToUpdate] = useMetadataProgressUpdate(); const [selectedDate, setSelectedDate] = useState( new Date(), @@ -1143,6 +1144,9 @@ const MetadataNewProgressUpdateForm = ({ ? ["metadataLot", metadataDetails.lot] : undefined, watchTime === WATCH_TIMES[3] ? ["progress", "0"] : undefined, + selectedDate + ? ["date", formatDateToNaiveDate(selectedDate)] + : undefined, ] .filter((v) => typeof v !== "undefined") .map(([k, v]) => ( @@ -1152,7 +1156,7 @@ const MetadataNewProgressUpdateForm = ({ ) : null} ))} - + {metadataDetails.lot === MediaLot.Anime ? ( <> ) : null} - {selectedDate ? ( - - ) : null} + @@ -128,16 +124,22 @@ export default function Page() { { if (range === "Custom") { setCustomRangeOpened(true); return; } - setP("range", range); - delP("startDate"); - delP("endDate"); + setTimeSpanSettings( + produce(timeSpanSettings, (draft) => { + draft.range = range; + draft.startDate = undefined; + draft.endDate = undefined; + }), + ); }} - color={loaderData.range === range ? "blue" : undefined} > {range} @@ -156,67 +158,104 @@ export default function Page() { ); } +const CustomDateSelectModal = (props: { + opened: boolean; + onClose: () => void; +}) => { + const { timeSpanSettings, setTimeSpanSettings, startDate, endDate } = + useTimeSpanSettings(); + const [value, setValue] = useState<[Date | null, Date | null]>([ + new Date(startDate), + new Date(endDate), + ]); + + return ( + + + + + + + ); +}; + const MusclesChart = () => { - const loaderData = useLoaderData(); const colors = useGetMantineColors(); - const data = loaderData.fitnessAnalytics.workoutMuscles; - const [count, setCount] = useLocalStorage( - "FitnessAnalyticsMusclesChartCount", - data.length > 7 ? 7 : data.length, - ); return ( - - ({ - value: item.count, - name: changeCase(item.muscle), - color: selectRandomElement(colors, item.muscle), - }))} - /> + + {(data, count) => ({ + totalItems: data.workoutMuscles.length, + render: ( + ({ + value: item.count, + name: changeCase(item.muscle), + color: selectRandomElement(colors, item.muscle), + }))} + /> + ), + })} ); }; const ExercisesChart = () => { - const loaderData = useLoaderData(); const colors = useGetMantineColors(); - const data = loaderData.fitnessAnalytics.workoutExercises; - const [count, setCount] = useLocalStorage( - "FitnessAnalyticsExercisesChartCount", - data.length > 7 ? 7 : data.length, - ); return ( - - ({ - value: item.count, - name: changeCase(item.exercise), - color: selectRandomElement(colors, item.exercise), - }))} - /> + + {(data, count) => ({ + totalItems: data.workoutExercises.length, + render: ( + ({ + value: item.count, + name: changeCase(item.exercise), + color: selectRandomElement(colors, item.exercise), + }))} + /> + ), + })} ); }; @@ -232,139 +271,113 @@ const formattedHourLabel = (hour: string) => { }; const TimeOfDayChart = () => { - const loaderData = useLoaderData(); - const hours = Object.entries( - groupBy( - loaderData.fitnessAnalytics.hours.map((h) => ({ - ...h, - hour: convertUtcHourToLocalHour(h.hour), - })), - (item) => - hourTuples.find( - ([start, end]) => item.hour >= start && item.hour < end, - ), - ), - ).map(([hour, values]) => ({ - index: 1, - hour: formattedHourLabel(hour), - count: values.reduce((acc, val) => acc + val.count, 0), - })); - return ( - - + + {(data) => { + const hours = Object.entries( + groupBy( + data.hours.map((h) => ({ + ...h, + hour: convertUtcHourToLocalHour(h.hour), + })), + (item) => + hourTuples.find( + ([start, end]) => item.hour >= start && item.hour < end, + ), + ), + ).map(([hour, values]) => ({ + index: 1, + hour: formattedHourLabel(hour), + count: values.reduce((acc, val) => acc + val.count, 0), + })); + return { + totalItems: hours.length, + render: ( + + ), + }; + }} ); }; -type ChartContainerProps = { +type FitnessChartContainerProps = { title: string; - totalItems: number; - children: ReactNode; - counter?: { - count: number; - setCount: (count: number) => void; + disableCounter?: boolean; + children: ( + data: FitnessAnalytics, + count: number, + ) => { + render: ReactNode; + totalItems: number; }; }; -const ChartContainer = (props: ChartContainerProps) => { - const counter = props.counter; +const FitnessChartContainer = (props: FitnessChartContainerProps) => { + const userPreferences = useUserPreferences(); + const { startDate, endDate } = useTimeSpanSettings(); + const [count, setCount] = useLocalStorage( + `FitnessChartContainer-${props.title}`, + 10, + ); + const input = { startDate, endDate }; + + const { data: fitnessAnalytics } = useQuery({ + queryKey: queryFactory.analytics.fitness({ input }).queryKey, + queryFn: async () => { + return await clientGqlService + .request(FitnessAnalyticsDocument, { input }) + .then((data) => data.fitnessAnalytics); + }, + }); - return ( - - {() => ( - - 0 ? "space-between" : undefined} - > - - {props.title} - {counter ? ( - e.target.select()} - onChange={(v) => counter.setCount(Number(v))} - /> - ) : null} - - {props.totalItems > 0 ? ( - props.children + const value = fitnessAnalytics + ? props.children(fitnessAnalytics, count) + : undefined; + + return userPreferences.featuresEnabled.fitness.enabled ? ( + + + + {props.title} + {props.disableCounter ? null : ( + e.target.select()} + onChange={(v) => setCount(Number(v))} + /> + )} + + + {value ? ( + value.totalItems > 0 ? ( + value.render ) : ( No data found - )} - - - )} - - ); -}; - -const FitnessChartContainer = forwardRef< - typeof ChartContainer, - ChartContainerProps ->((props) => { - const userPreferences = useUserPreferences(); - - return userPreferences.featuresEnabled.fitness.enabled ? ( - + ) + ) : ( + + )} + + + ) : null; -}); - -const CustomDateSelectModal = (props: { - opened: boolean; - onClose: () => void; -}) => { - const loaderData = useLoaderData(); - const [_, { setP }] = useAppSearchParam(loaderData.cookieName); - const [value, setValue] = useState<[Date | null, Date | null]>([ - new Date(loaderData.startDate), - new Date(loaderData.endDate), - ]); - - return ( - - - - - - - ); }; From d305745719f2a2e215b5ae2165c7e9b14ea033c6 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 5 Dec 2024 22:20:19 +0530 Subject: [PATCH 130/233] fix(frontend): do not display counter if not allowed --- apps/frontend/app/routes/_dashboard.analytics.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/app/routes/_dashboard.analytics.tsx b/apps/frontend/app/routes/_dashboard.analytics.tsx index e9c4774be2..e4788bb093 100644 --- a/apps/frontend/app/routes/_dashboard.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.analytics.tsx @@ -352,7 +352,7 @@ const FitnessChartContainer = (props: FitnessChartContainerProps) => { {props.title} - {props.disableCounter ? null : ( + {props.disableCounter || (value?.totalItems || 0) === 0 ? null : ( Date: Fri, 6 Dec 2024 06:59:22 +0530 Subject: [PATCH 131/233] feat(frontend): change the time of day graph --- .../app/routes/_dashboard.analytics.tsx | 52 +++++-------------- 1 file changed, 12 insertions(+), 40 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.analytics.tsx b/apps/frontend/app/routes/_dashboard.analytics.tsx index e4788bb093..299fe89ae7 100644 --- a/apps/frontend/app/routes/_dashboard.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.analytics.tsx @@ -1,4 +1,4 @@ -import { BarChart, BubbleChart, PieChart } from "@mantine/charts"; +import { BarChart, PieChart, ScatterChart } from "@mantine/charts"; import { Button, Container, @@ -19,7 +19,7 @@ import { type FitnessAnalytics, FitnessAnalyticsDocument, } from "@ryot/generated/graphql/backend/graphql"; -import { changeCase, formatDateToNaiveDate, groupBy } from "@ryot/ts-utils"; +import { changeCase, formatDateToNaiveDate } from "@ryot/ts-utils"; import { IconCalendar, IconDeviceFloppy } from "@tabler/icons-react"; import { useQuery } from "@tanstack/react-query"; import { produce } from "immer"; @@ -260,46 +260,22 @@ const ExercisesChart = () => { ); }; -const hourTuples = Array.from({ length: 8 }, (_, i) => [i * 3, i * 3 + 3]); - -const formattedHour = (hour: number) => - dayjsLib().hour(hour).minute(0).format("ha"); - -const formattedHourLabel = (hour: string) => { - const unGrouped = hour.split(",").map(Number); - return `${formattedHour(unGrouped[0])}-${formattedHour(unGrouped[1])}`; -}; - const TimeOfDayChart = () => { return ( {(data) => { - const hours = Object.entries( - groupBy( - data.hours.map((h) => ({ - ...h, - hour: convertUtcHourToLocalHour(h.hour), - })), - (item) => - hourTuples.find( - ([start, end]) => item.hour >= start && item.hour < end, - ), - ), - ).map(([hour, values]) => ({ - index: 1, - hour: formattedHourLabel(hour), - count: values.reduce((acc, val) => acc + val.count, 0), + const hours = data.hours.map((h) => ({ + Count: h.count, + Hour: convertUtcHourToLocalHour(h.hour), })); return { totalItems: hours.length, render: ( - ), }; @@ -310,6 +286,7 @@ const TimeOfDayChart = () => { type FitnessChartContainerProps = { title: string; + smallSize?: boolean; disableCounter?: boolean; children: ( data: FitnessAnalytics, @@ -343,12 +320,7 @@ const FitnessChartContainer = (props: FitnessChartContainerProps) => { : undefined; return userPreferences.featuresEnabled.fitness.enabled ? ( - + {props.title} From 338c839a6b383305cc658bf95be1e861acd8f923 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Fri, 6 Dec 2024 07:06:34 +0530 Subject: [PATCH 132/233] feat(frontend): display time ranges for analytics page --- .../app/routes/_dashboard.analytics.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.analytics.tsx b/apps/frontend/app/routes/_dashboard.analytics.tsx index 299fe89ae7..aa428593f3 100644 --- a/apps/frontend/app/routes/_dashboard.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.analytics.tsx @@ -20,7 +20,7 @@ import { FitnessAnalyticsDocument, } from "@ryot/generated/graphql/backend/graphql"; import { changeCase, formatDateToNaiveDate } from "@ryot/ts-utils"; -import { IconCalendar, IconDeviceFloppy } from "@tabler/icons-react"; +import { IconDeviceFloppy } from "@tabler/icons-react"; import { useQuery } from "@tanstack/react-query"; import { produce } from "immer"; import { type ReactNode, useState } from "react"; @@ -94,7 +94,8 @@ const useTimeSpanSettings = () => { export default function Page() { const [customRangeOpened, setCustomRangeOpened] = useState(false); - const { timeSpanSettings, setTimeSpanSettings } = useTimeSpanSettings(); + const { timeSpanSettings, setTimeSpanSettings, startDate, endDate } = + useTimeSpanSettings(); return ( <> @@ -110,13 +111,15 @@ export default function Page() { - From bd53be24190a57a9ed67bb9232d6b74ff2096579 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Fri, 6 Dec 2024 07:31:20 +0530 Subject: [PATCH 133/233] build(frontend): add screenshot deps --- apps/frontend/package.json | 1 + yarn.lock | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index f74622a966..13bf70c04b 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -41,6 +41,7 @@ "graphql": "16.9.0", "graphql-request": "7.1.2", "howler": "2.2.4", + "html2canvas": "1.4.1", "humanize-duration-ts": "2.1.1", "immer": "10.1.1", "isbot": "5.1.17", diff --git a/yarn.lock b/yarn.lock index ec304b1217..873a5c948b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5760,6 +5760,7 @@ __metadata: graphql: "npm:16.9.0" graphql-request: "npm:7.1.2" howler: "npm:2.2.4" + html2canvas: "npm:1.4.1" humanize-duration-ts: "npm:2.1.1" immer: "npm:10.1.1" isbot: "npm:5.1.17" @@ -6919,6 +6920,13 @@ __metadata: languageName: node linkType: hard +"base64-arraybuffer@npm:^1.0.2": + version: 1.0.2 + resolution: "base64-arraybuffer@npm:1.0.2" + checksum: 10/15e6400d2d028bf18be4ed97702b11418f8f8779fb8c743251c863b726638d52f69571d4cc1843224da7838abef0949c670bde46936663c45ad078e89fee5c62 + languageName: node + linkType: hard + "base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -7963,6 +7971,15 @@ __metadata: languageName: node linkType: hard +"css-line-break@npm:^2.1.0": + version: 2.1.0 + resolution: "css-line-break@npm:2.1.0" + dependencies: + utrie: "npm:^1.0.2" + checksum: 10/e75cae40de511026228d4fa69e8d464895714f8899880b8268a446b57f0faa84b490ba1bdda5ed9e7f38f99ab947c6bc941bb505d8119c49072db079c1cacea5 + languageName: node + linkType: hard + "css-what@npm:^6.1.0": version: 6.1.0 resolution: "css-what@npm:6.1.0" @@ -10463,6 +10480,16 @@ __metadata: languageName: node linkType: hard +"html2canvas@npm:1.4.1": + version: 1.4.1 + resolution: "html2canvas@npm:1.4.1" + dependencies: + css-line-break: "npm:^2.1.0" + text-segmentation: "npm:^1.0.3" + checksum: 10/595790810557a1d4287f07b6ead49aed4f169f08eb00e20c1f030b93344003e84797d28cd8a220e8ec1b78641d460ca6add11b8531960725526f11a7b9fa9900 + languageName: node + linkType: hard + "html@npm:^1.0.0": version: 1.0.0 resolution: "html@npm:1.0.0" @@ -15727,6 +15754,15 @@ __metadata: languageName: node linkType: hard +"text-segmentation@npm:^1.0.3": + version: 1.0.3 + resolution: "text-segmentation@npm:1.0.3" + dependencies: + utrie: "npm:^1.0.2" + checksum: 10/86191de83f09b96f356628c3dbaf6d281eda46a4dd4c94c3827495428871b9dd44ead4ef4cdf06a188b08a3b83e3943c5911841422b55286b121ba9e04c846dd + languageName: node + linkType: hard + "thenify-all@npm:^1.0.0": version: 1.6.0 resolution: "thenify-all@npm:1.6.0" @@ -16481,6 +16517,15 @@ __metadata: languageName: node linkType: hard +"utrie@npm:^1.0.2": + version: 1.0.2 + resolution: "utrie@npm:1.0.2" + dependencies: + base64-arraybuffer: "npm:^1.0.2" + checksum: 10/0c9458380bf3113f425a268e3d605aef163bfbaea62bf24de517b62b6e6744394d8ef1cdd9b07423359aec5ed402baab4bb2c5beec64f674ec635dc2f8dbd4bf + languageName: node + linkType: hard + "uuid@npm:11.0.3": version: 11.0.3 resolution: "uuid@npm:11.0.3" From c1ac11cadfa0e0da05f17c6de05ca2c300321811 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Fri, 6 Dec 2024 07:31:34 +0530 Subject: [PATCH 134/233] feat(frontend): button to download the analytics page as png --- .../app/routes/_dashboard.analytics.tsx | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.analytics.tsx b/apps/frontend/app/routes/_dashboard.analytics.tsx index aa428593f3..0da27051c5 100644 --- a/apps/frontend/app/routes/_dashboard.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.analytics.tsx @@ -14,16 +14,18 @@ import { Text, } from "@mantine/core"; import { DatePicker } from "@mantine/dates"; +import { notifications } from "@mantine/notifications"; import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/node"; import { type FitnessAnalytics, FitnessAnalyticsDocument, } from "@ryot/generated/graphql/backend/graphql"; import { changeCase, formatDateToNaiveDate } from "@ryot/ts-utils"; -import { IconDeviceFloppy } from "@tabler/icons-react"; +import { IconDeviceFloppy, IconImageInPicture } from "@tabler/icons-react"; import { useQuery } from "@tanstack/react-query"; +import html2canvas from "html2canvas"; import { produce } from "immer"; -import { type ReactNode, useState } from "react"; +import { type ReactNode, useRef, useState } from "react"; import { match } from "ts-pattern"; import { useLocalStorage } from "usehooks-ts"; import { z } from "zod"; @@ -94,8 +96,10 @@ const useTimeSpanSettings = () => { export default function Page() { const [customRangeOpened, setCustomRangeOpened] = useState(false); + const [isCaptureLoading, setIsCaptureLoading] = useState(false); const { timeSpanSettings, setTimeSpanSettings, startDate, endDate } = useTimeSpanSettings(); + const toCaptureRef = useRef(null); return ( <> @@ -103,7 +107,7 @@ export default function Page() { opened={customRangeOpened} onClose={() => setCustomRangeOpened(false)} /> - + @@ -157,6 +161,43 @@ export default function Page() { + + + ); } From 1c5f780b893a1146f55aaad60e93d6ff9131142a Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Fri, 6 Dec 2024 07:43:05 +0530 Subject: [PATCH 135/233] fix(frontend): add background color to container to fix screenshots --- apps/frontend/app/routes/_dashboard.analytics.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.analytics.tsx b/apps/frontend/app/routes/_dashboard.analytics.tsx index 0da27051c5..003693dc8d 100644 --- a/apps/frontend/app/routes/_dashboard.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.analytics.tsx @@ -97,9 +97,9 @@ const useTimeSpanSettings = () => { export default function Page() { const [customRangeOpened, setCustomRangeOpened] = useState(false); const [isCaptureLoading, setIsCaptureLoading] = useState(false); + const toCaptureRef = useRef(null); const { timeSpanSettings, setTimeSpanSettings, startDate, endDate } = useTimeSpanSettings(); - const toCaptureRef = useRef(null); return ( <> @@ -107,7 +107,11 @@ export default function Page() { opened={customRangeOpened} onClose={() => setCustomRangeOpened(false)} /> - + From 1017d9229176bd9ceb31ad18e67b29c0a64dba14 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Sat, 7 Dec 2024 09:06:23 +0530 Subject: [PATCH 136/233] feat(frontend): move to using new grid container --- .../app/routes/_dashboard.analytics.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.analytics.tsx b/apps/frontend/app/routes/_dashboard.analytics.tsx index 003693dc8d..7728203988 100644 --- a/apps/frontend/app/routes/_dashboard.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.analytics.tsx @@ -3,6 +3,7 @@ import { Button, Container, Flex, + Grid, Group, Loader, Menu, @@ -158,11 +159,17 @@ export default function Page() { - - - - - + + + + + + + + + + + From 843bbc0603dd6333dd13324505b28511e890c157 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Sat, 7 Dec 2024 09:13:06 +0530 Subject: [PATCH 137/233] feat(frontend): do not display border if not applicable --- .../app/routes/_dashboard.analytics.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.analytics.tsx b/apps/frontend/app/routes/_dashboard.analytics.tsx index 7728203988..491a94d805 100644 --- a/apps/frontend/app/routes/_dashboard.analytics.tsx +++ b/apps/frontend/app/routes/_dashboard.analytics.tsx @@ -264,7 +264,7 @@ const MusclesChart = () => { const colors = useGetMantineColors(); return ( - + {(data, count) => ({ totalItems: data.workoutMuscles.length, render: ( @@ -283,7 +283,7 @@ const MusclesChart = () => { /> ), })} - + ); }; @@ -291,7 +291,7 @@ const ExercisesChart = () => { const colors = useGetMantineColors(); return ( - + {(data, count) => ({ totalItems: data.workoutExercises.length, render: ( @@ -311,13 +311,13 @@ const ExercisesChart = () => { /> ), })} - + ); }; const TimeOfDayChart = () => { return ( - + {(data) => { const hours = data.hours.map((h) => ({ Count: h.count, @@ -335,13 +335,12 @@ const TimeOfDayChart = () => { ), }; }} - + ); }; -type FitnessChartContainerProps = { +type ChartContainerProps = { title: string; - smallSize?: boolean; disableCounter?: boolean; children: ( data: FitnessAnalytics, @@ -352,7 +351,7 @@ type FitnessChartContainerProps = { }; }; -const FitnessChartContainer = (props: FitnessChartContainerProps) => { +const ChartContainer = (props: ChartContainerProps) => { const userPreferences = useUserPreferences(); const { startDate, endDate } = useTimeSpanSettings(); const [count, setCount] = useLocalStorage( @@ -375,10 +374,12 @@ const FitnessChartContainer = (props: FitnessChartContainerProps) => { : undefined; return userPreferences.featuresEnabled.fitness.enabled ? ( - + - {props.title} + + {props.title} + {props.disableCounter || (value?.totalItems || 0) === 0 ? null : ( Date: Sun, 8 Dec 2024 10:05:41 +0530 Subject: [PATCH 138/233] feat(backend): remove the activity section --- .../src/m20241126_changes_for_issue_1113.rs | 16 ++++++++++++++++ crates/models/user/src/lib.rs | 5 ----- libs/generated/src/graphql/backend/graphql.ts | 1 - .../src/graphql/backend/types.generated.ts | 1 - 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/migrations/src/m20241126_changes_for_issue_1113.rs b/crates/migrations/src/m20241126_changes_for_issue_1113.rs index 2667a4bae9..a30951ae7f 100644 --- a/crates/migrations/src/m20241126_changes_for_issue_1113.rs +++ b/crates/migrations/src/m20241126_changes_for_issue_1113.rs @@ -76,6 +76,22 @@ WHERE "exercise_extra_information" IS NOT NULL; "#, ) .await?; + db.execute_unprepared( + r#" +UPDATE "user" +SET preferences = + jsonb_set( + preferences, + '{general, dashboard}', + ( + SELECT jsonb_agg(element) + FROM jsonb_array_elements(preferences->'general'->'dashboard') AS element + WHERE element->>'section' != 'ACTIVITY' + ) + ); + "#, + ) + .await?; Ok(()) } diff --git a/crates/models/user/src/lib.rs b/crates/models/user/src/lib.rs index 859c02ebe1..e23675971d 100644 --- a/crates/models/user/src/lib.rs +++ b/crates/models/user/src/lib.rs @@ -260,7 +260,6 @@ pub enum DashboardElementLot { #[default] Summary, Recommendations, - Activity, } #[skip_serializing_none] @@ -337,10 +336,6 @@ pub struct UserGeneralPreferences { section: DashboardElementLot::Recommendations, ..Default::default() }, - UserGeneralDashboardElement { - section: DashboardElementLot::Activity, - ..Default::default() - }, ]))] pub dashboard: Vec, } diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts index e019023f09..56604d1750 100644 --- a/libs/generated/src/graphql/backend/graphql.ts +++ b/libs/generated/src/graphql/backend/graphql.ts @@ -377,7 +377,6 @@ export type DailyUserActivityItem = { }; export enum DashboardElementLot { - Activity = 'ACTIVITY', InProgress = 'IN_PROGRESS', Recommendations = 'RECOMMENDATIONS', Summary = 'SUMMARY', diff --git a/libs/generated/src/graphql/backend/types.generated.ts b/libs/generated/src/graphql/backend/types.generated.ts index 3868054399..a3925ad77b 100644 --- a/libs/generated/src/graphql/backend/types.generated.ts +++ b/libs/generated/src/graphql/backend/types.generated.ts @@ -364,7 +364,6 @@ export type DailyUserActivityItem = { }; export enum DashboardElementLot { - Activity = 'ACTIVITY', InProgress = 'IN_PROGRESS', Recommendations = 'RECOMMENDATIONS', Summary = 'SUMMARY', From 245df92dc86861526c6b6176154624993c5f84a7 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Sun, 8 Dec 2024 10:07:15 +0530 Subject: [PATCH 139/233] feat(frontend): move activity graph to analytics section --- apps/frontend/app/lib/generals.ts | 18 ++ .../frontend/app/routes/_dashboard._index.tsx | 199 +----------------- .../app/routes/_dashboard.analytics.tsx | 139 +++++++++++- 3 files changed, 159 insertions(+), 197 deletions(-) diff --git a/apps/frontend/app/lib/generals.ts b/apps/frontend/app/lib/generals.ts index 119e30a48d..12bda3a4bd 100644 --- a/apps/frontend/app/lib/generals.ts +++ b/apps/frontend/app/lib/generals.ts @@ -3,6 +3,7 @@ import { createQueryKeys, mergeQueryKeys, } from "@lukemorales/query-key-factory"; +import type { MantineColor } from "@mantine/core"; import { type FitnessAnalyticsQueryVariables, MediaLot, @@ -484,3 +485,20 @@ export const getExerciseDetailsPath = (exerciseId: string) => $path("/fitness/exercises/item/:id", { id: encodeURIComponent(exerciseId), }); + +type EntityColor = Record; + +export const MediaColors: EntityColor = { + ANIME: "blue", + AUDIO_BOOK: "orange", + BOOK: "lime", + MANGA: "purple", + MOVIE: "cyan", + PODCAST: "yellow", + SHOW: "red", + VISUAL_NOVEL: "pink", + VIDEO_GAME: "teal", + WORKOUT: "violet", + MEASUREMENT: "indigo", + REVIEW: "green.5", +}; diff --git a/apps/frontend/app/routes/_dashboard._index.tsx b/apps/frontend/app/routes/_dashboard._index.tsx index 53dd651993..18a74e2573 100644 --- a/apps/frontend/app/routes/_dashboard._index.tsx +++ b/apps/frontend/app/routes/_dashboard._index.tsx @@ -1,29 +1,21 @@ -import { BarChart } from "@mantine/charts"; import { Alert, Box, Center, Container, Flex, - LoadingOverlay, - type MantineColor, - Paper, RingProgress, - Select, SimpleGrid, Stack, Text, Title, useMantineTheme, } from "@mantine/core"; -import { useInViewport } from "@mantine/hooks"; import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/node"; import { Link, useLoaderData } from "@remix-run/react"; import { type CalendarEventPartFragment, CollectionContentsDocument, - DailyUserActivitiesDocument, - DailyUserActivitiesResponseGroupedBy, DashboardElementLot, GraphqlSortOrder, LatestUserSummaryDocument, @@ -32,16 +24,7 @@ import { UserRecommendationsDocument, UserUpcomingCalendarEventsDocument, } from "@ryot/generated/graphql/backend/graphql"; -import { - changeCase, - formatDateToNaiveDate, - humanizeDuration, - isBoolean, - isNumber, - mapValues, - pickBy, - snakeCase, -} from "@ryot/ts-utils"; +import { humanizeDuration, isNumber } from "@ryot/ts-utils"; import { IconBarbell, IconFriends, @@ -49,9 +32,8 @@ import { IconScaleOutline, IconServer, } from "@tabler/icons-react"; -import { useQuery } from "@tanstack/react-query"; import CryptoJS from "crypto-js"; -import { Fragment, type ReactNode, useMemo } from "react"; +import { Fragment, type ReactNode } from "react"; import { $path } from "remix-routes"; import { ClientOnly } from "remix-utils/client-only"; import invariant from "tiny-invariant"; @@ -61,15 +43,7 @@ import { ApplicationGrid, ProRequiredAlert } from "~/components/common"; import { DisplayCollectionEntity } from "~/components/common"; import { displayWeightWithUnit } from "~/components/fitness"; import { MetadataDisplayItem } from "~/components/media"; -import { - TimeSpan, - clientGqlService, - dayjsLib, - getDateFromTimeSpan, - getLot, - getMetadataIcon, - queryFactory, -} from "~/lib/generals"; +import { MediaColors, dayjsLib, getLot, getMetadataIcon } from "~/lib/generals"; import { useCoreDetails, useGetMantineColors, @@ -150,23 +124,6 @@ export const meta = (_args: MetaArgs) => { return [{ title: "Home | Ryot" }]; }; -type EntityColor = Record; - -const MediaColors: EntityColor = { - ANIME: "blue", - AUDIO_BOOK: "orange", - BOOK: "lime", - MANGA: "purple", - MOVIE: "cyan", - PODCAST: "yellow", - SHOW: "red", - VISUAL_NOVEL: "pink", - VIDEO_GAME: "teal", - WORKOUT: "violet", - MEASUREMENT: "indigo", - REVIEW: "green.5", -}; - export default function Page() { const loaderData = useLoaderData(); const coreDetails = useCoreDetails(); @@ -246,12 +203,6 @@ export default function Page() { )}
)) - .with([DashboardElementLot.Activity, false], ([v, _]) => ( -
- Activity - -
- )) .with([DashboardElementLot.Summary, false], ([v, _]) => (
Summary @@ -637,147 +588,3 @@ const UnstyledLink = (props: { children: ReactNode; to: string }) => { ); }; - -const ActivitySection = () => { - const { ref, inViewport } = useInViewport(); - const [timeSpan, setTimeSpan] = useLocalStorage( - "ActivitySectionTimeSpan", - TimeSpan.Last7Days, - ); - const { startDate, endDate } = useMemo(() => { - const now = dayjsLib(); - const end = now.endOf("day"); - const startDate = getDateFromTimeSpan(timeSpan); - return { - startDate: startDate - ? formatDateToNaiveDate(startDate.toDate()) - : undefined, - endDate: formatDateToNaiveDate(end.toDate()), - }; - }, [timeSpan]); - const { data: dailyUserActivitiesData } = useQuery({ - queryKey: queryFactory.miscellaneous.dailyUserActivities(startDate, endDate) - .queryKey, - enabled: inViewport, - queryFn: async () => { - const { dailyUserActivities } = await clientGqlService.request( - DailyUserActivitiesDocument, - { input: { dateRange: { startDate, endDate } } }, - ); - const trackSeries = mapValues(MediaColors, () => false); - const data = dailyUserActivities.items.map((d) => { - const data = Object.entries(d) - .filter(([_, value]) => value !== 0) - .map(([key, value]) => ({ - [snakeCase( - key.replace("Count", "").replace("total", ""), - ).toUpperCase()]: value, - })) - .reduce(Object.assign, {}); - for (const key in data) - if (isBoolean(trackSeries[key])) trackSeries[key] = true; - return data; - }); - const series = pickBy(trackSeries); - return { - data, - series, - groupedBy: dailyUserActivities.groupedBy, - totalCount: dailyUserActivities.totalCount, - totalDuration: dailyUserActivities.totalDuration, - }; - }, - }); - const items = dailyUserActivitiesData?.totalCount || 0; - - return ( - - - - - - ) : null} @@ -187,8 +187,8 @@ export default function Page() { label="Name" required autoFocus - name="id" - defaultValue={loaderData.details?.id} + name="name" + defaultValue={loaderData.details?.name} /> + data={Object.values(WatchTimes).filter((v) => [ MediaLot.Show, MediaLot.Podcast, MediaLot.Anime, MediaLot.Manga, ].includes(metadataDetails.lot) - ? v !== WATCH_TIMES[3] + ? v !== WatchTimes.JustStartedIt : true, )} label={`When did you ${getVerb(Verb.Read, metadataDetails.lot)} it?`} onChange={(v) => { setWatchTime(v as typeof watchTime); match(v) - .with(WATCH_TIMES[0], () => setSelectedDate(new Date())) - .with(WATCH_TIMES[1], WATCH_TIMES[2], WATCH_TIMES[3], () => - setSelectedDate(null), + .with(WatchTimes.JustCompletedNow, () => + setSelectedDate(new Date()), + ) + .with( + WatchTimes.IDontRemember, + WatchTimes.CustomDate, + WatchTimes.JustStartedIt, + () => setSelectedDate(null), ) .run(); }} /> - {watchTime === WATCH_TIMES[2] ? ( + {watchTime === WatchTimes.CustomDate ? ( ) : null} - {watchTime !== WATCH_TIMES[3] ? ( + {watchTime !== WatchTimes.JustStartedIt ? (