From f41135ceb37a3173f1a04038d9f5b7a73c1f5bfd Mon Sep 17 00:00:00 2001 From: Gregory Conrad Date: Fri, 18 Oct 2024 19:30:28 -0400 Subject: [PATCH] docs(example): add Smolov Jr calculator for Hevy app (#232) --- .../firebase-hosting-hevy-smolov-jr.yml | 26 + examples/hevy_smolov_jr/.firebaserc | 5 + examples/hevy_smolov_jr/.gitignore | 43 + examples/hevy_smolov_jr/.metadata | 30 + examples/hevy_smolov_jr/build.yaml | 7 + examples/hevy_smolov_jr/firebase.json | 16 + .../hevy_smolov_jr/lib/api/raw_hevy_api.dart | 181 +++ .../lib/api/wrapped_hevy_api.dart | 206 +++ .../lib/api/wrapped_hevy_api.freezed.dart | 1229 +++++++++++++++++ .../lib/api/wrapped_hevy_api.g.dart | 114 ++ examples/hevy_smolov_jr/lib/main.dart | 119 ++ examples/hevy_smolov_jr/lib/shared_prefs.dart | 57 + .../lib/smolov_jr_config/config.dart | 160 +++ .../lib/smolov_jr_config/config.freezed.dart | 276 ++++ .../hevy_smolov_jr/lib/steps/api_key.dart | 61 + .../lib/steps/confirmation_creation.dart | 86 ++ .../lib/steps/exercise_selection.dart | 92 ++ examples/hevy_smolov_jr/lib/steps/intro.dart | 61 + .../lib/steps/program_config.dart | 123 ++ .../lib/widgets/warning_card.dart | 49 + examples/hevy_smolov_jr/pubspec.yaml | 25 + examples/hevy_smolov_jr/web/index.html | 38 + examples/hevy_smolov_jr/web/manifest.json | 12 + 23 files changed, 3016 insertions(+) create mode 100644 .github/workflows/firebase-hosting-hevy-smolov-jr.yml create mode 100644 examples/hevy_smolov_jr/.firebaserc create mode 100644 examples/hevy_smolov_jr/.gitignore create mode 100644 examples/hevy_smolov_jr/.metadata create mode 100644 examples/hevy_smolov_jr/build.yaml create mode 100644 examples/hevy_smolov_jr/firebase.json create mode 100644 examples/hevy_smolov_jr/lib/api/raw_hevy_api.dart create mode 100644 examples/hevy_smolov_jr/lib/api/wrapped_hevy_api.dart create mode 100644 examples/hevy_smolov_jr/lib/api/wrapped_hevy_api.freezed.dart create mode 100644 examples/hevy_smolov_jr/lib/api/wrapped_hevy_api.g.dart create mode 100644 examples/hevy_smolov_jr/lib/main.dart create mode 100644 examples/hevy_smolov_jr/lib/shared_prefs.dart create mode 100644 examples/hevy_smolov_jr/lib/smolov_jr_config/config.dart create mode 100644 examples/hevy_smolov_jr/lib/smolov_jr_config/config.freezed.dart create mode 100644 examples/hevy_smolov_jr/lib/steps/api_key.dart create mode 100644 examples/hevy_smolov_jr/lib/steps/confirmation_creation.dart create mode 100644 examples/hevy_smolov_jr/lib/steps/exercise_selection.dart create mode 100644 examples/hevy_smolov_jr/lib/steps/intro.dart create mode 100644 examples/hevy_smolov_jr/lib/steps/program_config.dart create mode 100644 examples/hevy_smolov_jr/lib/widgets/warning_card.dart create mode 100644 examples/hevy_smolov_jr/pubspec.yaml create mode 100644 examples/hevy_smolov_jr/web/index.html create mode 100644 examples/hevy_smolov_jr/web/manifest.json diff --git a/.github/workflows/firebase-hosting-hevy-smolov-jr.yml b/.github/workflows/firebase-hosting-hevy-smolov-jr.yml new file mode 100644 index 0000000..efbd383 --- /dev/null +++ b/.github/workflows/firebase-hosting-hevy-smolov-jr.yml @@ -0,0 +1,26 @@ +name: Deploy Hevy Smolov Jr to Firebase Hosting + +on: + push: + branches: + - main + +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + - uses: bluefireteam/melos-action@v2 + - name: Run tests + run: melos run test + - name: Build for web + run: flutter build web + working-directory: examples/hevy_smolov_jr + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + entryPoint: examples/hevy_smolov_jr + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_HEVY_SMOLOV_JR }}' + channelId: live + projectId: hevy-smolov-jr diff --git a/examples/hevy_smolov_jr/.firebaserc b/examples/hevy_smolov_jr/.firebaserc new file mode 100644 index 0000000..0b0d80f --- /dev/null +++ b/examples/hevy_smolov_jr/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "hevy-smolov-jr" + } +} diff --git a/examples/hevy_smolov_jr/.gitignore b/examples/hevy_smolov_jr/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/examples/hevy_smolov_jr/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/examples/hevy_smolov_jr/.metadata b/examples/hevy_smolov_jr/.metadata new file mode 100644 index 0000000..395986d --- /dev/null +++ b/examples/hevy_smolov_jr/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "2663184aa79047d0a33a14a3b607954f8fdd8730" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + - platform: macos + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/examples/hevy_smolov_jr/build.yaml b/examples/hevy_smolov_jr/build.yaml new file mode 100644 index 0000000..670ef0c --- /dev/null +++ b/examples/hevy_smolov_jr/build.yaml @@ -0,0 +1,7 @@ +targets: + $default: + builders: + json_serializable: + options: + # explicit_to_json: true + field_rename: snake diff --git a/examples/hevy_smolov_jr/firebase.json b/examples/hevy_smolov_jr/firebase.json new file mode 100644 index 0000000..6603732 --- /dev/null +++ b/examples/hevy_smolov_jr/firebase.json @@ -0,0 +1,16 @@ +{ + "hosting": { + "public": "build/web", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/examples/hevy_smolov_jr/lib/api/raw_hevy_api.dart b/examples/hevy_smolov_jr/lib/api/raw_hevy_api.dart new file mode 100644 index 0000000..ed1ea57 --- /dev/null +++ b/examples/hevy_smolov_jr/lib/api/raw_hevy_api.dart @@ -0,0 +1,181 @@ +import 'dart:convert'; + +import 'package:hevy_smolov_jr/shared_prefs.dart'; +import 'package:http/http.dart' as http; +import 'package:rearch/experimental.dart'; +import 'package:rearch/rearch.dart'; + +/// [Capsule] representing the user's Hevy API key. +final Capsule<(String, void Function(String))> apiKeyCapsule = capsule((use) { + const sharedPrefsKey = 'api-key'; + final sharedPrefs = use(sharedPrefsCapsule); + final (apiKey, setApiKey) = + use.state(sharedPrefs.getString(sharedPrefsKey) ?? ''); + return ( + apiKey, + (newApiKey) { + sharedPrefs.setString(sharedPrefsKey, newApiKey); + setApiKey(newApiKey); + }, + ); +}); + +final Capsule _apiDomainCapsule = capsule((use) => 'api.hevyapp.com'); + +/// Represents an [Exception] from the Hevy API. +sealed class HevyApiException implements Exception {} + +/// Represents an [Exception] while completing a Hevy API request. +final class HevyApiNetworkException implements HevyApiException { + /// Represents an [Exception] while completing a Hevy API request. + const HevyApiNetworkException(this.underlyingException); + + /// The underlying [Exception] or [Error] thrown during the request. + final Object underlyingException; + + @override + String toString() { + return 'HevyApiNetworkException(underlyingException: $underlyingException)'; + } +} + +/// Represents an [Exception] regarding the [http.Response]. +final class HevyApiResponseException implements HevyApiException { + /// Represents an [Exception] regarding the [http.Response]. + const HevyApiResponseException(this.statusCode, this.errorMessage); + + /// The HTTP status code returned by the Hevy API. + final int statusCode; + + /// The error message returned by the Hevy API. + final String errorMessage; + + @override + String toString() { + return 'HevyApiResponseException(statusCode: $statusCode, ' + 'errorMessage: "$errorMessage")'; + } +} + +/// Represents an [Exception] parsing the [http.Response] from the Hevy API. +final class HevyApiResponseParseException implements HevyApiException { + /// Represents an [Exception] parsing the [http.Response] from the Hevy API. + HevyApiResponseParseException({ + required this.statusCode, + required this.responseBody, + required this.parseException, + }); + + /// The HTTP status code returned by the Hevy API. + final int statusCode; + + /// The response body returned by the Hevy API that could not be parsed. + final String responseBody; + + /// The [Exception] or [Error] thrown while parsing [responseBody]. + final Object parseException; + + @override + String toString() { + return 'HevyApiResponseParseException(statusCode: $statusCode, ' + 'parseException: $parseException, responseBody: $responseBody)'; + } +} + +/// Wraps a raw Hevy API HTTP call so that it: +/// - returns the response body as the decoded `Map` +/// - throws the appropriate type of [Exception] as needed +final Capsule> Function(Future)> + _parseApiRequestAction = capsule((use) { + return (hevyApiRequest) async { + late http.Response response; + try { + response = await hevyApiRequest; + } catch (underlyingException, stackTrace) { + Error.throwWithStackTrace( + HevyApiNetworkException(underlyingException), + stackTrace, + ); + } + + late Map body; + try { + body = json.decode(response.body) as Map; + + if (response.statusCode < 200 || response.statusCode > 299) { + throw HevyApiResponseException( + response.statusCode, + body['error'] as String, + ); + } + } catch (e, stackTrace) { + Error.throwWithStackTrace( + HevyApiResponseParseException( + statusCode: response.statusCode, + responseBody: response.body, + parseException: e, + ), + stackTrace, + ); + } + + return body; + }; +}); + +/// Represents an HTTP GET request that returns JSON. +typedef GetRequest = Future> Function({ + required String path, + Map? queryParams, +}); + +/// Represents an HTTP GET request to the Hevy API. +final Capsule apiGetAction = capsule((use) { + final parseRequest = use(_parseApiRequestAction); + final apiDomain = use(_apiDomainCapsule); + final headers = { + 'accept': 'application/json', + 'api-key': use(apiKeyCapsule).$1, + }; + + return ({required String path, Map? queryParams}) { + return parseRequest( + http.get( + Uri.https(apiDomain, path, queryParams), + headers: headers, + ), + ); + }; +}); + +/// Represents an HTTP POST request that returns JSON. +typedef PostRequest = Future> Function({ + required String path, + Object? jsonBody, + Map? queryParams, +}); + +/// Represents an HTTP POST request to the Hevy API. +final Capsule apiPostAction = capsule((use) { + final parseRequest = use(_parseApiRequestAction); + final apiDomain = use(_apiDomainCapsule); + final headers = { + 'accept': 'application/json', + 'api-key': use(apiKeyCapsule).$1, + 'Content-Type': 'application/json', + }; + + return ({ + required String path, + Object? jsonBody, + Map? queryParams, + }) { + return parseRequest( + http.post( + Uri.https(apiDomain, path, queryParams), + headers: headers, + body: json.encode(jsonBody), + ), + ); + }; +}); diff --git a/examples/hevy_smolov_jr/lib/api/wrapped_hevy_api.dart b/examples/hevy_smolov_jr/lib/api/wrapped_hevy_api.dart new file mode 100644 index 0000000..7e6744a --- /dev/null +++ b/examples/hevy_smolov_jr/lib/api/wrapped_hevy_api.dart @@ -0,0 +1,206 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hevy_smolov_jr/api/raw_hevy_api.dart'; +import 'package:rearch/experimental.dart'; +import 'package:rearch/rearch.dart'; + +part 'wrapped_hevy_api.g.dart'; +part 'wrapped_hevy_api.freezed.dart'; + +// NOTE: The stuff here is all pretty trivial--just API wrappers. +// ignore_for_file: public_member_api_docs + +@freezed +class ExerciseTemplatesResponse with _$ExerciseTemplatesResponse { + const factory ExerciseTemplatesResponse({ + required int pageCount, + required int page, + required List exerciseTemplates, + }) = _ExerciseTemplatesResponse; + + factory ExerciseTemplatesResponse.fromJson(Map json) => + _$ExerciseTemplatesResponseFromJson(json); +} + +@freezed +class Exercise with _$Exercise { + const factory Exercise({ + required String id, + required String title, + required String type, + required String primaryMuscleGroup, + required List secondaryMuscleGroups, + required String equipment, + required bool isCustom, + }) = _Exercise; + + factory Exercise.fromJson(Map json) => + _$ExerciseFromJson(json); +} + +final Capsule>> exercisesCapsule = capsule((use) async { + final get = use(apiGetAction); + + final exercises = []; + var pageCount = 1; + for (var page = 1; page <= pageCount; ++page) { + final responseJson = await get( + path: '/v1/exercise_templates', + queryParams: { + 'page': page.toString(), + 'pageSize': '100', // NOTE: maximum allowed value according to docs + }, + ); + final response = ExerciseTemplatesResponse.fromJson(responseJson); + pageCount = response.pageCount; + exercises.addAll(response.exerciseTemplates); + } + + return exercises; +}); + +final Capsule Function({required String folderName})> + createRoutineFolderAction = capsule((use) { + final post = use(apiPostAction); + return ({required String folderName}) async { + final response = await post( + path: '/v1/routine_folders', + jsonBody: { + 'routine_folder': { + 'title': folderName, + }, + }, + ); + return (response['routine_folder'] as Map)['id'] as int; + }; +}); + +@freezed +class RoutineTemplate with _$RoutineTemplate { + const factory RoutineTemplate({ + required String title, + required List exercises, + String? notes, + int? folderId, + }) = _RoutineTemplate; + + factory RoutineTemplate.fromJson(Map json) => + _$RoutineTemplateFromJson(json); +} + +@freezed +class RoutineTemplateExercise with _$RoutineTemplateExercise { + const factory RoutineTemplateExercise({ + required String exerciseTemplateId, + required List sets, + int? supersetId, + int? restSeconds, + String? notes, + }) = _RoutineTemplateExercise; + + factory RoutineTemplateExercise.fromJson(Map json) => + _$RoutineTemplateExerciseFromJson(json); +} + +@freezed +class RoutineTemplateSet with _$RoutineTemplateSet { + const factory RoutineTemplateSet._({ + required String type, + num? weightKg, + int? reps, + int? distanceMeters, + int? durationSeconds, + }) = _RoutineTemplateSet; + + factory RoutineTemplateSet.fromJson(Map json) => + _$RoutineTemplateSetFromJson(json); + + factory RoutineTemplateSet.normal({ + num? weightKg, + int? reps, + int? distanceMeters, + int? durationSeconds, + }) { + return RoutineTemplateSet._( + type: 'normal', + weightKg: weightKg, + reps: reps, + distanceMeters: distanceMeters, + durationSeconds: durationSeconds, + ); + } + + factory RoutineTemplateSet.warmup({ + num? weightKg, + int? reps, + int? distanceMeters, + int? durationSeconds, + }) { + return RoutineTemplateSet._( + type: 'warmup', + weightKg: weightKg, + reps: reps, + distanceMeters: distanceMeters, + durationSeconds: durationSeconds, + ); + } + + factory RoutineTemplateSet.failure({ + num? weightKg, + int? reps, + int? distanceMeters, + int? durationSeconds, + }) { + return RoutineTemplateSet._( + type: 'failure', + weightKg: weightKg, + reps: reps, + distanceMeters: distanceMeters, + durationSeconds: durationSeconds, + ); + } + + factory RoutineTemplateSet.dropset({ + num? weightKg, + int? reps, + int? distanceMeters, + int? durationSeconds, + }) { + return RoutineTemplateSet._( + type: 'dropset', + weightKg: weightKg, + reps: reps, + distanceMeters: distanceMeters, + durationSeconds: durationSeconds, + ); + } +} + +final Capsule Function(RoutineTemplate)> createRoutineAction = + capsule((use) { + final post = use(apiPostAction); + return (RoutineTemplate routine) { + return post( + path: '/v1/routines', + jsonBody: {'routine': routine.toJson()}, + ); + }; +}); + +final Capsule< + Future Function({ + required String programName, + required List routines, + })> createProgramAction = capsule((use) { + final createFolder = use(createRoutineFolderAction); + final createRoutine = use(createRoutineAction); + + return ({ + required String programName, + required List routines, + }) async { + final folderId = await createFolder(folderName: programName); + for (final routine in routines) { + await createRoutine(routine.copyWith(folderId: folderId)); + } + }; +}); diff --git a/examples/hevy_smolov_jr/lib/api/wrapped_hevy_api.freezed.dart b/examples/hevy_smolov_jr/lib/api/wrapped_hevy_api.freezed.dart new file mode 100644 index 0000000..93a0417 --- /dev/null +++ b/examples/hevy_smolov_jr/lib/api/wrapped_hevy_api.freezed.dart @@ -0,0 +1,1229 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'wrapped_hevy_api.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +ExerciseTemplatesResponse _$ExerciseTemplatesResponseFromJson( + Map json) { + return _ExerciseTemplatesResponse.fromJson(json); +} + +/// @nodoc +mixin _$ExerciseTemplatesResponse { + int get pageCount => throw _privateConstructorUsedError; + int get page => throw _privateConstructorUsedError; + List get exerciseTemplates => throw _privateConstructorUsedError; + + /// Serializes this ExerciseTemplatesResponse to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ExerciseTemplatesResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ExerciseTemplatesResponseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ExerciseTemplatesResponseCopyWith<$Res> { + factory $ExerciseTemplatesResponseCopyWith(ExerciseTemplatesResponse value, + $Res Function(ExerciseTemplatesResponse) then) = + _$ExerciseTemplatesResponseCopyWithImpl<$Res, ExerciseTemplatesResponse>; + @useResult + $Res call({int pageCount, int page, List exerciseTemplates}); +} + +/// @nodoc +class _$ExerciseTemplatesResponseCopyWithImpl<$Res, + $Val extends ExerciseTemplatesResponse> + implements $ExerciseTemplatesResponseCopyWith<$Res> { + _$ExerciseTemplatesResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ExerciseTemplatesResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? pageCount = null, + Object? page = null, + Object? exerciseTemplates = null, + }) { + return _then(_value.copyWith( + pageCount: null == pageCount + ? _value.pageCount + : pageCount // ignore: cast_nullable_to_non_nullable + as int, + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + exerciseTemplates: null == exerciseTemplates + ? _value.exerciseTemplates + : exerciseTemplates // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ExerciseTemplatesResponseImplCopyWith<$Res> + implements $ExerciseTemplatesResponseCopyWith<$Res> { + factory _$$ExerciseTemplatesResponseImplCopyWith( + _$ExerciseTemplatesResponseImpl value, + $Res Function(_$ExerciseTemplatesResponseImpl) then) = + __$$ExerciseTemplatesResponseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int pageCount, int page, List exerciseTemplates}); +} + +/// @nodoc +class __$$ExerciseTemplatesResponseImplCopyWithImpl<$Res> + extends _$ExerciseTemplatesResponseCopyWithImpl<$Res, + _$ExerciseTemplatesResponseImpl> + implements _$$ExerciseTemplatesResponseImplCopyWith<$Res> { + __$$ExerciseTemplatesResponseImplCopyWithImpl( + _$ExerciseTemplatesResponseImpl _value, + $Res Function(_$ExerciseTemplatesResponseImpl) _then) + : super(_value, _then); + + /// Create a copy of ExerciseTemplatesResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? pageCount = null, + Object? page = null, + Object? exerciseTemplates = null, + }) { + return _then(_$ExerciseTemplatesResponseImpl( + pageCount: null == pageCount + ? _value.pageCount + : pageCount // ignore: cast_nullable_to_non_nullable + as int, + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + exerciseTemplates: null == exerciseTemplates + ? _value._exerciseTemplates + : exerciseTemplates // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ExerciseTemplatesResponseImpl implements _ExerciseTemplatesResponse { + const _$ExerciseTemplatesResponseImpl( + {required this.pageCount, + required this.page, + required final List exerciseTemplates}) + : _exerciseTemplates = exerciseTemplates; + + factory _$ExerciseTemplatesResponseImpl.fromJson(Map json) => + _$$ExerciseTemplatesResponseImplFromJson(json); + + @override + final int pageCount; + @override + final int page; + final List _exerciseTemplates; + @override + List get exerciseTemplates { + if (_exerciseTemplates is EqualUnmodifiableListView) + return _exerciseTemplates; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_exerciseTemplates); + } + + @override + String toString() { + return 'ExerciseTemplatesResponse(pageCount: $pageCount, page: $page, exerciseTemplates: $exerciseTemplates)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ExerciseTemplatesResponseImpl && + (identical(other.pageCount, pageCount) || + other.pageCount == pageCount) && + (identical(other.page, page) || other.page == page) && + const DeepCollectionEquality() + .equals(other._exerciseTemplates, _exerciseTemplates)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, pageCount, page, + const DeepCollectionEquality().hash(_exerciseTemplates)); + + /// Create a copy of ExerciseTemplatesResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ExerciseTemplatesResponseImplCopyWith<_$ExerciseTemplatesResponseImpl> + get copyWith => __$$ExerciseTemplatesResponseImplCopyWithImpl< + _$ExerciseTemplatesResponseImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ExerciseTemplatesResponseImplToJson( + this, + ); + } +} + +abstract class _ExerciseTemplatesResponse implements ExerciseTemplatesResponse { + const factory _ExerciseTemplatesResponse( + {required final int pageCount, + required final int page, + required final List exerciseTemplates}) = + _$ExerciseTemplatesResponseImpl; + + factory _ExerciseTemplatesResponse.fromJson(Map json) = + _$ExerciseTemplatesResponseImpl.fromJson; + + @override + int get pageCount; + @override + int get page; + @override + List get exerciseTemplates; + + /// Create a copy of ExerciseTemplatesResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ExerciseTemplatesResponseImplCopyWith<_$ExerciseTemplatesResponseImpl> + get copyWith => throw _privateConstructorUsedError; +} + +Exercise _$ExerciseFromJson(Map json) { + return _Exercise.fromJson(json); +} + +/// @nodoc +mixin _$Exercise { + String get id => throw _privateConstructorUsedError; + String get title => throw _privateConstructorUsedError; + String get type => throw _privateConstructorUsedError; + String get primaryMuscleGroup => throw _privateConstructorUsedError; + List get secondaryMuscleGroups => throw _privateConstructorUsedError; + String get equipment => throw _privateConstructorUsedError; + bool get isCustom => throw _privateConstructorUsedError; + + /// Serializes this Exercise to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of Exercise + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ExerciseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ExerciseCopyWith<$Res> { + factory $ExerciseCopyWith(Exercise value, $Res Function(Exercise) then) = + _$ExerciseCopyWithImpl<$Res, Exercise>; + @useResult + $Res call( + {String id, + String title, + String type, + String primaryMuscleGroup, + List secondaryMuscleGroups, + String equipment, + bool isCustom}); +} + +/// @nodoc +class _$ExerciseCopyWithImpl<$Res, $Val extends Exercise> + implements $ExerciseCopyWith<$Res> { + _$ExerciseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Exercise + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = null, + Object? type = null, + Object? primaryMuscleGroup = null, + Object? secondaryMuscleGroups = null, + Object? equipment = null, + Object? isCustom = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String, + primaryMuscleGroup: null == primaryMuscleGroup + ? _value.primaryMuscleGroup + : primaryMuscleGroup // ignore: cast_nullable_to_non_nullable + as String, + secondaryMuscleGroups: null == secondaryMuscleGroups + ? _value.secondaryMuscleGroups + : secondaryMuscleGroups // ignore: cast_nullable_to_non_nullable + as List, + equipment: null == equipment + ? _value.equipment + : equipment // ignore: cast_nullable_to_non_nullable + as String, + isCustom: null == isCustom + ? _value.isCustom + : isCustom // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ExerciseImplCopyWith<$Res> + implements $ExerciseCopyWith<$Res> { + factory _$$ExerciseImplCopyWith( + _$ExerciseImpl value, $Res Function(_$ExerciseImpl) then) = + __$$ExerciseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String title, + String type, + String primaryMuscleGroup, + List secondaryMuscleGroups, + String equipment, + bool isCustom}); +} + +/// @nodoc +class __$$ExerciseImplCopyWithImpl<$Res> + extends _$ExerciseCopyWithImpl<$Res, _$ExerciseImpl> + implements _$$ExerciseImplCopyWith<$Res> { + __$$ExerciseImplCopyWithImpl( + _$ExerciseImpl _value, $Res Function(_$ExerciseImpl) _then) + : super(_value, _then); + + /// Create a copy of Exercise + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = null, + Object? type = null, + Object? primaryMuscleGroup = null, + Object? secondaryMuscleGroups = null, + Object? equipment = null, + Object? isCustom = null, + }) { + return _then(_$ExerciseImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String, + primaryMuscleGroup: null == primaryMuscleGroup + ? _value.primaryMuscleGroup + : primaryMuscleGroup // ignore: cast_nullable_to_non_nullable + as String, + secondaryMuscleGroups: null == secondaryMuscleGroups + ? _value._secondaryMuscleGroups + : secondaryMuscleGroups // ignore: cast_nullable_to_non_nullable + as List, + equipment: null == equipment + ? _value.equipment + : equipment // ignore: cast_nullable_to_non_nullable + as String, + isCustom: null == isCustom + ? _value.isCustom + : isCustom // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ExerciseImpl implements _Exercise { + const _$ExerciseImpl( + {required this.id, + required this.title, + required this.type, + required this.primaryMuscleGroup, + required final List secondaryMuscleGroups, + required this.equipment, + required this.isCustom}) + : _secondaryMuscleGroups = secondaryMuscleGroups; + + factory _$ExerciseImpl.fromJson(Map json) => + _$$ExerciseImplFromJson(json); + + @override + final String id; + @override + final String title; + @override + final String type; + @override + final String primaryMuscleGroup; + final List _secondaryMuscleGroups; + @override + List get secondaryMuscleGroups { + if (_secondaryMuscleGroups is EqualUnmodifiableListView) + return _secondaryMuscleGroups; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_secondaryMuscleGroups); + } + + @override + final String equipment; + @override + final bool isCustom; + + @override + String toString() { + return 'Exercise(id: $id, title: $title, type: $type, primaryMuscleGroup: $primaryMuscleGroup, secondaryMuscleGroups: $secondaryMuscleGroups, equipment: $equipment, isCustom: $isCustom)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ExerciseImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.title, title) || other.title == title) && + (identical(other.type, type) || other.type == type) && + (identical(other.primaryMuscleGroup, primaryMuscleGroup) || + other.primaryMuscleGroup == primaryMuscleGroup) && + const DeepCollectionEquality() + .equals(other._secondaryMuscleGroups, _secondaryMuscleGroups) && + (identical(other.equipment, equipment) || + other.equipment == equipment) && + (identical(other.isCustom, isCustom) || + other.isCustom == isCustom)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + title, + type, + primaryMuscleGroup, + const DeepCollectionEquality().hash(_secondaryMuscleGroups), + equipment, + isCustom); + + /// Create a copy of Exercise + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ExerciseImplCopyWith<_$ExerciseImpl> get copyWith => + __$$ExerciseImplCopyWithImpl<_$ExerciseImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ExerciseImplToJson( + this, + ); + } +} + +abstract class _Exercise implements Exercise { + const factory _Exercise( + {required final String id, + required final String title, + required final String type, + required final String primaryMuscleGroup, + required final List secondaryMuscleGroups, + required final String equipment, + required final bool isCustom}) = _$ExerciseImpl; + + factory _Exercise.fromJson(Map json) = + _$ExerciseImpl.fromJson; + + @override + String get id; + @override + String get title; + @override + String get type; + @override + String get primaryMuscleGroup; + @override + List get secondaryMuscleGroups; + @override + String get equipment; + @override + bool get isCustom; + + /// Create a copy of Exercise + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ExerciseImplCopyWith<_$ExerciseImpl> get copyWith => + throw _privateConstructorUsedError; +} + +RoutineTemplate _$RoutineTemplateFromJson(Map json) { + return _RoutineTemplate.fromJson(json); +} + +/// @nodoc +mixin _$RoutineTemplate { + String get title => throw _privateConstructorUsedError; + List get exercises => + throw _privateConstructorUsedError; + String? get notes => throw _privateConstructorUsedError; + int? get folderId => throw _privateConstructorUsedError; + + /// Serializes this RoutineTemplate to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of RoutineTemplate + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RoutineTemplateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RoutineTemplateCopyWith<$Res> { + factory $RoutineTemplateCopyWith( + RoutineTemplate value, $Res Function(RoutineTemplate) then) = + _$RoutineTemplateCopyWithImpl<$Res, RoutineTemplate>; + @useResult + $Res call( + {String title, + List exercises, + String? notes, + int? folderId}); +} + +/// @nodoc +class _$RoutineTemplateCopyWithImpl<$Res, $Val extends RoutineTemplate> + implements $RoutineTemplateCopyWith<$Res> { + _$RoutineTemplateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of RoutineTemplate + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? title = null, + Object? exercises = null, + Object? notes = freezed, + Object? folderId = freezed, + }) { + return _then(_value.copyWith( + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + exercises: null == exercises + ? _value.exercises + : exercises // ignore: cast_nullable_to_non_nullable + as List, + notes: freezed == notes + ? _value.notes + : notes // ignore: cast_nullable_to_non_nullable + as String?, + folderId: freezed == folderId + ? _value.folderId + : folderId // ignore: cast_nullable_to_non_nullable + as int?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$RoutineTemplateImplCopyWith<$Res> + implements $RoutineTemplateCopyWith<$Res> { + factory _$$RoutineTemplateImplCopyWith(_$RoutineTemplateImpl value, + $Res Function(_$RoutineTemplateImpl) then) = + __$$RoutineTemplateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String title, + List exercises, + String? notes, + int? folderId}); +} + +/// @nodoc +class __$$RoutineTemplateImplCopyWithImpl<$Res> + extends _$RoutineTemplateCopyWithImpl<$Res, _$RoutineTemplateImpl> + implements _$$RoutineTemplateImplCopyWith<$Res> { + __$$RoutineTemplateImplCopyWithImpl( + _$RoutineTemplateImpl _value, $Res Function(_$RoutineTemplateImpl) _then) + : super(_value, _then); + + /// Create a copy of RoutineTemplate + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? title = null, + Object? exercises = null, + Object? notes = freezed, + Object? folderId = freezed, + }) { + return _then(_$RoutineTemplateImpl( + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + exercises: null == exercises + ? _value._exercises + : exercises // ignore: cast_nullable_to_non_nullable + as List, + notes: freezed == notes + ? _value.notes + : notes // ignore: cast_nullable_to_non_nullable + as String?, + folderId: freezed == folderId + ? _value.folderId + : folderId // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$RoutineTemplateImpl implements _RoutineTemplate { + const _$RoutineTemplateImpl( + {required this.title, + required final List exercises, + this.notes, + this.folderId}) + : _exercises = exercises; + + factory _$RoutineTemplateImpl.fromJson(Map json) => + _$$RoutineTemplateImplFromJson(json); + + @override + final String title; + final List _exercises; + @override + List get exercises { + if (_exercises is EqualUnmodifiableListView) return _exercises; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_exercises); + } + + @override + final String? notes; + @override + final int? folderId; + + @override + String toString() { + return 'RoutineTemplate(title: $title, exercises: $exercises, notes: $notes, folderId: $folderId)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RoutineTemplateImpl && + (identical(other.title, title) || other.title == title) && + const DeepCollectionEquality() + .equals(other._exercises, _exercises) && + (identical(other.notes, notes) || other.notes == notes) && + (identical(other.folderId, folderId) || + other.folderId == folderId)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, title, + const DeepCollectionEquality().hash(_exercises), notes, folderId); + + /// Create a copy of RoutineTemplate + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RoutineTemplateImplCopyWith<_$RoutineTemplateImpl> get copyWith => + __$$RoutineTemplateImplCopyWithImpl<_$RoutineTemplateImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$RoutineTemplateImplToJson( + this, + ); + } +} + +abstract class _RoutineTemplate implements RoutineTemplate { + const factory _RoutineTemplate( + {required final String title, + required final List exercises, + final String? notes, + final int? folderId}) = _$RoutineTemplateImpl; + + factory _RoutineTemplate.fromJson(Map json) = + _$RoutineTemplateImpl.fromJson; + + @override + String get title; + @override + List get exercises; + @override + String? get notes; + @override + int? get folderId; + + /// Create a copy of RoutineTemplate + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RoutineTemplateImplCopyWith<_$RoutineTemplateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +RoutineTemplateExercise _$RoutineTemplateExerciseFromJson( + Map json) { + return _RoutineTemplateExercise.fromJson(json); +} + +/// @nodoc +mixin _$RoutineTemplateExercise { + String get exerciseTemplateId => throw _privateConstructorUsedError; + List get sets => throw _privateConstructorUsedError; + int? get supersetId => throw _privateConstructorUsedError; + int? get restSeconds => throw _privateConstructorUsedError; + String? get notes => throw _privateConstructorUsedError; + + /// Serializes this RoutineTemplateExercise to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of RoutineTemplateExercise + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RoutineTemplateExerciseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RoutineTemplateExerciseCopyWith<$Res> { + factory $RoutineTemplateExerciseCopyWith(RoutineTemplateExercise value, + $Res Function(RoutineTemplateExercise) then) = + _$RoutineTemplateExerciseCopyWithImpl<$Res, RoutineTemplateExercise>; + @useResult + $Res call( + {String exerciseTemplateId, + List sets, + int? supersetId, + int? restSeconds, + String? notes}); +} + +/// @nodoc +class _$RoutineTemplateExerciseCopyWithImpl<$Res, + $Val extends RoutineTemplateExercise> + implements $RoutineTemplateExerciseCopyWith<$Res> { + _$RoutineTemplateExerciseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of RoutineTemplateExercise + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? exerciseTemplateId = null, + Object? sets = null, + Object? supersetId = freezed, + Object? restSeconds = freezed, + Object? notes = freezed, + }) { + return _then(_value.copyWith( + exerciseTemplateId: null == exerciseTemplateId + ? _value.exerciseTemplateId + : exerciseTemplateId // ignore: cast_nullable_to_non_nullable + as String, + sets: null == sets + ? _value.sets + : sets // ignore: cast_nullable_to_non_nullable + as List, + supersetId: freezed == supersetId + ? _value.supersetId + : supersetId // ignore: cast_nullable_to_non_nullable + as int?, + restSeconds: freezed == restSeconds + ? _value.restSeconds + : restSeconds // ignore: cast_nullable_to_non_nullable + as int?, + notes: freezed == notes + ? _value.notes + : notes // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$RoutineTemplateExerciseImplCopyWith<$Res> + implements $RoutineTemplateExerciseCopyWith<$Res> { + factory _$$RoutineTemplateExerciseImplCopyWith( + _$RoutineTemplateExerciseImpl value, + $Res Function(_$RoutineTemplateExerciseImpl) then) = + __$$RoutineTemplateExerciseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String exerciseTemplateId, + List sets, + int? supersetId, + int? restSeconds, + String? notes}); +} + +/// @nodoc +class __$$RoutineTemplateExerciseImplCopyWithImpl<$Res> + extends _$RoutineTemplateExerciseCopyWithImpl<$Res, + _$RoutineTemplateExerciseImpl> + implements _$$RoutineTemplateExerciseImplCopyWith<$Res> { + __$$RoutineTemplateExerciseImplCopyWithImpl( + _$RoutineTemplateExerciseImpl _value, + $Res Function(_$RoutineTemplateExerciseImpl) _then) + : super(_value, _then); + + /// Create a copy of RoutineTemplateExercise + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? exerciseTemplateId = null, + Object? sets = null, + Object? supersetId = freezed, + Object? restSeconds = freezed, + Object? notes = freezed, + }) { + return _then(_$RoutineTemplateExerciseImpl( + exerciseTemplateId: null == exerciseTemplateId + ? _value.exerciseTemplateId + : exerciseTemplateId // ignore: cast_nullable_to_non_nullable + as String, + sets: null == sets + ? _value._sets + : sets // ignore: cast_nullable_to_non_nullable + as List, + supersetId: freezed == supersetId + ? _value.supersetId + : supersetId // ignore: cast_nullable_to_non_nullable + as int?, + restSeconds: freezed == restSeconds + ? _value.restSeconds + : restSeconds // ignore: cast_nullable_to_non_nullable + as int?, + notes: freezed == notes + ? _value.notes + : notes // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$RoutineTemplateExerciseImpl implements _RoutineTemplateExercise { + const _$RoutineTemplateExerciseImpl( + {required this.exerciseTemplateId, + required final List sets, + this.supersetId, + this.restSeconds, + this.notes}) + : _sets = sets; + + factory _$RoutineTemplateExerciseImpl.fromJson(Map json) => + _$$RoutineTemplateExerciseImplFromJson(json); + + @override + final String exerciseTemplateId; + final List _sets; + @override + List get sets { + if (_sets is EqualUnmodifiableListView) return _sets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_sets); + } + + @override + final int? supersetId; + @override + final int? restSeconds; + @override + final String? notes; + + @override + String toString() { + return 'RoutineTemplateExercise(exerciseTemplateId: $exerciseTemplateId, sets: $sets, supersetId: $supersetId, restSeconds: $restSeconds, notes: $notes)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RoutineTemplateExerciseImpl && + (identical(other.exerciseTemplateId, exerciseTemplateId) || + other.exerciseTemplateId == exerciseTemplateId) && + const DeepCollectionEquality().equals(other._sets, _sets) && + (identical(other.supersetId, supersetId) || + other.supersetId == supersetId) && + (identical(other.restSeconds, restSeconds) || + other.restSeconds == restSeconds) && + (identical(other.notes, notes) || other.notes == notes)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + exerciseTemplateId, + const DeepCollectionEquality().hash(_sets), + supersetId, + restSeconds, + notes); + + /// Create a copy of RoutineTemplateExercise + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RoutineTemplateExerciseImplCopyWith<_$RoutineTemplateExerciseImpl> + get copyWith => __$$RoutineTemplateExerciseImplCopyWithImpl< + _$RoutineTemplateExerciseImpl>(this, _$identity); + + @override + Map toJson() { + return _$$RoutineTemplateExerciseImplToJson( + this, + ); + } +} + +abstract class _RoutineTemplateExercise implements RoutineTemplateExercise { + const factory _RoutineTemplateExercise( + {required final String exerciseTemplateId, + required final List sets, + final int? supersetId, + final int? restSeconds, + final String? notes}) = _$RoutineTemplateExerciseImpl; + + factory _RoutineTemplateExercise.fromJson(Map json) = + _$RoutineTemplateExerciseImpl.fromJson; + + @override + String get exerciseTemplateId; + @override + List get sets; + @override + int? get supersetId; + @override + int? get restSeconds; + @override + String? get notes; + + /// Create a copy of RoutineTemplateExercise + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RoutineTemplateExerciseImplCopyWith<_$RoutineTemplateExerciseImpl> + get copyWith => throw _privateConstructorUsedError; +} + +RoutineTemplateSet _$RoutineTemplateSetFromJson(Map json) { + return _RoutineTemplateSet.fromJson(json); +} + +/// @nodoc +mixin _$RoutineTemplateSet { + String get type => throw _privateConstructorUsedError; + num? get weightKg => throw _privateConstructorUsedError; + int? get reps => throw _privateConstructorUsedError; + int? get distanceMeters => throw _privateConstructorUsedError; + int? get durationSeconds => throw _privateConstructorUsedError; + + /// Serializes this RoutineTemplateSet to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of RoutineTemplateSet + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RoutineTemplateSetCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RoutineTemplateSetCopyWith<$Res> { + factory $RoutineTemplateSetCopyWith( + RoutineTemplateSet value, $Res Function(RoutineTemplateSet) then) = + _$RoutineTemplateSetCopyWithImpl<$Res, RoutineTemplateSet>; + @useResult + $Res call( + {String type, + num? weightKg, + int? reps, + int? distanceMeters, + int? durationSeconds}); +} + +/// @nodoc +class _$RoutineTemplateSetCopyWithImpl<$Res, $Val extends RoutineTemplateSet> + implements $RoutineTemplateSetCopyWith<$Res> { + _$RoutineTemplateSetCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of RoutineTemplateSet + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? weightKg = freezed, + Object? reps = freezed, + Object? distanceMeters = freezed, + Object? durationSeconds = freezed, + }) { + return _then(_value.copyWith( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String, + weightKg: freezed == weightKg + ? _value.weightKg + : weightKg // ignore: cast_nullable_to_non_nullable + as num?, + reps: freezed == reps + ? _value.reps + : reps // ignore: cast_nullable_to_non_nullable + as int?, + distanceMeters: freezed == distanceMeters + ? _value.distanceMeters + : distanceMeters // ignore: cast_nullable_to_non_nullable + as int?, + durationSeconds: freezed == durationSeconds + ? _value.durationSeconds + : durationSeconds // ignore: cast_nullable_to_non_nullable + as int?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$RoutineTemplateSetImplCopyWith<$Res> + implements $RoutineTemplateSetCopyWith<$Res> { + factory _$$RoutineTemplateSetImplCopyWith(_$RoutineTemplateSetImpl value, + $Res Function(_$RoutineTemplateSetImpl) then) = + __$$RoutineTemplateSetImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String type, + num? weightKg, + int? reps, + int? distanceMeters, + int? durationSeconds}); +} + +/// @nodoc +class __$$RoutineTemplateSetImplCopyWithImpl<$Res> + extends _$RoutineTemplateSetCopyWithImpl<$Res, _$RoutineTemplateSetImpl> + implements _$$RoutineTemplateSetImplCopyWith<$Res> { + __$$RoutineTemplateSetImplCopyWithImpl(_$RoutineTemplateSetImpl _value, + $Res Function(_$RoutineTemplateSetImpl) _then) + : super(_value, _then); + + /// Create a copy of RoutineTemplateSet + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? weightKg = freezed, + Object? reps = freezed, + Object? distanceMeters = freezed, + Object? durationSeconds = freezed, + }) { + return _then(_$RoutineTemplateSetImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String, + weightKg: freezed == weightKg + ? _value.weightKg + : weightKg // ignore: cast_nullable_to_non_nullable + as num?, + reps: freezed == reps + ? _value.reps + : reps // ignore: cast_nullable_to_non_nullable + as int?, + distanceMeters: freezed == distanceMeters + ? _value.distanceMeters + : distanceMeters // ignore: cast_nullable_to_non_nullable + as int?, + durationSeconds: freezed == durationSeconds + ? _value.durationSeconds + : durationSeconds // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$RoutineTemplateSetImpl implements _RoutineTemplateSet { + const _$RoutineTemplateSetImpl( + {required this.type, + this.weightKg, + this.reps, + this.distanceMeters, + this.durationSeconds}); + + factory _$RoutineTemplateSetImpl.fromJson(Map json) => + _$$RoutineTemplateSetImplFromJson(json); + + @override + final String type; + @override + final num? weightKg; + @override + final int? reps; + @override + final int? distanceMeters; + @override + final int? durationSeconds; + + @override + String toString() { + return 'RoutineTemplateSet._(type: $type, weightKg: $weightKg, reps: $reps, distanceMeters: $distanceMeters, durationSeconds: $durationSeconds)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RoutineTemplateSetImpl && + (identical(other.type, type) || other.type == type) && + (identical(other.weightKg, weightKg) || + other.weightKg == weightKg) && + (identical(other.reps, reps) || other.reps == reps) && + (identical(other.distanceMeters, distanceMeters) || + other.distanceMeters == distanceMeters) && + (identical(other.durationSeconds, durationSeconds) || + other.durationSeconds == durationSeconds)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, type, weightKg, reps, distanceMeters, durationSeconds); + + /// Create a copy of RoutineTemplateSet + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RoutineTemplateSetImplCopyWith<_$RoutineTemplateSetImpl> get copyWith => + __$$RoutineTemplateSetImplCopyWithImpl<_$RoutineTemplateSetImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$RoutineTemplateSetImplToJson( + this, + ); + } +} + +abstract class _RoutineTemplateSet implements RoutineTemplateSet { + const factory _RoutineTemplateSet( + {required final String type, + final num? weightKg, + final int? reps, + final int? distanceMeters, + final int? durationSeconds}) = _$RoutineTemplateSetImpl; + + factory _RoutineTemplateSet.fromJson(Map json) = + _$RoutineTemplateSetImpl.fromJson; + + @override + String get type; + @override + num? get weightKg; + @override + int? get reps; + @override + int? get distanceMeters; + @override + int? get durationSeconds; + + /// Create a copy of RoutineTemplateSet + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RoutineTemplateSetImplCopyWith<_$RoutineTemplateSetImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/examples/hevy_smolov_jr/lib/api/wrapped_hevy_api.g.dart b/examples/hevy_smolov_jr/lib/api/wrapped_hevy_api.g.dart new file mode 100644 index 0000000..c0f1243 --- /dev/null +++ b/examples/hevy_smolov_jr/lib/api/wrapped_hevy_api.g.dart @@ -0,0 +1,114 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: require_trailing_commas + +part of 'wrapped_hevy_api.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ExerciseTemplatesResponseImpl _$$ExerciseTemplatesResponseImplFromJson( + Map json) => + _$ExerciseTemplatesResponseImpl( + pageCount: (json['page_count'] as num).toInt(), + page: (json['page'] as num).toInt(), + exerciseTemplates: (json['exercise_templates'] as List) + .map((e) => Exercise.fromJson(e as Map)) + .toList(), + ); + +Map _$$ExerciseTemplatesResponseImplToJson( + _$ExerciseTemplatesResponseImpl instance) => + { + 'page_count': instance.pageCount, + 'page': instance.page, + 'exercise_templates': instance.exerciseTemplates, + }; + +_$ExerciseImpl _$$ExerciseImplFromJson(Map json) => + _$ExerciseImpl( + id: json['id'] as String, + title: json['title'] as String, + type: json['type'] as String, + primaryMuscleGroup: json['primary_muscle_group'] as String, + secondaryMuscleGroups: (json['secondary_muscle_groups'] as List) + .map((e) => e as String) + .toList(), + equipment: json['equipment'] as String, + isCustom: json['is_custom'] as bool, + ); + +Map _$$ExerciseImplToJson(_$ExerciseImpl instance) => + { + 'id': instance.id, + 'title': instance.title, + 'type': instance.type, + 'primary_muscle_group': instance.primaryMuscleGroup, + 'secondary_muscle_groups': instance.secondaryMuscleGroups, + 'equipment': instance.equipment, + 'is_custom': instance.isCustom, + }; + +_$RoutineTemplateImpl _$$RoutineTemplateImplFromJson( + Map json) => + _$RoutineTemplateImpl( + title: json['title'] as String, + exercises: (json['exercises'] as List) + .map((e) => + RoutineTemplateExercise.fromJson(e as Map)) + .toList(), + notes: json['notes'] as String?, + folderId: (json['folder_id'] as num?)?.toInt(), + ); + +Map _$$RoutineTemplateImplToJson( + _$RoutineTemplateImpl instance) => + { + 'title': instance.title, + 'exercises': instance.exercises, + 'notes': instance.notes, + 'folder_id': instance.folderId, + }; + +_$RoutineTemplateExerciseImpl _$$RoutineTemplateExerciseImplFromJson( + Map json) => + _$RoutineTemplateExerciseImpl( + exerciseTemplateId: json['exercise_template_id'] as String, + sets: (json['sets'] as List) + .map((e) => RoutineTemplateSet.fromJson(e as Map)) + .toList(), + supersetId: (json['superset_id'] as num?)?.toInt(), + restSeconds: (json['rest_seconds'] as num?)?.toInt(), + notes: json['notes'] as String?, + ); + +Map _$$RoutineTemplateExerciseImplToJson( + _$RoutineTemplateExerciseImpl instance) => + { + 'exercise_template_id': instance.exerciseTemplateId, + 'sets': instance.sets, + 'superset_id': instance.supersetId, + 'rest_seconds': instance.restSeconds, + 'notes': instance.notes, + }; + +_$RoutineTemplateSetImpl _$$RoutineTemplateSetImplFromJson( + Map json) => + _$RoutineTemplateSetImpl( + type: json['type'] as String, + weightKg: json['weight_kg'] as num?, + reps: (json['reps'] as num?)?.toInt(), + distanceMeters: (json['distance_meters'] as num?)?.toInt(), + durationSeconds: (json['duration_seconds'] as num?)?.toInt(), + ); + +Map _$$RoutineTemplateSetImplToJson( + _$RoutineTemplateSetImpl instance) => + { + 'type': instance.type, + 'weight_kg': instance.weightKg, + 'reps': instance.reps, + 'distance_meters': instance.distanceMeters, + 'duration_seconds': instance.durationSeconds, + }; diff --git a/examples/hevy_smolov_jr/lib/main.dart b/examples/hevy_smolov_jr/lib/main.dart new file mode 100644 index 0000000..34948a8 --- /dev/null +++ b/examples/hevy_smolov_jr/lib/main.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rearch/flutter_rearch.dart'; +import 'package:hevy_smolov_jr/shared_prefs.dart'; +import 'package:hevy_smolov_jr/smolov_jr_config/config.dart'; +import 'package:hevy_smolov_jr/steps/api_key.dart'; +import 'package:hevy_smolov_jr/steps/confirmation_creation.dart'; +import 'package:hevy_smolov_jr/steps/exercise_selection.dart'; +import 'package:hevy_smolov_jr/steps/intro.dart'; +import 'package:hevy_smolov_jr/steps/program_config.dart'; +import 'package:rearch/rearch.dart'; + +void main() { + runApp(const HevySmolovJrApp()); +} + +/// Represents the root of the application. +class HevySmolovJrApp extends StatelessWidget { + /// Represents the root of the application. + const HevySmolovJrApp({super.key}); + + @override + Widget build(BuildContext context) { + return RearchBootstrapper( + child: MaterialApp( + title: 'Smolov Jr Calculator for Hevy', + theme: ThemeData(colorSchemeSeed: Colors.deepPurpleAccent), + home: const SharedPrefsWarmUp(child: HomePage()), + ), + ); + } +} + +/// Displays the home page of the application. +class HomePage extends RearchConsumer { + /// Displays the home page of the application. + const HomePage({super.key}); + + @override + Widget build(BuildContext context, WidgetHandle use) { + final currStep = use.data(0); + final steps = [ + Step( + title: const Text('Introduction'), + content: const IntroStep(), + isActive: currStep.value == 0, + ), + Step( + title: const Text('API Key'), + content: const ApiKeyInputStep(), + isActive: currStep.value == 1, + ), + Step( + title: const Text('Exercise Selection'), + subtitle: const Text('More exercise options coming soon!'), + content: const ExerciseSelectionStep(), + isActive: currStep.value == 2, + ), + Step( + title: const Text('Program Configuration'), + content: const ProgramConfigInputStep(), + isActive: currStep.value == 3, + ), + Step( + title: const Text('Confirmation & Creation'), + content: const ConfirmationAndCreationStep(), + isActive: currStep.value == 4, + ), + ]; + final isFirstStep = currStep.value == 0; + final isLastStep = currStep.value == steps.length - 1; + + return Scaffold( + body: ScopedSmolovJrConfig( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: Stepper( + steps: steps, + currentStep: currStep.value, + onStepTapped: currStep.$2, + onStepCancel: isFirstStep ? null : () => currStep.value--, + onStepContinue: isLastStep ? null : () => currStep.value++, + controlsBuilder: (context, details) { + final theme = Theme.of(context); + return ElevatedButtonTheme( + data: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + ), + ), + child: Padding( + padding: const EdgeInsets.only(top: 32), + child: Row( + children: [ + if (!isFirstStep) + TextButton( + onPressed: details.onStepCancel, + child: const Text('Go Back'), + ), + const SizedBox(width: 16), + if (!isLastStep) + ElevatedButton.icon( + onPressed: details.onStepContinue, + label: const Text('Continue'), + ), + if (isLastStep) const SaveProgramButton(), + ], + ), + ), + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/examples/hevy_smolov_jr/lib/shared_prefs.dart b/examples/hevy_smolov_jr/lib/shared_prefs.dart new file mode 100644 index 0000000..3ce5608 --- /dev/null +++ b/examples/hevy_smolov_jr/lib/shared_prefs.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rearch/flutter_rearch.dart'; +import 'package:rearch/experimental.dart'; +import 'package:rearch/rearch.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// The underlying [Capsule] providing the app's [SharedPreferencesWithCache]. +final Capsule> sharedPrefsAsyncCapsule = + capsule((use) { + return SharedPreferencesWithCache.create( + cacheOptions: const SharedPreferencesWithCacheOptions(), + ); +}); + +/// A wrapper around the [sharedPrefsAsyncCapsule] +/// to be warmed up by the [SharedPrefsWarmUp]. +final Capsule> sharedPrefsWarmUpCapsule = + capsule((use) { + return use.future(use(sharedPrefsAsyncCapsule)); +}); + +///The [Capsule] providing the [SharedPreferencesWithCache] to other [Capsule]s. +final Capsule sharedPrefsCapsule = capsule((use) { + return use(sharedPrefsWarmUpCapsule).dataOrElse( + () => throw StateError('sharedPrefsWarmUpCapsule not warmed up'), + ); +}); + +/// Warms up the [sharedPrefsCapsule]. +class SharedPrefsWarmUp extends RearchConsumer { + /// Warms up the [sharedPrefsCapsule]. + const SharedPrefsWarmUp({required this.child, super.key}); + + /// The child of this [SharedPrefsWarmUp] when [sharedPrefsCapsule] + /// has been warmed up. + final Widget child; + + @override + Widget build(BuildContext context, WidgetHandle use) { + return [use(sharedPrefsWarmUpCapsule)].toWarmUpWidget( + child: child, + loading: const Scaffold(body: Center(child: CircularProgressIndicator())), + errorBuilder: (List> errors) { + return Scaffold( + body: Column( + children: [ + const Text('Failed to load application!'), + for (final error in errors) ...[ + Text(error.error.toString()), + ], + ], + ), + ); + }, + ); + } +} diff --git a/examples/hevy_smolov_jr/lib/smolov_jr_config/config.dart b/examples/hevy_smolov_jr/lib/smolov_jr_config/config.dart new file mode 100644 index 0000000..0994a8f --- /dev/null +++ b/examples/hevy_smolov_jr/lib/smolov_jr_config/config.dart @@ -0,0 +1,160 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_rearch/flutter_rearch.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hevy_smolov_jr/api/wrapped_hevy_api.dart'; +import 'package:rearch/rearch.dart'; + +part 'config.freezed.dart'; + +/// Represents the valid weight units. +enum WeightUnit { + /// The pound weight unit. + lb, + + /// The kilogram weight unit. + kg, +} + +extension on WeightUnit { + num toKg(num amount) { + return switch (this) { + WeightUnit.lb => amount * 0.45359237, + WeightUnit.kg => amount, + }; + } +} + +/// Represents the necessary config for the Smolov Jr program. +@freezed +class SmolovJrConfig with _$SmolovJrConfig { + /// Creates a [SmolovJrConfig]. + const factory SmolovJrConfig({ + /// 1rm _without_ bodyweight included, in case this is a body weight lift + required Exercise? exercise, + required num oneRepMax, + required num bodyWeight, + required int? restSeconds, + required num increment, + required WeightUnit unit, + }) = _SmolovJrConfig; + + /// Creates the initial [SmolovJrConfig]. + factory SmolovJrConfig.initial() { + return const SmolovJrConfig( + exercise: null, + oneRepMax: 100, + bodyWeight: 100, + restSeconds: 3 * 60, + increment: 5, + unit: WeightUnit.lb, + ); + } +} + +/// Provides several convenience functions on the [SmolovJrConfig]. +extension SmolovJrConfigConvenience on SmolovJrConfig { + /// If [exercise] is set, and it's a bodyweight-based exercise, returns true, + /// false otherwise. + bool get isBodyWeight => + exercise != null && exercise!.type.contains('bodyweight'); + + /// Creates a Smolov Jr for Hevy using this [SmolovJrConfig]. + ({String programName, List routines}) toProgram() { + if (exercise == null) throw StateError('No exercise selected'); + final totalOneRepMax = isBodyWeight ? (bodyWeight + oneRepMax) : oneRepMax; + + final smolovJrDays = [ + (day: 'Monday', sets: 6, reps: 6, starting1rmPercent: 0.70), + (day: 'Wednesday', sets: 7, reps: 5, starting1rmPercent: 0.75), + (day: 'Friday', sets: 8, reps: 4, starting1rmPercent: 0.80), + (day: 'Saturday', sets: 10, reps: 3, starting1rmPercent: 0.85), + ]; + + final routines = []; + for (final week in Iterable.generate(3, (i) => i + 1)) { + for (final (:day, :sets, :reps, :starting1rmPercent) in smolovJrDays) { + final weightIncrement = (week - 1) * increment; + final totalWeight = + starting1rmPercent * totalOneRepMax + weightIncrement; + final weight = isBodyWeight ? (totalWeight - bodyWeight) : totalWeight; + + routines.add( + RoutineTemplate( + title: 'Week $week — $day', + notes: '${sets}x$reps @ ${weight.toStringAsFixed(2)} ${unit.name}', + exercises: [ + RoutineTemplateExercise( + exerciseTemplateId: exercise!.id, + restSeconds: restSeconds, + notes: isBodyWeight + ? 'Calculated weight assumes a body weight of $bodyWeight; ' + 'adjust your set weight up/down accordingly.' + : null, + sets: List.generate( + sets, + (_) => RoutineTemplateSet.normal( + reps: reps, + weightKg: unit.toKg(weight).toDouble(), + ), + ), + ), + ], + ), + ); + } + } + + return ( + programName: 'Smolov Jr for ${exercise!.title}', + routines: routines, + ); + } +} + +/// Provides the current [SmolovJrConfig] to descendants in the [Widget] tree. +ValueWrapper scopedSmolovJrConfig(WidgetHandle use) { + return use.lazyData(SmolovJrConfig.initial); +} + +// NOTE: we really need macros... +// ignore: public_member_api_docs +class ScopedSmolovJrConfig extends RearchConsumer { + // ignore: public_member_api_docs + const ScopedSmolovJrConfig({required this.child, super.key}); + // ignore: public_member_api_docs + final Widget child; + + @override + Widget build(BuildContext context, WidgetHandle use) { + return _ScopedSmolovJrConfig( + scopedSmolovJrConfig(use), + child: child, + ); + } + + // ignore: public_member_api_docs + static ValueWrapper? maybeOf(BuildContext context) => + _ScopedSmolovJrConfig._maybeOf(context)?._data; + + // ignore: public_member_api_docs + static ValueWrapper of(BuildContext context) => + _ScopedSmolovJrConfig._of(context)._data; +} + +class _ScopedSmolovJrConfig extends InheritedWidget { + const _ScopedSmolovJrConfig(this._data, {required super.child}); + final ValueWrapper _data; + + @override + bool updateShouldNotify(_ScopedSmolovJrConfig oldWidget) => + oldWidget._data != _data; + + static _ScopedSmolovJrConfig? _maybeOf(BuildContext context) => + context.dependOnInheritedWidgetOfExactType<_ScopedSmolovJrConfig>(); + + static _ScopedSmolovJrConfig _of(BuildContext context) { + final widget = _maybeOf(context); + assert(widget != null, 'No ScopedSmolovJrConfig found in context'); + return widget!; + } +} diff --git a/examples/hevy_smolov_jr/lib/smolov_jr_config/config.freezed.dart b/examples/hevy_smolov_jr/lib/smolov_jr_config/config.freezed.dart new file mode 100644 index 0000000..fc3ec13 --- /dev/null +++ b/examples/hevy_smolov_jr/lib/smolov_jr_config/config.freezed.dart @@ -0,0 +1,276 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'config.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$SmolovJrConfig { + /// 1rm _without_ bodyweight included, in case this is a body weight lift + Exercise? get exercise => throw _privateConstructorUsedError; + num get oneRepMax => throw _privateConstructorUsedError; + num get bodyWeight => throw _privateConstructorUsedError; + int? get restSeconds => throw _privateConstructorUsedError; + num get increment => throw _privateConstructorUsedError; + WeightUnit get unit => throw _privateConstructorUsedError; + + /// Create a copy of SmolovJrConfig + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SmolovJrConfigCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SmolovJrConfigCopyWith<$Res> { + factory $SmolovJrConfigCopyWith( + SmolovJrConfig value, $Res Function(SmolovJrConfig) then) = + _$SmolovJrConfigCopyWithImpl<$Res, SmolovJrConfig>; + @useResult + $Res call( + {Exercise? exercise, + num oneRepMax, + num bodyWeight, + int? restSeconds, + num increment, + WeightUnit unit}); + + $ExerciseCopyWith<$Res>? get exercise; +} + +/// @nodoc +class _$SmolovJrConfigCopyWithImpl<$Res, $Val extends SmolovJrConfig> + implements $SmolovJrConfigCopyWith<$Res> { + _$SmolovJrConfigCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SmolovJrConfig + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? exercise = freezed, + Object? oneRepMax = null, + Object? bodyWeight = null, + Object? restSeconds = freezed, + Object? increment = null, + Object? unit = null, + }) { + return _then(_value.copyWith( + exercise: freezed == exercise + ? _value.exercise + : exercise // ignore: cast_nullable_to_non_nullable + as Exercise?, + oneRepMax: null == oneRepMax + ? _value.oneRepMax + : oneRepMax // ignore: cast_nullable_to_non_nullable + as num, + bodyWeight: null == bodyWeight + ? _value.bodyWeight + : bodyWeight // ignore: cast_nullable_to_non_nullable + as num, + restSeconds: freezed == restSeconds + ? _value.restSeconds + : restSeconds // ignore: cast_nullable_to_non_nullable + as int?, + increment: null == increment + ? _value.increment + : increment // ignore: cast_nullable_to_non_nullable + as num, + unit: null == unit + ? _value.unit + : unit // ignore: cast_nullable_to_non_nullable + as WeightUnit, + ) as $Val); + } + + /// Create a copy of SmolovJrConfig + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ExerciseCopyWith<$Res>? get exercise { + if (_value.exercise == null) { + return null; + } + + return $ExerciseCopyWith<$Res>(_value.exercise!, (value) { + return _then(_value.copyWith(exercise: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$SmolovJrConfigImplCopyWith<$Res> + implements $SmolovJrConfigCopyWith<$Res> { + factory _$$SmolovJrConfigImplCopyWith(_$SmolovJrConfigImpl value, + $Res Function(_$SmolovJrConfigImpl) then) = + __$$SmolovJrConfigImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {Exercise? exercise, + num oneRepMax, + num bodyWeight, + int? restSeconds, + num increment, + WeightUnit unit}); + + @override + $ExerciseCopyWith<$Res>? get exercise; +} + +/// @nodoc +class __$$SmolovJrConfigImplCopyWithImpl<$Res> + extends _$SmolovJrConfigCopyWithImpl<$Res, _$SmolovJrConfigImpl> + implements _$$SmolovJrConfigImplCopyWith<$Res> { + __$$SmolovJrConfigImplCopyWithImpl( + _$SmolovJrConfigImpl _value, $Res Function(_$SmolovJrConfigImpl) _then) + : super(_value, _then); + + /// Create a copy of SmolovJrConfig + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? exercise = freezed, + Object? oneRepMax = null, + Object? bodyWeight = null, + Object? restSeconds = freezed, + Object? increment = null, + Object? unit = null, + }) { + return _then(_$SmolovJrConfigImpl( + exercise: freezed == exercise + ? _value.exercise + : exercise // ignore: cast_nullable_to_non_nullable + as Exercise?, + oneRepMax: null == oneRepMax + ? _value.oneRepMax + : oneRepMax // ignore: cast_nullable_to_non_nullable + as num, + bodyWeight: null == bodyWeight + ? _value.bodyWeight + : bodyWeight // ignore: cast_nullable_to_non_nullable + as num, + restSeconds: freezed == restSeconds + ? _value.restSeconds + : restSeconds // ignore: cast_nullable_to_non_nullable + as int?, + increment: null == increment + ? _value.increment + : increment // ignore: cast_nullable_to_non_nullable + as num, + unit: null == unit + ? _value.unit + : unit // ignore: cast_nullable_to_non_nullable + as WeightUnit, + )); + } +} + +/// @nodoc + +class _$SmolovJrConfigImpl implements _SmolovJrConfig { + const _$SmolovJrConfigImpl( + {required this.exercise, + required this.oneRepMax, + required this.bodyWeight, + required this.restSeconds, + required this.increment, + required this.unit}); + + /// 1rm _without_ bodyweight included, in case this is a body weight lift + @override + final Exercise? exercise; + @override + final num oneRepMax; + @override + final num bodyWeight; + @override + final int? restSeconds; + @override + final num increment; + @override + final WeightUnit unit; + + @override + String toString() { + return 'SmolovJrConfig(exercise: $exercise, oneRepMax: $oneRepMax, bodyWeight: $bodyWeight, restSeconds: $restSeconds, increment: $increment, unit: $unit)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SmolovJrConfigImpl && + (identical(other.exercise, exercise) || + other.exercise == exercise) && + (identical(other.oneRepMax, oneRepMax) || + other.oneRepMax == oneRepMax) && + (identical(other.bodyWeight, bodyWeight) || + other.bodyWeight == bodyWeight) && + (identical(other.restSeconds, restSeconds) || + other.restSeconds == restSeconds) && + (identical(other.increment, increment) || + other.increment == increment) && + (identical(other.unit, unit) || other.unit == unit)); + } + + @override + int get hashCode => Object.hash(runtimeType, exercise, oneRepMax, bodyWeight, + restSeconds, increment, unit); + + /// Create a copy of SmolovJrConfig + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SmolovJrConfigImplCopyWith<_$SmolovJrConfigImpl> get copyWith => + __$$SmolovJrConfigImplCopyWithImpl<_$SmolovJrConfigImpl>( + this, _$identity); +} + +abstract class _SmolovJrConfig implements SmolovJrConfig { + const factory _SmolovJrConfig( + {required final Exercise? exercise, + required final num oneRepMax, + required final num bodyWeight, + required final int? restSeconds, + required final num increment, + required final WeightUnit unit}) = _$SmolovJrConfigImpl; + + /// 1rm _without_ bodyweight included, in case this is a body weight lift + @override + Exercise? get exercise; + @override + num get oneRepMax; + @override + num get bodyWeight; + @override + int? get restSeconds; + @override + num get increment; + @override + WeightUnit get unit; + + /// Create a copy of SmolovJrConfig + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SmolovJrConfigImplCopyWith<_$SmolovJrConfigImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/examples/hevy_smolov_jr/lib/steps/api_key.dart b/examples/hevy_smolov_jr/lib/steps/api_key.dart new file mode 100644 index 0000000..be710e8 --- /dev/null +++ b/examples/hevy_smolov_jr/lib/steps/api_key.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rearch/flutter_rearch.dart'; +import 'package:hevy_smolov_jr/api/raw_hevy_api.dart'; +import 'package:hevy_smolov_jr/widgets/warning_card.dart'; +import 'package:url_launcher/url_launcher.dart'; + +Future _openHevySettings() { + return launchUrl( + Uri.parse('https://hevy.com/settings?developer'), + mode: LaunchMode.externalApplication, + ); +} + +Future _openSourceCode() { + return launchUrl( + Uri.parse( + 'https://github.com/GregoryConrad/rearch-dart/tree/main/examples/hevy_smolov_jr', + ), + mode: LaunchMode.externalApplication, + ); +} + +/// The Hevy API key input step that requests the user's API key. +class ApiKeyInputStep extends RearchConsumer { + /// The Hevy API key input step that requests the user's API key. + const ApiKeyInputStep({super.key}); + + @override + Widget build(BuildContext context, WidgetHandle use) { + final (apiKey, setApiKey) = use(apiKeyCapsule); + return Column( + children: [ + const Text('Please input your Hevy API key below.'), + const SizedBox(height: 16), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: TextFormField( + initialValue: apiKey, + onChanged: setApiKey, + decoration: const InputDecoration( + hintText: 'Hevy API Key', + suffixIcon: IconButton( + icon: Icon(Icons.help_rounded), + onPressed: _openHevySettings, + ), + ), + ), + ), + const SizedBox(height: 32), + const WarningCard( + title: 'Be careful with whom you give your API key!', + details: + 'Someone can use your API key to act on your behalf in Hevy. ' + 'In the interest of transparency, ' + 'click here to view the source code of this tool.', + onPressed: _openSourceCode, + ), + ], + ); + } +} diff --git a/examples/hevy_smolov_jr/lib/steps/confirmation_creation.dart b/examples/hevy_smolov_jr/lib/steps/confirmation_creation.dart new file mode 100644 index 0000000..a1abf34 --- /dev/null +++ b/examples/hevy_smolov_jr/lib/steps/confirmation_creation.dart @@ -0,0 +1,86 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_rearch/flutter_rearch.dart'; +import 'package:hevy_smolov_jr/api/wrapped_hevy_api.dart'; +import 'package:hevy_smolov_jr/smolov_jr_config/config.dart'; +import 'package:rearch/rearch.dart'; + +/// Displays the confirmation and creation step contents. +class ConfirmationAndCreationStep extends StatelessWidget { + /// Displays the confirmation and creation step contents. + const ConfirmationAndCreationStep({super.key}); + + @override + Widget build(BuildContext context) { + final smolovJrConfig = ScopedSmolovJrConfig.of(context).value; + if (smolovJrConfig.exercise == null) { + return const Text('Select an exercise in the previous step to continue'); + } + final (:programName, :routines) = smolovJrConfig.toProgram(); + return Column( + children: [ + Text(programName, style: Theme.of(context).textTheme.titleMedium), + for (final routine in routines) + Builder( + builder: (context) { + var routineText = routine.title; + if (routine.notes != null) routineText += ' (${routine.notes})'; + return Text(routineText); + }, + ), + ], + ); + } +} + +/// Displays an animated button that saves the current [SmolovJrConfig] +/// to the user's Hevy account. +class SaveProgramButton extends RearchConsumer { + /// Displays an animated button that saves the current [SmolovJrConfig] + /// to the user's Hevy account. + const SaveProgramButton({super.key}); + + @override + Widget build(BuildContext context, WidgetHandle use) { + final (:mutate, :state, clear: _) = use.mutation(); + final config = ScopedSmolovJrConfig.of(context).value; + final rawCreateProgram = use(createProgramAction); + final createProgram = use.memo( + () { + if (config.exercise == null) return null; + final (:programName, :routines) = config.toProgram(); + return () => mutate( + rawCreateProgram(programName: programName, routines: routines), + ); + }, + [config, rawCreateProgram], + ); + if (state is AsyncError) { + log('Error saving program: ${state.error}\n${state.stackTrace}'); + } + + return switch (state) { + AsyncLoading() => ElevatedButton.icon( + icon: const CircularProgressIndicator(), + onPressed: null, + label: const Text('Save Program'), + ), + AsyncError() => ElevatedButton.icon( + icon: const Icon(Icons.error), + onPressed: createProgram, + label: const Text('Retry Save Program'), + ), + AsyncData() => ElevatedButton.icon( + icon: const Icon(Icons.check), + onPressed: createProgram, + label: const Text('Save Program'), + ), + null => ElevatedButton.icon( + icon: const Icon(Icons.save), + onPressed: createProgram, + label: const Text('Save Program'), + ), + }; + } +} diff --git a/examples/hevy_smolov_jr/lib/steps/exercise_selection.dart b/examples/hevy_smolov_jr/lib/steps/exercise_selection.dart new file mode 100644 index 0000000..5f28e96 --- /dev/null +++ b/examples/hevy_smolov_jr/lib/steps/exercise_selection.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rearch/flutter_rearch.dart'; +import 'package:hevy_smolov_jr/api/wrapped_hevy_api.dart'; +import 'package:hevy_smolov_jr/smolov_jr_config/config.dart'; +import 'package:rearch/experimental.dart'; +import 'package:rearch/rearch.dart'; + +/// Displays the exercise selection step contents. +// TODO(GregoryConrad): add exercise search feature +// - exercise search is based on levenshtein distance across words +// - remove the subtitle/note in the main.dart Steps list +class ExerciseSelectionStep extends RearchConsumer { + /// Displays the exercise selection step contents. + const ExerciseSelectionStep({super.key}); + + @override + Widget build(BuildContext context, WidgetHandle use) { + final exercisesFuture = use(_curatedExercisesCapsule); + final exercises = use.future(exercisesFuture); + return switch (exercises) { + AsyncLoading>() => + const Center(child: CircularProgressIndicator()), + AsyncError>(:final error) => Text( + '${error.runtimeType} encountered while loading your exercises; ' + 'try checking your API key and/or refreshing the page.\n' + '$error', + ), + AsyncData>(:final data) => + _CuratedExercisePicker(curatedExercises: data), + }; + } +} + +class _CuratedExercisePicker extends RearchConsumer { + const _CuratedExercisePicker({required this.curatedExercises}); + final List curatedExercises; + + @override + Widget build(BuildContext context, WidgetHandle use) { + final smolovJrConfig = ScopedSmolovJrConfig.of(context); + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 8, + children: [ + for (final exercise in curatedExercises) + ChoiceChip( + label: Text(exercise.title), + selected: smolovJrConfig.value.exercise == exercise, + onSelected: (selected) { + smolovJrConfig.value = + smolovJrConfig.value.copyWith(exercise: exercise); + }, + ), + ], + ), + ); + } +} + +final Capsule> _curatedExerciseNamesCapsule = capsule((use) { + return const [ + 'Squat (Barbell)', + 'Bench Press (Barbell)', + 'Deadlift (Barbell)', + 'Pull Up (Weighted)', + 'Chin Up (Weighted)', + ]; +}); + +/// Pre-selected exercises from the entire exercise list, +/// including SBD and Weighted Pull/Chin Up. +final Capsule>> _curatedExercisesCapsule = + capsule((use) async { + final curatedExerciseNames = use(_curatedExerciseNamesCapsule); + final exercises = await use(exercisesCapsule); + return curatedExerciseNames + .map((name) => exercises.where((e) => e.title == name).firstOrNull) + .whereType() + .toList(); +}); + +/// Provides a mechanism to search for exercises based on a provided query. +// final Capsule> Function(String query)> +// _searchExercisesAction = capsule((use) { +// final exercisesFuture = use(exercisesCapsule); +// return (query) async { +// return exercisesFuture; +// }; +// }); diff --git a/examples/hevy_smolov_jr/lib/steps/intro.dart b/examples/hevy_smolov_jr/lib/steps/intro.dart new file mode 100644 index 0000000..0e65232 --- /dev/null +++ b/examples/hevy_smolov_jr/lib/steps/intro.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:hevy_smolov_jr/widgets/warning_card.dart'; +import 'package:url_launcher/url_launcher.dart'; + +Future _openSmolovJrInfo() { + return launchUrl( + Uri.parse('https://www.smolovjr.com/smolov-squat-program/'), + mode: LaunchMode.externalApplication, + ); +} + +Future _openHevyProSubscription() { + return launchUrl( + Uri.parse('https://hevy.com/settings?subscription'), + mode: LaunchMode.externalApplication, + ); +} + +/// Displays the content of the Introduction step. +class IntroStep extends StatelessWidget { + /// Displays the content of the Introduction step. + const IntroStep({super.key}); + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Column( + children: [ + Text( + 'Welcome to the Smolov Jr to Hevy importer!', + style: textTheme.headlineSmall, + ), + const SizedBox(height: 16), + const Text( + 'Smolov Jr is a specialized program ' + 'that can put upwards of 20 lb/10 kg ' + 'onto a lift of your choice in only 3 weeks. ' + 'Although originally designed for squats, Smolov Jr has been known ' + 'to also work well on a variety of other lifts.', + ), + const OutlinedButton( + onPressed: _openSmolovJrInfo, + child: Text('Click here for more Smolov Jr information'), + ), + const SizedBox(height: 16), + const Text( + 'This tool creates a folder of routines in your Hevy account ' + 'with the exact loads to follow, ' + 'saving you a lot of additional time and effort.', + ), + const SizedBox(height: 16), + const WarningCard( + title: 'Hevy Pro Required', + details: "If you don't already have Hevy Pro, " + 'the lifetime plan is well worth it and supports the Hevy devs!', + onPressed: _openHevyProSubscription, + ), + ], + ); + } +} diff --git a/examples/hevy_smolov_jr/lib/steps/program_config.dart b/examples/hevy_smolov_jr/lib/steps/program_config.dart new file mode 100644 index 0000000..7e40b17 --- /dev/null +++ b/examples/hevy_smolov_jr/lib/steps/program_config.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:hevy_smolov_jr/smolov_jr_config/config.dart'; +import 'package:rearch/rearch.dart'; + +/// Displays the program configuration step/ +class ProgramConfigInputStep extends StatelessWidget { + /// Displays the program configuration step/ + const ProgramConfigInputStep({super.key}); + + @override + Widget build(BuildContext context) { + final smolovJrConfig = ScopedSmolovJrConfig.of(context); + return Column( + children: [ + Row( + children: [ + const Text('Rest Between Sets'), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + keyboardType: TextInputType.number, + initialValue: smolovJrConfig.value.restSeconds.toString(), + onChanged: (s) => smolovJrConfig.value = + smolovJrConfig.value.copyWith(restSeconds: int.tryParse(s)), + decoration: const InputDecoration( + hintText: 'rest', + suffixIcon: Text('seconds'), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Text('Unit'), + const SizedBox(width: 16), + Expanded( + child: DropdownMenu( + initialSelection: smolovJrConfig.value.unit, + onSelected: (unit) { + if (unit == null) return; + smolovJrConfig.value = + smolovJrConfig.value.copyWith(unit: unit); + }, + dropdownMenuEntries: [ + for (final unit in WeightUnit.values) + DropdownMenuEntry( + value: unit, + label: unit.name, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Text('Weekly Increment'), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + initialValue: smolovJrConfig.value.increment.toString(), + onChanged: (s) => smolovJrConfig.value = + smolovJrConfig.value.copyWith(increment: num.parse(s)), + decoration: InputDecoration( + hintText: 'weight', + suffixIcon: Text(smolovJrConfig.value.unit.name), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Text('1RM'), + if (smolovJrConfig.value.isBodyWeight) + const Text(' (Excluding Body Weight)'), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + initialValue: smolovJrConfig.value.oneRepMax.toString(), + onChanged: (s) => smolovJrConfig.value = + smolovJrConfig.value.copyWith(oneRepMax: num.parse(s)), + decoration: InputDecoration( + hintText: 'weight', + suffixIcon: Text(smolovJrConfig.value.unit.name), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + if (smolovJrConfig.value.isBodyWeight) + Row( + children: [ + const Text('Body Weight'), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + initialValue: smolovJrConfig.value.bodyWeight.toString(), + onChanged: (s) => smolovJrConfig.value = + smolovJrConfig.value.copyWith(bodyWeight: num.parse(s)), + decoration: InputDecoration( + hintText: 'weight', + suffixIcon: Text(smolovJrConfig.value.unit.name), + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/examples/hevy_smolov_jr/lib/widgets/warning_card.dart b/examples/hevy_smolov_jr/lib/widgets/warning_card.dart new file mode 100644 index 0000000..e7d530c --- /dev/null +++ b/examples/hevy_smolov_jr/lib/widgets/warning_card.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +/// Displays a warning in a [Card]. +class WarningCard extends StatelessWidget { + /// Displays a warning in a [Card]. + const WarningCard({ + required this.title, + required this.details, + this.onPressed, + super.key, + }); + + /// The title of the warning. + final String title; + + /// The details of the warning. + final String details; + + /// What to do when the [WarningCard] is pressed. + final void Function()? onPressed; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Card( + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onPressed, + child: Row( + children: [ + const SizedBox(width: 16), + const Icon(Icons.warning), + const SizedBox(width: 16), + Expanded( + child: Column( + children: [ + const SizedBox(height: 16), + Text(title, style: textTheme.titleMedium), + Text(details), + const SizedBox(height: 16), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/examples/hevy_smolov_jr/pubspec.yaml b/examples/hevy_smolov_jr/pubspec.yaml new file mode 100644 index 0000000..1632fd4 --- /dev/null +++ b/examples/hevy_smolov_jr/pubspec.yaml @@ -0,0 +1,25 @@ +name: hevy_smolov_jr +description: Smolov Jr Calculator for Hevy +publish_to: none + +environment: + sdk: ">=3.5.3 <4.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_rearch: ^1.6.11 + freezed_annotation: ^2.4.4 + http: ^1.2.2 + json_annotation: ^4.9.0 + rearch: ^1.13.0 + shared_preferences: ^2.3.2 + url_launcher: ^6.3.1 + +dev_dependencies: + build_runner: ^2.4.13 + freezed: ^2.5.7 + json_serializable: ^6.8.0 + +flutter: + uses-material-design: true diff --git a/examples/hevy_smolov_jr/web/index.html b/examples/hevy_smolov_jr/web/index.html new file mode 100644 index 0000000..cdbd6fe --- /dev/null +++ b/examples/hevy_smolov_jr/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + Hevy Smolov Jr + + + + + + diff --git a/examples/hevy_smolov_jr/web/manifest.json b/examples/hevy_smolov_jr/web/manifest.json new file mode 100644 index 0000000..326d88c --- /dev/null +++ b/examples/hevy_smolov_jr/web/manifest.json @@ -0,0 +1,12 @@ +{ + "name": "Smolov Jr Calculator for Hevy", + "short_name": "Hevy Smolov Jr", + "start_url": ".", + "display": "standalone", + "background_color": "#7C4DFF", + "theme_color": "#7C4DFF", + "description": "Smolov Jr Calculator for Hevy", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [] +}