From 1892ec5e7a8a74513b7fdbf901b97bcf12107baa Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 4 Feb 2023 17:17:00 +0100 Subject: [PATCH] feat(ui): Implement a simple details screen --- lib/main.dart | 7 + lib/src/data/anime.dart | 12 ++ lib/src/data/manga.dart | 20 ++- lib/src/ui/bloc/anime_list_bloc.dart | 34 +++++ lib/src/ui/bloc/anime_list_event.dart | 12 ++ lib/src/ui/bloc/details_bloc.dart | 84 +++++++++++ lib/src/ui/bloc/details_bloc.freezed.dart | 151 +++++++++++++++++++ lib/src/ui/bloc/details_event.dart | 23 +++ lib/src/ui/bloc/details_state.dart | 9 ++ lib/src/ui/constants.dart | 1 + lib/src/ui/pages/anime_list.dart | 87 ++++++----- lib/src/ui/pages/details.dart | 174 ++++++++++++++++++++++ 12 files changed, 574 insertions(+), 40 deletions(-) create mode 100644 lib/src/ui/bloc/details_bloc.dart create mode 100644 lib/src/ui/bloc/details_bloc.freezed.dart create mode 100644 lib/src/ui/bloc/details_event.dart create mode 100644 lib/src/ui/bloc/details_state.dart create mode 100644 lib/src/ui/pages/details.dart diff --git a/lib/main.dart b/lib/main.dart index 30c6390..1c34dac 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(database); GetIt.I.registerSingleton(AnimeListBloc()); GetIt.I.registerSingleton(AnimeSearchBloc()); + GetIt.I.registerSingleton(DetailsBloc()); GetIt.I.registerSingleton(NavigationBloc(navKey)); // Load animes @@ -39,6 +42,9 @@ void main() async { BlocProvider( create: (_) => GetIt.I.get(), ), + BlocProvider( + create: (_) => GetIt.I.get(), + ), BlocProvider( create: (_) => GetIt.I.get(), ), @@ -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; diff --git a/lib/src/data/anime.dart b/lib/src/data/anime.dart index 0b4a40c..951f63c 100644 --- a/lib/src/data/anime.dart +++ b/lib/src/data/anime.dart @@ -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 { diff --git a/lib/src/data/manga.dart b/lib/src/data/manga.dart index bf0ec0d..47a0c42 100644 --- a/lib/src/data/manga.dart +++ b/lib/src/data/manga.dart @@ -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 { @@ -34,11 +46,11 @@ class MangaTrackingStateConverter implements JsonConverter { on(_onMangasFiltered); on(_onMangaIncremented); on(_onMangaDecremented); + on(_onAnimeUpdated); + on(_onMangaUpdated); } Future _onAnimeAdded(AnimeAddedEvent event, Emitter emit) async { @@ -173,4 +175,36 @@ class AnimeListBloc extends Bloc { await GetIt.I.get().updateManga(newManga); } + + Future _onAnimeUpdated(AnimeUpdatedEvent event, Emitter emit) async { + emit( + state.copyWith( + animes: List.from( + state.animes.map((anime) { + if (anime.id == event.anime.id) { + return event.anime; + } + + return anime; + }), + ), + ), + ); + } + + Future _onMangaUpdated(MangaUpdatedEvent event, Emitter emit) async { + emit( + state.copyWith( + mangas: List.from( + state.mangas.map((manga) { + if (manga.id == event.manga.id) { + return event.manga; + } + + return manga; + }), + ), + ), + ); + } } diff --git a/lib/src/ui/bloc/anime_list_event.dart b/lib/src/ui/bloc/anime_list_event.dart index 4707526..69b55b4 100644 --- a/lib/src/ui/bloc/anime_list_event.dart +++ b/lib/src/ui/bloc/anime_list_event.dart @@ -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; +} diff --git a/lib/src/ui/bloc/details_bloc.dart b/lib/src/ui/bloc/details_bloc.dart new file mode 100644 index 0000000..500b9d6 --- /dev/null +++ b/lib/src/ui/bloc/details_bloc.dart @@ -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 { + DetailsBloc() : super(DetailsState()) { + on(_onAnimeRequested); + on(_onMangaRequested); + on(_onDetailsUpdated); + } + + Future _onAnimeRequested(AnimeDetailsRequestedEvent event, Emitter emit) async { + emit( + state.copyWith( + trackingType: TrackingMediumType.anime, + data: event.anime, + ), + ); + + GetIt.I.get().add( + PushedNamedEvent( + NavigationDestination( + detailsRoute, + ), + ), + ); + } + + Future _onMangaRequested(MangaDetailsRequestedEvent event, Emitter emit) async { + emit( + state.copyWith( + trackingType: TrackingMediumType.manga, + data: event.manga, + ), + ); + + GetIt.I.get().add( + PushedNamedEvent( + NavigationDestination( + detailsRoute, + ), + ), + ); + } + + Future _onDetailsUpdated(DetailsUpdatedEvent event, Emitter emit) async { + if (state.trackingType == TrackingMediumType.anime) { + emit( + state.copyWith( + data: event.data, + ), + ); + + await GetIt.I.get().updateAnime(event.data as AnimeTrackingData); + + GetIt.I.get().add( + AnimeUpdatedEvent(event.data as AnimeTrackingData), + ); + } else { + emit( + state.copyWith( + data: event.data, + ), + ); + + await GetIt.I.get().updateManga(event.data as MangaTrackingData); + + GetIt.I.get().add( + MangaUpdatedEvent(event.data as MangaTrackingData), + ); + } + } +} diff --git a/lib/src/ui/bloc/details_bloc.freezed.dart b/lib/src/ui/bloc/details_bloc.freezed.dart new file mode 100644 index 0000000..98d961d --- /dev/null +++ b/lib/src/ui/bloc/details_bloc.freezed.dart @@ -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 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 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; +} diff --git a/lib/src/ui/bloc/details_event.dart b/lib/src/ui/bloc/details_event.dart new file mode 100644 index 0000000..abc39cc --- /dev/null +++ b/lib/src/ui/bloc/details_event.dart @@ -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; +} diff --git a/lib/src/ui/bloc/details_state.dart b/lib/src/ui/bloc/details_state.dart new file mode 100644 index 0000000..c1af52c --- /dev/null +++ b/lib/src/ui/bloc/details_state.dart @@ -0,0 +1,9 @@ +part of 'details_bloc.dart'; + +@freezed +class DetailsState with _$DetailsState { + factory DetailsState({ + dynamic data, + @Default(TrackingMediumType.anime) TrackingMediumType trackingType, + }) = _DetailsState; +} diff --git a/lib/src/ui/constants.dart b/lib/src/ui/constants.dart index b74755e..8cc7b16 100644 --- a/lib/src/ui/constants.dart +++ b/lib/src/ui/constants.dart @@ -1,2 +1,3 @@ const animeListRoute = '/anime/list'; const animeSearchRoute = '/anime/search'; +const detailsRoute = '/anime/details'; diff --git a/lib/src/ui/pages/anime_list.dart b/lib/src/ui/pages/anime_list.dart index 8ec2ed0..5903277 100644 --- a/lib/src/ui/pages/anime_list.dart +++ b/lib/src/ui/pages/anime_list.dart @@ -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( - value: MangaTrackingState.planToWatch, - child: Text('Plan to watch'), + value: MangaTrackingState.planToRead, + child: Text('Plan to read'), ), const PopupMenuItem( value: MangaTrackingState.dropped, @@ -123,25 +124,32 @@ class AnimeListPage extends StatelessWidget { if (anime.state != state.animeFilterState) return Container(); } - return ListItem( - title: anime.title, - thumbnailUrl: anime.thumbnailUrl, - extra: [ - Text( - '${anime.episodesWatched}/${anime.episodesTotal ?? "???"}', - style: Theme.of(context).textTheme.titleMedium, - ), - ], - onLeftSwipe: () { - context.read().add( - AnimeEpisodeDecrementedEvent(state.animes[index].id), - ); - }, - onRightSwipe: () { - context.read().add( - AnimeEpisodeIncrementedEvent(state.animes[index].id), + return InkWell( + onTap: () { + context.read().add( + AnimeDetailsRequestedEvent(anime), ); }, + child: ListItem( + title: anime.title, + thumbnailUrl: anime.thumbnailUrl, + extra: [ + Text( + '${anime.episodesWatched}/${anime.episodesTotal ?? "???"}', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + onLeftSwipe: () { + context.read().add( + AnimeEpisodeDecrementedEvent(state.animes[index].id), + ); + }, + onRightSwipe: () { + context.read().add( + AnimeEpisodeIncrementedEvent(state.animes[index].id), + ); + }, + ), ); }, ), @@ -153,25 +161,32 @@ class AnimeListPage extends StatelessWidget { if (manga.state != state.mangaFilterState) return Container(); } - return ListItem( - title: manga.title, - thumbnailUrl: manga.thumbnailUrl, - extra: [ - Text( - '${manga.chaptersRead}/${manga.chaptersTotal ?? "???"}', - style: Theme.of(context).textTheme.titleMedium, - ), - ], - onLeftSwipe: () { - context.read().add( - MangaChapterDecrementedEvent(state.mangas[index].id), - ); - }, - onRightSwipe: () { - context.read().add( - MangaChapterIncrementedEvent(state.mangas[index].id), + return InkWell( + onTap: () { + context.read().add( + MangaDetailsRequestedEvent(manga), ); }, + child: ListItem( + title: manga.title, + thumbnailUrl: manga.thumbnailUrl, + extra: [ + Text( + '${manga.chaptersRead}/${manga.chaptersTotal ?? "???"}', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + onLeftSwipe: () { + context.read().add( + MangaChapterDecrementedEvent(state.mangas[index].id), + ); + }, + onRightSwipe: () { + context.read().add( + MangaChapterIncrementedEvent(state.mangas[index].id), + ); + }, + ), ); }, ), diff --git a/lib/src/ui/pages/details.dart b/lib/src/ui/pages/details.dart new file mode 100644 index 0000000..202c588 --- /dev/null +++ b/lib/src/ui/pages/details.dart @@ -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 get route => MaterialPageRoute( + 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( + 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( + 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().add( + DetailsUpdatedEvent( + (state.data as AnimeTrackingData).copyWith( + state: result as AnimeTrackingState, + ), + ), + ); + } else { + context.read().add( + DetailsUpdatedEvent( + (state.data as MangaTrackingData).copyWith( + state: result as MangaTrackingState, + ), + ), + ); + } + }, + ), + ], + ), + ), + ], + ), + ], + ), + ); + }, + ), + ); + } +}