feat(ui): Implement a simple details screen
This commit is contained in:
		
							parent
							
								
									8794c3cd60
								
							
						
					
					
						commit
						1892ec5e7a
					
				| @ -1,9 +1,11 @@ | ||||
| import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart'; | ||||
| import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart'; | ||||
| import 'package:anitrack/src/ui/bloc/details_bloc.dart'; | ||||
| import 'package:anitrack/src/ui/bloc/navigation_bloc.dart'; | ||||
| import 'package:anitrack/src/ui/constants.dart'; | ||||
| import 'package:anitrack/src/ui/pages/anime_list.dart'; | ||||
| import 'package:anitrack/src/ui/pages/anime_search.dart'; | ||||
| import 'package:anitrack/src/ui/pages/details.dart'; | ||||
| import 'package:anitrack/src/service/database.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_bloc/flutter_bloc.dart'; | ||||
| @ -23,6 +25,7 @@ void main() async { | ||||
|   GetIt.I.registerSingleton<DatabaseService>(database); | ||||
|   GetIt.I.registerSingleton<AnimeListBloc>(AnimeListBloc()); | ||||
|   GetIt.I.registerSingleton<AnimeSearchBloc>(AnimeSearchBloc()); | ||||
|   GetIt.I.registerSingleton<DetailsBloc>(DetailsBloc()); | ||||
|   GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navKey)); | ||||
| 
 | ||||
