From 396f2586e6000d94b3b48fd66f7ec9447c220587 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Wed, 10 Sep 2025 21:13:00 -0300 Subject: [PATCH 01/17] [feat](pnalvarez): implemented ReadingDataSource --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 2 +- ios/Podfile.lock | 4 +- ios/Runner.xcodeproj/project.pbxproj | 6 +- .../designsystem/organisms/app_nav_bar.dart | 17 ++++-- .../designsystem/organisms/list_item.dart | 3 + lib/core/di/di.config.dart | 55 ++++++++++++++++++ .../data/datasource/reading_data_source.dart | 58 +++++++++++++++++++ lib/layers/data/models/reading_data.dart | 18 ++++++ lib/layers/data/models/reading_data.g.dart | 15 +++++ .../data/repository/reading_repository.dart | 0 .../search_repository.dart | 0 .../domain/repository/reading_repository.dart | 0 lib/layers/domain/usecases/start_reading.dart | 17 ++++++ .../bookdetails/bool_details_page.dart | 42 +++++++++----- pubspec.lock | 28 ++++----- 16 files changed, 225 insertions(+), 42 deletions(-) create mode 100644 lib/core/di/di.config.dart create mode 100644 lib/layers/data/datasource/reading_data_source.dart create mode 100644 lib/layers/data/models/reading_data.dart create mode 100644 lib/layers/data/models/reading_data.g.dart create mode 100644 lib/layers/data/repository/reading_repository.dart rename lib/layers/data/{datasource => repository}/search_repository.dart (100%) create mode 100644 lib/layers/domain/repository/reading_repository.dart create mode 100644 lib/layers/domain/usecases/start_reading.dart diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c56964..1dc6cf7 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/ios/Podfile b/ios/Podfile index e549ee2..620e46e 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5d5df2f..65a07d2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -27,11 +27,11 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sqflite_darwin/darwin" SPEC CHECKSUMS: - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d -PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 40e99ab..8f07c66 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -455,7 +455,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -585,7 +585,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -636,7 +636,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/lib/core/designsystem/organisms/app_nav_bar.dart b/lib/core/designsystem/organisms/app_nav_bar.dart index 4ca346e..0cf4e27 100644 --- a/lib/core/designsystem/organisms/app_nav_bar.dart +++ b/lib/core/designsystem/organisms/app_nav_bar.dart @@ -65,12 +65,17 @@ class AppNavBar extends StatelessWidget implements PreferredSizeWidget { Widget _title() { return DisplayAsLoader( isLoading: isTitleLoading, - child: Text( - titleText, - style: TextStyle( - color: Colors.black, - fontSize: titleText.length > 20 ? 14 : 22, - fontWeight: FontWeight.bold, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 250), + child: Text( + titleText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.black, + fontSize: titleText.length > 20 ? 14 : 22, + fontWeight: FontWeight.bold, + ), ), ), ); diff --git a/lib/core/designsystem/organisms/list_item.dart b/lib/core/designsystem/organisms/list_item.dart index d524803..82a0e6d 100644 --- a/lib/core/designsystem/organisms/list_item.dart +++ b/lib/core/designsystem/organisms/list_item.dart @@ -73,11 +73,13 @@ class GenericInput extends ListItemInput { class ListItem extends StatelessWidget { final ListItemInput input; + final bool isExpanded; final Function()? onTap; const ListItem({ super.key, required this.input, + this.isExpanded = false, this.onTap, }); @@ -86,6 +88,7 @@ class ListItem extends StatelessWidget { return InkWell( onTap: onTap, child: Container( + width: isExpanded ? double.infinity : null, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: onBackground, diff --git a/lib/core/di/di.config.dart b/lib/core/di/di.config.dart new file mode 100644 index 0000000..e98717b --- /dev/null +++ b/lib/core/di/di.config.dart @@ -0,0 +1,55 @@ +// dart format width=80 +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// InjectableConfigGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +// coverage:ignore-file + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:get_it/get_it.dart' as _i174; +import 'package:injectable/injectable.dart' as _i526; +import 'package:mibook/layers/data/api/api_client.dart' as _i721; +import 'package:mibook/layers/data/datasource/search_data_source.dart' as _i400; +import 'package:mibook/layers/data/repository/search_repository.dart' as _i967; +import 'package:mibook/layers/domain/repository/search_repository.dart' + as _i303; +import 'package:mibook/layers/domain/usecases/get_book_details.dart' as _i814; +import 'package:mibook/layers/domain/usecases/search_books.dart' as _i663; +import 'package:mibook/layers/presentation/screens/bookdetails/book_details_view_model.dart' + as _i46; +import 'package:mibook/layers/presentation/screens/booksearch/book_search_view_model.dart' + as _i688; + +extension GetItInjectableX on _i174.GetIt { + // initializes the registration of main-scope dependencies inside of GetIt + _i174.GetIt init({ + String? environment, + _i526.EnvironmentFilter? environmentFilter, + }) { + final gh = _i526.GetItHelper(this, environment, environmentFilter); + gh.factory<_i721.IApiClient>(() => _i721.ApiClient()); + gh.factory<_i400.ISearchDataSource>( + () => _i400.SearchDataSource(gh<_i721.IApiClient>()), + ); + gh.factory<_i303.ISearchRepository>( + () => _i967.SearchRepository(gh<_i400.ISearchDataSource>()), + ); + gh.factory<_i663.ISearchBooks>( + () => _i663.SearchBooks(gh<_i303.ISearchRepository>()), + ); + gh.factory<_i814.IGetBookDetails>( + () => _i814.GetBookDetails(gh<_i303.ISearchRepository>()), + ); + gh.factoryParam<_i46.BookDetailsViewModel, String?, dynamic>( + (bookId, _) => + _i46.BookDetailsViewModel(gh<_i814.IGetBookDetails>(), bookId), + ); + gh.factory<_i688.BookSearchViewModel>( + () => _i688.BookSearchViewModel(gh<_i663.ISearchBooks>()), + ); + return this; + } +} diff --git a/lib/layers/data/datasource/reading_data_source.dart b/lib/layers/data/datasource/reading_data_source.dart new file mode 100644 index 0000000..7179bda --- /dev/null +++ b/lib/layers/data/datasource/reading_data_source.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; + +import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; +import 'package:mibook/layers/data/models/reading_data.dart'; + +const _readingListKey = 'reading_list'; + +abstract class IReadingDataSource { + Future startReading({ + required ReadingData readingData, + }); + Future> getReadingData(); +} + +class ReadingDataSource implements IReadingDataSource { + final _storage = EncryptedSharedPreferences(); + + @override + Future startReading({ + required ReadingData readingData, + }) async { + final readingList = await _storage.getString(_readingListKey); + if (readingList.isEmpty) { + final string = jsonEncode([readingData.toJson()]); + await _storage.setString(_readingListKey, string); + } else { + final json = jsonDecode(readingList) as List; + final list = json + .map((e) => ReadingData.fromJson(e as Map)) + .toList(); + final index = list.indexWhere( + (element) => element.bookId == readingData.bookId, + ); + if (index != -1) { + list[index] = readingData; + } else { + list.add(readingData); + } + final string = jsonEncode(list.map((e) => e.toJson()).toList()); + await _storage.setString(_readingListKey, string); + } + final json = readingData.toJson(); + await _storage.setString(readingData.bookId, json.toString()); + } + + @override + Future> getReadingData() async { + final readingList = await _storage.getString(_readingListKey); + if (readingList.isEmpty) { + return []; + } + final json = jsonDecode(readingList) as List; + final list = json + .map((e) => ReadingData.fromJson(e as Map)) + .toList(); + return list; + } +} diff --git a/lib/layers/data/models/reading_data.dart b/lib/layers/data/models/reading_data.dart new file mode 100644 index 0000000..e67e22f --- /dev/null +++ b/lib/layers/data/models/reading_data.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'reading_data.g.dart'; + +@JsonSerializable() +class ReadingData { + final String bookId; + final double progress; + + ReadingData({ + required this.bookId, + required this.progress, + }); + + factory ReadingData.fromJson(Map json) => + _$ReadingDataFromJson(json); + + Map toJson() => _$ReadingDataToJson(this); +} diff --git a/lib/layers/data/models/reading_data.g.dart b/lib/layers/data/models/reading_data.g.dart new file mode 100644 index 0000000..149bfa3 --- /dev/null +++ b/lib/layers/data/models/reading_data.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'reading_data.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ReadingData _$ReadingDataFromJson(Map json) => ReadingData( + bookId: json['bookId'] as String, + progress: (json['progress'] as num).toDouble(), +); + +Map _$ReadingDataToJson(ReadingData instance) => + {'bookId': instance.bookId, 'progress': instance.progress}; diff --git a/lib/layers/data/repository/reading_repository.dart b/lib/layers/data/repository/reading_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/layers/data/datasource/search_repository.dart b/lib/layers/data/repository/search_repository.dart similarity index 100% rename from lib/layers/data/datasource/search_repository.dart rename to lib/layers/data/repository/search_repository.dart diff --git a/lib/layers/domain/repository/reading_repository.dart b/lib/layers/domain/repository/reading_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/layers/domain/usecases/start_reading.dart b/lib/layers/domain/usecases/start_reading.dart new file mode 100644 index 0000000..47d3a6e --- /dev/null +++ b/lib/layers/domain/usecases/start_reading.dart @@ -0,0 +1,17 @@ +abstract class IStartReading { + Future call({ + required String bookId, + required double progress, + }); +} + +class StartReading implements IStartReading { + @override + Future call({ + required String bookId, + required double progress, + }) { + // TODO: implement call + throw UnimplementedError(); + } +} diff --git a/lib/layers/presentation/screens/bookdetails/bool_details_page.dart b/lib/layers/presentation/screens/bookdetails/bool_details_page.dart index 9ac907b..f127664 100644 --- a/lib/layers/presentation/screens/bookdetails/bool_details_page.dart +++ b/lib/layers/presentation/screens/bookdetails/bool_details_page.dart @@ -1,8 +1,10 @@ import 'package:auto_route/auto_route.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:mibook/core/designsystem/organisms/app_nav_bar.dart'; +import 'package:mibook/core/designsystem/organisms/list_item.dart'; import 'package:mibook/core/di/di.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_state.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_view_model.dart'; @@ -69,26 +71,36 @@ class _BookDetailsContent extends StatelessWidget { return Padding( padding: const EdgeInsets.all(16.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ if (book.thumbnail != null) - Image.network(book.thumbnail!), + CachedNetworkImage(imageUrl: book.thumbnail!), const SizedBox(height: 16), - Text( - book.title, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, + ListItem( + isExpanded: true, + input: GenericInput( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + book.title, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + if (book.authors.isNotEmpty) + Text( + 'By ${book.authors}', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 16), + Html(data: book.description), + ], + ), ), ), - const SizedBox(height: 8), - if (book.authors.isNotEmpty) - Text( - 'By ${book.authors}', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 16), - Html(data: book.description), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index eaa4bf2..3058123 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -500,26 +500,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.1" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -993,26 +993,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" url: "https://pub.dev" source: hosted - version: "1.25.15" + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" test_core: dependency: transitive description: name: test_core - sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" url: "https://pub.dev" source: hosted - version: "0.6.8" + version: "0.6.11" timing: dependency: transitive description: @@ -1041,10 +1041,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: From 54d2426a1d340906ee104f9441e26acf19a071a5 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Wed, 10 Sep 2025 21:14:44 -0300 Subject: [PATCH 02/17] [feat](pnalvarez): implemented ReadingRepository --- .../data/repository/reading_repository.dart | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/layers/data/repository/reading_repository.dart b/lib/layers/data/repository/reading_repository.dart index e69de29..19ceebf 100644 --- a/lib/layers/data/repository/reading_repository.dart +++ b/lib/layers/data/repository/reading_repository.dart @@ -0,0 +1,21 @@ +import 'package:mibook/layers/data/datasource/reading_data_source.dart'; +import 'package:mibook/layers/data/models/reading_data.dart'; +import 'package:mibook/layers/domain/repository/reading_repository.dart'; + +class ReadingRepository implements IReadingRepository { + final IReadingDataSource _dataSource; + + ReadingRepository(this._dataSource); + + @override + Future startReading({ + required String bookId, + required double progress, + }) async { + final readingData = ReadingData( + bookId: bookId, + progress: progress, + ); + await _dataSource.startReading(readingData: readingData); + } +} From 5403a15a4aa4c030f3fbd3cb2fa4363a8f0eeef3 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Thu, 11 Sep 2025 09:32:16 -0300 Subject: [PATCH 03/17] [feat](pnalvarez): displaying start reading dialog --- lib/core/di/di.config.dart | 26 +- lib/core/utils/strings.dart | 1 + .../data/datasource/reading_data_source.dart | 2 + lib/layers/data/models/book_list_data.dart | 5 +- lib/layers/data/models/book_list_data.g.dart | 265 ++++++++++++++++++ lib/layers/data/models/reading_data.dart | 11 + .../data/repository/reading_repository.dart | 20 +- .../domain/models/book_list_domain.dart | 2 + lib/layers/domain/models/reading_domain.dart | 9 + .../domain/repository/reading_repository.dart | 8 + lib/layers/domain/usecases/start_reading.dart | 20 +- .../bookdetails/book_details_event.dart | 5 +- .../bookdetails/book_details_view_model.dart | 19 +- .../bookdetails/bool_details_page.dart | 82 ++++++ .../screens/booksearch/book_ui.dart | 3 + 15 files changed, 457 insertions(+), 21 deletions(-) create mode 100644 lib/layers/data/models/book_list_data.g.dart create mode 100644 lib/layers/domain/models/reading_domain.dart diff --git a/lib/core/di/di.config.dart b/lib/core/di/di.config.dart index e98717b..1959414 100644 --- a/lib/core/di/di.config.dart +++ b/lib/core/di/di.config.dart @@ -12,12 +12,21 @@ import 'package:get_it/get_it.dart' as _i174; import 'package:injectable/injectable.dart' as _i526; import 'package:mibook/layers/data/api/api_client.dart' as _i721; +import 'package:mibook/layers/data/datasource/reading_data_source.dart' as _i44; import 'package:mibook/layers/data/datasource/search_data_source.dart' as _i400; +<<<<<<< HEAD import 'package:mibook/layers/data/repository/search_repository.dart' as _i967; +======= +import 'package:mibook/layers/data/repository/reading_repository.dart' as _i600; +import 'package:mibook/layers/data/repository/search_repository.dart' as _i967; +import 'package:mibook/layers/domain/repository/reading_repository.dart' + as _i649; +>>>>>>> 7741511 ([feat](pnalvarez): displaying start reading dialog) import 'package:mibook/layers/domain/repository/search_repository.dart' as _i303; import 'package:mibook/layers/domain/usecases/get_book_details.dart' as _i814; import 'package:mibook/layers/domain/usecases/search_books.dart' as _i663; +import 'package:mibook/layers/domain/usecases/start_reading.dart' as _i369; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_view_model.dart' as _i46; import 'package:mibook/layers/presentation/screens/booksearch/book_search_view_model.dart' @@ -31,11 +40,21 @@ extension GetItInjectableX on _i174.GetIt { }) { final gh = _i526.GetItHelper(this, environment, environmentFilter); gh.factory<_i721.IApiClient>(() => _i721.ApiClient()); + gh.factory<_i44.IReadingDataSource>(() => _i44.ReadingDataSource()); gh.factory<_i400.ISearchDataSource>( () => _i400.SearchDataSource(gh<_i721.IApiClient>()), ); + gh.factory<_i649.IReadingRepository>( + () => _i600.ReadingRepository(gh<_i44.IReadingDataSource>()), + ); gh.factory<_i303.ISearchRepository>( () => _i967.SearchRepository(gh<_i400.ISearchDataSource>()), +<<<<<<< HEAD +======= + ); + gh.factory<_i369.IStartReading>( + () => _i369.StartReading(gh<_i649.IReadingRepository>()), +>>>>>>> 7741511 ([feat](pnalvarez): displaying start reading dialog) ); gh.factory<_i663.ISearchBooks>( () => _i663.SearchBooks(gh<_i303.ISearchRepository>()), @@ -44,8 +63,11 @@ extension GetItInjectableX on _i174.GetIt { () => _i814.GetBookDetails(gh<_i303.ISearchRepository>()), ); gh.factoryParam<_i46.BookDetailsViewModel, String?, dynamic>( - (bookId, _) => - _i46.BookDetailsViewModel(gh<_i814.IGetBookDetails>(), bookId), + (bookId, _) => _i46.BookDetailsViewModel( + gh<_i814.IGetBookDetails>(), + gh<_i369.IStartReading>(), + bookId, + ), ); gh.factory<_i688.BookSearchViewModel>( () => _i688.BookSearchViewModel(gh<_i663.ISearchBooks>()), diff --git a/lib/core/utils/strings.dart b/lib/core/utils/strings.dart index 0669853..eafefd8 100644 --- a/lib/core/utils/strings.dart +++ b/lib/core/utils/strings.dart @@ -2,3 +2,4 @@ const home = 'Home'; const search = 'Search'; const objectives = 'Objectives'; const searchBooks = 'Search Books'; +const startReading = 'Start Reading'; diff --git a/lib/layers/data/datasource/reading_data_source.dart b/lib/layers/data/datasource/reading_data_source.dart index 7179bda..a9d9228 100644 --- a/lib/layers/data/datasource/reading_data_source.dart +++ b/lib/layers/data/datasource/reading_data_source.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; +import 'package:injectable/injectable.dart'; import 'package:mibook/layers/data/models/reading_data.dart'; const _readingListKey = 'reading_list'; @@ -12,6 +13,7 @@ abstract class IReadingDataSource { Future> getReadingData(); } +@Injectable(as: IReadingDataSource) class ReadingDataSource implements IReadingDataSource { final _storage = EncryptedSharedPreferences(); diff --git a/lib/layers/data/models/book_list_data.dart b/lib/layers/data/models/book_list_data.dart index e89e5e5..8b49b68 100644 --- a/lib/layers/data/models/book_list_data.dart +++ b/lib/layers/data/models/book_list_data.dart @@ -64,6 +64,7 @@ class BookItem { authors: volumeInfo.authors, description: volumeInfo.description, thumbnail: volumeInfo.imageLinks?.thumbnail, + pageCount: volumeInfo.pageCount, ); } } @@ -78,7 +79,7 @@ class VolumeInfo { final String? description; final List? industryIdentifiers; final ReadingModes? readingModes; - final int? pageCount; + final int pageCount; final String? printType; final List? categories; final double? averageRating; @@ -102,7 +103,7 @@ class VolumeInfo { this.description, this.industryIdentifiers, this.readingModes, - this.pageCount, + required this.pageCount, this.printType, this.categories, this.averageRating, diff --git a/lib/layers/data/models/book_list_data.g.dart b/lib/layers/data/models/book_list_data.g.dart new file mode 100644 index 0000000..cd2940d --- /dev/null +++ b/lib/layers/data/models/book_list_data.g.dart @@ -0,0 +1,265 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'book_list_data.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +BookListData _$BookListDataFromJson(Map json) => BookListData( + kind: json['kind'] as String, + totalItems: (json['totalItems'] as num).toInt(), + items: + (json['items'] as List?) + ?.map((e) => BookItem.fromJson(e as Map)) + .toList() ?? + [], +); + +Map _$BookListDataToJson(BookListData instance) => + { + 'kind': instance.kind, + 'totalItems': instance.totalItems, + 'items': instance.items, + }; + +BookItem _$BookItemFromJson(Map json) => BookItem( + kind: json['kind'] as String, + id: json['id'] as String, + etag: json['etag'] as String, + selfLink: json['selfLink'] as String, + volumeInfo: VolumeInfo.fromJson(json['volumeInfo'] as Map), + saleInfo: SaleInfo.fromJson(json['saleInfo'] as Map), + accessInfo: AccessInfo.fromJson(json['accessInfo'] as Map), + searchInfo: json['searchInfo'] == null + ? null + : SearchInfo.fromJson(json['searchInfo'] as Map), +); + +Map _$BookItemToJson(BookItem instance) => { + 'kind': instance.kind, + 'id': instance.id, + 'etag': instance.etag, + 'selfLink': instance.selfLink, + 'volumeInfo': instance.volumeInfo, + 'saleInfo': instance.saleInfo, + 'accessInfo': instance.accessInfo, + 'searchInfo': instance.searchInfo, +}; + +VolumeInfo _$VolumeInfoFromJson(Map json) => VolumeInfo( + title: json['title'] as String, + authors: (json['authors'] as List?) + ?.map((e) => e as String) + .toList(), + subtitle: json['subtitle'] as String?, + publisher: json['publisher'] as String?, + publishedDate: json['publishedDate'] as String?, + description: json['description'] as String?, + industryIdentifiers: (json['industryIdentifiers'] as List?) + ?.map((e) => IndustryIdentifier.fromJson(e as Map)) + .toList(), + readingModes: json['readingModes'] == null + ? null + : ReadingModes.fromJson(json['readingModes'] as Map), + pageCount: (json['pageCount'] as num).toInt(), + printType: json['printType'] as String?, + categories: (json['categories'] as List?) + ?.map((e) => e as String) + .toList(), + averageRating: (json['averageRating'] as num?)?.toDouble(), + ratingsCount: (json['ratingsCount'] as num?)?.toInt(), + maturityRating: json['maturityRating'] as String?, + allowAnonLogging: json['allowAnonLogging'] as bool?, + contentVersion: json['contentVersion'] as String?, + panelizationSummary: json['panelizationSummary'] == null + ? null + : PanelizationSummary.fromJson( + json['panelizationSummary'] as Map, + ), + imageLinks: json['imageLinks'] == null + ? null + : ImageLinks.fromJson(json['imageLinks'] as Map), + language: json['language'] as String?, + previewLink: json['previewLink'] as String?, + infoLink: json['infoLink'] as String?, + canonicalVolumeLink: json['canonicalVolumeLink'] as String?, +); + +Map _$VolumeInfoToJson(VolumeInfo instance) => + { + 'title': instance.title, + 'authors': instance.authors, + 'subtitle': instance.subtitle, + 'publisher': instance.publisher, + 'publishedDate': instance.publishedDate, + 'description': instance.description, + 'industryIdentifiers': instance.industryIdentifiers, + 'readingModes': instance.readingModes, + 'pageCount': instance.pageCount, + 'printType': instance.printType, + 'categories': instance.categories, + 'averageRating': instance.averageRating, + 'ratingsCount': instance.ratingsCount, + 'maturityRating': instance.maturityRating, + 'allowAnonLogging': instance.allowAnonLogging, + 'contentVersion': instance.contentVersion, + 'panelizationSummary': instance.panelizationSummary, + 'imageLinks': instance.imageLinks, + 'language': instance.language, + 'previewLink': instance.previewLink, + 'infoLink': instance.infoLink, + 'canonicalVolumeLink': instance.canonicalVolumeLink, + }; + +IndustryIdentifier _$IndustryIdentifierFromJson(Map json) => + IndustryIdentifier( + type: json['type'] as String, + identifier: json['identifier'] as String, + ); + +Map _$IndustryIdentifierToJson(IndustryIdentifier instance) => + {'type': instance.type, 'identifier': instance.identifier}; + +ReadingModes _$ReadingModesFromJson(Map json) => + ReadingModes(text: json['text'] as bool, image: json['image'] as bool); + +Map _$ReadingModesToJson(ReadingModes instance) => + {'text': instance.text, 'image': instance.image}; + +PanelizationSummary _$PanelizationSummaryFromJson(Map json) => + PanelizationSummary( + containsEpubBubbles: json['containsEpubBubbles'] as bool, + containsImageBubbles: json['containsImageBubbles'] as bool, + ); + +Map _$PanelizationSummaryToJson( + PanelizationSummary instance, +) => { + 'containsEpubBubbles': instance.containsEpubBubbles, + 'containsImageBubbles': instance.containsImageBubbles, +}; + +ImageLinks _$ImageLinksFromJson(Map json) => ImageLinks( + smallThumbnail: json['smallThumbnail'] as String?, + thumbnail: json['thumbnail'] as String?, +); + +Map _$ImageLinksToJson(ImageLinks instance) => + { + 'smallThumbnail': instance.smallThumbnail, + 'thumbnail': instance.thumbnail, + }; + +SaleInfo _$SaleInfoFromJson(Map json) => SaleInfo( + country: json['country'] as String, + saleability: json['saleability'] as String, + isEbook: json['isEbook'] as bool, + listPrice: json['listPrice'] == null + ? null + : Price.fromJson(json['listPrice'] as Map), + retailPrice: json['retailPrice'] == null + ? null + : Price.fromJson(json['retailPrice'] as Map), + buyLink: json['buyLink'] as String?, + offers: (json['offers'] as List?) + ?.map((e) => Offer.fromJson(e as Map)) + .toList(), +); + +Map _$SaleInfoToJson(SaleInfo instance) => { + 'country': instance.country, + 'saleability': instance.saleability, + 'isEbook': instance.isEbook, + 'listPrice': instance.listPrice, + 'retailPrice': instance.retailPrice, + 'buyLink': instance.buyLink, + 'offers': instance.offers, +}; + +Price _$PriceFromJson(Map json) => Price( + amount: (json['amount'] as num?)?.toDouble(), + currencyCode: json['currencyCode'] as String?, + amountInMicros: (json['amountInMicros'] as num?)?.toInt(), +); + +Map _$PriceToJson(Price instance) => { + 'amount': instance.amount, + 'currencyCode': instance.currencyCode, + 'amountInMicros': instance.amountInMicros, +}; + +Offer _$OfferFromJson(Map json) => Offer( + finskyOfferType: (json['finskyOfferType'] as num).toInt(), + listPrice: json['listPrice'] == null + ? null + : Price.fromJson(json['listPrice'] as Map), + retailPrice: json['retailPrice'] == null + ? null + : Price.fromJson(json['retailPrice'] as Map), + giftable: json['giftable'] as bool?, +); + +Map _$OfferToJson(Offer instance) => { + 'finskyOfferType': instance.finskyOfferType, + 'listPrice': instance.listPrice, + 'retailPrice': instance.retailPrice, + 'giftable': instance.giftable, +}; + +AccessInfo _$AccessInfoFromJson(Map json) => AccessInfo( + country: json['country'] as String, + viewability: json['viewability'] as String, + embeddable: json['embeddable'] as bool, + publicDomain: json['publicDomain'] as bool, + textToSpeechPermission: json['textToSpeechPermission'] as String, + epub: json['epub'] == null + ? null + : EpubInfo.fromJson(json['epub'] as Map), + pdf: json['pdf'] == null + ? null + : PdfInfo.fromJson(json['pdf'] as Map), + webReaderLink: json['webReaderLink'] as String?, + accessViewStatus: json['accessViewStatus'] as String?, + quoteSharingAllowed: json['quoteSharingAllowed'] as bool?, +); + +Map _$AccessInfoToJson(AccessInfo instance) => + { + 'country': instance.country, + 'viewability': instance.viewability, + 'embeddable': instance.embeddable, + 'publicDomain': instance.publicDomain, + 'textToSpeechPermission': instance.textToSpeechPermission, + 'epub': instance.epub, + 'pdf': instance.pdf, + 'webReaderLink': instance.webReaderLink, + 'accessViewStatus': instance.accessViewStatus, + 'quoteSharingAllowed': instance.quoteSharingAllowed, + }; + +EpubInfo _$EpubInfoFromJson(Map json) => EpubInfo( + isAvailable: json['isAvailable'] as bool, + acsTokenLink: json['acsTokenLink'] as String?, +); + +Map _$EpubInfoToJson(EpubInfo instance) => { + 'isAvailable': instance.isAvailable, + 'acsTokenLink': instance.acsTokenLink, +}; + +PdfInfo _$PdfInfoFromJson(Map json) => PdfInfo( + isAvailable: json['isAvailable'] as bool, + acsTokenLink: json['acsTokenLink'] as String?, +); + +Map _$PdfInfoToJson(PdfInfo instance) => { + 'isAvailable': instance.isAvailable, + 'acsTokenLink': instance.acsTokenLink, +}; + +SearchInfo _$SearchInfoFromJson(Map json) => + SearchInfo(textSnippet: json['textSnippet'] as String?); + +Map _$SearchInfoToJson(SearchInfo instance) => + {'textSnippet': instance.textSnippet}; diff --git a/lib/layers/data/models/reading_data.dart b/lib/layers/data/models/reading_data.dart index e67e22f..6ad64ce 100644 --- a/lib/layers/data/models/reading_data.dart +++ b/lib/layers/data/models/reading_data.dart @@ -1,4 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:mibook/layers/domain/models/reading_domain.dart'; part 'reading_data.g.dart'; @JsonSerializable() @@ -15,4 +16,14 @@ class ReadingData { _$ReadingDataFromJson(json); Map toJson() => _$ReadingDataToJson(this); + + factory ReadingData.fromDomain(ReadingDomain domain) => ReadingData( + bookId: domain.bookId, + progress: domain.progress, + ); + + ReadingDomain toDomain() => ReadingDomain( + bookId: bookId, + progress: progress, + ); } diff --git a/lib/layers/data/repository/reading_repository.dart b/lib/layers/data/repository/reading_repository.dart index 19ceebf..a225bbe 100644 --- a/lib/layers/data/repository/reading_repository.dart +++ b/lib/layers/data/repository/reading_repository.dart @@ -1,7 +1,10 @@ +import 'package:injectable/injectable.dart'; import 'package:mibook/layers/data/datasource/reading_data_source.dart'; import 'package:mibook/layers/data/models/reading_data.dart'; +import 'package:mibook/layers/domain/models/reading_domain.dart'; import 'package:mibook/layers/domain/repository/reading_repository.dart'; +@Injectable(as: IReadingRepository) class ReadingRepository implements IReadingRepository { final IReadingDataSource _dataSource; @@ -9,13 +12,18 @@ class ReadingRepository implements IReadingRepository { @override Future startReading({ - required String bookId, - required double progress, + required ReadingDomain reading, }) async { - final readingData = ReadingData( - bookId: bookId, - progress: progress, + final data = ReadingData( + bookId: reading.bookId, + progress: reading.progress, ); - await _dataSource.startReading(readingData: readingData); + await _dataSource.startReading(readingData: data); + } + + @override + Future> getReadings() async { + final data = await _dataSource.getReadingData(); + return data.map((e) => e.toDomain()).toList(); } } diff --git a/lib/layers/domain/models/book_list_domain.dart b/lib/layers/domain/models/book_list_domain.dart index f106814..c130ea3 100644 --- a/lib/layers/domain/models/book_list_domain.dart +++ b/lib/layers/domain/models/book_list_domain.dart @@ -15,6 +15,7 @@ class BookDomain { final List? authors; final String? description; final String? thumbnail; + final int pageCount; BookDomain({ required this.id, @@ -23,5 +24,6 @@ class BookDomain { required this.authors, required this.description, required this.thumbnail, + required this.pageCount, }); } diff --git a/lib/layers/domain/models/reading_domain.dart b/lib/layers/domain/models/reading_domain.dart new file mode 100644 index 0000000..7dca4af --- /dev/null +++ b/lib/layers/domain/models/reading_domain.dart @@ -0,0 +1,9 @@ +class ReadingDomain { + final String bookId; + final double progress; + + ReadingDomain({ + required this.bookId, + required this.progress, + }); +} diff --git a/lib/layers/domain/repository/reading_repository.dart b/lib/layers/domain/repository/reading_repository.dart index e69de29..899ae66 100644 --- a/lib/layers/domain/repository/reading_repository.dart +++ b/lib/layers/domain/repository/reading_repository.dart @@ -0,0 +1,8 @@ +import 'package:mibook/layers/domain/models/reading_domain.dart'; + +abstract class IReadingRepository { + Future startReading({ + required ReadingDomain reading, + }); + Future> getReadings(); +} diff --git a/lib/layers/domain/usecases/start_reading.dart b/lib/layers/domain/usecases/start_reading.dart index 47d3a6e..494bd88 100644 --- a/lib/layers/domain/usecases/start_reading.dart +++ b/lib/layers/domain/usecases/start_reading.dart @@ -1,17 +1,23 @@ +import 'package:injectable/injectable.dart'; +import 'package:mibook/layers/domain/models/reading_domain.dart'; +import 'package:mibook/layers/domain/repository/reading_repository.dart'; + abstract class IStartReading { Future call({ - required String bookId, - required double progress, + required ReadingDomain reading, }); } +@Injectable(as: IStartReading) class StartReading implements IStartReading { + final IReadingRepository _repository; + + StartReading(this._repository); + @override Future call({ - required String bookId, - required double progress, - }) { - // TODO: implement call - throw UnimplementedError(); + required ReadingDomain reading, + }) async { + await _repository.startReading(reading: reading); } } diff --git a/lib/layers/presentation/screens/bookdetails/book_details_event.dart b/lib/layers/presentation/screens/bookdetails/book_details_event.dart index ff95196..264a989 100644 --- a/lib/layers/presentation/screens/bookdetails/book_details_event.dart +++ b/lib/layers/presentation/screens/bookdetails/book_details_event.dart @@ -5,4 +5,7 @@ class DidLoadEvent extends BookDetailsEvent { DidLoadEvent(this.bookId); } -class DidClickStartReadingEvent extends BookDetailsEvent {} +class DidClickStartReadingEvent extends BookDetailsEvent { + final double progress; + DidClickStartReadingEvent(this.progress); +} diff --git a/lib/layers/presentation/screens/bookdetails/book_details_view_model.dart b/lib/layers/presentation/screens/bookdetails/book_details_view_model.dart index 1a7f8de..834c0be 100644 --- a/lib/layers/presentation/screens/bookdetails/book_details_view_model.dart +++ b/lib/layers/presentation/screens/bookdetails/book_details_view_model.dart @@ -1,6 +1,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; +import 'package:mibook/layers/domain/models/reading_domain.dart'; import 'package:mibook/layers/domain/usecases/get_book_details.dart'; +import 'package:mibook/layers/domain/usecases/start_reading.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_event.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_state.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.dart'; @@ -8,10 +10,12 @@ import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.d @injectable class BookDetailsViewModel extends Bloc { final IGetBookDetails _getBookDetails; + final IStartReading _startReading; String? bookId; BookDetailsViewModel( this._getBookDetails, + this._startReading, @factoryParam this.bookId, ) : super(BookDetailsState.initial()) { on((event, emit) async { @@ -38,9 +42,7 @@ class BookDetailsViewModel extends Bloc { ); } }); - on((event, emit) async { - // Handle start reading event - }); + on((event, emit) async {}); } Future loadBookDetails() async { @@ -51,4 +53,15 @@ class BookDetailsViewModel extends Bloc { return null; } } + + Future startReading({required double progress}) async { + if (bookId != null) { + await _startReading( + reading: ReadingDomain( + bookId: bookId!, + progress: progress, + ), + ); + } + } } diff --git a/lib/layers/presentation/screens/bookdetails/bool_details_page.dart b/lib/layers/presentation/screens/bookdetails/bool_details_page.dart index f127664..2446f75 100644 --- a/lib/layers/presentation/screens/bookdetails/bool_details_page.dart +++ b/lib/layers/presentation/screens/bookdetails/bool_details_page.dart @@ -3,10 +3,13 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:mibook/core/designsystem/molecules/buttons/primary_button.dart'; import 'package:mibook/core/designsystem/organisms/app_nav_bar.dart'; import 'package:mibook/core/designsystem/organisms/list_item.dart'; import 'package:mibook/core/di/di.dart'; +import 'package:mibook/core/utils/strings.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_state.dart'; +import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_view_model.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_event.dart'; @@ -101,6 +104,55 @@ class _BookDetailsContent extends StatelessWidget { ), ), ), + const SizedBox(height: 24), + PrimaryButton( + title: startReading, + onPressed: () => showGeneralDialog( + context: context, + barrierDismissible: + true, // allows tap outside to close + barrierLabel: "Dismiss", + barrierColor: Colors.black54, // dim background + transitionDuration: const Duration(milliseconds: 300), + pageBuilder: (context, anim1, anim2) { + // Must return a full-screen widget so barrier can detect taps + return SafeArea( + child: Builder( + builder: (context) { + return Center( + child: Material( + color: Colors.transparent, + child: AlertDialog( + content: _StartReadingDialogContent( + book: book, + onClickStartReading: + (double progress) { + Navigator.of(context).pop(); + }, + ), + ), + ), + ); + }, + ), + ); + }, + transitionBuilder: (context, anim1, anim2, child) { + // Slide + fade + return SlideTransition( + position: Tween( + begin: const Offset(0, 0.2), // from bottom + end: Offset.zero, + ).animate(anim1), + child: FadeTransition( + opacity: anim1, + child: child, + ), + ); + }, + ), + ), + const SizedBox(height: 32), ], ), ); @@ -114,3 +166,33 @@ class _BookDetailsContent extends StatelessWidget { ); } } + +class _StartReadingDialogContent extends StatelessWidget { + final BookDetailsUI book; + final Function(double) onClickStartReading; + + const _StartReadingDialogContent({ + required this.book, + required this.onClickStartReading, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (book.thumbnail != null) + CachedNetworkImage(imageUrl: book.thumbnail!), + const SizedBox(height: 16), + Text( + book.title, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + ], + ); + } +} diff --git a/lib/layers/presentation/screens/booksearch/book_ui.dart b/lib/layers/presentation/screens/booksearch/book_ui.dart index 346cef2..53d5c12 100644 --- a/lib/layers/presentation/screens/booksearch/book_ui.dart +++ b/lib/layers/presentation/screens/booksearch/book_ui.dart @@ -7,6 +7,7 @@ class BookUI { final String authors; final String description; final String? thumbnail; + final int pageCount; BookUI({ required this.id, @@ -15,6 +16,7 @@ class BookUI { required this.authors, required this.description, required this.thumbnail, + required this.pageCount, }); factory BookUI.fromDomain(BookDomain domain) { @@ -25,6 +27,7 @@ class BookUI { authors: (domain.authors ?? []).join(', '), description: domain.description ?? '', thumbnail: domain.thumbnail, + pageCount: domain.pageCount, ); } } From 811ada6078e59c340ab84cca36000188a4c974c6 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:27:39 -0300 Subject: [PATCH 04/17] [feat](pnalvarez): showing progress input field --- .../molecules/inputfields/input_field.dart | 9 +++ lib/core/utils/strings.dart | 4 + .../screens/bookdetails/book_details_ui.dart | 3 + .../bookdetails/bool_details_page.dart | 80 ++++++++++++++----- 4 files changed, 75 insertions(+), 21 deletions(-) diff --git a/lib/core/designsystem/molecules/inputfields/input_field.dart b/lib/core/designsystem/molecules/inputfields/input_field.dart index b112c54..71e1cf3 100644 --- a/lib/core/designsystem/molecules/inputfields/input_field.dart +++ b/lib/core/designsystem/molecules/inputfields/input_field.dart @@ -6,6 +6,9 @@ class InputField extends StatelessWidget { final TextEditingController controller; final bool isEnabled; final String placeholder; + final String? suffixText; + final String? prefixText; + final TextInputType? keyboardType; final Function(String) onChanged; const InputField({ @@ -14,6 +17,9 @@ class InputField extends StatelessWidget { required this.controller, this.isEnabled = true, this.placeholder = '', + this.suffixText, + this.prefixText, + this.keyboardType, required this.onChanged, }); @@ -27,11 +33,14 @@ class InputField extends StatelessWidget { SizedBox( height: 48, child: TextField( + keyboardType: keyboardType, decoration: InputDecoration( border: OutlineInputBorder(), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: onBorder), ), + suffixText: suffixText, + prefixText: prefixText, ), onChanged: onChanged, ), diff --git a/lib/core/utils/strings.dart b/lib/core/utils/strings.dart index eafefd8..8166bd2 100644 --- a/lib/core/utils/strings.dart +++ b/lib/core/utils/strings.dart @@ -3,3 +3,7 @@ const search = 'Search'; const objectives = 'Objectives'; const searchBooks = 'Search Books'; const startReading = 'Start Reading'; +const percent = '%'; +const page = 'Page'; +const progress = 'Progress'; +const confirm = 'Confirm'; diff --git a/lib/layers/presentation/screens/bookdetails/book_details_ui.dart b/lib/layers/presentation/screens/bookdetails/book_details_ui.dart index e495b4d..97cf405 100644 --- a/lib/layers/presentation/screens/bookdetails/book_details_ui.dart +++ b/lib/layers/presentation/screens/bookdetails/book_details_ui.dart @@ -7,6 +7,7 @@ class BookDetailsUI { final String authors; final String description; final String? thumbnail; + final int pageCount; BookDetailsUI({ required this.id, @@ -15,6 +16,7 @@ class BookDetailsUI { required this.authors, required this.description, required this.thumbnail, + required this.pageCount, }); factory BookDetailsUI.fromDomain(BookDomain domain) { @@ -25,6 +27,7 @@ class BookDetailsUI { authors: (domain.authors ?? []).join(', '), description: domain.description ?? '', thumbnail: domain.thumbnail, + pageCount: domain.pageCount, ); } } diff --git a/lib/layers/presentation/screens/bookdetails/bool_details_page.dart b/lib/layers/presentation/screens/bookdetails/bool_details_page.dart index 2446f75..020feb5 100644 --- a/lib/layers/presentation/screens/bookdetails/bool_details_page.dart +++ b/lib/layers/presentation/screens/bookdetails/bool_details_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:mibook/core/designsystem/molecules/buttons/primary_button.dart'; +import 'package:mibook/core/designsystem/molecules/inputfields/input_field.dart'; import 'package:mibook/core/designsystem/organisms/app_nav_bar.dart'; import 'package:mibook/core/designsystem/organisms/list_item.dart'; import 'package:mibook/core/di/di.dart'; @@ -123,12 +124,24 @@ class _BookDetailsContent extends StatelessWidget { child: Material( color: Colors.transparent, child: AlertDialog( - content: _StartReadingDialogContent( - book: book, - onClickStartReading: - (double progress) { - Navigator.of(context).pop(); - }, + backgroundColor: Colors.white, + insetPadding: + EdgeInsets.zero, // remove margin + contentPadding: EdgeInsets + .zero, // remove inner padding if needed + content: SizedBox( + width: + MediaQuery.of( + context, + ).size.width - + 32, + child: _StartReadingDialogContent( + book: book, + onClickStartReading: + (double progress) { + Navigator.of(context).pop(); + }, + ), ), ), ), @@ -169,30 +182,55 @@ class _BookDetailsContent extends StatelessWidget { class _StartReadingDialogContent extends StatelessWidget { final BookDetailsUI book; + final TextEditingController _controller = TextEditingController(); final Function(double) onClickStartReading; - const _StartReadingDialogContent({ + _StartReadingDialogContent({ required this.book, required this.onClickStartReading, }); @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (book.thumbnail != null) - CachedNetworkImage(imageUrl: book.thumbnail!), - const SizedBox(height: 16), - Text( - book.title, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.topLeft, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), ), - ), - const SizedBox(height: 8), - ], + if (book.thumbnail != null) + CachedNetworkImage(imageUrl: book.thumbnail!), + const SizedBox(height: 16), + Text( + book.title, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text('Maximum pages: ${book.pageCount}'), + const SizedBox(height: 24), + InputField( + keyboardType: TextInputType.number, + label: progress, + controller: _controller, + onChanged: (_) {}, + prefixText: '$page ', + ), + const SizedBox(height: 24), + PrimaryButton( + title: confirm, + onPressed: () {}, + ), + ], + ), ); } } From 9a980dddffe896c0886abf0a7da89b4bb7f66944 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Fri, 12 Sep 2025 23:42:56 -0300 Subject: [PATCH 05/17] [feat](pnalvarez): start reading page --- lib/core/designsystem/atoms/colors.dart | 2 + .../molecules/buttons/primary_button.dart | 56 +++-- .../molecules/buttons/secondary_button.dart | 1 + .../indicators/progress_stepper.dart | 25 +++ .../molecules/inputfields/input_field.dart | 7 + lib/core/di/di.config.dart | 7 + lib/core/routes/app_router.dart | 3 +- lib/core/routes/app_router.gr.dart | 155 +++++++++----- lib/core/utils/strings.dart | 2 + lib/layers/data/models/book_list_data.dart | 10 +- lib/layers/data/models/book_list_data.g.dart | 6 +- .../bookdetails/book_details_event.dart | 5 + ...tails_page.dart => book_details_page.dart} | 88 +++----- .../bookdetails/book_details_state.dart | 4 + .../book_details_state.freezed.dart | 202 ++++++++++++++++++ .../bookdetails/book_details_view_model.dart | 7 + .../screens/booksearch/book_search_page.dart | 2 +- .../startreading/start_reading_event.dart | 8 + .../startreading/start_reading_page.dart | 121 +++++++++++ .../startreading/start_reading_state.dart | 24 +++ .../start_reading_state.freezed.dart | 201 +++++++++++++++++ .../start_reading_view_model.dart | 39 ++++ 22 files changed, 823 insertions(+), 152 deletions(-) create mode 100644 lib/core/designsystem/molecules/buttons/secondary_button.dart create mode 100644 lib/core/designsystem/molecules/indicators/progress_stepper.dart rename lib/layers/presentation/screens/bookdetails/{bool_details_page.dart => book_details_page.dart} (64%) create mode 100644 lib/layers/presentation/screens/bookdetails/book_details_state.freezed.dart create mode 100644 lib/layers/presentation/screens/startreading/start_reading_event.dart create mode 100644 lib/layers/presentation/screens/startreading/start_reading_page.dart create mode 100644 lib/layers/presentation/screens/startreading/start_reading_state.dart create mode 100644 lib/layers/presentation/screens/startreading/start_reading_state.freezed.dart create mode 100644 lib/layers/presentation/screens/startreading/start_reading_view_model.dart diff --git a/lib/core/designsystem/atoms/colors.dart b/lib/core/designsystem/atoms/colors.dart index d2383d8..ebbd25a 100644 --- a/lib/core/designsystem/atoms/colors.dart +++ b/lib/core/designsystem/atoms/colors.dart @@ -4,3 +4,5 @@ const primary = Color(0xFF1338BE); const disabled = Color(0xFF565656); const onBorder = Color(0xFF0D5EAF); const onBackground = Color(0xFFC6E0FA); +const background = Color(0xFF3D67FF); +const error = Color.fromARGB(255, 196, 8, 8); diff --git a/lib/core/designsystem/molecules/buttons/primary_button.dart b/lib/core/designsystem/molecules/buttons/primary_button.dart index 87ac8e3..9ecf1ba 100644 --- a/lib/core/designsystem/molecules/buttons/primary_button.dart +++ b/lib/core/designsystem/molecules/buttons/primary_button.dart @@ -4,6 +4,7 @@ import 'package:mibook/core/designsystem/atoms/colors.dart'; class PrimaryButton extends StatelessWidget { final String title; final bool isEnabled; + final bool isExpanded; final bool isLoading; final VoidCallback onPressed; @@ -11,36 +12,47 @@ class PrimaryButton extends StatelessWidget { super.key, required this.title, this.isEnabled = true, + this.isExpanded = false, this.isLoading = false, required this.onPressed, }); @override Widget build(BuildContext context) { - return SizedBox( - height: 48, - child: ElevatedButton( - onPressed: isEnabled ? onPressed : null, - style: ElevatedButton.styleFrom( - backgroundColor: isEnabled ? primary : disabled, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - minimumSize: const Size(80, 48), + final button = ElevatedButton( + onPressed: isEnabled ? onPressed : null, + style: ElevatedButton.styleFrom( + backgroundColor: isEnabled ? primary : disabled, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), - child: isLoading - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - color: Colors.white, - ), - ) - : Text(title), + minimumSize: const Size(80, 48), ), + child: isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + ), + ) + : Text(title), ); + + if (isExpanded) { + return SizedBox( + height: 48, + width: double.infinity, + child: button, + ); + } else { + return SizedBox( + height: 48, + child: button, + ); + } } } diff --git a/lib/core/designsystem/molecules/buttons/secondary_button.dart b/lib/core/designsystem/molecules/buttons/secondary_button.dart new file mode 100644 index 0000000..7937c68 --- /dev/null +++ b/lib/core/designsystem/molecules/buttons/secondary_button.dart @@ -0,0 +1 @@ +g \ No newline at end of file diff --git a/lib/core/designsystem/molecules/indicators/progress_stepper.dart b/lib/core/designsystem/molecules/indicators/progress_stepper.dart new file mode 100644 index 0000000..39f108b --- /dev/null +++ b/lib/core/designsystem/molecules/indicators/progress_stepper.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:mibook/core/designsystem/atoms/colors.dart'; + +class ProgressStepper extends StatelessWidget { + final double progress; + + const ProgressStepper({super.key, required this.progress}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Progress: ${(progress * 100).toStringAsFixed(1)}%'), + const SizedBox(height: 8), + LinearProgressIndicator( + value: progress, + minHeight: 8, + backgroundColor: Colors.grey[300], + valueColor: AlwaysStoppedAnimation(background), + ), + ], + ); + } +} diff --git a/lib/core/designsystem/molecules/inputfields/input_field.dart b/lib/core/designsystem/molecules/inputfields/input_field.dart index 71e1cf3..54edef9 100644 --- a/lib/core/designsystem/molecules/inputfields/input_field.dart +++ b/lib/core/designsystem/molecules/inputfields/input_field.dart @@ -8,6 +8,7 @@ class InputField extends StatelessWidget { final String placeholder; final String? suffixText; final String? prefixText; + final String? errorMessage; final TextInputType? keyboardType; final Function(String) onChanged; @@ -19,6 +20,7 @@ class InputField extends StatelessWidget { this.placeholder = '', this.suffixText, this.prefixText, + this.errorMessage, this.keyboardType, required this.onChanged, }); @@ -39,8 +41,13 @@ class InputField extends StatelessWidget { focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: onBorder), ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: error), + ), + hintText: placeholder, suffixText: suffixText, prefixText: prefixText, + errorText: errorMessage, ), onChanged: onChanged, ), diff --git a/lib/core/di/di.config.dart b/lib/core/di/di.config.dart index 1959414..99bb7cd 100644 --- a/lib/core/di/di.config.dart +++ b/lib/core/di/di.config.dart @@ -27,10 +27,14 @@ import 'package:mibook/layers/domain/repository/search_repository.dart' import 'package:mibook/layers/domain/usecases/get_book_details.dart' as _i814; import 'package:mibook/layers/domain/usecases/search_books.dart' as _i663; import 'package:mibook/layers/domain/usecases/start_reading.dart' as _i369; +import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.dart' + as _i66; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_view_model.dart' as _i46; import 'package:mibook/layers/presentation/screens/booksearch/book_search_view_model.dart' as _i688; +import 'package:mibook/layers/presentation/screens/startreading/start_reading_view_model.dart' + as _i35; extension GetItInjectableX on _i174.GetIt { // initializes the registration of main-scope dependencies inside of GetIt @@ -69,6 +73,9 @@ extension GetItInjectableX on _i174.GetIt { bookId, ), ); + gh.factoryParam<_i35.StartReadingViewModel, _i66.BookDetailsUI, dynamic>( + (book, _) => _i35.StartReadingViewModel(gh<_i369.IStartReading>(), book), + ); gh.factory<_i688.BookSearchViewModel>( () => _i688.BookSearchViewModel(gh<_i663.ISearchBooks>()), ); diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 3771925..9d79f0d 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -18,7 +18,8 @@ class AppRouter extends RootStackRouter { page: SearchRoute.page, children: [ AutoRoute(page: BookSearchRoute.page), - AutoRoute(page: BoolDetailsRoute.page), + AutoRoute(page: BookDetailsRoute.page), + AutoRoute(page: StartReadingRoute.page), ], ), AutoRoute( diff --git a/lib/core/routes/app_router.gr.dart b/lib/core/routes/app_router.gr.dart index 7e0406f..650fdbd 100644 --- a/lib/core/routes/app_router.gr.dart +++ b/lib/core/routes/app_router.gr.dart @@ -9,8 +9,8 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i10; -import 'package:flutter/material.dart' as _i11; +import 'package:auto_route/auto_route.dart' as _i11; +import 'package:flutter/material.dart' as _i12; import 'package:mibook/layers/presentation/navigation/dashboard/dashboard_page.dart' as _i4; import 'package:mibook/layers/presentation/navigation/tabs/home_tab.dart' @@ -19,79 +19,83 @@ import 'package:mibook/layers/presentation/navigation/tabs/objective_tab.dart' as _i6; import 'package:mibook/layers/presentation/navigation/tabs/search_tab.dart' as _i9; -import 'package:mibook/layers/presentation/screens/bookdetails/bool_details_page.dart' - as _i2; -import 'package:mibook/layers/presentation/screens/booksearch/book_search_page.dart' +import 'package:mibook/layers/presentation/screens/bookdetails/book_details_page.dart' as _i1; +import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.dart' + as _i13; +import 'package:mibook/layers/presentation/screens/booksearch/book_search_page.dart' + as _i2; import 'package:mibook/layers/presentation/screens/currentobjective/current_objective_page.dart' as _i3; import 'package:mibook/layers/presentation/screens/onboarding/pre_onboarding_page.dart' as _i7; import 'package:mibook/layers/presentation/screens/readinglist/reading_list_page.dart' as _i8; +import 'package:mibook/layers/presentation/screens/startreading/start_reading_page.dart' + as _i10; /// generated route for -/// [_i1.BookSearchPage] -class BookSearchRoute extends _i10.PageRouteInfo { - const BookSearchRoute({List<_i10.PageRouteInfo>? children}) - : super(BookSearchRoute.name, initialChildren: children); - - static const String name = 'BookSearchRoute'; - - static _i10.PageInfo page = _i10.PageInfo( - name, - builder: (data) { - return const _i1.BookSearchPage(); - }, - ); -} - -/// generated route for -/// [_i2.BoolDetailsPage] -class BoolDetailsRoute extends _i10.PageRouteInfo { - BoolDetailsRoute({ - _i11.Key? key, +/// [_i1.BookDetailsPage] +class BookDetailsRoute extends _i11.PageRouteInfo { + BookDetailsRoute({ + _i12.Key? key, required String id, - List<_i10.PageRouteInfo>? children, + List<_i11.PageRouteInfo>? children, }) : super( - BoolDetailsRoute.name, - args: BoolDetailsRouteArgs(key: key, id: id), + BookDetailsRoute.name, + args: BookDetailsRouteArgs(key: key, id: id), initialChildren: children, ); - static const String name = 'BoolDetailsRoute'; + static const String name = 'BookDetailsRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { - final args = data.argsAs(); - return _i2.BoolDetailsPage(key: args.key, id: args.id); + final args = data.argsAs(); + return _i1.BookDetailsPage(key: args.key, id: args.id); }, ); } -class BoolDetailsRouteArgs { - const BoolDetailsRouteArgs({this.key, required this.id}); +class BookDetailsRouteArgs { + const BookDetailsRouteArgs({this.key, required this.id}); - final _i11.Key? key; + final _i12.Key? key; final String id; @override String toString() { - return 'BoolDetailsRouteArgs{key: $key, id: $id}'; + return 'BookDetailsRouteArgs{key: $key, id: $id}'; } } +/// generated route for +/// [_i2.BookSearchPage] +class BookSearchRoute extends _i11.PageRouteInfo { + const BookSearchRoute({List<_i11.PageRouteInfo>? children}) + : super(BookSearchRoute.name, initialChildren: children); + + static const String name = 'BookSearchRoute'; + + static _i11.PageInfo page = _i11.PageInfo( + name, + builder: (data) { + return const _i2.BookSearchPage(); + }, + ); +} + /// generated route for /// [_i3.CurrentObjectivePage] -class CurrentObjectiveRoute extends _i10.PageRouteInfo { - const CurrentObjectiveRoute({List<_i10.PageRouteInfo>? children}) +class CurrentObjectiveRoute extends _i11.PageRouteInfo { + const CurrentObjectiveRoute({List<_i11.PageRouteInfo>? children}) : super(CurrentObjectiveRoute.name, initialChildren: children); static const String name = 'CurrentObjectiveRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { return const _i3.CurrentObjectivePage(); @@ -101,13 +105,13 @@ class CurrentObjectiveRoute extends _i10.PageRouteInfo { /// generated route for /// [_i4.DashboardPage] -class DashboardRoute extends _i10.PageRouteInfo { - const DashboardRoute({List<_i10.PageRouteInfo>? children}) +class DashboardRoute extends _i11.PageRouteInfo { + const DashboardRoute({List<_i11.PageRouteInfo>? children}) : super(DashboardRoute.name, initialChildren: children); static const String name = 'DashboardRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { return const _i4.DashboardPage(); @@ -117,13 +121,13 @@ class DashboardRoute extends _i10.PageRouteInfo { /// generated route for /// [_i5.HomeTab] -class HomeRoute extends _i10.PageRouteInfo { - const HomeRoute({List<_i10.PageRouteInfo>? children}) +class HomeRoute extends _i11.PageRouteInfo { + const HomeRoute({List<_i11.PageRouteInfo>? children}) : super(HomeRoute.name, initialChildren: children); static const String name = 'HomeRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { return const _i5.HomeTab(); @@ -133,13 +137,13 @@ class HomeRoute extends _i10.PageRouteInfo { /// generated route for /// [_i6.ObjectiveTab] -class ObjectiveRoute extends _i10.PageRouteInfo { - const ObjectiveRoute({List<_i10.PageRouteInfo>? children}) +class ObjectiveRoute extends _i11.PageRouteInfo { + const ObjectiveRoute({List<_i11.PageRouteInfo>? children}) : super(ObjectiveRoute.name, initialChildren: children); static const String name = 'ObjectiveRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { return const _i6.ObjectiveTab(); @@ -149,13 +153,13 @@ class ObjectiveRoute extends _i10.PageRouteInfo { /// generated route for /// [_i7.PreOnboardingPage] -class PreOnboardingRoute extends _i10.PageRouteInfo { - const PreOnboardingRoute({List<_i10.PageRouteInfo>? children}) +class PreOnboardingRoute extends _i11.PageRouteInfo { + const PreOnboardingRoute({List<_i11.PageRouteInfo>? children}) : super(PreOnboardingRoute.name, initialChildren: children); static const String name = 'PreOnboardingRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { return const _i7.PreOnboardingPage(); @@ -165,13 +169,13 @@ class PreOnboardingRoute extends _i10.PageRouteInfo { /// generated route for /// [_i8.ReadingListPage] -class ReadingListRoute extends _i10.PageRouteInfo { - const ReadingListRoute({List<_i10.PageRouteInfo>? children}) +class ReadingListRoute extends _i11.PageRouteInfo { + const ReadingListRoute({List<_i11.PageRouteInfo>? children}) : super(ReadingListRoute.name, initialChildren: children); static const String name = 'ReadingListRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { return const _i8.ReadingListPage(); @@ -181,16 +185,53 @@ class ReadingListRoute extends _i10.PageRouteInfo { /// generated route for /// [_i9.SearchTab] -class SearchRoute extends _i10.PageRouteInfo { - const SearchRoute({List<_i10.PageRouteInfo>? children}) +class SearchRoute extends _i11.PageRouteInfo { + const SearchRoute({List<_i11.PageRouteInfo>? children}) : super(SearchRoute.name, initialChildren: children); static const String name = 'SearchRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { return const _i9.SearchTab(); }, ); } + +/// generated route for +/// [_i10.StartReadingPage] +class StartReadingRoute extends _i11.PageRouteInfo { + StartReadingRoute({ + _i12.Key? key, + required _i13.BookDetailsUI book, + List<_i11.PageRouteInfo>? children, + }) : super( + StartReadingRoute.name, + args: StartReadingRouteArgs(key: key, book: book), + initialChildren: children, + ); + + static const String name = 'StartReadingRoute'; + + static _i11.PageInfo page = _i11.PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return _i10.StartReadingPage(key: args.key, book: args.book); + }, + ); +} + +class StartReadingRouteArgs { + const StartReadingRouteArgs({this.key, required this.book}); + + final _i12.Key? key; + + final _i13.BookDetailsUI book; + + @override + String toString() { + return 'StartReadingRouteArgs{key: $key, book: $book}'; + } +} diff --git a/lib/core/utils/strings.dart b/lib/core/utils/strings.dart index 8166bd2..24cf561 100644 --- a/lib/core/utils/strings.dart +++ b/lib/core/utils/strings.dart @@ -7,3 +7,5 @@ const percent = '%'; const page = 'Page'; const progress = 'Progress'; const confirm = 'Confirm'; +const progressErrorMessage = 'You can t enter more pages than the book maximum'; +const zero = '0'; diff --git a/lib/layers/data/models/book_list_data.dart b/lib/layers/data/models/book_list_data.dart index 8b49b68..3171981 100644 --- a/lib/layers/data/models/book_list_data.dart +++ b/lib/layers/data/models/book_list_data.dart @@ -6,7 +6,7 @@ part 'book_list_data.g.dart'; @JsonSerializable() class BookListData { final String kind; - final int totalItems; + final int? totalItems; @JsonKey(defaultValue: []) final List items; @@ -23,7 +23,7 @@ class BookListData { BookListDomain toDomain() { return BookListDomain( - totalItems: totalItems, + totalItems: totalItems ?? 0, books: items.map((item) => item.toDomain()).toList(), ); } @@ -64,7 +64,7 @@ class BookItem { authors: volumeInfo.authors, description: volumeInfo.description, thumbnail: volumeInfo.imageLinks?.thumbnail, - pageCount: volumeInfo.pageCount, + pageCount: volumeInfo.pageCount ?? 100, ); } } @@ -79,7 +79,7 @@ class VolumeInfo { final String? description; final List? industryIdentifiers; final ReadingModes? readingModes; - final int pageCount; + final int? pageCount; final String? printType; final List? categories; final double? averageRating; @@ -234,7 +234,7 @@ class Price { @JsonSerializable() class Offer { - final int finskyOfferType; + final int? finskyOfferType; final Price? listPrice; final Price? retailPrice; final bool? giftable; diff --git a/lib/layers/data/models/book_list_data.g.dart b/lib/layers/data/models/book_list_data.g.dart index cd2940d..295b723 100644 --- a/lib/layers/data/models/book_list_data.g.dart +++ b/lib/layers/data/models/book_list_data.g.dart @@ -8,7 +8,7 @@ part of 'book_list_data.dart'; BookListData _$BookListDataFromJson(Map json) => BookListData( kind: json['kind'] as String, - totalItems: (json['totalItems'] as num).toInt(), + totalItems: (json['totalItems'] as num?)?.toInt(), items: (json['items'] as List?) ?.map((e) => BookItem.fromJson(e as Map)) @@ -62,7 +62,7 @@ VolumeInfo _$VolumeInfoFromJson(Map json) => VolumeInfo( readingModes: json['readingModes'] == null ? null : ReadingModes.fromJson(json['readingModes'] as Map), - pageCount: (json['pageCount'] as num).toInt(), + pageCount: (json['pageCount'] as num?)?.toInt(), printType: json['printType'] as String?, categories: (json['categories'] as List?) ?.map((e) => e as String) @@ -190,7 +190,7 @@ Map _$PriceToJson(Price instance) => { }; Offer _$OfferFromJson(Map json) => Offer( - finskyOfferType: (json['finskyOfferType'] as num).toInt(), + finskyOfferType: (json['finskyOfferType'] as num?)?.toInt(), listPrice: json['listPrice'] == null ? null : Price.fromJson(json['listPrice'] as Map), diff --git a/lib/layers/presentation/screens/bookdetails/book_details_event.dart b/lib/layers/presentation/screens/bookdetails/book_details_event.dart index 264a989..cc0b1c3 100644 --- a/lib/layers/presentation/screens/bookdetails/book_details_event.dart +++ b/lib/layers/presentation/screens/bookdetails/book_details_event.dart @@ -9,3 +9,8 @@ class DidClickStartReadingEvent extends BookDetailsEvent { final double progress; DidClickStartReadingEvent(this.progress); } + +class DidChangeProgressTextEvent extends BookDetailsEvent { + final int progress; + DidChangeProgressTextEvent(this.progress); +} diff --git a/lib/layers/presentation/screens/bookdetails/bool_details_page.dart b/lib/layers/presentation/screens/bookdetails/book_details_page.dart similarity index 64% rename from lib/layers/presentation/screens/bookdetails/bool_details_page.dart rename to lib/layers/presentation/screens/bookdetails/book_details_page.dart index 020feb5..ef1e94b 100644 --- a/lib/layers/presentation/screens/bookdetails/bool_details_page.dart +++ b/lib/layers/presentation/screens/bookdetails/book_details_page.dart @@ -4,21 +4,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:mibook/core/designsystem/molecules/buttons/primary_button.dart'; +import 'package:mibook/core/designsystem/molecules/indicators/progress_stepper.dart'; import 'package:mibook/core/designsystem/molecules/inputfields/input_field.dart'; import 'package:mibook/core/designsystem/organisms/app_nav_bar.dart'; import 'package:mibook/core/designsystem/organisms/list_item.dart'; import 'package:mibook/core/di/di.dart'; -import 'package:mibook/core/utils/strings.dart'; +import 'package:mibook/core/routes/app_router.gr.dart'; +import 'package:mibook/core/utils/strings.dart' as strings; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_state.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_view_model.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_event.dart'; @RoutePage() -class BoolDetailsPage extends StatelessWidget { +class BookDetailsPage extends StatelessWidget { final String id; - const BoolDetailsPage({ + const BookDetailsPage({ super.key, required this.id, }); @@ -61,6 +63,7 @@ class _BookDetailsContent extends StatelessWidget { @override Widget build(BuildContext context) { + final viewModel = context.read(); return SingleChildScrollView( child: Column( children: [ @@ -72,6 +75,8 @@ class _BookDetailsContent extends StatelessWidget { return Center(child: Text('Error: ${state.errorMessage}')); } else if (state.bookDetails != null) { final book = state.bookDetails!; + final bookProgress = state.bookProgress; + return Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -107,63 +112,14 @@ class _BookDetailsContent extends StatelessWidget { ), const SizedBox(height: 24), PrimaryButton( - title: startReading, - onPressed: () => showGeneralDialog( - context: context, - barrierDismissible: - true, // allows tap outside to close - barrierLabel: "Dismiss", - barrierColor: Colors.black54, // dim background - transitionDuration: const Duration(milliseconds: 300), - pageBuilder: (context, anim1, anim2) { - // Must return a full-screen widget so barrier can detect taps - return SafeArea( - child: Builder( - builder: (context) { - return Center( - child: Material( - color: Colors.transparent, - child: AlertDialog( - backgroundColor: Colors.white, - insetPadding: - EdgeInsets.zero, // remove margin - contentPadding: EdgeInsets - .zero, // remove inner padding if needed - content: SizedBox( - width: - MediaQuery.of( - context, - ).size.width - - 32, - child: _StartReadingDialogContent( - book: book, - onClickStartReading: - (double progress) { - Navigator.of(context).pop(); - }, - ), - ), - ), - ), - ); - }, - ), - ); - }, - transitionBuilder: (context, anim1, anim2, child) { - // Slide + fade - return SlideTransition( - position: Tween( - begin: const Offset(0, 0.2), // from bottom - end: Offset.zero, - ).animate(anim1), - child: FadeTransition( - opacity: anim1, - child: child, - ), + title: strings.startReading, + onPressed: () { + if (state.bookDetails != null) { + context.router.push( + StartReadingRoute(book: state.bookDetails!), ); - }, - ), + } + }, ), const SizedBox(height: 32), ], @@ -182,16 +138,21 @@ class _BookDetailsContent extends StatelessWidget { class _StartReadingDialogContent extends StatelessWidget { final BookDetailsUI book; + final double progress; final TextEditingController _controller = TextEditingController(); + final Function(String) onChangeProgressText; final Function(double) onClickStartReading; _StartReadingDialogContent({ required this.book, + required this.progress, + required this.onChangeProgressText, required this.onClickStartReading, }); @override Widget build(BuildContext context) { + debugPrint('Progress $progress'); return Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -219,14 +180,15 @@ class _StartReadingDialogContent extends StatelessWidget { const SizedBox(height: 24), InputField( keyboardType: TextInputType.number, - label: progress, + label: strings.progress, controller: _controller, - onChanged: (_) {}, - prefixText: '$page ', + onChanged: onChangeProgressText, ), const SizedBox(height: 24), + ProgressStepper(progress: progress), + const SizedBox(height: 24), PrimaryButton( - title: confirm, + title: strings.confirm, onPressed: () {}, ), ], diff --git a/lib/layers/presentation/screens/bookdetails/book_details_state.dart b/lib/layers/presentation/screens/bookdetails/book_details_state.dart index 2e7a4aa..841c49f 100644 --- a/lib/layers/presentation/screens/bookdetails/book_details_state.dart +++ b/lib/layers/presentation/screens/bookdetails/book_details_state.dart @@ -10,16 +10,20 @@ class BookDetailsState with _$BookDetailsState { final String? errorMessage; @override final BookDetailsUI? bookDetails; + @override + final double bookProgress; BookDetailsState( this.errorMessage, this.bookDetails, { required this.isLoading, + required this.bookProgress, }); factory BookDetailsState.initial() => BookDetailsState( null, null, isLoading: false, + bookProgress: 0.0, ); } diff --git a/lib/layers/presentation/screens/bookdetails/book_details_state.freezed.dart b/lib/layers/presentation/screens/bookdetails/book_details_state.freezed.dart new file mode 100644 index 0000000..20619d4 --- /dev/null +++ b/lib/layers/presentation/screens/bookdetails/book_details_state.freezed.dart @@ -0,0 +1,202 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// 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 'book_details_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$BookDetailsState { + + bool get isLoading; String? get errorMessage; BookDetailsUI? get bookDetails; double get bookProgress; +/// Create a copy of BookDetailsState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BookDetailsStateCopyWith get copyWith => _$BookDetailsStateCopyWithImpl(this as BookDetailsState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BookDetailsState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.bookDetails, bookDetails) || other.bookDetails == bookDetails)&&(identical(other.bookProgress, bookProgress) || other.bookProgress == bookProgress)); +} + + +@override +int get hashCode => Object.hash(runtimeType,isLoading,errorMessage,bookDetails,bookProgress); + +@override +String toString() { + return 'BookDetailsState(isLoading: $isLoading, errorMessage: $errorMessage, bookDetails: $bookDetails, bookProgress: $bookProgress)'; +} + + +} + +/// @nodoc +abstract mixin class $BookDetailsStateCopyWith<$Res> { + factory $BookDetailsStateCopyWith(BookDetailsState value, $Res Function(BookDetailsState) _then) = _$BookDetailsStateCopyWithImpl; +@useResult +$Res call({ + String? errorMessage, BookDetailsUI? bookDetails, bool isLoading, double bookProgress +}); + + + + +} +/// @nodoc +class _$BookDetailsStateCopyWithImpl<$Res> + implements $BookDetailsStateCopyWith<$Res> { + _$BookDetailsStateCopyWithImpl(this._self, this._then); + + final BookDetailsState _self; + final $Res Function(BookDetailsState) _then; + +/// Create a copy of BookDetailsState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? errorMessage = freezed,Object? bookDetails = freezed,Object? isLoading = null,Object? bookProgress = null,}) { + return _then(BookDetailsState( +freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable +as String?,freezed == bookDetails ? _self.bookDetails : bookDetails // ignore: cast_nullable_to_non_nullable +as BookDetailsUI?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable +as bool,bookProgress: null == bookProgress ? _self.bookProgress : bookProgress // ignore: cast_nullable_to_non_nullable +as double, + )); +} + +} + + +/// Adds pattern-matching-related methods to [BookDetailsState]. +extension BookDetailsStatePatterns on BookDetailsState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(){ +final _that = this; +switch (_that) { +case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(){ +final _that = this; +switch (_that) { +case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({required TResult orElse(),}) {final _that = this; +switch (_that) { +case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when() {final _that = this; +switch (_that) { +case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull() {final _that = this; +switch (_that) { +case _: + return null; + +} +} + +} + +// dart format on diff --git a/lib/layers/presentation/screens/bookdetails/book_details_view_model.dart b/lib/layers/presentation/screens/bookdetails/book_details_view_model.dart index 834c0be..e89c06d 100644 --- a/lib/layers/presentation/screens/bookdetails/book_details_view_model.dart +++ b/lib/layers/presentation/screens/bookdetails/book_details_view_model.dart @@ -43,6 +43,13 @@ class BookDetailsViewModel extends Bloc { } }); on((event, emit) async {}); + on((event, emit) { + final pageCount = state.bookDetails?.pageCount; + if (pageCount != null) { + final progress = event.progress.toDouble() / pageCount.toDouble(); + emit(state.copyWith(bookProgress: progress)); + } + }); } Future loadBookDetails() async { diff --git a/lib/layers/presentation/screens/booksearch/book_search_page.dart b/lib/layers/presentation/screens/booksearch/book_search_page.dart index 4ca0d02..15ff8d6 100644 --- a/lib/layers/presentation/screens/booksearch/book_search_page.dart +++ b/lib/layers/presentation/screens/booksearch/book_search_page.dart @@ -110,7 +110,7 @@ class _SearchScaffold extends StatelessWidget { ), child: ListItem( onTap: () => context.router.push( - BoolDetailsRoute(id: book.id), + BookDetailsRoute(id: book.id), ), input: BookItemInput( id: book.id, diff --git a/lib/layers/presentation/screens/startreading/start_reading_event.dart b/lib/layers/presentation/screens/startreading/start_reading_event.dart new file mode 100644 index 0000000..a5a0be4 --- /dev/null +++ b/lib/layers/presentation/screens/startreading/start_reading_event.dart @@ -0,0 +1,8 @@ +abstract class StartReadingEvent {} + +class DidEditProgressEvent extends StartReadingEvent { + int progress; + DidEditProgressEvent({required this.progress}); +} + +class DidClickConfirmEvent extends StartReadingEvent {} diff --git a/lib/layers/presentation/screens/startreading/start_reading_page.dart b/lib/layers/presentation/screens/startreading/start_reading_page.dart new file mode 100644 index 0000000..93cb2d8 --- /dev/null +++ b/lib/layers/presentation/screens/startreading/start_reading_page.dart @@ -0,0 +1,121 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mibook/core/designsystem/molecules/buttons/primary_button.dart'; +import 'package:mibook/core/designsystem/molecules/indicators/progress_stepper.dart'; +import 'package:mibook/core/designsystem/molecules/inputfields/input_field.dart'; +import 'package:mibook/core/designsystem/organisms/app_nav_bar.dart'; +import 'package:mibook/core/di/di.dart'; +import 'package:mibook/core/utils/strings.dart'; +import 'package:mibook/core/utils/strings.dart' as strings; +import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.dart'; +import 'package:mibook/layers/presentation/screens/startreading/start_reading_event.dart'; +import 'package:mibook/layers/presentation/screens/startreading/start_reading_state.dart'; +import 'package:mibook/layers/presentation/screens/startreading/start_reading_view_model.dart'; + +@RoutePage() +class StartReadingPage extends StatelessWidget { + final BookDetailsUI book; + + const StartReadingPage({ + super.key, + required this.book, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: getIt(param1: book), + child: _StartReadingScaffold(), + ); + } +} + +class _StartReadingScaffold extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppNavBar( + titleText: startReading, + onBack: context.router.maybePop, + ), + body: BlocBuilder( + builder: (context, state) { + final viewModel = context.read(); + return _StartReadingContent( + book: viewModel.book, + progress: state.progress, + errorMessage: state.inputErrorMessage, + onChangeProgressText: (progress) { + viewModel.add( + DidEditProgressEvent(progress: int.tryParse(progress) ?? 0), + ); + }, + onClickStartReading: () { + viewModel.add(DidClickConfirmEvent()); + }, + ); + }, + ), + ); + } +} + +class _StartReadingContent extends StatelessWidget { + final BookDetailsUI book; + final double progress; + final String? errorMessage; + final TextEditingController _controller = TextEditingController(); + final Function(String) onChangeProgressText; + final Function onClickStartReading; + + _StartReadingContent({ + required this.book, + required this.progress, + required this.errorMessage, + required this.onChangeProgressText, + required this.onClickStartReading, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (book.thumbnail != null) + CachedNetworkImage(imageUrl: book.thumbnail!), + const SizedBox(height: 16), + Text( + book.title, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text('Maximum pages: ${book.pageCount}'), + const SizedBox(height: 24), + InputField( + keyboardType: TextInputType.number, + label: strings.progress, + controller: _controller, + onChanged: onChangeProgressText, + placeholder: strings.zero, + errorMessage: errorMessage, + ), + const SizedBox(height: 24), + ProgressStepper(progress: progress), + const Spacer(), + PrimaryButton( + title: strings.confirm, + isExpanded: true, + onPressed: () {}, + ), + ], + ), + ); + } +} diff --git a/lib/layers/presentation/screens/startreading/start_reading_state.dart b/lib/layers/presentation/screens/startreading/start_reading_state.dart new file mode 100644 index 0000000..4036d73 --- /dev/null +++ b/lib/layers/presentation/screens/startreading/start_reading_state.dart @@ -0,0 +1,24 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'start_reading_state.freezed.dart'; + +@freezed +class StartReadingState with _$StartReadingState { + @override + final String? inputErrorMessage; + @override + final double progress; + @override + final bool shouldNavigateBack; + + StartReadingState({ + required this.inputErrorMessage, + required this.progress, + required this.shouldNavigateBack, + }); + + static StartReadingState get initial => StartReadingState( + inputErrorMessage: null, + progress: 0.0, + shouldNavigateBack: false, + ); +} diff --git a/lib/layers/presentation/screens/startreading/start_reading_state.freezed.dart b/lib/layers/presentation/screens/startreading/start_reading_state.freezed.dart new file mode 100644 index 0000000..52af551 --- /dev/null +++ b/lib/layers/presentation/screens/startreading/start_reading_state.freezed.dart @@ -0,0 +1,201 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// 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 'start_reading_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$StartReadingState { + + String? get inputErrorMessage; double get progress; bool get shouldNavigateBack; +/// Create a copy of StartReadingState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$StartReadingStateCopyWith get copyWith => _$StartReadingStateCopyWithImpl(this as StartReadingState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is StartReadingState&&(identical(other.inputErrorMessage, inputErrorMessage) || other.inputErrorMessage == inputErrorMessage)&&(identical(other.progress, progress) || other.progress == progress)&&(identical(other.shouldNavigateBack, shouldNavigateBack) || other.shouldNavigateBack == shouldNavigateBack)); +} + + +@override +int get hashCode => Object.hash(runtimeType,inputErrorMessage,progress,shouldNavigateBack); + +@override +String toString() { + return 'StartReadingState(inputErrorMessage: $inputErrorMessage, progress: $progress, shouldNavigateBack: $shouldNavigateBack)'; +} + + +} + +/// @nodoc +abstract mixin class $StartReadingStateCopyWith<$Res> { + factory $StartReadingStateCopyWith(StartReadingState value, $Res Function(StartReadingState) _then) = _$StartReadingStateCopyWithImpl; +@useResult +$Res call({ + String? inputErrorMessage, double progress, bool shouldNavigateBack +}); + + + + +} +/// @nodoc +class _$StartReadingStateCopyWithImpl<$Res> + implements $StartReadingStateCopyWith<$Res> { + _$StartReadingStateCopyWithImpl(this._self, this._then); + + final StartReadingState _self; + final $Res Function(StartReadingState) _then; + +/// Create a copy of StartReadingState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? inputErrorMessage = freezed,Object? progress = null,Object? shouldNavigateBack = null,}) { + return _then(StartReadingState( +inputErrorMessage: freezed == inputErrorMessage ? _self.inputErrorMessage : inputErrorMessage // ignore: cast_nullable_to_non_nullable +as String?,progress: null == progress ? _self.progress : progress // ignore: cast_nullable_to_non_nullable +as double,shouldNavigateBack: null == shouldNavigateBack ? _self.shouldNavigateBack : shouldNavigateBack // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [StartReadingState]. +extension StartReadingStatePatterns on StartReadingState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(){ +final _that = this; +switch (_that) { +case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(){ +final _that = this; +switch (_that) { +case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({required TResult orElse(),}) {final _that = this; +switch (_that) { +case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when() {final _that = this; +switch (_that) { +case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull() {final _that = this; +switch (_that) { +case _: + return null; + +} +} + +} + +// dart format on diff --git a/lib/layers/presentation/screens/startreading/start_reading_view_model.dart b/lib/layers/presentation/screens/startreading/start_reading_view_model.dart new file mode 100644 index 0000000..54cae43 --- /dev/null +++ b/lib/layers/presentation/screens/startreading/start_reading_view_model.dart @@ -0,0 +1,39 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:mibook/core/utils/strings.dart'; +import 'package:mibook/layers/domain/usecases/start_reading.dart'; +import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.dart'; +import 'package:mibook/layers/presentation/screens/startreading/start_reading_event.dart'; +import 'package:mibook/layers/presentation/screens/startreading/start_reading_state.dart'; + +@injectable +class StartReadingViewModel extends Bloc { + final IStartReading _startReading; + final BookDetailsUI book; + + StartReadingViewModel( + this._startReading, + @factoryParam this.book, + ) : super(StartReadingState.initial) { + on((event, emit) { + final progress = event.progress.toDouble() / book.pageCount.toDouble(); + if (progress > 1.0) { + emit( + state.copyWith( + inputErrorMessage: progressErrorMessage, + ), + ); + } else { + emit( + state.copyWith( + progress: progress, + inputErrorMessage: null, + ), + ); + } + }); + on((event, emit) { + // TO DO + }); + } +} From ea8f4d797976430c902d1cfea2e3b037c90a4d26 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Fri, 26 Sep 2025 21:34:02 -0300 Subject: [PATCH 06/17] [feat](pnalvarez): removed radio box and finishing book --- lib/core/designsystem/atoms/colors.dart | 1 + .../molecules/buttons/secondary_button.dart | 68 +- .../molecules/indicators/radio_box.dart | 32 + .../molecules/inputfields/input_field.dart | 43 +- lib/core/utils/strings.dart | 2 + .../startreading/start_reading_event.dart | 2 + .../startreading/start_reading_page.dart | 24 +- .../start_reading_view_model.dart | 58 +- macos/Podfile | 42 + macos/Podfile.lock | 37 + macos/Runner.xcodeproj/project.pbxproj | 801 ++++++++++++++++++ 11 files changed, 1073 insertions(+), 37 deletions(-) create mode 100644 lib/core/designsystem/molecules/indicators/radio_box.dart create mode 100644 macos/Podfile create mode 100644 macos/Podfile.lock create mode 100644 macos/Runner.xcodeproj/project.pbxproj diff --git a/lib/core/designsystem/atoms/colors.dart b/lib/core/designsystem/atoms/colors.dart index ebbd25a..d620da6 100644 --- a/lib/core/designsystem/atoms/colors.dart +++ b/lib/core/designsystem/atoms/colors.dart @@ -6,3 +6,4 @@ const onBorder = Color(0xFF0D5EAF); const onBackground = Color(0xFFC6E0FA); const background = Color(0xFF3D67FF); const error = Color.fromARGB(255, 196, 8, 8); +const primaryText = Color(0xFF110000); diff --git a/lib/core/designsystem/molecules/buttons/secondary_button.dart b/lib/core/designsystem/molecules/buttons/secondary_button.dart index 7937c68..735b4aa 100644 --- a/lib/core/designsystem/molecules/buttons/secondary_button.dart +++ b/lib/core/designsystem/molecules/buttons/secondary_button.dart @@ -1 +1,67 @@ -g \ No newline at end of file +import 'package:flutter/material.dart'; +import 'package:mibook/core/designsystem/atoms/colors.dart'; + +class SecondaryButton extends StatelessWidget { + final String title; + final bool isEnabled; + final bool isExpanded; + final bool isLoading; + final VoidCallback onPressed; + + const SecondaryButton({ + super.key, + required this.title, + this.isEnabled = true, + this.isExpanded = false, + this.isLoading = false, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + final button = OutlinedButton( + onPressed: isEnabled ? onPressed : null, + style: OutlinedButton.styleFrom( + side: BorderSide( + color: isEnabled ? primary : disabled, + width: 1.0, + ), + foregroundColor: isEnabled ? primary : disabled, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + minimumSize: const Size(80, 48), + ), + child: isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + ), + ) + : Text(title), + ); + + if (isExpanded) { + return SizedBox( + height: 48, + width: double.infinity, + child: button, + ); + } else { + return SizedBox( + height: 48, + child: button, + ); + } + } +} diff --git a/lib/core/designsystem/molecules/indicators/radio_box.dart b/lib/core/designsystem/molecules/indicators/radio_box.dart new file mode 100644 index 0000000..0ab17a3 --- /dev/null +++ b/lib/core/designsystem/molecules/indicators/radio_box.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:mibook/core/designsystem/atoms/colors.dart'; + +class RadioBox extends StatelessWidget { + final bool value; + final ValueChanged onChanged; + final String label; + + const RadioBox({ + super.key, + required this.value, + required this.onChanged, + required this.label, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () => onChanged(!value), + child: Row( + children: [ + Checkbox( + fillColor: WidgetStateProperty.all(primary), + value: value, + onChanged: onChanged, + ), + Text(label), + ], + ), + ); + } +} diff --git a/lib/core/designsystem/molecules/inputfields/input_field.dart b/lib/core/designsystem/molecules/inputfields/input_field.dart index 54edef9..f8ffc4e 100644 --- a/lib/core/designsystem/molecules/inputfields/input_field.dart +++ b/lib/core/designsystem/molecules/inputfields/input_field.dart @@ -30,27 +30,34 @@ class InputField extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (label.isNotEmpty) Text(label), + if (label.isNotEmpty) + Text( + label, + style: TextStyle( + color: errorMessage == null ? primaryText : error, + ), + ), const SizedBox(height: 8), - SizedBox( - height: 48, - child: TextField( - keyboardType: keyboardType, - decoration: InputDecoration( - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: onBorder), - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: error), - ), - hintText: placeholder, - suffixText: suffixText, - prefixText: prefixText, - errorText: errorMessage, + TextField( + keyboardType: keyboardType, + decoration: InputDecoration( + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: onBorder), + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: error), + ), + hintText: placeholder, + suffixText: suffixText, + prefixText: prefixText, + errorText: errorMessage, + contentPadding: const EdgeInsets.symmetric( + vertical: 16, // controls height + horizontal: 16, ), - onChanged: onChanged, ), + onChanged: onChanged, ), ], ); diff --git a/lib/core/utils/strings.dart b/lib/core/utils/strings.dart index 24cf561..0be5186 100644 --- a/lib/core/utils/strings.dart +++ b/lib/core/utils/strings.dart @@ -9,3 +9,5 @@ const progress = 'Progress'; const confirm = 'Confirm'; const progressErrorMessage = 'You can t enter more pages than the book maximum'; const zero = '0'; +const finishedBook = 'I finished this book'; +const finishBook = 'Finish Book'; diff --git a/lib/layers/presentation/screens/startreading/start_reading_event.dart b/lib/layers/presentation/screens/startreading/start_reading_event.dart index a5a0be4..013f7cb 100644 --- a/lib/layers/presentation/screens/startreading/start_reading_event.dart +++ b/lib/layers/presentation/screens/startreading/start_reading_event.dart @@ -6,3 +6,5 @@ class DidEditProgressEvent extends StartReadingEvent { } class DidClickConfirmEvent extends StartReadingEvent {} + +class DidClickFinishBookEvent extends StartReadingEvent {} diff --git a/lib/layers/presentation/screens/startreading/start_reading_page.dart b/lib/layers/presentation/screens/startreading/start_reading_page.dart index 93cb2d8..42cc59e 100644 --- a/lib/layers/presentation/screens/startreading/start_reading_page.dart +++ b/lib/layers/presentation/screens/startreading/start_reading_page.dart @@ -3,7 +3,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mibook/core/designsystem/molecules/buttons/primary_button.dart'; +import 'package:mibook/core/designsystem/molecules/buttons/secondary_button.dart'; import 'package:mibook/core/designsystem/molecules/indicators/progress_stepper.dart'; +import 'package:mibook/core/designsystem/molecules/indicators/radio_box.dart'; import 'package:mibook/core/designsystem/molecules/inputfields/input_field.dart'; import 'package:mibook/core/designsystem/organisms/app_nav_bar.dart'; import 'package:mibook/core/di/di.dart'; @@ -43,6 +45,9 @@ class _StartReadingScaffold extends StatelessWidget { body: BlocBuilder( builder: (context, state) { final viewModel = context.read(); + if (state.shouldNavigateBack) { + context.router.maybePop(); + } return _StartReadingContent( book: viewModel.book, progress: state.progress, @@ -55,6 +60,9 @@ class _StartReadingScaffold extends StatelessWidget { onClickStartReading: () { viewModel.add(DidClickConfirmEvent()); }, + onClickFinishBook: () { + viewModel.add(DidClickFinishBookEvent()); + }, ); }, ), @@ -69,6 +77,7 @@ class _StartReadingContent extends StatelessWidget { final TextEditingController _controller = TextEditingController(); final Function(String) onChangeProgressText; final Function onClickStartReading; + final Function onClickFinishBook; _StartReadingContent({ required this.book, @@ -76,6 +85,7 @@ class _StartReadingContent extends StatelessWidget { required this.errorMessage, required this.onChangeProgressText, required this.onClickStartReading, + required this.onClickFinishBook, }); @override @@ -108,11 +118,23 @@ class _StartReadingContent extends StatelessWidget { ), const SizedBox(height: 24), ProgressStepper(progress: progress), + const SizedBox( + height: 24, + ), const Spacer(), PrimaryButton( title: strings.confirm, + isEnabled: errorMessage == null, + isExpanded: true, + onPressed: onClickStartReading as VoidCallback, + ), + const SizedBox( + height: 16, + ), + SecondaryButton( + title: finishBook, isExpanded: true, - onPressed: () {}, + onPressed: onClickFinishBook as VoidCallback, ), ], ), diff --git a/lib/layers/presentation/screens/startreading/start_reading_view_model.dart b/lib/layers/presentation/screens/startreading/start_reading_view_model.dart index 54cae43..c8f1d7e 100644 --- a/lib/layers/presentation/screens/startreading/start_reading_view_model.dart +++ b/lib/layers/presentation/screens/startreading/start_reading_view_model.dart @@ -1,6 +1,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; import 'package:mibook/core/utils/strings.dart'; +import 'package:mibook/layers/domain/models/reading_domain.dart'; import 'package:mibook/layers/domain/usecases/start_reading.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.dart'; import 'package:mibook/layers/presentation/screens/startreading/start_reading_event.dart'; @@ -15,25 +16,48 @@ class StartReadingViewModel extends Bloc { this._startReading, @factoryParam this.book, ) : super(StartReadingState.initial) { + // Handle DidEditProgress Event on((event, emit) { - final progress = event.progress.toDouble() / book.pageCount.toDouble(); - if (progress > 1.0) { - emit( - state.copyWith( - inputErrorMessage: progressErrorMessage, - ), - ); - } else { - emit( - state.copyWith( - progress: progress, - inputErrorMessage: null, - ), - ); - } + emit(_didEditProgress(event)); }); - on((event, emit) { - // TO DO + // Handle DidClickConfirm Event + on((event, emit) async { + final response = await _didClickConfirm(); + emit(response); }); + // Handle DidFinishBook Event + on((event, emit) async { + final response = await _didClickFinishBook(); + emit(response); + }); + } + + // Event Handler to DidEditProgressEvent + StartReadingState _didEditProgress(DidEditProgressEvent event) { + final progress = event.progress.toDouble() / book.pageCount.toDouble(); + if (progress > 1.0) { + return state.copyWith( + inputErrorMessage: progressErrorMessage, + ); + } else { + return state.copyWith( + progress: progress, + inputErrorMessage: null, + ); + } + } + + // Event Handler to DidClickConfirm Event + Future _didClickConfirm() async => + _handleStartReading(state.progress); + + // Event Handler to DidClickFinishBook Event + Future _didClickFinishBook() async => + _handleStartReading(1.0); + + Future _handleStartReading(double progress) async { + final reading = ReadingDomain(bookId: book.id, progress: progress); + await _startReading(reading: reading); + return state.copyWith(shouldNavigateBack: true); } } diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..8b72aeb --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,37 @@ +PODS: + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + sqflite_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin + +SPEC CHECKSUMS: + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.15.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..6e789d1 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 86598D225E2B3C9C652FB981 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1C44362B8606DEA4742800A /* Pods_Runner.framework */; }; + C5DD5E3E437B30F2BB9B6CBD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A51812E6CA24747E4C5F14F /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* mibook.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mibook.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 61747C17440559CA3799386A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 7A51812E6CA24747E4C5F14F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 9F8C8FE9A8D09F432132BDFB /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + A1C44362B8606DEA4742800A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A87E62B5834E483BD7947E1F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + C7AA65BE03122A7CE76B0214 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + CBE43B9703A2C6829E30C67E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + FDC70B5307628862A4C1C6D0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C5DD5E3E437B30F2BB9B6CBD /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 86598D225E2B3C9C652FB981 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 3FE533F525665BFD9FA8A471 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* mibook.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 3FE533F525665BFD9FA8A471 /* Pods */ = { + isa = PBXGroup; + children = ( + CBE43B9703A2C6829E30C67E /* Pods-Runner.debug.xcconfig */, + FDC70B5307628862A4C1C6D0 /* Pods-Runner.release.xcconfig */, + C7AA65BE03122A7CE76B0214 /* Pods-Runner.profile.xcconfig */, + 61747C17440559CA3799386A /* Pods-RunnerTests.debug.xcconfig */, + 9F8C8FE9A8D09F432132BDFB /* Pods-RunnerTests.release.xcconfig */, + A87E62B5834E483BD7947E1F /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A1C44362B8606DEA4742800A /* Pods_Runner.framework */, + 7A51812E6CA24747E4C5F14F /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 7512FB06EA385FF9B3AB8DB2 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + DC4E08B37EDF9B9FDC1FECDD /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ECC94004D71F30E414DB5311 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* mibook.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 7512FB06EA385FF9B3AB8DB2 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + DC4E08B37EDF9B9FDC1FECDD /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + ECC94004D71F30E414DB5311 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61747C17440559CA3799386A /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.mibook.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mibook.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mibook"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9F8C8FE9A8D09F432132BDFB /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.mibook.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mibook.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mibook"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A87E62B5834E483BD7947E1F /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.mibook.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mibook.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mibook"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} From 6293afdc1865575f957fb940c5e05bb471555414 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Tue, 30 Sep 2025 09:41:23 -0300 Subject: [PATCH 07/17] [test](pnalvarez): start reading feature unit tests --- lib/core/di/app_module.dart | 9 ++ lib/core/di/di.config.dart | 20 +-- .../data/datasource/reading_data_source.dart | 4 +- pubspec.lock | 2 +- pubspec.yaml | 1 + .../mock_encrypted_shared_preferences.dart | 5 + .../repository/reading_repository_test.dart | 60 +++++++++ .../layers/domain/fakes/fake_book_domain.dart | 1 + test/layers/domain/start_reading_test.dart | 41 +++++++ .../start_reading_view_model_test.dart | 116 ++++++++++++++++++ 10 files changed, 249 insertions(+), 10 deletions(-) create mode 100644 lib/core/di/app_module.dart create mode 100644 test/layers/data/mocks/mock_encrypted_shared_preferences.dart create mode 100644 test/layers/data/repository/reading_repository_test.dart create mode 100644 test/layers/domain/start_reading_test.dart create mode 100644 test/layers/presentation/viewmodel/start_reading_view_model_test.dart diff --git a/lib/core/di/app_module.dart b/lib/core/di/app_module.dart new file mode 100644 index 0000000..b6c1486 --- /dev/null +++ b/lib/core/di/app_module.dart @@ -0,0 +1,9 @@ +import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; +import 'package:injectable/injectable.dart'; + +@module +abstract class AppModule { + @lazySingleton + EncryptedSharedPreferences get encryptedSharedPreferences => + EncryptedSharedPreferences(); +} diff --git a/lib/core/di/di.config.dart b/lib/core/di/di.config.dart index 99bb7cd..f92a118 100644 --- a/lib/core/di/di.config.dart +++ b/lib/core/di/di.config.dart @@ -9,19 +9,18 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart' + as _i847; import 'package:get_it/get_it.dart' as _i174; import 'package:injectable/injectable.dart' as _i526; +import 'package:mibook/core/di/app_module.dart' as _i516; import 'package:mibook/layers/data/api/api_client.dart' as _i721; import 'package:mibook/layers/data/datasource/reading_data_source.dart' as _i44; import 'package:mibook/layers/data/datasource/search_data_source.dart' as _i400; -<<<<<<< HEAD -import 'package:mibook/layers/data/repository/search_repository.dart' as _i967; -======= import 'package:mibook/layers/data/repository/reading_repository.dart' as _i600; import 'package:mibook/layers/data/repository/search_repository.dart' as _i967; import 'package:mibook/layers/domain/repository/reading_repository.dart' as _i649; ->>>>>>> 7741511 ([feat](pnalvarez): displaying start reading dialog) import 'package:mibook/layers/domain/repository/search_repository.dart' as _i303; import 'package:mibook/layers/domain/usecases/get_book_details.dart' as _i814; @@ -43,8 +42,14 @@ extension GetItInjectableX on _i174.GetIt { _i526.EnvironmentFilter? environmentFilter, }) { final gh = _i526.GetItHelper(this, environment, environmentFilter); + final appModule = _$AppModule(); + gh.lazySingleton<_i847.EncryptedSharedPreferences>( + () => appModule.encryptedSharedPreferences, + ); gh.factory<_i721.IApiClient>(() => _i721.ApiClient()); - gh.factory<_i44.IReadingDataSource>(() => _i44.ReadingDataSource()); + gh.factory<_i44.IReadingDataSource>( + () => _i44.ReadingDataSource(gh<_i847.EncryptedSharedPreferences>()), + ); gh.factory<_i400.ISearchDataSource>( () => _i400.SearchDataSource(gh<_i721.IApiClient>()), ); @@ -53,12 +58,9 @@ extension GetItInjectableX on _i174.GetIt { ); gh.factory<_i303.ISearchRepository>( () => _i967.SearchRepository(gh<_i400.ISearchDataSource>()), -<<<<<<< HEAD -======= ); gh.factory<_i369.IStartReading>( () => _i369.StartReading(gh<_i649.IReadingRepository>()), ->>>>>>> 7741511 ([feat](pnalvarez): displaying start reading dialog) ); gh.factory<_i663.ISearchBooks>( () => _i663.SearchBooks(gh<_i303.ISearchRepository>()), @@ -82,3 +84,5 @@ extension GetItInjectableX on _i174.GetIt { return this; } } + +class _$AppModule extends _i516.AppModule {} diff --git a/lib/layers/data/datasource/reading_data_source.dart b/lib/layers/data/datasource/reading_data_source.dart index a9d9228..937d9b0 100644 --- a/lib/layers/data/datasource/reading_data_source.dart +++ b/lib/layers/data/datasource/reading_data_source.dart @@ -15,7 +15,9 @@ abstract class IReadingDataSource { @Injectable(as: IReadingDataSource) class ReadingDataSource implements IReadingDataSource { - final _storage = EncryptedSharedPreferences(); + final EncryptedSharedPreferences _storage; + + ReadingDataSource(this._storage); @override Future startReading({ diff --git a/pubspec.lock b/pubspec.lock index 3058123..049542f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -753,7 +753,7 @@ packages: source: hosted version: "0.28.0" shared_preferences: - dependency: transitive + dependency: "direct main" description: name: shared_preferences sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" diff --git a/pubspec.yaml b/pubspec.yaml index 8c40f00..07dee38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: mockito: ^5.4.6 shimmer: ^3.0.0 flutter_html: ^3.0.0 + shared_preferences: ^2.5.3 dev_dependencies: flutter_test: diff --git a/test/layers/data/mocks/mock_encrypted_shared_preferences.dart b/test/layers/data/mocks/mock_encrypted_shared_preferences.dart new file mode 100644 index 0000000..88068db --- /dev/null +++ b/test/layers/data/mocks/mock_encrypted_shared_preferences.dart @@ -0,0 +1,5 @@ +import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; +import 'package:mockito/mockito.dart'; + +class MockEncryptedSharedPreferences extends Mock + implements EncryptedSharedPreferences {} diff --git a/test/layers/data/repository/reading_repository_test.dart b/test/layers/data/repository/reading_repository_test.dart new file mode 100644 index 0000000..8fc0e0a --- /dev/null +++ b/test/layers/data/repository/reading_repository_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mibook/layers/data/datasource/reading_data_source.dart'; +import 'package:mibook/layers/data/models/reading_data.dart'; +import 'package:mibook/layers/data/repository/reading_repository.dart'; +import 'package:mibook/layers/domain/models/reading_domain.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'reading_repository_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + late MockIReadingDataSource mockIReadingDataSource; + late ReadingRepository sut; + + setUp() { + mockIReadingDataSource = MockIReadingDataSource(); + sut = ReadingRepository(mockIReadingDataSource); + } + + group('test ReadingRepository', () { + setUp(); + + test('startReading', () async { + final readingDomain = ReadingDomain(bookId: 'id1', progress: 0.5); + final readingData = ReadingData(bookId: 'id1', progress: 0.5); + + await sut.startReading(reading: readingDomain); + + verify( + mockIReadingDataSource.startReading( + readingData: argThat( + predicate( + (data) => + data.bookId == readingData.bookId && + data.progress == readingData.progress, + ), + named: 'readingData', + ), + ), + ).called(1); + }); + }); + + test('getReadings', () async { + final fakeData = [ + ReadingData(bookId: 'id1', progress: 0.5), + ReadingData(bookId: 'id2', progress: 0.5), + ]; + + when( + mockIReadingDataSource.getReadingData(), + ).thenAnswer((_) async => fakeData); + + final response = await sut.getReadings(); + + verify(mockIReadingDataSource.getReadingData()).called(1); + expect(response.length, 2); + }); +} diff --git a/test/layers/domain/fakes/fake_book_domain.dart b/test/layers/domain/fakes/fake_book_domain.dart index e7aa8a9..a5fad09 100644 --- a/test/layers/domain/fakes/fake_book_domain.dart +++ b/test/layers/domain/fakes/fake_book_domain.dart @@ -7,4 +7,5 @@ final fakeBookDomain = BookDomain( thumbnail: 'thumbnail', authors: [], kind: 'kind', + pageCount: 0, ); diff --git a/test/layers/domain/start_reading_test.dart b/test/layers/domain/start_reading_test.dart new file mode 100644 index 0000000..11c7848 --- /dev/null +++ b/test/layers/domain/start_reading_test.dart @@ -0,0 +1,41 @@ +@GenerateNiceMocks([MockSpec()]) +import 'package:flutter_test/flutter_test.dart'; +import 'package:mibook/layers/domain/models/reading_domain.dart'; +import 'package:mibook/layers/domain/repository/reading_repository.dart'; +import 'package:mibook/layers/domain/usecases/start_reading.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'start_reading_test.mocks.dart'; + +void main() { + late MockIReadingRepository mockIReadingRepository; + late StartReading sut; + + setUp() { + mockIReadingRepository = MockIReadingRepository(); + sut = StartReading(mockIReadingRepository); + } + + group('test StartReading', () { + setUp(); + + test('start', () async { + final readingDomain = ReadingDomain(bookId: 'id1', progress: 0.5); + // when( + // mockIReadingRepository.startReading(reading: readingDomain), + // ).thenAnswer((_) async => Future.value); + await sut(reading: readingDomain); + verify( + mockIReadingRepository.startReading( + reading: argThat( + predicate( + (data) => data.bookId == 'id1' && data.progress == 0.5, + ), + named: 'reading', + ), + ), + ).called(1); + }); + }); +} diff --git a/test/layers/presentation/viewmodel/start_reading_view_model_test.dart b/test/layers/presentation/viewmodel/start_reading_view_model_test.dart new file mode 100644 index 0000000..1e304af --- /dev/null +++ b/test/layers/presentation/viewmodel/start_reading_view_model_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mibook/core/utils/strings.dart'; +import 'package:mibook/layers/domain/usecases/start_reading.dart'; +import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.dart'; +import 'package:mibook/layers/presentation/screens/startreading/start_reading_event.dart'; +import 'package:mibook/layers/presentation/screens/startreading/start_reading_state.dart'; +import 'package:mibook/layers/presentation/screens/startreading/start_reading_view_model.dart'; +import 'package:mockito/annotations.dart'; + +@GenerateNiceMocks([MockSpec()]) +import 'start_reading_view_model_test.mocks.dart'; + +void main() { + late MockIStartReading startReading; + late StartReadingViewModel sut; + final fakeBook = BookDetailsUI( + id: 'id', + kind: 'kind', + title: 'title', + authors: 'authors', + description: 'description', + thumbnail: 'thumbnail', + pageCount: 100, + ); + + setUp() { + startReading = MockIStartReading(); + sut = StartReadingViewModel(startReading, fakeBook); + } + + group('test StartReadingViewModel', () { + setUp(); + test( + 'given progress > 1.0, when add didEditProgressEvent, should state have input error message', + () async { + sut.add(DidEditProgressEvent(progress: 200)); + + await expectLater( + sut.stream, + emits( + predicate( + (state) => + state.inputErrorMessage == progressErrorMessage && + state.progress == 0.0, + ), + ), + ); + }, + ); + + test( + 'given progress <= 1.0, when add didEditProgressEvent, should state have progress', + () async { + sut.add(DidEditProgressEvent(progress: 50)); + + await expectLater( + sut.stream, + emits( + predicate( + (state) => + state.inputErrorMessage == null && state.progress == 0.5, + ), + ), + ); + }, + ); + + test( + 'didClickConfirmEvent', + () async { + sut.emit( + StartReadingState( + inputErrorMessage: null, + progress: 0.5, + shouldNavigateBack: false, + ), + ); + + sut.add(DidClickConfirmEvent()); + + await expectLater( + sut.stream, + emits( + predicate( + (state) => state.shouldNavigateBack, + ), + ), + ); + }, + ); + + test( + 'didFinishBookEvent', + () async { + sut.emit( + StartReadingState( + inputErrorMessage: null, + progress: 0.5, + shouldNavigateBack: false, + ), + ); + + sut.add(DidClickFinishBookEvent()); + + await expectLater( + sut.stream, + emits( + predicate( + (state) => state.shouldNavigateBack, + ), + ), + ); + }, + ); + }); +} From 821911b04cc3458e1738380857b9915e2c743c2c Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:32:47 -0300 Subject: [PATCH 08/17] [refactor](pnalvarez): addressed Github warnings --- .../bookdetails/book_details_page.dart | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/lib/layers/presentation/screens/bookdetails/book_details_page.dart b/lib/layers/presentation/screens/bookdetails/book_details_page.dart index ef1e94b..0015cbb 100644 --- a/lib/layers/presentation/screens/bookdetails/book_details_page.dart +++ b/lib/layers/presentation/screens/bookdetails/book_details_page.dart @@ -63,7 +63,6 @@ class _BookDetailsContent extends StatelessWidget { @override Widget build(BuildContext context) { - final viewModel = context.read(); return SingleChildScrollView( child: Column( children: [ @@ -75,7 +74,6 @@ class _BookDetailsContent extends StatelessWidget { return Center(child: Text('Error: ${state.errorMessage}')); } else if (state.bookDetails != null) { final book = state.bookDetails!; - final bookProgress = state.bookProgress; return Padding( padding: const EdgeInsets.all(16.0), @@ -135,64 +133,3 @@ class _BookDetailsContent extends StatelessWidget { ); } } - -class _StartReadingDialogContent extends StatelessWidget { - final BookDetailsUI book; - final double progress; - final TextEditingController _controller = TextEditingController(); - final Function(String) onChangeProgressText; - final Function(double) onClickStartReading; - - _StartReadingDialogContent({ - required this.book, - required this.progress, - required this.onChangeProgressText, - required this.onClickStartReading, - }); - - @override - Widget build(BuildContext context) { - debugPrint('Progress $progress'); - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Align( - alignment: Alignment.topLeft, - child: IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), - ), - if (book.thumbnail != null) - CachedNetworkImage(imageUrl: book.thumbnail!), - const SizedBox(height: 16), - Text( - book.title, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text('Maximum pages: ${book.pageCount}'), - const SizedBox(height: 24), - InputField( - keyboardType: TextInputType.number, - label: strings.progress, - controller: _controller, - onChanged: onChangeProgressText, - ), - const SizedBox(height: 24), - ProgressStepper(progress: progress), - const SizedBox(height: 24), - PrimaryButton( - title: strings.confirm, - onPressed: () {}, - ), - ], - ), - ); - } -} From 69d9d68eeb519727177018004160ec086aa45d0e Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:20:10 -0300 Subject: [PATCH 09/17] removed auto generated file --- .../molecules/buttons/primary_button.dart | 55 ++-- .../designsystem/organisms/app_nav_bar.dart | 9 +- lib/core/routes/app_router.gr.dart | 237 ------------------ pubspec.lock | 8 + pubspec.yaml | 1 + 5 files changed, 38 insertions(+), 272 deletions(-) delete mode 100644 lib/core/routes/app_router.gr.dart diff --git a/lib/core/designsystem/molecules/buttons/primary_button.dart b/lib/core/designsystem/molecules/buttons/primary_button.dart index 9ecf1ba..70e42c6 100644 --- a/lib/core/designsystem/molecules/buttons/primary_button.dart +++ b/lib/core/designsystem/molecules/buttons/primary_button.dart @@ -19,40 +19,31 @@ class PrimaryButton extends StatelessWidget { @override Widget build(BuildContext context) { - final button = ElevatedButton( - onPressed: isEnabled ? onPressed : null, - style: ElevatedButton.styleFrom( - backgroundColor: isEnabled ? primary : disabled, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + return SizedBox( + height: 48, + width: isExpanded ? double.infinity : null, + child: ElevatedButton( + onPressed: isEnabled ? onPressed : null, + style: ElevatedButton.styleFrom( + backgroundColor: isEnabled ? primary : disabled, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + minimumSize: const Size(80, 48), ), - minimumSize: const Size(80, 48), + child: isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + ), + ) + : Text(title), ), - child: isLoading - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - color: Colors.white, - ), - ) - : Text(title), ); - - if (isExpanded) { - return SizedBox( - height: 48, - width: double.infinity, - child: button, - ); - } else { - return SizedBox( - height: 48, - child: button, - ); - } } } diff --git a/lib/core/designsystem/organisms/app_nav_bar.dart b/lib/core/designsystem/organisms/app_nav_bar.dart index 0cf4e27..a8928cc 100644 --- a/lib/core/designsystem/organisms/app_nav_bar.dart +++ b/lib/core/designsystem/organisms/app_nav_bar.dart @@ -1,3 +1,4 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:mibook/core/designsystem/modifiers/display_as_loader.dart'; @@ -67,15 +68,17 @@ class AppNavBar extends StatelessWidget implements PreferredSizeWidget { isLoading: isTitleLoading, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 250), - child: Text( + child: AutoSizeText( titleText, - maxLines: 1, + maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.black, - fontSize: titleText.length > 20 ? 14 : 22, + fontSize: 22, fontWeight: FontWeight.bold, ), + minFontSize: 12, + stepGranularity: 1, ), ), ); diff --git a/lib/core/routes/app_router.gr.dart b/lib/core/routes/app_router.gr.dart deleted file mode 100644 index 650fdbd..0000000 --- a/lib/core/routes/app_router.gr.dart +++ /dev/null @@ -1,237 +0,0 @@ -// dart format width=80 -// GENERATED CODE - DO NOT MODIFY BY HAND - -// ************************************************************************** -// AutoRouterGenerator -// ************************************************************************** - -// ignore_for_file: type=lint -// coverage:ignore-file - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i11; -import 'package:flutter/material.dart' as _i12; -import 'package:mibook/layers/presentation/navigation/dashboard/dashboard_page.dart' - as _i4; -import 'package:mibook/layers/presentation/navigation/tabs/home_tab.dart' - as _i5; -import 'package:mibook/layers/presentation/navigation/tabs/objective_tab.dart' - as _i6; -import 'package:mibook/layers/presentation/navigation/tabs/search_tab.dart' - as _i9; -import 'package:mibook/layers/presentation/screens/bookdetails/book_details_page.dart' - as _i1; -import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.dart' - as _i13; -import 'package:mibook/layers/presentation/screens/booksearch/book_search_page.dart' - as _i2; -import 'package:mibook/layers/presentation/screens/currentobjective/current_objective_page.dart' - as _i3; -import 'package:mibook/layers/presentation/screens/onboarding/pre_onboarding_page.dart' - as _i7; -import 'package:mibook/layers/presentation/screens/readinglist/reading_list_page.dart' - as _i8; -import 'package:mibook/layers/presentation/screens/startreading/start_reading_page.dart' - as _i10; - -/// generated route for -/// [_i1.BookDetailsPage] -class BookDetailsRoute extends _i11.PageRouteInfo { - BookDetailsRoute({ - _i12.Key? key, - required String id, - List<_i11.PageRouteInfo>? children, - }) : super( - BookDetailsRoute.name, - args: BookDetailsRouteArgs(key: key, id: id), - initialChildren: children, - ); - - static const String name = 'BookDetailsRoute'; - - static _i11.PageInfo page = _i11.PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return _i1.BookDetailsPage(key: args.key, id: args.id); - }, - ); -} - -class BookDetailsRouteArgs { - const BookDetailsRouteArgs({this.key, required this.id}); - - final _i12.Key? key; - - final String id; - - @override - String toString() { - return 'BookDetailsRouteArgs{key: $key, id: $id}'; - } -} - -/// generated route for -/// [_i2.BookSearchPage] -class BookSearchRoute extends _i11.PageRouteInfo { - const BookSearchRoute({List<_i11.PageRouteInfo>? children}) - : super(BookSearchRoute.name, initialChildren: children); - - static const String name = 'BookSearchRoute'; - - static _i11.PageInfo page = _i11.PageInfo( - name, - builder: (data) { - return const _i2.BookSearchPage(); - }, - ); -} - -/// generated route for -/// [_i3.CurrentObjectivePage] -class CurrentObjectiveRoute extends _i11.PageRouteInfo { - const CurrentObjectiveRoute({List<_i11.PageRouteInfo>? children}) - : super(CurrentObjectiveRoute.name, initialChildren: children); - - static const String name = 'CurrentObjectiveRoute'; - - static _i11.PageInfo page = _i11.PageInfo( - name, - builder: (data) { - return const _i3.CurrentObjectivePage(); - }, - ); -} - -/// generated route for -/// [_i4.DashboardPage] -class DashboardRoute extends _i11.PageRouteInfo { - const DashboardRoute({List<_i11.PageRouteInfo>? children}) - : super(DashboardRoute.name, initialChildren: children); - - static const String name = 'DashboardRoute'; - - static _i11.PageInfo page = _i11.PageInfo( - name, - builder: (data) { - return const _i4.DashboardPage(); - }, - ); -} - -/// generated route for -/// [_i5.HomeTab] -class HomeRoute extends _i11.PageRouteInfo { - const HomeRoute({List<_i11.PageRouteInfo>? children}) - : super(HomeRoute.name, initialChildren: children); - - static const String name = 'HomeRoute'; - - static _i11.PageInfo page = _i11.PageInfo( - name, - builder: (data) { - return const _i5.HomeTab(); - }, - ); -} - -/// generated route for -/// [_i6.ObjectiveTab] -class ObjectiveRoute extends _i11.PageRouteInfo { - const ObjectiveRoute({List<_i11.PageRouteInfo>? children}) - : super(ObjectiveRoute.name, initialChildren: children); - - static const String name = 'ObjectiveRoute'; - - static _i11.PageInfo page = _i11.PageInfo( - name, - builder: (data) { - return const _i6.ObjectiveTab(); - }, - ); -} - -/// generated route for -/// [_i7.PreOnboardingPage] -class PreOnboardingRoute extends _i11.PageRouteInfo { - const PreOnboardingRoute({List<_i11.PageRouteInfo>? children}) - : super(PreOnboardingRoute.name, initialChildren: children); - - static const String name = 'PreOnboardingRoute'; - - static _i11.PageInfo page = _i11.PageInfo( - name, - builder: (data) { - return const _i7.PreOnboardingPage(); - }, - ); -} - -/// generated route for -/// [_i8.ReadingListPage] -class ReadingListRoute extends _i11.PageRouteInfo { - const ReadingListRoute({List<_i11.PageRouteInfo>? children}) - : super(ReadingListRoute.name, initialChildren: children); - - static const String name = 'ReadingListRoute'; - - static _i11.PageInfo page = _i11.PageInfo( - name, - builder: (data) { - return const _i8.ReadingListPage(); - }, - ); -} - -/// generated route for -/// [_i9.SearchTab] -class SearchRoute extends _i11.PageRouteInfo { - const SearchRoute({List<_i11.PageRouteInfo>? children}) - : super(SearchRoute.name, initialChildren: children); - - static const String name = 'SearchRoute'; - - static _i11.PageInfo page = _i11.PageInfo( - name, - builder: (data) { - return const _i9.SearchTab(); - }, - ); -} - -/// generated route for -/// [_i10.StartReadingPage] -class StartReadingRoute extends _i11.PageRouteInfo { - StartReadingRoute({ - _i12.Key? key, - required _i13.BookDetailsUI book, - List<_i11.PageRouteInfo>? children, - }) : super( - StartReadingRoute.name, - args: StartReadingRouteArgs(key: key, book: book), - initialChildren: children, - ); - - static const String name = 'StartReadingRoute'; - - static _i11.PageInfo page = _i11.PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return _i10.StartReadingPage(key: args.key, book: args.book); - }, - ); -} - -class StartReadingRouteArgs { - const StartReadingRouteArgs({this.key, required this.book}); - - final _i12.Key? key; - - final _i13.BookDetailsUI book; - - @override - String toString() { - return 'StartReadingRouteArgs{key: $key, book: $book}'; - } -} diff --git a/pubspec.lock b/pubspec.lock index 049542f..f970ac6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.3.1" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" bloc: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 07dee38..3b33d7d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: shimmer: ^3.0.0 flutter_html: ^3.0.0 shared_preferences: ^2.5.3 + auto_size_text: ^3.0.0 dev_dependencies: flutter_test: From 3cb301a32993bc61372b304977c3106de979ae0e Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:24:33 -0300 Subject: [PATCH 10/17] [feat](pnalvarez): ignoring gr.dart files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 0122a41..6341c5e 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,5 @@ app.*.symbols */di.config.dart *.mocks.dart lib/core/di/di.config.dart +*.gr.dart + From 6acdf748aa208d15365cebf0e0df910b394037c8 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:28:43 -0300 Subject: [PATCH 11/17] [feat](pnalvarez): ignoring g.dart files --- .../data/datasource/reading_data_source.dart | 39 +-- lib/layers/data/models/book_list_data.g.dart | 265 ------------------ lib/layers/data/models/reading_data.g.dart | 15 - 3 files changed, 2 insertions(+), 317 deletions(-) delete mode 100644 lib/layers/data/models/book_list_data.g.dart delete mode 100644 lib/layers/data/models/reading_data.g.dart diff --git a/lib/layers/data/datasource/reading_data_source.dart b/lib/layers/data/datasource/reading_data_source.dart index 937d9b0..b7dd76f 100644 --- a/lib/layers/data/datasource/reading_data_source.dart +++ b/lib/layers/data/datasource/reading_data_source.dart @@ -1,11 +1,7 @@ -import 'dart:convert'; - import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:injectable/injectable.dart'; import 'package:mibook/layers/data/models/reading_data.dart'; -const _readingListKey = 'reading_list'; - abstract class IReadingDataSource { Future startReading({ required ReadingData readingData, @@ -23,40 +19,9 @@ class ReadingDataSource implements IReadingDataSource { Future startReading({ required ReadingData readingData, }) async { - final readingList = await _storage.getString(_readingListKey); - if (readingList.isEmpty) { - final string = jsonEncode([readingData.toJson()]); - await _storage.setString(_readingListKey, string); - } else { - final json = jsonDecode(readingList) as List; - final list = json - .map((e) => ReadingData.fromJson(e as Map)) - .toList(); - final index = list.indexWhere( - (element) => element.bookId == readingData.bookId, - ); - if (index != -1) { - list[index] = readingData; - } else { - list.add(readingData); - } - final string = jsonEncode(list.map((e) => e.toJson()).toList()); - await _storage.setString(_readingListKey, string); - } - final json = readingData.toJson(); - await _storage.setString(readingData.bookId, json.toString()); + // TO DO } @override - Future> getReadingData() async { - final readingList = await _storage.getString(_readingListKey); - if (readingList.isEmpty) { - return []; - } - final json = jsonDecode(readingList) as List; - final list = json - .map((e) => ReadingData.fromJson(e as Map)) - .toList(); - return list; - } + Future> getReadingData() async => []; } diff --git a/lib/layers/data/models/book_list_data.g.dart b/lib/layers/data/models/book_list_data.g.dart deleted file mode 100644 index 295b723..0000000 --- a/lib/layers/data/models/book_list_data.g.dart +++ /dev/null @@ -1,265 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'book_list_data.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -BookListData _$BookListDataFromJson(Map json) => BookListData( - kind: json['kind'] as String, - totalItems: (json['totalItems'] as num?)?.toInt(), - items: - (json['items'] as List?) - ?.map((e) => BookItem.fromJson(e as Map)) - .toList() ?? - [], -); - -Map _$BookListDataToJson(BookListData instance) => - { - 'kind': instance.kind, - 'totalItems': instance.totalItems, - 'items': instance.items, - }; - -BookItem _$BookItemFromJson(Map json) => BookItem( - kind: json['kind'] as String, - id: json['id'] as String, - etag: json['etag'] as String, - selfLink: json['selfLink'] as String, - volumeInfo: VolumeInfo.fromJson(json['volumeInfo'] as Map), - saleInfo: SaleInfo.fromJson(json['saleInfo'] as Map), - accessInfo: AccessInfo.fromJson(json['accessInfo'] as Map), - searchInfo: json['searchInfo'] == null - ? null - : SearchInfo.fromJson(json['searchInfo'] as Map), -); - -Map _$BookItemToJson(BookItem instance) => { - 'kind': instance.kind, - 'id': instance.id, - 'etag': instance.etag, - 'selfLink': instance.selfLink, - 'volumeInfo': instance.volumeInfo, - 'saleInfo': instance.saleInfo, - 'accessInfo': instance.accessInfo, - 'searchInfo': instance.searchInfo, -}; - -VolumeInfo _$VolumeInfoFromJson(Map json) => VolumeInfo( - title: json['title'] as String, - authors: (json['authors'] as List?) - ?.map((e) => e as String) - .toList(), - subtitle: json['subtitle'] as String?, - publisher: json['publisher'] as String?, - publishedDate: json['publishedDate'] as String?, - description: json['description'] as String?, - industryIdentifiers: (json['industryIdentifiers'] as List?) - ?.map((e) => IndustryIdentifier.fromJson(e as Map)) - .toList(), - readingModes: json['readingModes'] == null - ? null - : ReadingModes.fromJson(json['readingModes'] as Map), - pageCount: (json['pageCount'] as num?)?.toInt(), - printType: json['printType'] as String?, - categories: (json['categories'] as List?) - ?.map((e) => e as String) - .toList(), - averageRating: (json['averageRating'] as num?)?.toDouble(), - ratingsCount: (json['ratingsCount'] as num?)?.toInt(), - maturityRating: json['maturityRating'] as String?, - allowAnonLogging: json['allowAnonLogging'] as bool?, - contentVersion: json['contentVersion'] as String?, - panelizationSummary: json['panelizationSummary'] == null - ? null - : PanelizationSummary.fromJson( - json['panelizationSummary'] as Map, - ), - imageLinks: json['imageLinks'] == null - ? null - : ImageLinks.fromJson(json['imageLinks'] as Map), - language: json['language'] as String?, - previewLink: json['previewLink'] as String?, - infoLink: json['infoLink'] as String?, - canonicalVolumeLink: json['canonicalVolumeLink'] as String?, -); - -Map _$VolumeInfoToJson(VolumeInfo instance) => - { - 'title': instance.title, - 'authors': instance.authors, - 'subtitle': instance.subtitle, - 'publisher': instance.publisher, - 'publishedDate': instance.publishedDate, - 'description': instance.description, - 'industryIdentifiers': instance.industryIdentifiers, - 'readingModes': instance.readingModes, - 'pageCount': instance.pageCount, - 'printType': instance.printType, - 'categories': instance.categories, - 'averageRating': instance.averageRating, - 'ratingsCount': instance.ratingsCount, - 'maturityRating': instance.maturityRating, - 'allowAnonLogging': instance.allowAnonLogging, - 'contentVersion': instance.contentVersion, - 'panelizationSummary': instance.panelizationSummary, - 'imageLinks': instance.imageLinks, - 'language': instance.language, - 'previewLink': instance.previewLink, - 'infoLink': instance.infoLink, - 'canonicalVolumeLink': instance.canonicalVolumeLink, - }; - -IndustryIdentifier _$IndustryIdentifierFromJson(Map json) => - IndustryIdentifier( - type: json['type'] as String, - identifier: json['identifier'] as String, - ); - -Map _$IndustryIdentifierToJson(IndustryIdentifier instance) => - {'type': instance.type, 'identifier': instance.identifier}; - -ReadingModes _$ReadingModesFromJson(Map json) => - ReadingModes(text: json['text'] as bool, image: json['image'] as bool); - -Map _$ReadingModesToJson(ReadingModes instance) => - {'text': instance.text, 'image': instance.image}; - -PanelizationSummary _$PanelizationSummaryFromJson(Map json) => - PanelizationSummary( - containsEpubBubbles: json['containsEpubBubbles'] as bool, - containsImageBubbles: json['containsImageBubbles'] as bool, - ); - -Map _$PanelizationSummaryToJson( - PanelizationSummary instance, -) => { - 'containsEpubBubbles': instance.containsEpubBubbles, - 'containsImageBubbles': instance.containsImageBubbles, -}; - -ImageLinks _$ImageLinksFromJson(Map json) => ImageLinks( - smallThumbnail: json['smallThumbnail'] as String?, - thumbnail: json['thumbnail'] as String?, -); - -Map _$ImageLinksToJson(ImageLinks instance) => - { - 'smallThumbnail': instance.smallThumbnail, - 'thumbnail': instance.thumbnail, - }; - -SaleInfo _$SaleInfoFromJson(Map json) => SaleInfo( - country: json['country'] as String, - saleability: json['saleability'] as String, - isEbook: json['isEbook'] as bool, - listPrice: json['listPrice'] == null - ? null - : Price.fromJson(json['listPrice'] as Map), - retailPrice: json['retailPrice'] == null - ? null - : Price.fromJson(json['retailPrice'] as Map), - buyLink: json['buyLink'] as String?, - offers: (json['offers'] as List?) - ?.map((e) => Offer.fromJson(e as Map)) - .toList(), -); - -Map _$SaleInfoToJson(SaleInfo instance) => { - 'country': instance.country, - 'saleability': instance.saleability, - 'isEbook': instance.isEbook, - 'listPrice': instance.listPrice, - 'retailPrice': instance.retailPrice, - 'buyLink': instance.buyLink, - 'offers': instance.offers, -}; - -Price _$PriceFromJson(Map json) => Price( - amount: (json['amount'] as num?)?.toDouble(), - currencyCode: json['currencyCode'] as String?, - amountInMicros: (json['amountInMicros'] as num?)?.toInt(), -); - -Map _$PriceToJson(Price instance) => { - 'amount': instance.amount, - 'currencyCode': instance.currencyCode, - 'amountInMicros': instance.amountInMicros, -}; - -Offer _$OfferFromJson(Map json) => Offer( - finskyOfferType: (json['finskyOfferType'] as num?)?.toInt(), - listPrice: json['listPrice'] == null - ? null - : Price.fromJson(json['listPrice'] as Map), - retailPrice: json['retailPrice'] == null - ? null - : Price.fromJson(json['retailPrice'] as Map), - giftable: json['giftable'] as bool?, -); - -Map _$OfferToJson(Offer instance) => { - 'finskyOfferType': instance.finskyOfferType, - 'listPrice': instance.listPrice, - 'retailPrice': instance.retailPrice, - 'giftable': instance.giftable, -}; - -AccessInfo _$AccessInfoFromJson(Map json) => AccessInfo( - country: json['country'] as String, - viewability: json['viewability'] as String, - embeddable: json['embeddable'] as bool, - publicDomain: json['publicDomain'] as bool, - textToSpeechPermission: json['textToSpeechPermission'] as String, - epub: json['epub'] == null - ? null - : EpubInfo.fromJson(json['epub'] as Map), - pdf: json['pdf'] == null - ? null - : PdfInfo.fromJson(json['pdf'] as Map), - webReaderLink: json['webReaderLink'] as String?, - accessViewStatus: json['accessViewStatus'] as String?, - quoteSharingAllowed: json['quoteSharingAllowed'] as bool?, -); - -Map _$AccessInfoToJson(AccessInfo instance) => - { - 'country': instance.country, - 'viewability': instance.viewability, - 'embeddable': instance.embeddable, - 'publicDomain': instance.publicDomain, - 'textToSpeechPermission': instance.textToSpeechPermission, - 'epub': instance.epub, - 'pdf': instance.pdf, - 'webReaderLink': instance.webReaderLink, - 'accessViewStatus': instance.accessViewStatus, - 'quoteSharingAllowed': instance.quoteSharingAllowed, - }; - -EpubInfo _$EpubInfoFromJson(Map json) => EpubInfo( - isAvailable: json['isAvailable'] as bool, - acsTokenLink: json['acsTokenLink'] as String?, -); - -Map _$EpubInfoToJson(EpubInfo instance) => { - 'isAvailable': instance.isAvailable, - 'acsTokenLink': instance.acsTokenLink, -}; - -PdfInfo _$PdfInfoFromJson(Map json) => PdfInfo( - isAvailable: json['isAvailable'] as bool, - acsTokenLink: json['acsTokenLink'] as String?, -); - -Map _$PdfInfoToJson(PdfInfo instance) => { - 'isAvailable': instance.isAvailable, - 'acsTokenLink': instance.acsTokenLink, -}; - -SearchInfo _$SearchInfoFromJson(Map json) => - SearchInfo(textSnippet: json['textSnippet'] as String?); - -Map _$SearchInfoToJson(SearchInfo instance) => - {'textSnippet': instance.textSnippet}; diff --git a/lib/layers/data/models/reading_data.g.dart b/lib/layers/data/models/reading_data.g.dart deleted file mode 100644 index 149bfa3..0000000 --- a/lib/layers/data/models/reading_data.g.dart +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'reading_data.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -ReadingData _$ReadingDataFromJson(Map json) => ReadingData( - bookId: json['bookId'] as String, - progress: (json['progress'] as num).toDouble(), -); - -Map _$ReadingDataToJson(ReadingData instance) => - {'bookId': instance.bookId, 'progress': instance.progress}; From 5540c2fd82e5093c868603e6f3af6dc13aee5be7 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:33:22 -0300 Subject: [PATCH 12/17] [feat](pnalvarez): delete freezed files from git control --- .../book_details_state.freezed.dart | 202 ------------------ .../start_reading_state.freezed.dart | 201 ----------------- 2 files changed, 403 deletions(-) delete mode 100644 lib/layers/presentation/screens/bookdetails/book_details_state.freezed.dart delete mode 100644 lib/layers/presentation/screens/startreading/start_reading_state.freezed.dart diff --git a/lib/layers/presentation/screens/bookdetails/book_details_state.freezed.dart b/lib/layers/presentation/screens/bookdetails/book_details_state.freezed.dart deleted file mode 100644 index 20619d4..0000000 --- a/lib/layers/presentation/screens/bookdetails/book_details_state.freezed.dart +++ /dev/null @@ -1,202 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// 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 'book_details_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$BookDetailsState { - - bool get isLoading; String? get errorMessage; BookDetailsUI? get bookDetails; double get bookProgress; -/// Create a copy of BookDetailsState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$BookDetailsStateCopyWith get copyWith => _$BookDetailsStateCopyWithImpl(this as BookDetailsState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is BookDetailsState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.bookDetails, bookDetails) || other.bookDetails == bookDetails)&&(identical(other.bookProgress, bookProgress) || other.bookProgress == bookProgress)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isLoading,errorMessage,bookDetails,bookProgress); - -@override -String toString() { - return 'BookDetailsState(isLoading: $isLoading, errorMessage: $errorMessage, bookDetails: $bookDetails, bookProgress: $bookProgress)'; -} - - -} - -/// @nodoc -abstract mixin class $BookDetailsStateCopyWith<$Res> { - factory $BookDetailsStateCopyWith(BookDetailsState value, $Res Function(BookDetailsState) _then) = _$BookDetailsStateCopyWithImpl; -@useResult -$Res call({ - String? errorMessage, BookDetailsUI? bookDetails, bool isLoading, double bookProgress -}); - - - - -} -/// @nodoc -class _$BookDetailsStateCopyWithImpl<$Res> - implements $BookDetailsStateCopyWith<$Res> { - _$BookDetailsStateCopyWithImpl(this._self, this._then); - - final BookDetailsState _self; - final $Res Function(BookDetailsState) _then; - -/// Create a copy of BookDetailsState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? errorMessage = freezed,Object? bookDetails = freezed,Object? isLoading = null,Object? bookProgress = null,}) { - return _then(BookDetailsState( -freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String?,freezed == bookDetails ? _self.bookDetails : bookDetails // ignore: cast_nullable_to_non_nullable -as BookDetailsUI?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,bookProgress: null == bookProgress ? _self.bookProgress : bookProgress // ignore: cast_nullable_to_non_nullable -as double, - )); -} - -} - - -/// Adds pattern-matching-related methods to [BookDetailsState]. -extension BookDetailsStatePatterns on BookDetailsState { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap({required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(){ -final _that = this; -switch (_that) { -case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(){ -final _that = this; -switch (_that) { -case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen({required TResult orElse(),}) {final _that = this; -switch (_that) { -case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when() {final _that = this; -switch (_that) { -case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull() {final _that = this; -switch (_that) { -case _: - return null; - -} -} - -} - -// dart format on diff --git a/lib/layers/presentation/screens/startreading/start_reading_state.freezed.dart b/lib/layers/presentation/screens/startreading/start_reading_state.freezed.dart deleted file mode 100644 index 52af551..0000000 --- a/lib/layers/presentation/screens/startreading/start_reading_state.freezed.dart +++ /dev/null @@ -1,201 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// 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 'start_reading_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$StartReadingState { - - String? get inputErrorMessage; double get progress; bool get shouldNavigateBack; -/// Create a copy of StartReadingState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$StartReadingStateCopyWith get copyWith => _$StartReadingStateCopyWithImpl(this as StartReadingState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is StartReadingState&&(identical(other.inputErrorMessage, inputErrorMessage) || other.inputErrorMessage == inputErrorMessage)&&(identical(other.progress, progress) || other.progress == progress)&&(identical(other.shouldNavigateBack, shouldNavigateBack) || other.shouldNavigateBack == shouldNavigateBack)); -} - - -@override -int get hashCode => Object.hash(runtimeType,inputErrorMessage,progress,shouldNavigateBack); - -@override -String toString() { - return 'StartReadingState(inputErrorMessage: $inputErrorMessage, progress: $progress, shouldNavigateBack: $shouldNavigateBack)'; -} - - -} - -/// @nodoc -abstract mixin class $StartReadingStateCopyWith<$Res> { - factory $StartReadingStateCopyWith(StartReadingState value, $Res Function(StartReadingState) _then) = _$StartReadingStateCopyWithImpl; -@useResult -$Res call({ - String? inputErrorMessage, double progress, bool shouldNavigateBack -}); - - - - -} -/// @nodoc -class _$StartReadingStateCopyWithImpl<$Res> - implements $StartReadingStateCopyWith<$Res> { - _$StartReadingStateCopyWithImpl(this._self, this._then); - - final StartReadingState _self; - final $Res Function(StartReadingState) _then; - -/// Create a copy of StartReadingState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? inputErrorMessage = freezed,Object? progress = null,Object? shouldNavigateBack = null,}) { - return _then(StartReadingState( -inputErrorMessage: freezed == inputErrorMessage ? _self.inputErrorMessage : inputErrorMessage // ignore: cast_nullable_to_non_nullable -as String?,progress: null == progress ? _self.progress : progress // ignore: cast_nullable_to_non_nullable -as double,shouldNavigateBack: null == shouldNavigateBack ? _self.shouldNavigateBack : shouldNavigateBack // ignore: cast_nullable_to_non_nullable -as bool, - )); -} - -} - - -/// Adds pattern-matching-related methods to [StartReadingState]. -extension StartReadingStatePatterns on StartReadingState { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap({required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(){ -final _that = this; -switch (_that) { -case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(){ -final _that = this; -switch (_that) { -case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen({required TResult orElse(),}) {final _that = this; -switch (_that) { -case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when() {final _that = this; -switch (_that) { -case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull() {final _that = this; -switch (_that) { -case _: - return null; - -} -} - -} - -// dart format on From 0627ae10956a2723832b8faa93fddf444f22b447 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:40:05 -0300 Subject: [PATCH 13/17] [refactor](pnalvarez): secondary button --- .../molecules/buttons/secondary_button.dart | 73 ++++++++----------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/lib/core/designsystem/molecules/buttons/secondary_button.dart b/lib/core/designsystem/molecules/buttons/secondary_button.dart index 735b4aa..163f709 100644 --- a/lib/core/designsystem/molecules/buttons/secondary_button.dart +++ b/lib/core/designsystem/molecules/buttons/secondary_button.dart @@ -19,49 +19,40 @@ class SecondaryButton extends StatelessWidget { @override Widget build(BuildContext context) { - final button = OutlinedButton( - onPressed: isEnabled ? onPressed : null, - style: OutlinedButton.styleFrom( - side: BorderSide( - color: isEnabled ? primary : disabled, - width: 1.0, + return SizedBox( + height: 48, + width: isExpanded ? double.infinity : null, + child: OutlinedButton( + onPressed: isEnabled ? onPressed : null, + style: OutlinedButton.styleFrom( + side: BorderSide( + color: isEnabled ? primary : disabled, + width: 1.0, + ), + foregroundColor: isEnabled ? primary : disabled, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + minimumSize: const Size(80, 48), ), - foregroundColor: isEnabled ? primary : disabled, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - textStyle: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - minimumSize: const Size(80, 48), + child: isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + ), + ) + : Text(title), ), - child: isLoading - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - color: Colors.white, - ), - ) - : Text(title), ); - - if (isExpanded) { - return SizedBox( - height: 48, - width: double.infinity, - child: button, - ); - } else { - return SizedBox( - height: 48, - child: button, - ); - } } } From 5be5c243ec6324034e5a4f9586c67f1f49009d39 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:41:36 -0300 Subject: [PATCH 14/17] [fix](pnalvarez): removed unused imports and variables --- lib/layers/data/datasource/reading_data_source.dart | 4 ---- .../presentation/screens/bookdetails/book_details_page.dart | 3 --- .../presentation/screens/startreading/start_reading_page.dart | 1 - 3 files changed, 8 deletions(-) diff --git a/lib/layers/data/datasource/reading_data_source.dart b/lib/layers/data/datasource/reading_data_source.dart index b7dd76f..19fea09 100644 --- a/lib/layers/data/datasource/reading_data_source.dart +++ b/lib/layers/data/datasource/reading_data_source.dart @@ -11,10 +11,6 @@ abstract class IReadingDataSource { @Injectable(as: IReadingDataSource) class ReadingDataSource implements IReadingDataSource { - final EncryptedSharedPreferences _storage; - - ReadingDataSource(this._storage); - @override Future startReading({ required ReadingData readingData, diff --git a/lib/layers/presentation/screens/bookdetails/book_details_page.dart b/lib/layers/presentation/screens/bookdetails/book_details_page.dart index 0015cbb..4fc0629 100644 --- a/lib/layers/presentation/screens/bookdetails/book_details_page.dart +++ b/lib/layers/presentation/screens/bookdetails/book_details_page.dart @@ -4,15 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:mibook/core/designsystem/molecules/buttons/primary_button.dart'; -import 'package:mibook/core/designsystem/molecules/indicators/progress_stepper.dart'; -import 'package:mibook/core/designsystem/molecules/inputfields/input_field.dart'; import 'package:mibook/core/designsystem/organisms/app_nav_bar.dart'; import 'package:mibook/core/designsystem/organisms/list_item.dart'; import 'package:mibook/core/di/di.dart'; import 'package:mibook/core/routes/app_router.gr.dart'; import 'package:mibook/core/utils/strings.dart' as strings; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_state.dart'; -import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_view_model.dart'; import 'package:mibook/layers/presentation/screens/bookdetails/book_details_event.dart'; diff --git a/lib/layers/presentation/screens/startreading/start_reading_page.dart b/lib/layers/presentation/screens/startreading/start_reading_page.dart index 42cc59e..e275027 100644 --- a/lib/layers/presentation/screens/startreading/start_reading_page.dart +++ b/lib/layers/presentation/screens/startreading/start_reading_page.dart @@ -5,7 +5,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mibook/core/designsystem/molecules/buttons/primary_button.dart'; import 'package:mibook/core/designsystem/molecules/buttons/secondary_button.dart'; import 'package:mibook/core/designsystem/molecules/indicators/progress_stepper.dart'; -import 'package:mibook/core/designsystem/molecules/indicators/radio_box.dart'; import 'package:mibook/core/designsystem/molecules/inputfields/input_field.dart'; import 'package:mibook/core/designsystem/organisms/app_nav_bar.dart'; import 'package:mibook/core/di/di.dart'; From d5cfbbfb9880f2148006949adf3007baed2b5215 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:44:17 -0300 Subject: [PATCH 15/17] [fix](pnalvarez): removed di.config.dart file and app module --- .gitignore | 2 +- lib/core/di/app_module.dart | 9 ---- lib/core/di/di.config.dart | 88 ------------------------------------- 3 files changed, 1 insertion(+), 98 deletions(-) delete mode 100644 lib/core/di/app_module.dart delete mode 100644 lib/core/di/di.config.dart diff --git a/.gitignore b/.gitignore index 6341c5e..99d7ef4 100644 --- a/.gitignore +++ b/.gitignore @@ -167,7 +167,7 @@ app.*.symbols /lib/main.config.dart *.freezed.dart *.g.dart -*/di.config.dart +*.config.dart *.mocks.dart lib/core/di/di.config.dart *.gr.dart diff --git a/lib/core/di/app_module.dart b/lib/core/di/app_module.dart deleted file mode 100644 index b6c1486..0000000 --- a/lib/core/di/app_module.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; -import 'package:injectable/injectable.dart'; - -@module -abstract class AppModule { - @lazySingleton - EncryptedSharedPreferences get encryptedSharedPreferences => - EncryptedSharedPreferences(); -} diff --git a/lib/core/di/di.config.dart b/lib/core/di/di.config.dart deleted file mode 100644 index f92a118..0000000 --- a/lib/core/di/di.config.dart +++ /dev/null @@ -1,88 +0,0 @@ -// dart format width=80 -// GENERATED CODE - DO NOT MODIFY BY HAND - -// ************************************************************************** -// InjectableConfigGenerator -// ************************************************************************** - -// ignore_for_file: type=lint -// coverage:ignore-file - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart' - as _i847; -import 'package:get_it/get_it.dart' as _i174; -import 'package:injectable/injectable.dart' as _i526; -import 'package:mibook/core/di/app_module.dart' as _i516; -import 'package:mibook/layers/data/api/api_client.dart' as _i721; -import 'package:mibook/layers/data/datasource/reading_data_source.dart' as _i44; -import 'package:mibook/layers/data/datasource/search_data_source.dart' as _i400; -import 'package:mibook/layers/data/repository/reading_repository.dart' as _i600; -import 'package:mibook/layers/data/repository/search_repository.dart' as _i967; -import 'package:mibook/layers/domain/repository/reading_repository.dart' - as _i649; -import 'package:mibook/layers/domain/repository/search_repository.dart' - as _i303; -import 'package:mibook/layers/domain/usecases/get_book_details.dart' as _i814; -import 'package:mibook/layers/domain/usecases/search_books.dart' as _i663; -import 'package:mibook/layers/domain/usecases/start_reading.dart' as _i369; -import 'package:mibook/layers/presentation/screens/bookdetails/book_details_ui.dart' - as _i66; -import 'package:mibook/layers/presentation/screens/bookdetails/book_details_view_model.dart' - as _i46; -import 'package:mibook/layers/presentation/screens/booksearch/book_search_view_model.dart' - as _i688; -import 'package:mibook/layers/presentation/screens/startreading/start_reading_view_model.dart' - as _i35; - -extension GetItInjectableX on _i174.GetIt { - // initializes the registration of main-scope dependencies inside of GetIt - _i174.GetIt init({ - String? environment, - _i526.EnvironmentFilter? environmentFilter, - }) { - final gh = _i526.GetItHelper(this, environment, environmentFilter); - final appModule = _$AppModule(); - gh.lazySingleton<_i847.EncryptedSharedPreferences>( - () => appModule.encryptedSharedPreferences, - ); - gh.factory<_i721.IApiClient>(() => _i721.ApiClient()); - gh.factory<_i44.IReadingDataSource>( - () => _i44.ReadingDataSource(gh<_i847.EncryptedSharedPreferences>()), - ); - gh.factory<_i400.ISearchDataSource>( - () => _i400.SearchDataSource(gh<_i721.IApiClient>()), - ); - gh.factory<_i649.IReadingRepository>( - () => _i600.ReadingRepository(gh<_i44.IReadingDataSource>()), - ); - gh.factory<_i303.ISearchRepository>( - () => _i967.SearchRepository(gh<_i400.ISearchDataSource>()), - ); - gh.factory<_i369.IStartReading>( - () => _i369.StartReading(gh<_i649.IReadingRepository>()), - ); - gh.factory<_i663.ISearchBooks>( - () => _i663.SearchBooks(gh<_i303.ISearchRepository>()), - ); - gh.factory<_i814.IGetBookDetails>( - () => _i814.GetBookDetails(gh<_i303.ISearchRepository>()), - ); - gh.factoryParam<_i46.BookDetailsViewModel, String?, dynamic>( - (bookId, _) => _i46.BookDetailsViewModel( - gh<_i814.IGetBookDetails>(), - gh<_i369.IStartReading>(), - bookId, - ), - ); - gh.factoryParam<_i35.StartReadingViewModel, _i66.BookDetailsUI, dynamic>( - (book, _) => _i35.StartReadingViewModel(gh<_i369.IStartReading>(), book), - ); - gh.factory<_i688.BookSearchViewModel>( - () => _i688.BookSearchViewModel(gh<_i663.ISearchBooks>()), - ); - return this; - } -} - -class _$AppModule extends _i516.AppModule {} From cabc81eaadb060046db47b662568e96d2e72f402 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:49:54 -0300 Subject: [PATCH 16/17] [fix](pnalvarez): filtering only books that contain pagecount property --- lib/layers/data/models/book_list_data.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/layers/data/models/book_list_data.dart b/lib/layers/data/models/book_list_data.dart index 3171981..7e48e66 100644 --- a/lib/layers/data/models/book_list_data.dart +++ b/lib/layers/data/models/book_list_data.dart @@ -24,7 +24,10 @@ class BookListData { BookListDomain toDomain() { return BookListDomain( totalItems: totalItems ?? 0, - books: items.map((item) => item.toDomain()).toList(), + books: items + .where((item) => item.volumeInfo.pageCount != null) + .map((item) => item.toDomain()) + .toList(), ); } } From 1735bd3c7024df51b1db9b66bd556fbec0be0c5d Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:54:27 -0300 Subject: [PATCH 17/17] [fix](pnalvarez): removed unused import --- lib/layers/data/datasource/reading_data_source.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/layers/data/datasource/reading_data_source.dart b/lib/layers/data/datasource/reading_data_source.dart index 19fea09..0067fb0 100644 --- a/lib/layers/data/datasource/reading_data_source.dart +++ b/lib/layers/data/datasource/reading_data_source.dart @@ -1,4 +1,3 @@ -import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:injectable/injectable.dart'; import 'package:mibook/layers/data/models/reading_data.dart';