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_list_bloc.dart';
|
||||||
import 'package:anitrack/src/ui/bloc/anime_search_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/bloc/navigation_bloc.dart';
|
||||||
import 'package:anitrack/src/ui/constants.dart';
|
import 'package:anitrack/src/ui/constants.dart';
|
||||||
import 'package:anitrack/src/ui/pages/anime_list.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/anime_search.dart';
|
||||||
|
import 'package:anitrack/src/ui/pages/details.dart';
|
||||||
import 'package:anitrack/src/service/database.dart';
|
import 'package:anitrack/src/service/database.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -23,6 +25,7 @@ void main() async {
|
|||||||
GetIt.I.registerSingleton<DatabaseService>(database);
|
GetIt.I.registerSingleton<DatabaseService>(database);
|
||||||
GetIt.I.registerSingleton<AnimeListBloc>(AnimeListBloc());
|
GetIt.I.registerSingleton<AnimeListBloc>(AnimeListBloc());
|
||||||
GetIt.I.registerSingleton<AnimeSearchBloc>(AnimeSearchBloc());
|
GetIt.I.registerSingleton<AnimeSearchBloc>(AnimeSearchBloc());
|
||||||
|
GetIt.I.registerSingleton<DetailsBloc>(DetailsBloc());
|
||||||
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navKey));
|
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navKey));
|
||||||
|
|
||||||
// Load animes
|
// Load animes
|
||||||
@ -39,6 +42,9 @@ void main() async {
|
|||||||
BlocProvider<AnimeSearchBloc>(
|
BlocProvider<AnimeSearchBloc>(
|
||||||
create: (_) => GetIt.I.get<AnimeSearchBloc>(),
|
create: (_) => GetIt.I.get<AnimeSearchBloc>(),
|
||||||
),
|
),
|
||||||
|
BlocProvider<DetailsBloc>(
|
||||||
|
create: (_) => GetIt.I.get<DetailsBloc>(),
|
||||||
|
),
|
||||||
BlocProvider<NavigationBloc>(
|
BlocProvider<NavigationBloc>(
|
||||||
create: (_) => GetIt.I.get<NavigationBloc>(),
|
create: (_) => GetIt.I.get<NavigationBloc>(),
|
||||||
),
|
),
|
||||||
@ -77,6 +83,7 @@ class MyApp extends StatelessWidget {
|
|||||||
case '/':
|
case '/':
|
||||||
case animeListRoute: return AnimeListPage.route;
|
case animeListRoute: return AnimeListPage.route;
|
||||||
case animeSearchRoute: return AnimeSearchPage.route;
|
case animeSearchRoute: return AnimeSearchPage.route;
|
||||||
|
case detailsRoute: return DetailsPage.route;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -24,6 +24,18 @@ extension AnimeTrackStateExtension on AnimeTrackingState {
|
|||||||
case AnimeTrackingState.all: return -1;
|
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> {
|
class AnimeTrackingStateConverter implements JsonConverter<AnimeTrackingState, int> {
|
||||||
|
@ -7,7 +7,7 @@ part 'manga.g.dart';
|
|||||||
enum MangaTrackingState {
|
enum MangaTrackingState {
|
||||||
reading, // 0
|
reading, // 0
|
||||||
completed, // 1
|
completed, // 1
|
||||||
planToWatch, // 2
|
planToRead, // 2
|
||||||
dropped, // 3
|
dropped, // 3
|
||||||
/// This is a pseudo state, i.e. it should never be set
|
/// This is a pseudo state, i.e. it should never be set
|
||||||
all, // -1
|
all, // -1
|
||||||
@ -19,11 +19,23 @@ extension MangaTrackStateExtension on MangaTrackingState {
|
|||||||
switch (this) {
|
switch (this) {
|
||||||
case MangaTrackingState.reading: return 0;
|
case MangaTrackingState.reading: return 0;
|
||||||
case MangaTrackingState.completed: return 1;
|
case MangaTrackingState.completed: return 1;
|
||||||
case MangaTrackingState.planToWatch: return 2;
|
case MangaTrackingState.planToRead: return 2;
|
||||||
case MangaTrackingState.dropped: return 3;
|
case MangaTrackingState.dropped: return 3;
|
||||||
case MangaTrackingState.all: return -1;
|
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> {
|
class MangaTrackingStateConverter implements JsonConverter<MangaTrackingState, int> {
|
||||||
@ -34,11 +46,11 @@ class MangaTrackingStateConverter implements JsonConverter<MangaTrackingState, i
|
|||||||
switch (json) {
|
switch (json) {
|
||||||
case 0: return MangaTrackingState.reading;
|
case 0: return MangaTrackingState.reading;
|
||||||
case 1: return MangaTrackingState.completed;
|
case 1: return MangaTrackingState.completed;
|
||||||
case 2: return MangaTrackingState.planToWatch;
|
case 2: return MangaTrackingState.planToRead;
|
||||||
case 3: return MangaTrackingState.dropped;
|
case 3: return MangaTrackingState.dropped;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MangaTrackingState.planToWatch;
|
return MangaTrackingState.planToRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -23,6 +23,8 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
on<MangaFilterChangedEvent>(_onMangasFiltered);
|
on<MangaFilterChangedEvent>(_onMangasFiltered);
|
||||||
on<MangaChapterIncrementedEvent>(_onMangaIncremented);
|
on<MangaChapterIncrementedEvent>(_onMangaIncremented);
|
||||||
on<MangaChapterDecrementedEvent>(_onMangaDecremented);
|
on<MangaChapterDecrementedEvent>(_onMangaDecremented);
|
||||||
|
on<AnimeUpdatedEvent>(_onAnimeUpdated);
|
||||||
|
on<MangaUpdatedEvent>(_onMangaUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAnimeAdded(AnimeAddedEvent event, Emitter<AnimeListState> emit) async {
|
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);
|
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;
|
final TrackingMediumType type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AnimeUpdatedEvent extends AnimeListEvent {
|
||||||
|
AnimeUpdatedEvent(this.anime);
|
||||||
|
|
||||||
|
final AnimeTrackingData anime;
|
||||||
|
}
|
||||||
|
|
||||||
class MangaAddedEvent extends AnimeListEvent {
|
class MangaAddedEvent extends AnimeListEvent {
|
||||||
MangaAddedEvent(this.data);
|
MangaAddedEvent(this.data);
|
||||||
|
|
||||||
@ -70,3 +76,9 @@ class MangaChapterDecrementedEvent extends AnimeListEvent {
|
|||||||
/// The ID of the anime
|
/// The ID of the anime
|
||||||
final String id;
|
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 animeListRoute = '/anime/list';
|
||||||
const animeSearchRoute = '/anime/search';
|
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/data/type.dart';
|
||||||
import 'package:anitrack/src/ui/bloc/anime_list_bloc.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/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/constants.dart';
|
||||||
import 'package:anitrack/src/ui/widgets/list_item.dart';
|
import 'package:anitrack/src/ui/widgets/list_item.dart';
|
||||||
import 'package:bottom_bar/bottom_bar.dart';
|
import 'package:bottom_bar/bottom_bar.dart';
|
||||||
@ -83,8 +84,8 @@ class AnimeListPage extends StatelessWidget {
|
|||||||
child: Text('Completed'),
|
child: Text('Completed'),
|
||||||
),
|
),
|
||||||
const PopupMenuItem<MangaTrackingState>(
|
const PopupMenuItem<MangaTrackingState>(
|
||||||
value: MangaTrackingState.planToWatch,
|
value: MangaTrackingState.planToRead,
|
||||||
child: Text('Plan to watch'),
|
child: Text('Plan to read'),
|
||||||
),
|
),
|
||||||
const PopupMenuItem<MangaTrackingState>(
|
const PopupMenuItem<MangaTrackingState>(
|
||||||
value: MangaTrackingState.dropped,
|
value: MangaTrackingState.dropped,
|
||||||
@ -123,25 +124,32 @@ class AnimeListPage extends StatelessWidget {
|
|||||||
if (anime.state != state.animeFilterState) return Container();
|
if (anime.state != state.animeFilterState) return Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListItem(
|
return InkWell(
|
||||||
title: anime.title,
|
onTap: () {
|
||||||
thumbnailUrl: anime.thumbnailUrl,
|
context.read<DetailsBloc>().add(
|
||||||
extra: [
|
AnimeDetailsRequestedEvent(anime),
|
||||||
Text(
|
|
||||||
'${anime.episodesWatched}/${anime.episodesTotal ?? "???"}',
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onLeftSwipe: () {
|
|
||||||
context.read<AnimeListBloc>().add(
|
|
||||||
AnimeEpisodeDecrementedEvent(state.animes[index].id),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onRightSwipe: () {
|
|
||||||
context.read<AnimeListBloc>().add(
|
|
||||||
AnimeEpisodeIncrementedEvent(state.animes[index].id),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
child: ListItem(
|
||||||
|
title: anime.title,
|
||||||
|
thumbnailUrl: anime.thumbnailUrl,
|
||||||
|
extra: [
|
||||||
|
Text(
|
||||||
|
'${anime.episodesWatched}/${anime.episodesTotal ?? "???"}',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onLeftSwipe: () {
|
||||||
|
context.read<AnimeListBloc>().add(
|
||||||
|
AnimeEpisodeDecrementedEvent(state.animes[index].id),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onRightSwipe: () {
|
||||||
|
context.read<AnimeListBloc>().add(
|
||||||
|
AnimeEpisodeIncrementedEvent(state.animes[index].id),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -153,25 +161,32 @@ class AnimeListPage extends StatelessWidget {
|
|||||||
if (manga.state != state.mangaFilterState) return Container();
|
if (manga.state != state.mangaFilterState) return Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListItem(
|
return InkWell(
|
||||||
title: manga.title,
|
onTap: () {
|
||||||
thumbnailUrl: manga.thumbnailUrl,
|
context.read<DetailsBloc>().add(
|
||||||
extra: [
|
MangaDetailsRequestedEvent(manga),
|
||||||
Text(
|
|
||||||
'${manga.chaptersRead}/${manga.chaptersTotal ?? "???"}',
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onLeftSwipe: () {
|
|
||||||
context.read<AnimeListBloc>().add(
|
|
||||||
MangaChapterDecrementedEvent(state.mangas[index].id),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onRightSwipe: () {
|
|
||||||
context.read<AnimeListBloc>().add(
|
|
||||||
MangaChapterIncrementedEvent(state.mangas[index].id),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
child: ListItem(
|
||||||
|
title: manga.title,
|
||||||
|
thumbnailUrl: manga.thumbnailUrl,
|
||||||
|
extra: [
|
||||||
|
Text(
|
||||||
|
'${manga.chaptersRead}/${manga.chaptersTotal ?? "???"}',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onLeftSwipe: () {
|
||||||
|
context.read<AnimeListBloc>().add(
|
||||||
|
MangaChapterDecrementedEvent(state.mangas[index].id),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onRightSwipe: () {
|
||||||
|
context.read<AnimeListBloc>().add(
|
||||||
|
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