|   // Load animes | ||||
| @ -39,6 +42,9 @@ void main() async { | ||||
|         BlocProvider<AnimeSearchBloc>( | ||||
|           create: (_) => GetIt.I.get<AnimeSearchBloc>(), | ||||
|         ), | ||||
|         BlocProvider<DetailsBloc>( | ||||
|           create: (_) => GetIt.I.get<DetailsBloc>(), | ||||
|         ), | ||||
|         BlocProvider<NavigationBloc>( | ||||
|           create: (_) => GetIt.I.get<NavigationBloc>(), | ||||
|         ), | ||||
| @ -77,6 +83,7 @@ class MyApp extends StatelessWidget { | ||||
|           case '/': | ||||
|           case animeListRoute: return AnimeListPage.route; | ||||
|           case animeSearchRoute: return AnimeSearchPage.route; | ||||
|           case detailsRoute: return DetailsPage.route; | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|  | ||||
| @ -24,6 +24,18 @@ extension AnimeTrackStateExtension on AnimeTrackingState { | ||||
|       case AnimeTrackingState.all: return -1; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   String toNameString() { | ||||
|     assert(this != AnimeTrackingState.all, 'AnimeTrackingState.all must not be stringified'); | ||||
| 
 | ||||
|     switch (this) { | ||||
|       case AnimeTrackingState.watching: return 'Watching'; | ||||
|       case AnimeTrackingState.completed: return 'Completed'; | ||||
|       case AnimeTrackingState.planToWatch: return 'Plan to watch'; | ||||
|       case AnimeTrackingState.dropped: return 'Dropped'; | ||||
|       case AnimeTrackingState.all: return 'All'; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class AnimeTrackingStateConverter implements JsonConverter<AnimeTrackingState, int> { | ||||
|  | ||||
| @ -7,7 +7,7 @@ part 'manga.g.dart'; | ||||
| enum MangaTrackingState { | ||||
|   reading,    // 0 | ||||
|   completed,   // 1 | ||||
|   planToWatch, // 2 | ||||
|   planToRead, // 2 | ||||
|   dropped,     // 3 | ||||
|   /// This is a pseudo state, i.e. it should never be set | ||||
|   all,         // -1 | ||||
| @ -19,11 +19,23 @@ extension MangaTrackStateExtension on MangaTrackingState { | ||||
|     switch (this) { | ||||
|       case MangaTrackingState.reading: return 0; | ||||
|       case MangaTrackingState.completed: return 1; | ||||
|       case MangaTrackingState.planToWatch: return 2; | ||||
|       case MangaTrackingState.planToRead: return 2; | ||||
|       case MangaTrackingState.dropped: return 3; | ||||
|       case MangaTrackingState.all: return -1; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   String toNameString() { | ||||
|     assert(this != MangaTrackingState.all, 'MangaTrackingState.all must not be stringified'); | ||||
| 
 | ||||
|     switch (this) { | ||||
|       case MangaTrackingState.reading: return 'Reading'; | ||||
|       case MangaTrackingState.completed: return 'Completed'; | ||||
|       case MangaTrackingState.planToRead: return 'Plan to read'; | ||||
|       case MangaTrackingState.dropped: return 'Dropped'; | ||||
|       case MangaTrackingState.all: return 'All'; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class MangaTrackingStateConverter implements JsonConverter<MangaTrackingState, int> { | ||||
| @ -34,11 +46,11 @@ class MangaTrackingStateConverter implements JsonConverter<MangaTrackingState, i | ||||
|     switch (json) { | ||||
|       case 0: return MangaTrackingState.reading; | ||||
|       case 1: return MangaTrackingState.completed; | ||||
|       case 2: return MangaTrackingState.planToWatch; | ||||
|       case 2: return MangaTrackingState.planToRead; | ||||
|       case 3: return MangaTrackingState.dropped; | ||||
|     } | ||||
| 
 | ||||
|     return MangaTrackingState.planToWatch; | ||||
|     return MangaTrackingState.planToRead; | ||||
|   } | ||||
|    | ||||
|   @override | ||||
|  | ||||
| @ -23,6 +23,8 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> { | ||||
|     on<MangaFilterChangedEvent>(_onMangasFiltered); | ||||
|     on<MangaChapterIncrementedEvent>(_onMangaIncremented); | ||||
|     on<MangaChapterDecrementedEvent>(_onMangaDecremented); | ||||
|     on<AnimeUpdatedEvent>(_onAnimeUpdated); | ||||
|     on<MangaUpdatedEvent>(_onMangaUpdated); | ||||
|   } | ||||
| 
 | ||||
|   Future<void> _onAnimeAdded(AnimeAddedEvent event, Emitter<AnimeListState> emit) async { | ||||
| @ -173,4 +175,36 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> { | ||||
| 
 | ||||
|     await GetIt.I.get<DatabaseService>().updateManga(newManga); | ||||
|   } | ||||
| 
 | ||||
|   Future<void> _onAnimeUpdated(AnimeUpdatedEvent event, Emitter<AnimeListState> emit) async { | ||||
|     emit( | ||||
|       state.copyWith( | ||||
|         animes: List.from( | ||||
|           state.animes.map((anime) { | ||||
|             if (anime.id == event.anime.id) { | ||||
|               return event.anime; | ||||
|             } | ||||
| 
 | ||||
|             return anime; | ||||
|           }), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Future<void> _onMangaUpdated(MangaUpdatedEvent event, Emitter<AnimeListState> emit) async { | ||||
|     emit( | ||||
|       state.copyWith( | ||||
|         mangas: List.from( | ||||
|           state.mangas.map((manga) { | ||||
|             if (manga.id == event.manga.id) { | ||||
|               return event.manga; | ||||
|             } | ||||
| 
 | ||||
|             return manga; | ||||
|           }), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -42,6 +42,12 @@ class AnimeTrackingTypeChanged extends AnimeListEvent { | ||||
|   final TrackingMediumType type; | ||||
| } | ||||
| 
 | ||||
| class AnimeUpdatedEvent extends AnimeListEvent { | ||||
|   AnimeUpdatedEvent(this.anime); | ||||
| 
 | ||||
|   final AnimeTrackingData anime; | ||||
| } | ||||
| 
 | ||||
| class MangaAddedEvent extends AnimeListEvent { | ||||
|   MangaAddedEvent(this.data); | ||||
|    | ||||
| @ -70,3 +76,9 @@ class MangaChapterDecrementedEvent extends AnimeListEvent { | ||||
|   /// The ID of the anime | ||||
|   final String id; | ||||
| } | ||||
| 
 | ||||
| class MangaUpdatedEvent extends AnimeListEvent { | ||||
|   MangaUpdatedEvent(this.manga); | ||||
| 
 | ||||
|   final MangaTrackingData manga; | ||||
| } | ||||
|  | ||||
							
								
								
									
										84
									
								
								lib/src/ui/bloc/details_bloc.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								lib/src/ui/bloc/details_bloc.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | ||||
| import 'package:anitrack/src/data/anime.dart'; | ||||
| import 'package:anitrack/src/data/manga.dart'; | ||||
| import 'package:anitrack/src/data/type.dart'; | ||||
| import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart'; | ||||
| import 'package:anitrack/src/ui/bloc/navigation_bloc.dart'; | ||||
| import 'package:anitrack/src/ui/constants.dart'; | ||||
| import 'package:anitrack/src/service/database.dart'; | ||||
| import 'package:bloc/bloc.dart'; | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:get_it/get_it.dart'; | ||||
| 
 | ||||
| part 'details_state.dart'; | ||||
| part 'details_event.dart'; | ||||
| part 'details_bloc.freezed.dart'; | ||||
| 
 | ||||
| class DetailsBloc extends Bloc<DetailsEvent, DetailsState> { | ||||
|   DetailsBloc() : super(DetailsState()) { | ||||
|     on<AnimeDetailsRequestedEvent>(_onAnimeRequested); | ||||
|     on<MangaDetailsRequestedEvent>(_onMangaRequested); | ||||
|     on<DetailsUpdatedEvent>(_onDetailsUpdated); | ||||
|   } | ||||
| 
 | ||||
|   Future<void> _onAnimeRequested(AnimeDetailsRequestedEvent event, Emitter<DetailsState> emit) async { | ||||
|     emit( | ||||
|       state.copyWith( | ||||
|         trackingType: TrackingMediumType.anime, | ||||
|         data: event.anime, | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     GetIt.I.get<NavigationBloc>().add( | ||||
|       PushedNamedEvent( | ||||
|         NavigationDestination( | ||||
|           detailsRoute, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Future<void> _onMangaRequested(MangaDetailsRequestedEvent event, Emitter<DetailsState> emit) async { | ||||
|     emit( | ||||
|       state.copyWith( | ||||
|         trackingType: TrackingMediumType.manga, | ||||
|         data: event.manga, | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     GetIt.I.get<NavigationBloc>().add( | ||||
|       PushedNamedEvent( | ||||
|         NavigationDestination( | ||||
|           detailsRoute, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Future<void> _onDetailsUpdated(DetailsUpdatedEvent event, Emitter<DetailsState> emit) async { | ||||
|     if (state.trackingType == TrackingMediumType.anime) { | ||||
|       emit( | ||||
|         state.copyWith( | ||||
|           data: event.data, | ||||
|         ), | ||||
|       ); | ||||
| 
 | ||||
|       await GetIt.I.get<DatabaseService>().updateAnime(event.data as AnimeTrackingData); | ||||
| 
 | ||||
|       GetIt.I.get<AnimeListBloc>().add( | ||||
|         AnimeUpdatedEvent(event.data as AnimeTrackingData), | ||||
|       ); | ||||
|     } else { | ||||
|       emit( | ||||
|         state.copyWith( | ||||
|           data: event.data, | ||||
|         ), | ||||
|       ); | ||||
| 
 | ||||
|       await GetIt.I.get<DatabaseService>().updateManga(event.data as MangaTrackingData); | ||||
| 
 | ||||
|       GetIt.I.get<AnimeListBloc>().add( | ||||
|         MangaUpdatedEvent(event.data as MangaTrackingData), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										151
									
								
								lib/src/ui/bloc/details_bloc.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								lib/src/ui/bloc/details_bloc.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,151 @@ | ||||
| // coverage:ignore-file | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target | ||||
| 
 | ||||
| part of 'details_bloc.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
| 
 | ||||
| T _$identity<T>(T value) => value; | ||||
| 
 | ||||
| final _privateConstructorUsedError = UnsupportedError( | ||||
|     'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); | ||||
| 
 | ||||
| /// @nodoc | ||||
| mixin _$DetailsState { | ||||
|   dynamic get data => throw _privateConstructorUsedError; | ||||
|   TrackingMediumType get trackingType => throw _privateConstructorUsedError; | ||||
| 
 | ||||
|   @JsonKey(ignore: true) | ||||
|   $DetailsStateCopyWith<DetailsState> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| abstract class $DetailsStateCopyWith<$Res> { | ||||
|   factory $DetailsStateCopyWith( | ||||
|           DetailsState value, $Res Function(DetailsState) then) = | ||||
|       _$DetailsStateCopyWithImpl<$Res>; | ||||
|   $Res call({dynamic data, TrackingMediumType trackingType}); | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| class _$DetailsStateCopyWithImpl<$Res> implements $DetailsStateCopyWith<$Res> { | ||||
|   _$DetailsStateCopyWithImpl(this._value, this._then); | ||||
| 
 | ||||
|   final DetailsState _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function(DetailsState) _then; | ||||
| 
 | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? data = freezed, | ||||
|     Object? trackingType = freezed, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       data: data == freezed | ||||
|           ? _value.data | ||||
|           : data // ignore: cast_nullable_to_non_nullable | ||||
|               as dynamic, | ||||
|       trackingType: trackingType == freezed | ||||
|           ? _value.trackingType | ||||
|           : trackingType // ignore: cast_nullable_to_non_nullable | ||||
|               as TrackingMediumType, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| abstract class _$$_DetailsStateCopyWith<$Res> | ||||
|     implements $DetailsStateCopyWith<$Res> { | ||||
|   factory _$$_DetailsStateCopyWith( | ||||
|           _$_DetailsState value, $Res Function(_$_DetailsState) then) = | ||||
|       __$$_DetailsStateCopyWithImpl<$Res>; | ||||
|   @override | ||||
|   $Res call({dynamic data, TrackingMediumType trackingType}); | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| class __$$_DetailsStateCopyWithImpl<$Res> | ||||
|     extends _$DetailsStateCopyWithImpl<$Res> | ||||
|     implements _$$_DetailsStateCopyWith<$Res> { | ||||
|   __$$_DetailsStateCopyWithImpl( | ||||
|       _$_DetailsState _value, $Res Function(_$_DetailsState) _then) | ||||
|       : super(_value, (v) => _then(v as _$_DetailsState)); | ||||
| 
 | ||||
|   @override | ||||
|   _$_DetailsState get _value => super._value as _$_DetailsState; | ||||
| 
 | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? data = freezed, | ||||
|     Object? trackingType = freezed, | ||||
|   }) { | ||||
|     return _then(_$_DetailsState( | ||||
|       data: data == freezed | ||||
|           ? _value.data | ||||
|           : data // ignore: cast_nullable_to_non_nullable | ||||
|               as dynamic, | ||||
|       trackingType: trackingType == freezed | ||||
|           ? _value.trackingType | ||||
|           : trackingType // ignore: cast_nullable_to_non_nullable | ||||
|               as TrackingMediumType, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| 
 | ||||
| class _$_DetailsState implements _DetailsState { | ||||
|   _$_DetailsState({this.data, this.trackingType = TrackingMediumType.anime}); | ||||
| 
 | ||||
|   @override | ||||
|   final dynamic data; | ||||
|   @override | ||||
|   @JsonKey() | ||||
|   final TrackingMediumType trackingType; | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'DetailsState(data: $data, trackingType: $trackingType)'; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(dynamic other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$_DetailsState && | ||||
|             const DeepCollectionEquality().equals(other.data, data) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other.trackingType, trackingType)); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => Object.hash( | ||||
|       runtimeType, | ||||
|       const DeepCollectionEquality().hash(data), | ||||
|       const DeepCollectionEquality().hash(trackingType)); | ||||
| 
 | ||||
|   @JsonKey(ignore: true) | ||||
|   @override | ||||
|   _$$_DetailsStateCopyWith<_$_DetailsState> get copyWith => | ||||
|       __$$_DetailsStateCopyWithImpl<_$_DetailsState>(this, _$identity); | ||||
| } | ||||
| 
 | ||||
| abstract class _DetailsState implements DetailsState { | ||||
|   factory _DetailsState( | ||||
|       {final dynamic data, | ||||
|       final TrackingMediumType trackingType}) = _$_DetailsState; | ||||
| 
 | ||||
|   @override | ||||
|   dynamic get data; | ||||
|   @override | ||||
|   TrackingMediumType get trackingType; | ||||
|   @override | ||||
|   @JsonKey(ignore: true) | ||||
|   _$$_DetailsStateCopyWith<_$_DetailsState> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
							
								
								
									
										23
									
								
								lib/src/ui/bloc/details_event.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								lib/src/ui/bloc/details_event.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| part of 'details_bloc.dart'; | ||||
| 
 | ||||
| abstract class DetailsEvent {} | ||||
| 
 | ||||
| class AnimeDetailsRequestedEvent extends DetailsEvent { | ||||
|   AnimeDetailsRequestedEvent(this.anime); | ||||
| 
 | ||||
|   /// The anime to show details about | ||||
|   final AnimeTrackingData anime; | ||||
| } | ||||
| 
 | ||||
| class MangaDetailsRequestedEvent extends DetailsEvent { | ||||
|   MangaDetailsRequestedEvent(this.manga); | ||||
| 
 | ||||
|   /// The manga to show details about | ||||
|   final MangaTrackingData manga; | ||||
| } | ||||
| 
 | ||||
| class DetailsUpdatedEvent extends DetailsEvent { | ||||
|   DetailsUpdatedEvent(this.data); | ||||
| 
 | ||||
|   final dynamic data; | ||||
| } | ||||
							
								
								
									
										9
									
								
								lib/src/ui/bloc/details_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/src/ui/bloc/details_state.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| part of 'details_bloc.dart'; | ||||
| 
 | ||||
| @freezed | ||||
| class DetailsState with _$DetailsState { | ||||
|   factory DetailsState({ | ||||
|     dynamic data, | ||||
|     @Default(TrackingMediumType.anime) TrackingMediumType trackingType, | ||||
|   }) = _DetailsState; | ||||
| } | ||||
| @ -1,2 +1,3 @@ | ||||
| const animeListRoute = '/anime/list'; | ||||
| const animeSearchRoute = '/anime/search'; | ||||
| const detailsRoute = '/anime/details'; | ||||
|  | ||||
| @ -3,6 +3,7 @@ import 'package:anitrack/src/data/manga.dart'; | ||||
| import 'package:anitrack/src/data/type.dart'; | ||||
| import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart'; | ||||
| import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart'; | ||||
| import 'package:anitrack/src/ui/bloc/details_bloc.dart'; | ||||
| import 'package:anitrack/src/ui/constants.dart'; | ||||
| import 'package:anitrack/src/ui/widgets/list_item.dart'; | ||||
| import 'package:bottom_bar/bottom_bar.dart'; | ||||
| @ -83,8 +84,8 @@ class AnimeListPage extends StatelessWidget { | ||||
|               child: Text('Completed'), | ||||
|             ), | ||||
|             const PopupMenuItem<MangaTrackingState>( | ||||
|               value: MangaTrackingState.planToWatch, | ||||
|               child: Text('Plan to watch'), | ||||
|               value: MangaTrackingState.planToRead, | ||||
|               child: Text('Plan to read'), | ||||
|             ), | ||||
|             const PopupMenuItem<MangaTrackingState>( | ||||
|               value: MangaTrackingState.dropped, | ||||
| @ -123,7 +124,13 @@ class AnimeListPage extends StatelessWidget { | ||||
|                     if (anime.state != state.animeFilterState) return Container(); | ||||
|                   } | ||||
| 
 | ||||
|                   return ListItem( | ||||
|                   return InkWell( | ||||
|                     onTap: () { | ||||
|                       context.read<DetailsBloc>().add( | ||||
|                         AnimeDetailsRequestedEvent(anime), | ||||
|                       ); | ||||
|                     }, | ||||
|                     child: ListItem( | ||||
|                       title: anime.title, | ||||
|                       thumbnailUrl: anime.thumbnailUrl, | ||||
|                       extra: [ | ||||
| @ -142,6 +149,7 @@ class AnimeListPage extends StatelessWidget { | ||||
|                           AnimeEpisodeIncrementedEvent(state.animes[index].id), | ||||
|                         ); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
| @ -153,7 +161,13 @@ class AnimeListPage extends StatelessWidget { | ||||
|                     if (manga.state != state.mangaFilterState) return Container(); | ||||
|                   } | ||||
| 
 | ||||
|                   return ListItem( | ||||
|                   return InkWell( | ||||
|                     onTap: () { | ||||
|                       context.read<DetailsBloc>().add( | ||||
|                         MangaDetailsRequestedEvent(manga), | ||||
|                       ); | ||||
|                     }, | ||||
|                     child: ListItem( | ||||
|                       title: manga.title, | ||||
|                       thumbnailUrl: manga.thumbnailUrl, | ||||
|                       extra: [ | ||||
| @ -172,6 +186,7 @@ class AnimeListPage extends StatelessWidget { | ||||
|                           MangaChapterIncrementedEvent(state.mangas[index].id), | ||||
|                         ); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|  | ||||
							
								
								
									
										174
									
								
								lib/src/ui/pages/details.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								lib/src/ui/pages/details.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,174 @@ | ||||
| import 'package:anitrack/src/data/anime.dart'; | ||||
| import 'package:anitrack/src/data/manga.dart'; | ||||
| import 'package:anitrack/src/data/type.dart'; | ||||
| import 'package:anitrack/src/ui/bloc/details_bloc.dart'; | ||||
| import 'package:anitrack/src/ui/constants.dart'; | ||||
| import 'package:anitrack/src/ui/widgets/image.dart'; | ||||
| import 'package:anitrack/src/ui/widgets/list_item.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_bloc/flutter_bloc.dart'; | ||||
| 
 | ||||
| Widget _makeListTile(BuildContext context, dynamic value, String text, bool selected) { | ||||
|   return ListTile( | ||||
|     title: Text(text), | ||||
|     trailing: selected ? | ||||
|       Icon(Icons.check) : | ||||
|       null, | ||||
|     onTap: () { | ||||
|       Navigator.of(context).pop(value); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class DetailsPage extends StatelessWidget { | ||||
|   static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>( | ||||
|     builder: (_) => DetailsPage(), | ||||
|     settings: const RouteSettings( | ||||
|       name: detailsRoute, | ||||
|     ), | ||||
|   ); | ||||
| 
 | ||||
|   String _getTrackingStateText(DetailsState state) { | ||||
|     if (state.trackingType == TrackingMediumType.anime) { | ||||
|       return (state.data as AnimeTrackingData).state.toNameString(); | ||||
|     } else { | ||||
|       return (state.data as MangaTrackingData).state.toNameString(); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text('Details'), | ||||
|       ), | ||||
|       body: BlocBuilder<DetailsBloc, DetailsState>( | ||||
|         builder: (context, state) { | ||||
|           return state.data == null ? | ||||
|           Container() : | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.all(8), | ||||
|             child: ListView( | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     AnimeCoverImage( | ||||
|                       url: state.trackingType == TrackingMediumType.anime ? | ||||
|                       (state.data as AnimeTrackingData).thumbnailUrl : | ||||
|                       (state.data as MangaTrackingData).thumbnailUrl, | ||||
|                     ), | ||||
| 
 | ||||
|                     Padding( | ||||
|                       padding: const EdgeInsets.only( | ||||
|                         left: 8, | ||||
|                       ), | ||||
|                       child: Column( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         children: [ | ||||
|                           Text( | ||||
|                             state.trackingType == TrackingMediumType.anime ? | ||||
|                               (state.data as AnimeTrackingData).title : | ||||
|                               (state.data as MangaTrackingData).title, | ||||
|                             textAlign: TextAlign.left, | ||||
|                             style: Theme.of(context).textTheme.titleLarge, | ||||
|                             maxLines: 2, | ||||
|                             softWrap: true, | ||||
|                             overflow: TextOverflow.ellipsis, | ||||
|                           ), | ||||
| 
 | ||||
|                           TextButton( | ||||
|                             child: Text( | ||||
|                               _getTrackingStateText(state), | ||||
|                             ), | ||||
|                             onPressed: () async { | ||||
|                               final result = await showModalBottomSheet<dynamic>( | ||||
|                                 context: context, | ||||
|                                 builder: (context) { | ||||
|                                   return ListView( | ||||
|                                     shrinkWrap: true, | ||||
|                                     children: [ | ||||
|                                       _makeListTile( | ||||
|                                         context, | ||||
|                                         state.trackingType == TrackingMediumType.anime ? | ||||
|                                           AnimeTrackingState.watching : | ||||
|                                           MangaTrackingState.reading, | ||||
|                                         state.trackingType == TrackingMediumType.anime ? | ||||
|                                           'Watching' : | ||||
|                                           'Reading', | ||||
|                                         state.trackingType == TrackingMediumType.anime ? | ||||
|                                           (state.data as AnimeTrackingData).state == AnimeTrackingState.watching : | ||||
|                                           (state.data as MangaTrackingData).state == MangaTrackingState.reading, | ||||
|                                       ), | ||||
|                                       _makeListTile( | ||||
|                                         context, | ||||
|                                         state.trackingType == TrackingMediumType.anime ? | ||||
|                                           AnimeTrackingState.completed : | ||||
|                                           MangaTrackingState.completed, | ||||
|                                         'Completed', | ||||
|                                         state.trackingType == TrackingMediumType.anime ? | ||||
|                                           (state.data as AnimeTrackingData).state == AnimeTrackingState.completed : | ||||
|                                           (state.data as MangaTrackingData).state == MangaTrackingState.completed, | ||||
|                                       ), | ||||
|                                       _makeListTile( | ||||
|                                         context, | ||||
|                                         state.trackingType == TrackingMediumType.anime ? | ||||
|                                           AnimeTrackingState.planToWatch : | ||||
|                                           MangaTrackingState.planToRead, | ||||
|                                         state.trackingType == TrackingMediumType.anime ? | ||||
|                                           'Plan to watch' : | ||||
|                                           'Plan to read', | ||||
|                                         state.trackingType == TrackingMediumType.anime ? | ||||
|                                           (state.data as AnimeTrackingData).state == AnimeTrackingState.planToWatch : | ||||
|                                           (state.data as MangaTrackingData).state == MangaTrackingState.planToRead, | ||||
|                                       ), | ||||
|                                       _makeListTile( | ||||
|                                         context, | ||||
|                                         state.trackingType == TrackingMediumType.anime ? | ||||
|                                           AnimeTrackingState.dropped : | ||||
|                                           MangaTrackingState.dropped, | ||||
|                                         'Dropped', | ||||
|                                         state.trackingType == TrackingMediumType.anime ? | ||||
|                                           (state.data as AnimeTrackingData).state == AnimeTrackingState.dropped : | ||||
|                                           (state.data as MangaTrackingData).state == MangaTrackingState.dropped, | ||||
|                                       ), | ||||
|                                     ], | ||||
|                                   ); | ||||
|                                 }, | ||||
|                               ); | ||||
| 
 | ||||
|                               if (result == null) return; | ||||
| 
 | ||||
|                               if (state.trackingType == TrackingMediumType.anime) { | ||||
|                                 context.read<DetailsBloc>().add( | ||||
|                                   DetailsUpdatedEvent( | ||||
|                                     (state.data as AnimeTrackingData).copyWith( | ||||
|                                       state: result as AnimeTrackingState, | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                 ); | ||||
|                               } else { | ||||
|                                 context.read<DetailsBloc>().add( | ||||
|                                   DetailsUpdatedEvent( | ||||
|                                     (state.data as MangaTrackingData).copyWith( | ||||
|                                       state: result as MangaTrackingState, | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                 ); | ||||
|                               } | ||||
|                             }, | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user