From be57bf6a654ba69eddfe8a594fa9876259200293 Mon Sep 17 00:00:00 2001 From: Bailey Date: Sat, 25 Feb 2023 13:18:38 -0600 Subject: [PATCH] [#436] support syncing strength exercises to garmin when available (#444) * [#436] support syncing strength exercises to garmin when available * working :) * uncomment amrap * rename setting --- README.md | 1 + docs/configuration/json.md | 5 + docs/features.md | 1 + docs/index.md | 1 + src/Common/Configuration.cs | 13 + .../Peloton/CompletedMovementsSummaryData.cs | 64 + src/Common/Dto/Peloton/Workout.cs | 4 +- src/Common/Dto/Unit.cs | 19 + src/Conversion/ExerciseMapping.cs | 94 ++ src/Conversion/FitConverter.cs | 140 +- src/Conversion/FitDecoder.cs | 2 + src/Conversion/IConverter.cs | 13 + src/UnitTests/AdHocTests.cs | 4 +- src/UnitTests/Data/p2g_workouts/output.fit | Bin 7277 -> 0 bytes .../p2g_workouts/strength_with_exercises.json | 1354 +++++++++++++++++ .../sample_fit/strength_with_exercises.fit | Bin 0 -> 3689 bytes src/UnitTests/UnitTests.csproj | 12 + src/WebUI/Pages/Settings.razor | 2 + vNextReleaseNotes.md | 1 + 19 files changed, 1697 insertions(+), 33 deletions(-) create mode 100644 src/Common/Dto/Peloton/CompletedMovementsSummaryData.cs create mode 100644 src/Conversion/ExerciseMapping.cs delete mode 100644 src/UnitTests/Data/p2g_workouts/output.fit create mode 100644 src/UnitTests/Data/p2g_workouts/strength_with_exercises.json create mode 100644 src/UnitTests/Data/sample_fit/strength_with_exercises.fit diff --git a/README.md b/README.md index e0fcf6996..934009096 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Sync workouts from Peloton to Garmin. * Earn Badges and credit for Garmin Challenges * Counts towards VO2 Max [1]({{ site.baseurl }}{% link faq.md %}) and Training Stress Scores * Supports Garmin accounts protected by Two Step Verification +* Supports mapping Exercises from Strength workouts Head on over to the [Wiki](https://philosowaffle.github.io/peloton-to-garmin) to get started! diff --git a/docs/configuration/json.md b/docs/configuration/json.md index 365aa2de0..92b69b609 100644 --- a/docs/configuration/json.md +++ b/docs/configuration/json.md @@ -118,6 +118,9 @@ This section provides settings related to conversions and what formats should be }, "Rowing": { "PreferredLapType": "Class_Segments" + }, + "Strength": { + "DefaultSecondsPerRep": 3 } } ``` @@ -137,6 +140,8 @@ This section provides settings related to conversions and what formats should be | Running.PreferredLapType | no | `Default` | `Conversion Tab` | The preferred [lap type to use](#lap-types). | | Rowing | no | `null` | none | Configuration specific to Rowing workouts. | | Rowing.PreferredLapType | no | `Default` | `Conversion Tab` | The preferred [lap type to use](#lap-types). | +| Strength | no | `null` | `Conversion Tab` | Configuration specific to Strength workouts. | +| Strength.DefaultSecondsPerRep | no | `3` | `Conversion Tab` | For exercises that are done for time instead of reps, P2G can estimate how many reps you completed using this value. Ex. If `DefaultSecondsPerRep=3` and you do Curls for 15s, P2G will estimate you completed 5 reps. | ### Understanding Custom Zones diff --git a/docs/features.md b/docs/features.md index 69ad18cc6..178433295 100644 --- a/docs/features.md +++ b/docs/features.md @@ -17,6 +17,7 @@ Convert, Backup, and Sync. 1. Syncs laps and target cadence 1. Synced workouts count towards Garmin Badges and Challenges 1. Synced workouts count towards VO2 Max [1]({{ site.baseurl }}{% link faq.md %}) and Training Stress Scores +1. Syncs Exercise information (including reps and weight) for Strength and Core workouts (when available) 1. Syncs on demand or on a schedule 1. Highly Configurable 1. Docker-ized diff --git a/docs/index.md b/docs/index.md index 7294766bf..bcfb57598 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,6 +15,7 @@ Sync workouts from Peloton to Garmin. * Earn Badges and credit for Garmin Challenges * Counts towards VO2 Max [1]({{ site.baseurl }}{% link faq.md %}) and Training Stress Scores * Supports Garmin accounts protected by Two Step Verification +* Supports mapping Exercises from Strength workouts Head on over to the [Install]({{ site.baseurl }}{% link install/index.md %}) page to get started! diff --git a/src/Common/Configuration.cs b/src/Common/Configuration.cs index cac87bdc7..524729bf0 100644 --- a/src/Common/Configuration.cs +++ b/src/Common/Configuration.cs @@ -109,6 +109,7 @@ public Format() Cycling = new Cycling(); Running = new Running(); Rowing = new Rowing(); + Strength= new Strength(); } [DisplayName("FIT")] @@ -135,6 +136,7 @@ public Format() public Cycling Cycling { get; set; } public Running Running { get; set; } public Rowing Rowing { get; init; } + public Strength Strength { get; init; } } public record Cycling @@ -152,6 +154,17 @@ public record Rowing public PreferredLapType PreferredLapType { get; set; } } +public record Strength +{ + /// + /// When no Rep information is provided by Peloton, P2G will calculate number + /// of reps based on this default value. Example, if your DefaultNumSecondsPerRep is 3, + /// and the Exercise duration was 15 seconds, then P2G would credit you with 5 reps for that + /// exercise. + /// + public int DefaultSecondsPerRep { get; set; } = 3; +} + public enum PreferredLapType { Default = 0, diff --git a/src/Common/Dto/Peloton/CompletedMovementsSummaryData.cs b/src/Common/Dto/Peloton/CompletedMovementsSummaryData.cs new file mode 100644 index 000000000..0111b8e8c --- /dev/null +++ b/src/Common/Dto/Peloton/CompletedMovementsSummaryData.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Common.Dto.Peloton; + +public record MovementTrackerData +{ + public CompletedMovementsSummaryData Completed_Movements_Summary_Data { get; init; } +} + +public record CompletedMovementsSummaryData +{ + public ICollection Repetition_Summary_Data { get; init; } +} + +public record RepetitionSummaryData +{ + public string Movement_Id { get; init; } + public string Movement_Name { get; init; } + + /// + /// True when doing the exercise for Time instead of for Reps + /// + public bool Is_Hold { get; init; } + public int Target_Number { get; init; } + public int Completed_Number { get; init; } + /// + /// Seconds offset from start.... I think? + /// + public int Offset { get; init; } + /// + /// Length in seconds to complete exercise + /// + public int Length { get; init; } + /// + /// Total load of the weight lifted and number of reps + /// + public int? Total_Volume { get; init; } + public ICollection Weight { get; init; } +} + +public record Weight +{ + [JsonConverter(typeof(JsonStringEnumConverter))] + public WeightCategory Weight_Category { get; init; } + public WeightData Weight_Data { get; init; } +} + +public record WeightData +{ + public double Weight_Value { get; init; } + + /// + /// lb + /// + public string Weight_Unit { get; init; } +} + +public enum WeightCategory : byte +{ + Light = 1, + Medium = 2, + Heavy = 3 +} \ No newline at end of file diff --git a/src/Common/Dto/Peloton/Workout.cs b/src/Common/Dto/Peloton/Workout.cs index f6322bf20..4f2a11898 100644 --- a/src/Common/Dto/Peloton/Workout.cs +++ b/src/Common/Dto/Peloton/Workout.cs @@ -54,7 +54,9 @@ public record Workout //public string Device_Type_Display_Name { get; init; } //public bool Is_Skip_Intro_Available { get; init; } // total hr zones durations - // average effort score + // average effort score + + public MovementTrackerData Movement_Tracker_Data { get; init; } } diff --git a/src/Common/Dto/Unit.cs b/src/Common/Dto/Unit.cs index 3867d6eab..9ee284b22 100644 --- a/src/Common/Dto/Unit.cs +++ b/src/Common/Dto/Unit.cs @@ -20,6 +20,13 @@ public enum SpeedUnit : byte MinutesPer500Meters = 3, } +public enum WeightUnit : byte +{ + Unknown = 0, + Pounds = 1, + Kilograms = 2 +} + public static class UnitHelpers { public static DistanceUnit GetDistanceUnit(string unit) @@ -59,4 +66,16 @@ public static SpeedUnit GetSpeedUnit(string unit) return SpeedUnit.Unknown; } } + + public static WeightUnit GetWeightUnit(string unit) + { + switch(unit?.ToLower()) + { + case "lb": return WeightUnit.Pounds; + case "kg": return WeightUnit.Kilograms; + default: + Log.Error("Found unknown distance unit {@Unit}", unit); + return WeightUnit.Unknown; + } + } } \ No newline at end of file diff --git a/src/Conversion/ExerciseMapping.cs b/src/Conversion/ExerciseMapping.cs new file mode 100644 index 000000000..4990e8e19 --- /dev/null +++ b/src/Conversion/ExerciseMapping.cs @@ -0,0 +1,94 @@ +using Dynastream.Fit; +using System.Collections.Generic; + +namespace Conversion; + +public static class ExerciseMapping +{ + public static readonly Dictionary StrengthExerciseMappings = new() + { + // A + /* AMRAP */ { "98cde50f696746ff98727d5362229cfb", new (ExerciseCategory.Invalid, 0) }, + + // B + /* Bent Over Row */ { "d60a1dd8824a49a4926f826b24f3b061", new (ExerciseCategory.Row, RowExerciseName.OneArmBentOverRow) }, + /* Bicep Curl */ { "43d404595338443baab306a6589ae7fc", new (ExerciseCategory.Curl, CurlExerciseName.StandingDumbbellBicepsCurl) }, + /* Bird Dog */ { "df8e18c5082f408b8490c4adcb0678b5", new (ExerciseCategory.Plank, PlankExerciseName.PlankWithKneeToElbow) }, + + // C + /* Chest Fly */ { "45207949aa384783b5d71451f7fe1c3d", new (ExerciseCategory.Flye, FlyeExerciseName.DumbbellFlye) }, + /* Crunch */ { "61ac0d64602c48fba25af7e5e5dc1f97", new (ExerciseCategory.Crunch, CrunchExerciseName.Crunch) }, + /* Concentrated Curl */ { "3695ef0ec2ce484faedc8ce2bfa2819d", new (ExerciseCategory.Curl, CurlExerciseName.SeatedDumbbellBicepsCurl) }, + + // D + /* Deadlift */ { "cd6046306b2c4c4a8f40e169ec924eb9", new (ExerciseCategory.Deadlift, DeadliftExerciseName.DumbbellDeadlift) }, + /* Dumbbell Squat */ { "7d82b59462a54e61926077ded0becae5", new (ExerciseCategory.Squat, SquatExerciseName.DumbbellSquat) }, + /* Dumbbell Thruster */ { "5ab0baeebee94d3995cb7f2b0332f430", new (ExerciseCategory.Squat, SquatExerciseName.Thrusters) }, + + // F + /* Flutter Kick */ { "6091566fa0674afd96a22fcec3ab18ce", new (ExerciseCategory.Crunch, CrunchExerciseName.FlutterKicks) }, + /* Forearm Side Plank */ { "1c0403c4d7264d83b1c75d18c8cdac4f", new (ExerciseCategory.Plank, PlankExerciseName.SidePlank) }, + /* ForeArm Plank */ { "feb44f24e2b8487b870a35f4501069be", new (ExerciseCategory.Plank, PlankExerciseName.Plank) }, + /* Front to Back Lunge */ { "ed18d837c14746c5af38d4fa03b56918", new (ExerciseCategory.Lunge, LungeExerciseName.DumbbellLunge) }, + + // G + /* Goblet Squat */ { "588e35f7067842979485ff1e4f80df26", new (ExerciseCategory.Squat,SquatExerciseName.GobletSquat) }, + + // H + /* Hammer Curl */ { "114ce849b47a4fabbaad961188bf4f7d", new (ExerciseCategory.Curl, CurlExerciseName.DumbbellHammerCurl) }, + /* High Plank */ { "194cc4f6a88c4abd80afe9bbddb25915", new (ExerciseCategory.Plank, PlankExerciseName.StraightArmPlank) }, + /* Hip Bridge */ { "06a504988ace45faabd927af1479f454", new (ExerciseCategory.HipRaise, HipRaiseExerciseName.BridgeWithLegExtension) }, + /* Hollow Hold */ { "060174b84e3744e6a19fe4ce80411113", new (ExerciseCategory.Crunch, CrunchExerciseName.HollowRock) }, + + // L + /* Lateral Lunge */ { "fb63e1ea19264145ae6856eefacbcb22", new (ExerciseCategory.Lunge, LungeExerciseName.SlidingLateralLunge)}, + + // N + /* Neutral Grip Chest Press */ { "802f10996b5048d08f320d8661f13ee1", new (ExerciseCategory.BenchPress, BenchPressExerciseName.NeutralGripDumbbellBenchPress) }, + + // O + /* Overhead Carry */ { "12057d5f9e144913a824bcae5706966c", new (ExerciseCategory.Carry, CarryExerciseName.OverheadCarry) }, + /* Overhead Extension */ { "f260623343e74d37b165071ee5903199", new (ExerciseCategory.TricepsExtension, TricepsExtensionExerciseName.OverheadDumbbellTricepsExtension) }, + /* Overhead Press */ { "ef0279948228409298cd6bf62c5b122c", new (ExerciseCategory.ShoulderPress, ShoulderPressExerciseName.OverheadDumbbellPress) }, + + // P + /* Push Up */ { "1c4d81ad487849a6995f93e1a6a4b1e4", new (ExerciseCategory.PushUp, PushUpExerciseName.PushUp) }, + /* Punches */ { "d56b610f9958400eb4c40d2385f32aaf", new (ExerciseCategory.Invalid, 0) }, + + // R + /* Russian Twist */ { "5c7b2bc65abc4c44849e2119f1338120", new (ExerciseCategory.Core, CoreExerciseName.RussianTwist) }, + /* Reverse Lunge */ { "c430accc3802486a86ad2de9cb8f01cc", new (ExerciseCategory.Lunge, LungeExerciseName.ReverseSlidingLunge) }, + + // S + /// + /* Scissor Kick */ { "f6a10df381004afba2a2b63447d9968f", new (ExerciseCategory.Crunch, CrunchExerciseName.LegLevers) }, + /* Shoulder Tap */ { "5b33283433e7479390c0d5fc11722f80", new (ExerciseCategory.Plank, PlankExerciseName.StraightArmPlankWithShoulderTouch) }, + /* Skull Crusher */ { "3c72e60de73d43f4b5a774c90dea90cd", new (ExerciseCategory.TricepsExtension, TricepsExtensionExerciseName.DumbbellLyingTricepsExtension) }, + /* Snatch */ { "0ddf8f94acfe4c2289aef5a9bf59e8d9", new (ExerciseCategory.OlympicLift, OlympicLiftExerciseName.SingleArmDumbbellSnatch) }, + /* Split Squat */ { "28833fd99466476ea273d6b94747e3db", new (ExerciseCategory.Squat, SquatExerciseName.DumbbellSplitSquat) }, + ///* Squat Jump */ { "", new (ExerciseCategory.Plyo, PlyoExerciseName.DumbbellJumpSquat) }, + + // T + /* Tricep Kickback */ { "da89d743904640d58e8b3f667f08783c", new (ExerciseCategory.TricepsExtension, TricepsExtensionExerciseName.DumbbellKickback) }, + /* Tuck up */ { "3069e7ba28b84005b71c16a3781dda8d", new (ExerciseCategory.SitUp, SitUpExerciseName.BentKneeVUp) }, + /* Twisting Mountain Climber */ { "cc70d143627c45e5b64e2cb116619899", new (ExerciseCategory.Plank, PlankExerciseName.CrossBodyMountainClimber) }, + + // V + /* V-Up */ { "715caba11593427299342c378b444e05", new(ExerciseCategory.SitUp, SitUpExerciseName.VUp) }, + + // W + /* Wide Grip Bent Over Row */ { "d861cb497fcc4e1cba994b7a949a3bac", new (ExerciseCategory.Row, RowExerciseName.WideGripSeatedCableRow) }, + }; +} + +public record GarminExercise +{ + public ushort ExerciseCategory { get; init; } + public ushort ExerciseName { get; init; } + + public GarminExercise(ushort exerciseCategory, ushort exerciseName) + { + ExerciseCategory = exerciseCategory; + ExerciseName = exerciseName; + } +} diff --git a/src/Conversion/FitConverter.cs b/src/Conversion/FitConverter.cs index d179881a9..33302a980 100644 --- a/src/Conversion/FitConverter.cs +++ b/src/Conversion/FitConverter.cs @@ -122,45 +122,63 @@ protected override async Task>> ConvertInternalA AddMetrics(messages, workoutSamples, sport, startTime); - var workoutSteps = new List(); - var laps = new List(); - var preferredLapType = PreferredLapType.Default; + var workoutMesg = new WorkoutMesg(); + workoutMesg.SetWktName(title.Replace(WorkoutHelper.SpaceSeparator, ' ')); + workoutMesg.SetCapabilities(32); + workoutMesg.SetSport(sport); + workoutMesg.SetSubSport(subSport); - if (sport == Sport.Cycling) - preferredLapType = settings.Format.Cycling.PreferredLapType; - if (sport == Sport.Running) - preferredLapType = settings.Format.Running.PreferredLapType; - if (sport == Sport.Rowing) - preferredLapType = settings.Format.Rowing.PreferredLapType; + var lapCount = 0; - if ((preferredLapType == PreferredLapType.Class_Targets || preferredLapType == PreferredLapType.Default) - && workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "cadence")?.Graph_Data is object) + if (subSport == SubSport.StrengthTraining) { - var stepsAndLaps = GetWorkoutStepsAndLaps(workoutSamples, startTime, sport, subSport); - workoutSteps = stepsAndLaps.Values.Select(v => v.Item1).ToList(); - laps = stepsAndLaps.Values.Select(v => v.Item2).ToList(); + var sets = GetStrengthWorkoutSteps(workout, startTime, settings); + + // Add sets in order + foreach (var set in sets) + messages.Add(set); + } else { - laps = GetLaps(preferredLapType, workoutSamples, startTime, sport, subSport).ToList(); - } + var workoutSteps = new List(); + var laps = new List(); + var preferredLapType = PreferredLapType.Default; + + if (sport == Sport.Cycling) + preferredLapType = settings.Format.Cycling.PreferredLapType; + if (sport == Sport.Running) + preferredLapType = settings.Format.Running.PreferredLapType; + if (sport == Sport.Rowing) + preferredLapType = settings.Format.Rowing.PreferredLapType; + + if ((preferredLapType == PreferredLapType.Class_Targets || preferredLapType == PreferredLapType.Default) + && workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "cadence")?.Graph_Data is object) + { + var stepsAndLaps = GetWorkoutStepsAndLaps(workoutSamples, startTime, sport, subSport); + workoutSteps = stepsAndLaps.Values.Select(v => v.Item1).ToList(); + laps = stepsAndLaps.Values.Select(v => v.Item2).ToList(); + } + else + { + laps = GetLaps(preferredLapType, workoutSamples, startTime, sport, subSport).ToList(); + } - var workoutMesg = new WorkoutMesg(); - workoutMesg.SetWktName(title.Replace(WorkoutHelper.SpaceSeparator, ' ')); - workoutMesg.SetCapabilities(32); - workoutMesg.SetSport(sport); - workoutMesg.SetSubSport(subSport); - workoutMesg.SetNumValidSteps((ushort)workoutSteps.Count); - messages.Add(workoutMesg); + workoutMesg.SetNumValidSteps((ushort)workoutSteps.Count); + + // add steps in order + foreach (var step in workoutSteps) + messages.Add(step); - // add steps in order - foreach (var step in workoutSteps) - messages.Add(step); + // Add laps in order + foreach (var lap in laps) + messages.Add(lap); - // Add laps in order - foreach (var lap in laps) - messages.Add(lap); + lapCount = laps.Count; + } + + messages.Add(workoutMesg); - messages.Add(GetSessionMesg(workout, workoutSamples, userData, settings, sport, startTime, endTime, (ushort)laps.Count)); + messages.Add(GetSessionMesg(workout, workoutSamples, userData, settings, sport, startTime, endTime, (ushort)lapCount)); var activityMesg = new ActivityMesg(); activityMesg.SetTimestamp(endTime); @@ -491,6 +509,68 @@ private Dictionary> GetWorkoutStepsAndLaps( return stepsAndLaps; } + private ICollection GetStrengthWorkoutSteps(Workout workout, Dynastream.Fit.DateTime startTime, Settings settings) + { + using var tracing = Tracing.Trace($"{nameof(FitConverter)}.{nameof(GetStrengthWorkoutSteps)}") + .WithTag(TagKey.Format, FileFormat.Fit.ToString()); + + var steps = new List(); + + if (workout is null) + return steps; + + var trackedMovements = workout.Movement_Tracker_Data; + if (trackedMovements is null) return steps; + + var completedMovementSummary = trackedMovements.Completed_Movements_Summary_Data; + if (completedMovementSummary is null) return steps; + + var repSummaryData = completedMovementSummary.Repetition_Summary_Data; + if (repSummaryData is null) return steps; + + ushort stepIndex = 0; + foreach (var repdata in repSummaryData) + { + if (!ExerciseMapping.StrengthExerciseMappings.TryGetValue(repdata.Movement_Id, out var exercise)) + { + _logger.Error($"Found Peloton Strength exercise with no Garmin mapping: {repdata.Movement_Name} {repdata.Movement_Id}"); + continue; + } + + if (exercise.ExerciseCategory == ExerciseCategory.Invalid) continue; // AMRAP -- no details provided for these segments + + var setMesg = new SetMesg(); + var setStartTime = new Dynastream.Fit.DateTime(startTime); + setStartTime.Add(repdata.Offset); + setMesg.SetStartTime(setStartTime); + setMesg.SetDuration(repdata.Length); + + setMesg.SetCategory(0, exercise.ExerciseCategory); + setMesg.SetCategorySubtype(0, exercise.ExerciseName); + + setMesg.SetMessageIndex(stepIndex); + setMesg.SetSetType(SetType.Active); + setMesg.SetWktStepIndex(stepIndex); + + var reps = repdata.Completed_Number; + if (repdata.Is_Hold) + reps = repdata.Completed_Number / settings.Format.Strength.DefaultSecondsPerRep; + + setMesg.SetRepetitions((ushort)reps); + + if (repdata.Weight is object && repdata.Weight.FirstOrDefault() is not null) + { + var weight = repdata.Weight.FirstOrDefault(); + setMesg.SetWeight(ConvertWeightToKilograms(weight.Weight_Data.Weight_Value, weight.Weight_Data.Weight_Unit)); + } + + stepIndex++; + steps.Add(setMesg); + } + + return steps; + } + public ICollection GetLaps(PreferredLapType preferredLapType, WorkoutSamples workoutSamples, Dynastream.Fit.DateTime startTime, Sport sport, SubSport subSport) { using var tracing = Tracing.Trace($"{nameof(FitConverter)}.{nameof(GetLaps)}") diff --git a/src/Conversion/FitDecoder.cs b/src/Conversion/FitDecoder.cs index a55874dbc..175d4ca3e 100644 --- a/src/Conversion/FitDecoder.cs +++ b/src/Conversion/FitDecoder.cs @@ -1,6 +1,7 @@ using Common; using Common.Observe; using Dynastream.Fit; +using Microsoft.VisualBasic; using Serilog; using System.IO; @@ -87,6 +88,7 @@ public static void Decode(string filePath) mesgBroadcaster.SegmentLeaderboardEntryMesgEvent += Write; mesgBroadcaster.SegmentPointMesgEvent += Write; mesgBroadcaster.SessionMesgEvent += Write; + mesgBroadcaster.SetMesgEvent+= Write; mesgBroadcaster.SlaveDeviceMesgEvent += Write; mesgBroadcaster.SoftwareMesgEvent += Write; mesgBroadcaster.SpeedZoneMesgEvent += Write; diff --git a/src/Conversion/IConverter.cs b/src/Conversion/IConverter.cs index 44fb417e5..cffe3c3d3 100644 --- a/src/Conversion/IConverter.cs +++ b/src/Conversion/IConverter.cs @@ -238,6 +238,19 @@ public static float ConvertDistanceToMeters(double value, string unit) default: return (float)value; } + } + + public static float ConvertWeightToKilograms(double value, string unit) + { + var weightUnit = UnitHelpers.GetWeightUnit(unit); + switch (weightUnit) + { + case WeightUnit.Pounds: + return (float)(value * 0.453592); + case WeightUnit.Kilograms: + default: + return (float)value; + } } public static float GetTotalDistance(WorkoutSamples workoutSamples) diff --git a/src/UnitTests/AdHocTests.cs b/src/UnitTests/AdHocTests.cs index 6861c273d..3b1cb1098 100644 --- a/src/UnitTests/AdHocTests.cs +++ b/src/UnitTests/AdHocTests.cs @@ -58,7 +58,7 @@ public void Setup() //[Test] //public void DecodeFitFile() //{ - // var output = Path.Join(FitDirectory, "p2g_cycle_workout.fit"); + // var output = Path.Join(FitDirectory, "strength_with_exercises.fit"); // FitDecoder.Decode(output); //} @@ -138,7 +138,7 @@ public void Setup() //[Test] //public async Task Convert_From_File() //{ - // var file = Path.Join(DataDirectory, "just_walk_outdoor.json"); + // var file = Path.Join(DataDirectory, "strength_with_exercises.json"); // //var file = Path.Join(DataDirectory, "cycling_target_metrics.json"); // //var file = Path.Join(DataDirectory, "tread_run_workout.json"); diff --git a/src/UnitTests/Data/p2g_workouts/output.fit b/src/UnitTests/Data/p2g_workouts/output.fit deleted file mode 100644 index 6f243e6fa4924198882124cef0a80108427d355b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7277 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 Mr#Kdr;x^j(KTWH2PXGV_ diff --git a/src/UnitTests/Data/p2g_workouts/strength_with_exercises.json b/src/UnitTests/Data/p2g_workouts/strength_with_exercises.json new file mode 100644 index 000000000..3c2716cab --- /dev/null +++ b/src/UnitTests/Data/p2g_workouts/strength_with_exercises.json @@ -0,0 +1,1354 @@ +{ + "WorkoutType": 9, + "UserData": { + "Cycling_Ftp_Source": "Ftp_Workout_Source", + "Cycling_Workout_Ftp": 163, + "Cycling_Ftp_Workout_Id": "ab06a525b5bf45e3b2ef9e1ba8d72d68", + "Cycling_Ftp": 154, + "Estimated_Cycling_Ftp": 92, + "Default_Max_Heart_Rate": 189, + "Customized_Max_Heart_Rate": 189, + "Weight": 121 + }, + "Workout": { + "Id": "99f46c6eccdf4519a9334927b2cd8ecc", + "Status": "COMPLETE", + "Title": null, + "Name": "Strength Workout", + "Created_At": 1676979081, + "Fitness_Discipline": "Strength", + "Is_Outdoor": false, + "Ride": { + "Class_Type_Ids": [ + "af137d7195a34e7f96f4d306da554ebd" + ], + "Content_Provider": "peloton", + "Content_Format": "video", + "Description": "Strengthen the largest muscles in your body with this glutes \u0026 legs workout. ", + "Difficulty_Estimate": 7.1351, + "Overall_Estimate": 0.9908, + "Difficulty_Rating_Avg": 7.1351, + "Difficulty_Rating_Count": 5746, + "Duration": 600, + "Equipment_Ids": [], + "Fitness_Discipline": "strength", + "Fitness_Discipline_Display_Name": "Strength", + "Has_Pedaling_Metrics": false, + "Id": "321fdb114a7141ecb45b1473942f2995", + "Image_Url": "https://s3.amazonaws.com/peloton-ride-images/894cae4befb4d1dd100b525cf5908e3673033745/img_1658080602_46c3a1c3ca9e47739268777703750ecc.png", + "Instructor_Id": "76e245b7a0fa42b4a1cd41576943e788", + "Is_Archived": true, + "Length": 811, + "Metrics": [ + "heart_rate", + "calories" + ], + "Pedaling_Start_Offset": 60, + "Pedaling_End_Offset": 660, + "Pedaling_Duration": 600, + "Title": "10 min Glutes \u0026 Legs Strength", + "Instructor": { + "Name": "Logan Aldridge" + } + }, + "End_Time": 1676979741, + "Start_Time": 1676979347, + "Total_Work": 0, + "Ftp_Info": { + "Ftp": 0, + "Ftp_Source": null, + "Ftp_Workout_Id": null + }, + "Movement_Tracker_Data": { + "Completed_Movements_Summary_Data": { + "Repetition_Summary_Data": [ + { + "Movement_Id": "588e35f7067842979485ff1e4f80df26", + "Movement_Name": "Goblet Squat", + "Is_Hold": true, + "Target_Number": 30, + "Completed_Number": 30, + "Offset": 120, + "Length": 30, + "Total_Volume": null, + "Weight": [ + { + "weight_data": { + "weight_value": 15.0, + "weight_unit": "lb" + }, + "weight_category": "medium" + }] + }, + { + "Movement_Id": "ed18d837c14746c5af38d4fa03b56918", + "Movement_Name": "Front to Back Lunge", + "Is_Hold": true, + "Target_Number": 30, + "Completed_Number": 30, + "Offset": 155, + "Length": 30, + "Total_Volume": null, + "Weight": null + }, + { + "Movement_Id": "28833fd99466476ea273d6b94747e3db", + "Movement_Name": "Split Squat", + "Is_Hold": true, + "Target_Number": 20, + "Completed_Number": 20, + "Offset": 190, + "Length": 20, + "Total_Volume": null, + "Weight": null + }, + { + "Movement_Id": "28833fd99466476ea273d6b94747e3db", + "Movement_Name": "Split Squat", + "Is_Hold": true, + "Target_Number": 20, + "Completed_Number": 20, + "Offset": 210, + "Length": 20, + "Total_Volume": null, + "Weight": null + }, + { + "Movement_Id": "588e35f7067842979485ff1e4f80df26", + "Movement_Name": "Goblet Squat", + "Is_Hold": true, + "Target_Number": 30, + "Completed_Number": 30, + "Offset": 240, + "Length": 30, + "Total_Volume": null, + "Weight": null + }, + { + "Movement_Id": "ed18d837c14746c5af38d4fa03b56918", + "Movement_Name": "Front to Back Lunge", + "Is_Hold": true, + "Target_Number": 30, + "Completed_Number": 30, + "Offset": 275, + "Length": 30, + "Total_Volume": null, + "Weight": null + }, + { + "Movement_Id": "28833fd99466476ea273d6b94747e3db", + "Movement_Name": "Split Squat", + "Is_Hold": true, + "Target_Number": 20, + "Completed_Number": 20, + "Offset": 310, + "Length": 20, + "Total_Volume": null, + "Weight": null + }, + { + "Movement_Id": "28833fd99466476ea273d6b94747e3db", + "Movement_Name": "Split Squat", + "Is_Hold": true, + "Target_Number": 20, + "Completed_Number": 20, + "Offset": 330, + "Length": 20, + "Total_Volume": null, + "Weight": null + }, + { + "Movement_Id": "588e35f7067842979485ff1e4f80df26", + "Movement_Name": "Goblet Squat", + "Is_Hold": true, + "Target_Number": 30, + "Completed_Number": 30, + "Offset": 360, + "Length": 30, + "Total_Volume": null, + "Weight": null + }, + { + "Movement_Id": "ed18d837c14746c5af38d4fa03b56918", + "Movement_Name": "Front to Back Lunge", + "Is_Hold": true, + "Target_Number": 30, + "Completed_Number": 30, + "Offset": 395, + "Length": 30, + "Total_Volume": null, + "Weight": null + }, + { + "Movement_Id": "28833fd99466476ea273d6b94747e3db", + "Movement_Name": "Split Squat", + "Is_Hold": true, + "Target_Number": 20, + "Completed_Number": 17, + "Offset": 430, + "Length": 20, + "Total_Volume": null, + "Weight": null + }, + { + "Movement_Id": "28833fd99466476ea273d6b94747e3db", + "Movement_Name": "Split Squat", + "Is_Hold": true, + "Target_Number": 20, + "Completed_Number": 20, + "Offset": 450, + "Length": 20, + "Total_Volume": null, + "Weight": null + }, + { + "Movement_Id": "98cde50f696746ff98727d5362229cfb", + "Movement_Name": "AMRAP", + "Is_Hold": true, + "Target_Number": 90, + "Completed_Number": 90, + "Offset": 510, + "Length": 90, + "Total_Volume": null, + "Weight": null + } + ] + } + } + }, + "WorkoutSamples": { + "Duration": 600, + "Is_Class_Plan_Shown": true, + "Segment_List": [ + { + "Id": "88294de0b603470d82e2efbb3ef2bdfd", + "Length": 60, + "Start_Time_Offset": 0, + "Icon_Url": "https://s3.amazonaws.com/static-cdn.pelotoncycle.com/segment-icons/warmup.png", + "Intensity_In_Mets": 5, + "Metrics_Type": "floor", + "Icon_Name": "warmup", + "Icon_Slug": "warmup", + "Name": "Warm Up" + }, + { + "Id": "dd7189dab99044a6a99243b5e8dbffe9", + "Length": 540, + "Start_Time_Offset": 60, + "Icon_Url": "https://s3.amazonaws.com/static-cdn.pelotoncycle.com/segment-icons/lower_body.png", + "Intensity_In_Mets": 5, + "Metrics_Type": "floor", + "Icon_Name": "lower_body", + "Icon_Slug": "lower_body", + "Name": "Lower Body" + } + ], + "Seconds_Since_Pedaling_Start": [ + 60, + 61, + 62, + 63, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 209, + 210, + 211, + 212, + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225, + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245, + 246, + 247, + 248, + 249, + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257, + 258, + 259, + 261, + 262, + 263, + 264, + 265, + 266, + 267, + 268, + 269, + 270, + 271, + 272, + 273, + 274, + 275, + 276, + 277, + 278, + 279, + 280, + 281, + 282, + 284, + 285, + 286, + 287, + 288, + 289, + 290, + 291, + 292, + 293, + 294, + 295, + 296, + 297, + 298, + 299, + 300, + 301, + 302, + 303, + 304, + 305, + 306, + 308, + 309, + 310, + 311, + 312, + 313, + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321, + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329, + 331, + 332, + 333, + 334, + 335, + 336, + 337, + 338, + 339, + 340, + 341, + 342, + 343, + 344, + 345, + 346, + 347, + 348, + 349, + 350, + 351, + 352, + 353, + 354, + 356, + 357, + 358, + 359, + 360, + 361, + 362, + 363, + 364, + 365, + 366, + 367, + 368, + 369, + 370, + 371, + 372, + 373, + 374, + 375, + 376, + 377, + 379, + 380, + 381, + 382, + 383, + 384, + 385, + 386, + 387, + 388, + 389, + 390, + 391, + 392, + 393, + 394, + 395, + 396, + 397, + 398, + 399, + 401, + 402, + 403, + 404, + 405, + 406, + 407, + 408, + 409, + 410, + 411, + 412, + 413, + 414, + 415, + 416, + 417, + 418, + 419, + 420, + 421, + 423, + 424, + 425, + 426, + 427, + 428, + 429, + 430, + 431, + 432, + 433, + 434, + 435, + 436, + 437, + 438, + 439, + 440, + 441, + 442, + 443, + 445, + 446, + 447, + 448, + 449, + 450, + 451, + 452, + 453, + 454, + 455, + 456, + 457, + 458, + 459, + 460, + 461, + 462, + 463, + 464, + 465, + 466, + 467, + 468, + 469, + 470, + 472, + 473, + 474, + 475, + 476, + 477, + 478, + 479, + 480, + 481, + 482, + 483, + 484, + 485, + 486, + 487, + 488, + 489, + 490, + 491, + 492, + 493, + 495, + 496, + 497, + 498, + 499, + 500, + 501, + 502, + 503, + 504, + 505, + 506, + 507, + 508, + 509, + 510, + 511, + 512, + 513, + 514, + 515, + 516, + 518, + 519, + 520, + 521, + 522, + 523, + 524, + 525, + 526, + 527, + 528, + 529, + 530, + 531, + 532, + 533, + 534, + 535, + 536, + 537, + 538, + 539, + 540, + 541, + 543, + 544, + 545, + 546, + 547, + 548, + 549, + 550, + 551, + 552, + 553, + 554, + 555, + 556, + 557, + 558, + 559, + 560, + 561, + 562, + 563, + 564, + 565, + 566, + 567, + 569, + 570, + 571, + 572, + 573, + 574, + 575, + 576, + 577, + 578, + 579, + 580, + 581, + 582, + 583, + 584, + 585, + 586, + 587, + 588, + 589, + 590, + 591, + 592, + 593, + 594, + 596, + 597, + 598, + 599, + 600 + ], + "Average_Summaries": [], + "Summaries": [ + { + "Display_Name": "Calories", + "Display_Unit": "kcal", + "Value": 76, + "Slug": "calories" + } + ], + "Metrics": [ + { + "Display_Name": "Heart Rate", + "Display_Unit": "bpm", + "Max_Value": 152, + "Average_Value": 121, + "Values": [ + 79, + 79, + 79, + 79, + 79, + 80, + 79, + 80, + 82, + 84, + 86, + 89, + 91, + 92, + 92, + 95, + 96, + 99, + 100, + 103, + 106, + 108, + 110, + 112, + 112, + 112, + 110, + 108, + 106, + 106, + 101, + 99, + 98, + 98, + 97, + 97, + 96, + 94, + 92, + 90, + 88, + 88, + 88, + 89, + 90, + 92, + 92, + 92, + 90, + 88, + 84, + 84, + 84, + 86, + 87, + 88, + 88, + 90, + 92, + 93, + 93, + 94, + 96, + 97, + 98, + 100, + 101, + 104, + 104, + 105, + 106, + 106, + 108, + 109, + 109, + 110, + 111, + 111, + 113, + 114, + 115, + 115, + 115, + 116, + 116, + 116, + 117, + 117, + 117, + 117, + 118, + 119, + 120, + 119, + 116, + 115, + 114, + 113, + 113, + 113, + 115, + 116, + 116, + 118, + 118, + 119, + 119, + 119, + 121, + 122, + 122, + 125, + 125, + 125, + 126, + 127, + 127, + 128, + 129, + 130, + 130, + 130, + 131, + 131, + 132, + 132, + 132, + 133, + 133, + 133, + 133, + 133, + 133, + 133, + 132, + 131, + 131, + 131, + 130, + 130, + 129, + 129, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 128, + 129, + 130, + 130, + 130, + 130, + 130, + 130, + 129, + 129, + 128, + 129, + 129, + 129, + 128, + 128, + 128, + 128, + 127, + 125, + 124, + 124, + 124, + 124, + 125, + 126, + 126, + 126, + 126, + 126, + 126, + 126, + 126, + 126, + 125, + 125, + 125, + 125, + 126, + 126, + 126, + 126, + 125, + 125, + 124, + 124, + 124, + 124, + 125, + 126, + 126, + 127, + 127, + 127, + 128, + 128, + 129, + 130, + 130, + 129, + 130, + 130, + 131, + 132, + 132, + 132, + 132, + 132, + 133, + 133, + 134, + 134, + 134, + 135, + 136, + 136, + 137, + 137, + 137, + 138, + 138, + 138, + 138, + 140, + 140, + 140, + 140, + 141, + 141, + 142, + 142, + 142, + 142, + 143, + 143, + 143, + 143, + 142, + 141, + 139, + 139, + 138, + 138, + 138, + 137, + 137, + 136, + 136, + 136, + 135, + 135, + 135, + 136, + 136, + 136, + 136, + 136, + 137, + 138, + 138, + 138, + 138, + 138, + 139, + 139, + 139, + 139, + 139, + 139, + 139, + 139, + 138, + 136, + 135, + 130, + 129, + 129, + 131, + 131, + 132, + 132, + 133, + 133, + 134, + 134, + 134, + 134, + 134, + 134, + 134, + 134, + 134, + 135, + 134, + 135, + 135, + 135, + 135, + 135, + 135, + 136, + 136, + 137, + 137, + 137, + 138, + 139, + 139, + 139, + 140, + 140, + 140, + 140, + 141, + 141, + 141, + 141, + 141, + 141, + 140, + 141, + 141, + 141, + 140, + 140, + 141, + 141, + 141, + 141, + 141, + 141, + 141, + 141, + 141, + 140, + 141, + 142, + 142, + 143, + 143, + 143, + 143, + 143, + 143, + 143, + 143, + 144, + 144, + 144, + 144, + 143, + 143, + 142, + 141, + 140, + 138, + 139, + 138, + 138, + 138, + 138, + 138, + 138, + 137, + 136, + 135, + 134, + 133, + 133, + 134, + 135, + 135, + 135, + 135, + 134, + 134, + 133, + 133, + 133, + 133, + 133, + 133, + 134, + 134, + 134, + 134, + 134, + 134, + 133, + 131, + 130, + 130, + 130, + 131, + 132, + 132, + 132, + 133, + 133, + 134, + 135, + 135, + 135, + 136, + 136, + 135, + 135, + 134, + 133, + 132, + 131, + 130, + 129, + 127, + 125, + 125, + 124, + 124, + 123, + 123, + 122, + 121, + 119, + 118, + 117, + 117, + 118, + 121, + 123, + 124, + 124, + 124, + 124, + 125, + 127, + 124, + 122, + 119, + 116, + 116, + 116, + 118, + 118, + 117, + 117, + 117, + 117, + 120, + 121, + 124, + 126, + 126, + 128, + 130, + 131, + 131, + 132, + 132, + 132, + 133, + 133, + 134, + 134, + 135, + 136, + 136, + 137, + 137, + 139, + 139, + 139, + 138, + 139, + 139, + 139, + 140, + 140, + 140, + 141, + 143, + 143, + 145, + 145, + 146, + 146, + 147, + 147, + 147, + 147, + 147, + 148, + 148, + 148, + 149, + 149, + 150, + 150, + 148, + 148, + 148, + 149, + 149, + 149, + 149, + 149, + 150, + 151, + 152 + ], + "Slug": "heart_rate", + "Zones": [ + { + "Display_Name": "Zone 1", + "Slug": "zone1", + "Range": "\u003C122 bpm", + "Duration": 191, + "Max_Value": 121, + "Min_Value": 0 + }, + { + "Display_Name": "Zone 2", + "Slug": "zone2", + "Range": "122-140 bpm", + "Duration": 325, + "Max_Value": 140, + "Min_Value": 122 + }, + { + "Display_Name": "Zone 3", + "Slug": "zone3", + "Range": "141-159 bpm", + "Duration": 80, + "Max_Value": 159, + "Min_Value": 141 + }, + { + "Display_Name": "Zone 4", + "Slug": "zone4", + "Range": "160-178 bpm", + "Duration": 0, + "Max_Value": 178, + "Min_Value": 160 + }, + { + "Display_Name": "Zone 5", + "Slug": "zone5", + "Range": "\u003E=179 bpm", + "Duration": 0, + "Max_Value": 189, + "Min_Value": 179 + } + ], + "Missing_Data_Duration": 4, + "Alternatives": null + } + ], + "Has_Apple_Watch_Metrcis": false, + "Location_Data": [], + "Is_Location_Data_Accurate": null, + "Target_Performance_Metrics": null + }, + "Raw": null +} \ No newline at end of file diff --git a/src/UnitTests/Data/sample_fit/strength_with_exercises.fit b/src/UnitTests/Data/sample_fit/strength_with_exercises.fit new file mode 100644 index 0000000000000000000000000000000000000000..570af5ee867d92222b9fa332a5f1436fa90be618 GIT binary patch literal 3689 zcmYk<2UJs67zgnGdn5!9abXqgNNuaNtyZm8ZL7r&M@y?!DnnF2QiNjAumlN^ z9Dvx}+P(L-wR`Wq_p*CWZU66Y*FAZ^eEIIpckjtLdFOp;?yX7rX~2Xz^K)E*EO0mz z9C3#uVwjd`S}FAmVt4p4lWrE=PpMi!4ct%VZdRSnzp~okp+xEk`}~Sz+u-g zGYpIxRX&fe*6a27+%u+3MXQV7PJl7k-j!i0%sqP`C7mB#Fb%_W za&o&pI}eH6hDYUr|2wU{!T&dLa%DP9`#^smbOCy}W&wI-pWGt*=|XH(e3s&~=^~s% zb8s$Qg7auT&ZkRp0WHLZ@*=td+h`FkrYms?Eybm@442UgTuv)-1@+=ex&~LttK~Jy zUrQ@-9j(Ci@&@X`jdUe$q9wRl-a?CUtGtaC;dZ(lchExINegfn&BNVtyS#_y<6c^T z9kdYl$@}F4%0Ec+@DR~P(&c!L7U6kX zj2CDLUZkaXiF)udU4>WVtF#=i(F(jS-=LLvldi^F@@?wHJG2V#%J=A6yia}jfL7x} z`H}pX*5DKQsr-!AVyFCEej&f4b@+JO-zff;*5Er@jqj-sKhU-KQT`-w@JVl-=7tm%bl&8r>axwKIN1jfb zutes{Jeg1ZD4@+)Dhp`~mdWLE1#Lx&XIQe`}f&yZy_gjLi+xvY?9%1XJK zhT)Y}G=ep9t@P0-s%Z>0vX-`?PS(qHv>gqyQLdLwG!DOPmMyZCb|N5yv>@JX@Y4&z0xV zHk?n}ae=&0UL?24i)jZgk(bKL`l2sg=_ zsfAnQtu&0=+%ixCXM4Q#c#`ZP5n4V z&ZNBm%VX(!WXajI5y#O698c?U0B2T4$ETAn| zsLW}!5sPR&7SkH!$kXK#T8CVjM{AKU3*=H+NY`N*ZNPHcgcYipuY^S~Gka60FPT56!&@DH}jkFh=bFdEf@vwx}T*hGf$b*{ybJqyctAV-2_*kt9bXnQfHa zt^modl=PyVfPHD$&(Ox6^vom_@v=u2FccYVXISyy5)&Pf0j8C1TEk5%!?ZF@YrHmN zd&YJt(ZFEC9AcP54Re@b4l=DY)0$$2rkSDXW(fBG+CIXLvB|Kne@6D-fr)KMHd4|X n`u~}+-?MTCuzNu`nXxO{mDs6e8rui*F@O4BmiSRd literal 0 HcmV?d00001 diff --git a/src/UnitTests/UnitTests.csproj b/src/UnitTests/UnitTests.csproj index d0ece4a64..9af4f39ba 100644 --- a/src/UnitTests/UnitTests.csproj +++ b/src/UnitTests/UnitTests.csproj @@ -6,6 +6,18 @@ false + + + + + + + PreserveNewest + true + PreserveNewest + + + diff --git a/src/WebUI/Pages/Settings.razor b/src/WebUI/Pages/Settings.razor index e34e621c5..11ba0ae04 100644 --- a/src/WebUI/Pages/Settings.razor +++ b/src/WebUI/Pages/Settings.razor @@ -47,6 +47,8 @@
+ +