feat(meta): Implement manga tracking

This commit is contained in:
PapaTutuWawa 2023-02-04 12:48:57 +01:00
parent cd1291a192
commit 7af2277bb2
15 changed files with 883 additions and 126 deletions

70
lib/src/data/manga.dart Normal file
View File

@ -0,0 +1,70 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'manga.freezed.dart';
part 'manga.g.dart';
/// The watch state of an manga
enum MangaTrackingState {
reading, // 0
completed, // 1
planToWatch, // 2
dropped, // 3
/// This is a pseudo state, i.e. it should never be set
all, // -1
}
extension MangaTrackStateExtension on MangaTrackingState {
int toInteger() {
assert(this != MangaTrackingState.all, 'MangaTrackingState.all must not be serialized');
switch (this) {
case MangaTrackingState.reading: return 0;
case MangaTrackingState.completed: return 1;
case MangaTrackingState.planToWatch: return 2;
case MangaTrackingState.dropped: return 3;
case MangaTrackingState.all: return -1;
}
}
}
class MangaTrackingStateConverter implements JsonConverter<MangaTrackingState, int> {
const MangaTrackingStateConverter();
@override
MangaTrackingState fromJson(int json) {
switch (json) {
case 0: return MangaTrackingState.reading;
case 1: return MangaTrackingState.completed;
case 2: return MangaTrackingState.planToWatch;
case 3: return MangaTrackingState.dropped;
}
return MangaTrackingState.planToWatch;
}
@override
int toJson(MangaTrackingState state) => state.toInteger();
}
/// Data about a tracked anime
@freezed
class MangaTrackingData with _$MangaTrackingData{
factory MangaTrackingData(
/// The ID of the manga
String id,
/// The state of the manga
@MangaTrackingStateConverter() MangaTrackingState state,
/// The title of the manga
String title,
/// Chapters read.
int chaptersRead,
/// Chapters read.
int volumesOwned,
/// Episodes watched.
int? chaptersTotal,
/// URL to the thumbnail/cover art for the manga.
String thumbnailUrl,
) = _MangaTrackingData;
/// JSON
factory MangaTrackingData.fromJson(Map<String, dynamic> json) => _$MangaTrackingDataFromJson(json);
}

View File

@ -0,0 +1,328 @@
// 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 'manga.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');
MangaTrackingData _$MangaTrackingDataFromJson(Map<String, dynamic> json) {
return _MangaTrackingData.fromJson(json);
}
/// @nodoc
mixin _$MangaTrackingData {
/// The ID of the manga
String get id => throw _privateConstructorUsedError;
/// The state of the manga
@MangaTrackingStateConverter()
MangaTrackingState get state => throw _privateConstructorUsedError;
/// The title of the manga
String get title => throw _privateConstructorUsedError;
/// Chapters read.
int get chaptersRead => throw _privateConstructorUsedError;
/// Chapters read.
int get volumesOwned => throw _privateConstructorUsedError;
/// Episodes watched.
int? get chaptersTotal => throw _privateConstructorUsedError;
/// URL to the thumbnail/cover art for the manga.
String get thumbnailUrl => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$MangaTrackingDataCopyWith<MangaTrackingData> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $MangaTrackingDataCopyWith<$Res> {
factory $MangaTrackingDataCopyWith(
MangaTrackingData value, $Res Function(MangaTrackingData) then) =
_$MangaTrackingDataCopyWithImpl<$Res>;
$Res call(
{String id,
@MangaTrackingStateConverter() MangaTrackingState state,
String title,
int chaptersRead,
int volumesOwned,
int? chaptersTotal,
String thumbnailUrl});
}
/// @nodoc
class _$MangaTrackingDataCopyWithImpl<$Res>
implements $MangaTrackingDataCopyWith<$Res> {
_$MangaTrackingDataCopyWithImpl(this._value, this._then);
final MangaTrackingData _value;
// ignore: unused_field
final $Res Function(MangaTrackingData) _then;
@override
$Res call({
Object? id = freezed,
Object? state = freezed,
Object? title = freezed,
Object? chaptersRead = freezed,
Object? volumesOwned = freezed,
Object? chaptersTotal = freezed,
Object? thumbnailUrl = freezed,
}) {
return _then(_value.copyWith(
id: id == freezed
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
state: state == freezed
? _value.state
: state // ignore: cast_nullable_to_non_nullable
as MangaTrackingState,
title: title == freezed
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
chaptersRead: chaptersRead == freezed
? _value.chaptersRead
: chaptersRead // ignore: cast_nullable_to_non_nullable
as int,
volumesOwned: volumesOwned == freezed
? _value.volumesOwned
: volumesOwned // ignore: cast_nullable_to_non_nullable
as int,
chaptersTotal: chaptersTotal == freezed
? _value.chaptersTotal
: chaptersTotal // ignore: cast_nullable_to_non_nullable
as int?,
thumbnailUrl: thumbnailUrl == freezed
? _value.thumbnailUrl
: thumbnailUrl // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
abstract class _$$_MangaTrackingDataCopyWith<$Res>
implements $MangaTrackingDataCopyWith<$Res> {
factory _$$_MangaTrackingDataCopyWith(_$_MangaTrackingData value,
$Res Function(_$_MangaTrackingData) then) =
__$$_MangaTrackingDataCopyWithImpl<$Res>;
@override
$Res call(
{String id,
@MangaTrackingStateConverter() MangaTrackingState state,
String title,
int chaptersRead,
int volumesOwned,
int? chaptersTotal,
String thumbnailUrl});
}
/// @nodoc
class __$$_MangaTrackingDataCopyWithImpl<$Res>
extends _$MangaTrackingDataCopyWithImpl<$Res>
implements _$$_MangaTrackingDataCopyWith<$Res> {
__$$_MangaTrackingDataCopyWithImpl(
_$_MangaTrackingData _value, $Res Function(_$_MangaTrackingData) _then)
: super(_value, (v) => _then(v as _$_MangaTrackingData));
@override
_$_MangaTrackingData get _value => super._value as _$_MangaTrackingData;
@override
$Res call({
Object? id = freezed,
Object? state = freezed,
Object? title = freezed,
Object? chaptersRead = freezed,
Object? volumesOwned = freezed,
Object? chaptersTotal = freezed,
Object? thumbnailUrl = freezed,
}) {
return _then(_$_MangaTrackingData(
id == freezed
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
state == freezed
? _value.state
: state // ignore: cast_nullable_to_non_nullable
as MangaTrackingState,
title == freezed
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
chaptersRead == freezed
? _value.chaptersRead
: chaptersRead // ignore: cast_nullable_to_non_nullable
as int,
volumesOwned == freezed
? _value.volumesOwned
: volumesOwned // ignore: cast_nullable_to_non_nullable
as int,
chaptersTotal == freezed
? _value.chaptersTotal
: chaptersTotal // ignore: cast_nullable_to_non_nullable
as int?,
thumbnailUrl == freezed
? _value.thumbnailUrl
: thumbnailUrl // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_MangaTrackingData implements _MangaTrackingData {
_$_MangaTrackingData(
this.id,
@MangaTrackingStateConverter() this.state,
this.title,
this.chaptersRead,
this.volumesOwned,
this.chaptersTotal,
this.thumbnailUrl);
factory _$_MangaTrackingData.fromJson(Map<String, dynamic> json) =>
_$$_MangaTrackingDataFromJson(json);
/// The ID of the manga
@override
final String id;
/// The state of the manga
@override
@MangaTrackingStateConverter()
final MangaTrackingState state;
/// The title of the manga
@override
final String title;
/// Chapters read.
@override
final int chaptersRead;
/// Chapters read.
@override
final int volumesOwned;
/// Episodes watched.
@override
final int? chaptersTotal;
/// URL to the thumbnail/cover art for the manga.
@override
final String thumbnailUrl;
@override
String toString() {
return 'MangaTrackingData(id: $id, state: $state, title: $title, chaptersRead: $chaptersRead, volumesOwned: $volumesOwned, chaptersTotal: $chaptersTotal, thumbnailUrl: $thumbnailUrl)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_MangaTrackingData &&
const DeepCollectionEquality().equals(other.id, id) &&
const DeepCollectionEquality().equals(other.state, state) &&
const DeepCollectionEquality().equals(other.title, title) &&
const DeepCollectionEquality()
.equals(other.chaptersRead, chaptersRead) &&
const DeepCollectionEquality()
.equals(other.volumesOwned, volumesOwned) &&
const DeepCollectionEquality()
.equals(other.chaptersTotal, chaptersTotal) &&
const DeepCollectionEquality()
.equals(other.thumbnailUrl, thumbnailUrl));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(id),
const DeepCollectionEquality().hash(state),
const DeepCollectionEquality().hash(title),
const DeepCollectionEquality().hash(chaptersRead),
const DeepCollectionEquality().hash(volumesOwned),
const DeepCollectionEquality().hash(chaptersTotal),
const DeepCollectionEquality().hash(thumbnailUrl));
@JsonKey(ignore: true)
@override
_$$_MangaTrackingDataCopyWith<_$_MangaTrackingData> get copyWith =>
__$$_MangaTrackingDataCopyWithImpl<_$_MangaTrackingData>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_MangaTrackingDataToJson(
this,
);
}
}
abstract class _MangaTrackingData implements MangaTrackingData {
factory _MangaTrackingData(
final String id,
@MangaTrackingStateConverter() final MangaTrackingState state,
final String title,
final int chaptersRead,
final int volumesOwned,
final int? chaptersTotal,
final String thumbnailUrl) = _$_MangaTrackingData;
factory _MangaTrackingData.fromJson(Map<String, dynamic> json) =
_$_MangaTrackingData.fromJson;
@override
/// The ID of the manga
String get id;
@override
/// The state of the manga
@MangaTrackingStateConverter()
MangaTrackingState get state;
@override
/// The title of the manga
String get title;
@override
/// Chapters read.
int get chaptersRead;
@override
/// Chapters read.
int get volumesOwned;
@override
/// Episodes watched.
int? get chaptersTotal;
@override
/// URL to the thumbnail/cover art for the manga.
String get thumbnailUrl;
@override
@JsonKey(ignore: true)
_$$_MangaTrackingDataCopyWith<_$_MangaTrackingData> get copyWith =>
throw _privateConstructorUsedError;
}

30
lib/src/data/manga.g.dart Normal file
View File

@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'manga.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$_MangaTrackingData _$$_MangaTrackingDataFromJson(Map<String, dynamic> json) =>
_$_MangaTrackingData(
json['id'] as String,
const MangaTrackingStateConverter().fromJson(json['state'] as int),
json['title'] as String,
json['chaptersRead'] as int,
json['volumesOwned'] as int,
json['chaptersTotal'] as int?,
json['thumbnailUrl'] as String,
);
Map<String, dynamic> _$$_MangaTrackingDataToJson(
_$_MangaTrackingData instance) =>
<String, dynamic>{
'id': instance.id,
'state': const MangaTrackingStateConverter().toJson(instance.state),
'title': instance.title,
'chaptersRead': instance.chaptersRead,
'volumesOwned': instance.volumesOwned,
'chaptersTotal': instance.chaptersTotal,
'thumbnailUrl': instance.thumbnailUrl,
};

View File

@ -1,8 +1,8 @@
class AnimeSearchResult { class SearchResult {
const AnimeSearchResult( const SearchResult(
this.title, this.title,
this.id, this.id,
this.episodesTotal, this.total,
this.thumbnailUrl, this.thumbnailUrl,
this.description, this.description,
); );
@ -16,9 +16,9 @@ class AnimeSearchResult {
/// The URL to a thumbnail image. /// The URL to a thumbnail image.
final String thumbnailUrl; final String thumbnailUrl;
/// The amount of total episodes. If null, it means that there is not total amount /// The amount of total episodes or chapters. If null, it means that there is not
/// of episodes set. /// total amount of episodes set.
final int? episodesTotal; final int? total;
/// The description of the anime /// The description of the anime
final String description; final String description;

View File

@ -1,7 +1,9 @@
import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/manga.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
const animeTable = 'Anime'; const animeTable = 'Anime';
const mangaTable = 'Manga';
Future<void> _createDatabase(Database db, int version) async { Future<void> _createDatabase(Database db, int version) async {
await db.execute( await db.execute(
@ -15,6 +17,18 @@ Future<void> _createDatabase(Database db, int version) async {
title TEXT NOT NULL title TEXT NOT NULL
)''', )''',
); );
await db.execute(
'''
CREATE TABLE $mangaTable(
id TEXT NOT NULL PRIMARY KEY,
state INTEGER NOT NULL,
chaptersTotal INTEGER,
chaptersRead INTEGER NOT NULL,
volumesOwned INTEGER NOT NULL,
thumbnailUrl TEXT NOT NULL,
title TEXT NOT NULL
)''',
);
} }
class DatabaseService { class DatabaseService {
@ -36,6 +50,15 @@ class DatabaseService {
.map((Map<String, dynamic> anime) => AnimeTrackingData.fromJson(anime)) .map((Map<String, dynamic> anime) => AnimeTrackingData.fromJson(anime))
.toList(); .toList();
} }
Future<List<MangaTrackingData>> loadMangas() async {
final mangas = await _db.query(mangaTable);
return mangas
.cast<Map<String, dynamic>>()
.map((Map<String, dynamic> manga) => MangaTrackingData.fromJson(manga))
.toList();
}
Future<void> addAnime(AnimeTrackingData data) async { Future<void> addAnime(AnimeTrackingData data) async {
await _db.insert( await _db.insert(
@ -52,4 +75,20 @@ class DatabaseService {
whereArgs: [data.id], whereArgs: [data.id],
); );
} }
Future<void> addManga(MangaTrackingData data) async {
await _db.insert(
mangaTable,
data.toJson(),
);
}
Future<void> updateManga(MangaTrackingData data) async {
await _db.update(
mangaTable,
data.toJson(),
where: 'id = ?',
whereArgs: [data.id],
);
}
} }

View File

@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:anitrack/src/data/anime.dart'; 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/data/type.dart';
import 'package:anitrack/src/service/database.dart'; import 'package:anitrack/src/service/database.dart';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
@ -13,11 +14,15 @@ part 'anime_list_bloc.freezed.dart';
class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> { class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
AnimeListBloc() : super(AnimeListState()) { AnimeListBloc() : super(AnimeListState()) {
on<AnimeAddedEvent>(_onAnimeAdded); on<AnimeAddedEvent>(_onAnimeAdded);
on<AnimeEpisodeIncrementedEvent>(_onIncremented); on<MangaAddedEvent>(_onMangaAdded);
on<AnimeEpisodeDecrementedEvent>(_onDecremented); on<AnimeEpisodeIncrementedEvent>(_onAnimeIncremented);
on<AnimeEpisodeDecrementedEvent>(_onAnimeDecremented);
on<AnimesLoadedEvent>(_onAnimesLoaded); on<AnimesLoadedEvent>(_onAnimesLoaded);
on<AnimeFilterChangedEvent>(_onAnimesFiltered); on<AnimeFilterChangedEvent>(_onAnimesFiltered);
on<AnimeTrackingTypeChanged>(_onTrackingTypeChanged); on<AnimeTrackingTypeChanged>(_onTrackingTypeChanged);
on<MangaFilterChangedEvent>(_onMangasFiltered);
on<MangaChapterIncrementedEvent>(_onMangaIncremented);
on<MangaChapterDecrementedEvent>(_onMangaDecremented);
} }
Future<void> _onAnimeAdded(AnimeAddedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onAnimeAdded(AnimeAddedEvent event, Emitter<AnimeListState> emit) async {
@ -34,7 +39,21 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
); );
} }
Future<void> _onIncremented(AnimeEpisodeIncrementedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onMangaAdded(MangaAddedEvent event, Emitter<AnimeListState> emit) async {
// Add the manga to the database
await GetIt.I.get<DatabaseService>().addManga(event.data);
emit(
state.copyWith(
mangas: List.from([
...state.mangas,
event.data,
]),
),
);
}
Future<void> _onAnimeIncremented(AnimeEpisodeIncrementedEvent event, Emitter<AnimeListState> emit) async {
final index = state.animes.indexWhere((item) => item.id == event.id); final index = state.animes.indexWhere((item) => item.id == event.id);
if (index == -1) return; if (index == -1) return;
@ -56,7 +75,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
await GetIt.I.get<DatabaseService>().updateAnime(newAnime); await GetIt.I.get<DatabaseService>().updateAnime(newAnime);
} }
Future<void> _onDecremented(AnimeEpisodeDecrementedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onAnimeDecremented(AnimeEpisodeDecrementedEvent event, Emitter<AnimeListState> emit) async {
final index = state.animes.indexWhere((item) => item.id == event.id); final index = state.animes.indexWhere((item) => item.id == event.id);
if (index == -1) return; if (index == -1) return;
@ -82,6 +101,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
emit( emit(
state.copyWith( state.copyWith(
animes: await GetIt.I.get<DatabaseService>().loadAnimes(), animes: await GetIt.I.get<DatabaseService>().loadAnimes(),
mangas: await GetIt.I.get<DatabaseService>().loadMangas(),
), ),
); );
} }
@ -89,11 +109,19 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
Future<void> _onAnimesFiltered(AnimeFilterChangedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onAnimesFiltered(AnimeFilterChangedEvent event, Emitter<AnimeListState> emit) async {
emit( emit(
state.copyWith( state.copyWith(
filterState: event.filterState, animeFilterState: event.filterState,
), ),
); );
} }
Future<void> _onMangasFiltered(MangaFilterChangedEvent event, Emitter<AnimeListState> emit) async {
emit(
state.copyWith(
mangaFilterState: event.filterState,
),
);
}
Future<void> _onTrackingTypeChanged(AnimeTrackingTypeChanged event, Emitter<AnimeListState> emit) async { Future<void> _onTrackingTypeChanged(AnimeTrackingTypeChanged event, Emitter<AnimeListState> emit) async {
emit( emit(
state.copyWith( state.copyWith(
@ -101,4 +129,48 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
), ),
); );
} }
Future<void> _onMangaIncremented(MangaChapterIncrementedEvent event, Emitter<AnimeListState> emit) async {
final index = state.mangas.indexWhere((item) => item.id == event.id);
if (index == -1) return;
final manga = state.mangas[index];
if (manga.chaptersTotal != null && manga.chaptersRead + 1 > manga.chaptersTotal!) return;
final newList = List<MangaTrackingData>.from(state.mangas);
final newManga = manga.copyWith(
chaptersRead: manga.chaptersRead + 1,
);
newList[index] = newManga;
emit(
state.copyWith(
mangas: newList,
),
);
await GetIt.I.get<DatabaseService>().updateManga(newManga);
}
Future<void> _onMangaDecremented(MangaChapterDecrementedEvent event, Emitter<AnimeListState> emit) async {
final index = state.mangas.indexWhere((item) => item.id == event.id);
if (index == -1) return;
final manga = state.mangas[index];
if (manga.chaptersRead - 1 < 0) return;
final newList = List<MangaTrackingData>.from(state.mangas);
final newManga = manga.copyWith(
chaptersRead: manga.chaptersRead - 1,
);
newList[index] = newManga;
emit(
state.copyWith(
mangas: newList,
),
);
await GetIt.I.get<DatabaseService>().updateManga(newManga);
}
} }

View File

@ -17,7 +17,9 @@ final _privateConstructorUsedError = UnsupportedError(
/// @nodoc /// @nodoc
mixin _$AnimeListState { mixin _$AnimeListState {
List<AnimeTrackingData> get animes => throw _privateConstructorUsedError; List<AnimeTrackingData> get animes => throw _privateConstructorUsedError;
AnimeTrackingState get filterState => throw _privateConstructorUsedError; List<MangaTrackingData> get mangas => throw _privateConstructorUsedError;
AnimeTrackingState get animeFilterState => throw _privateConstructorUsedError;
MangaTrackingState get mangaFilterState => throw _privateConstructorUsedError;
TrackingMediumType get trackingType => throw _privateConstructorUsedError; TrackingMediumType get trackingType => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
@ -32,7 +34,9 @@ abstract class $AnimeListStateCopyWith<$Res> {
_$AnimeListStateCopyWithImpl<$Res>; _$AnimeListStateCopyWithImpl<$Res>;
$Res call( $Res call(
{List<AnimeTrackingData> animes, {List<AnimeTrackingData> animes,
AnimeTrackingState filterState, List<MangaTrackingData> mangas,
AnimeTrackingState animeFilterState,
MangaTrackingState mangaFilterState,
TrackingMediumType trackingType}); TrackingMediumType trackingType});
} }
@ -48,7 +52,9 @@ class _$AnimeListStateCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? animes = freezed, Object? animes = freezed,
Object? filterState = freezed, Object? mangas = freezed,
Object? animeFilterState = freezed,
Object? mangaFilterState = freezed,
Object? trackingType = freezed, Object? trackingType = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
@ -56,10 +62,18 @@ class _$AnimeListStateCopyWithImpl<$Res>
? _value.animes ? _value.animes
: animes // ignore: cast_nullable_to_non_nullable : animes // ignore: cast_nullable_to_non_nullable
as List<AnimeTrackingData>, as List<AnimeTrackingData>,
filterState: filterState == freezed mangas: mangas == freezed
? _value.filterState ? _value.mangas
: filterState // ignore: cast_nullable_to_non_nullable : mangas // ignore: cast_nullable_to_non_nullable
as List<MangaTrackingData>,
animeFilterState: animeFilterState == freezed
? _value.animeFilterState
: animeFilterState // ignore: cast_nullable_to_non_nullable
as AnimeTrackingState, as AnimeTrackingState,
mangaFilterState: mangaFilterState == freezed
? _value.mangaFilterState
: mangaFilterState // ignore: cast_nullable_to_non_nullable
as MangaTrackingState,
trackingType: trackingType == freezed trackingType: trackingType == freezed
? _value.trackingType ? _value.trackingType
: trackingType // ignore: cast_nullable_to_non_nullable : trackingType // ignore: cast_nullable_to_non_nullable
@ -77,7 +91,9 @@ abstract class _$$_AnimeListStateCopyWith<$Res>
@override @override
$Res call( $Res call(
{List<AnimeTrackingData> animes, {List<AnimeTrackingData> animes,
AnimeTrackingState filterState, List<MangaTrackingData> mangas,
AnimeTrackingState animeFilterState,
MangaTrackingState mangaFilterState,
TrackingMediumType trackingType}); TrackingMediumType trackingType});
} }
@ -95,7 +111,9 @@ class __$$_AnimeListStateCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? animes = freezed, Object? animes = freezed,
Object? filterState = freezed, Object? mangas = freezed,
Object? animeFilterState = freezed,
Object? mangaFilterState = freezed,
Object? trackingType = freezed, Object? trackingType = freezed,
}) { }) {
return _then(_$_AnimeListState( return _then(_$_AnimeListState(
@ -103,10 +121,18 @@ class __$$_AnimeListStateCopyWithImpl<$Res>
? _value._animes ? _value._animes
: animes // ignore: cast_nullable_to_non_nullable : animes // ignore: cast_nullable_to_non_nullable
as List<AnimeTrackingData>, as List<AnimeTrackingData>,
filterState: filterState == freezed mangas: mangas == freezed
? _value.filterState ? _value._mangas
: filterState // ignore: cast_nullable_to_non_nullable : mangas // ignore: cast_nullable_to_non_nullable
as List<MangaTrackingData>,
animeFilterState: animeFilterState == freezed
? _value.animeFilterState
: animeFilterState // ignore: cast_nullable_to_non_nullable
as AnimeTrackingState, as AnimeTrackingState,
mangaFilterState: mangaFilterState == freezed
? _value.mangaFilterState
: mangaFilterState // ignore: cast_nullable_to_non_nullable
as MangaTrackingState,
trackingType: trackingType == freezed trackingType: trackingType == freezed
? _value.trackingType ? _value.trackingType
: trackingType // ignore: cast_nullable_to_non_nullable : trackingType // ignore: cast_nullable_to_non_nullable
@ -120,9 +146,12 @@ class __$$_AnimeListStateCopyWithImpl<$Res>
class _$_AnimeListState implements _AnimeListState { class _$_AnimeListState implements _AnimeListState {
_$_AnimeListState( _$_AnimeListState(
{final List<AnimeTrackingData> animes = const [], {final List<AnimeTrackingData> animes = const [],
this.filterState = AnimeTrackingState.watching, final List<MangaTrackingData> mangas = const [],
this.animeFilterState = AnimeTrackingState.watching,
this.mangaFilterState = MangaTrackingState.reading,
this.trackingType = TrackingMediumType.anime}) this.trackingType = TrackingMediumType.anime})
: _animes = animes; : _animes = animes,
_mangas = mangas;
final List<AnimeTrackingData> _animes; final List<AnimeTrackingData> _animes;
@override @override
@ -132,16 +161,27 @@ class _$_AnimeListState implements _AnimeListState {
return EqualUnmodifiableListView(_animes); return EqualUnmodifiableListView(_animes);
} }
final List<MangaTrackingData> _mangas;
@override @override
@JsonKey() @JsonKey()
final AnimeTrackingState filterState; List<MangaTrackingData> get mangas {
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_mangas);
}
@override
@JsonKey()
final AnimeTrackingState animeFilterState;
@override
@JsonKey()
final MangaTrackingState mangaFilterState;
@override @override
@JsonKey() @JsonKey()
final TrackingMediumType trackingType; final TrackingMediumType trackingType;
@override @override
String toString() { String toString() {
return 'AnimeListState(animes: $animes, filterState: $filterState, trackingType: $trackingType)'; return 'AnimeListState(animes: $animes, mangas: $mangas, animeFilterState: $animeFilterState, mangaFilterState: $mangaFilterState, trackingType: $trackingType)';
} }
@override @override
@ -150,8 +190,11 @@ class _$_AnimeListState implements _AnimeListState {
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$_AnimeListState && other is _$_AnimeListState &&
const DeepCollectionEquality().equals(other._animes, _animes) && const DeepCollectionEquality().equals(other._animes, _animes) &&
const DeepCollectionEquality().equals(other._mangas, _mangas) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.filterState, filterState) && .equals(other.animeFilterState, animeFilterState) &&
const DeepCollectionEquality()
.equals(other.mangaFilterState, mangaFilterState) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.trackingType, trackingType)); .equals(other.trackingType, trackingType));
} }
@ -160,7 +203,9 @@ class _$_AnimeListState implements _AnimeListState {
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
const DeepCollectionEquality().hash(_animes), const DeepCollectionEquality().hash(_animes),
const DeepCollectionEquality().hash(filterState), const DeepCollectionEquality().hash(_mangas),
const DeepCollectionEquality().hash(animeFilterState),
const DeepCollectionEquality().hash(mangaFilterState),
const DeepCollectionEquality().hash(trackingType)); const DeepCollectionEquality().hash(trackingType));
@JsonKey(ignore: true) @JsonKey(ignore: true)
@ -172,13 +217,19 @@ class _$_AnimeListState implements _AnimeListState {
abstract class _AnimeListState implements AnimeListState { abstract class _AnimeListState implements AnimeListState {
factory _AnimeListState( factory _AnimeListState(
{final List<AnimeTrackingData> animes, {final List<AnimeTrackingData> animes,
final AnimeTrackingState filterState, final List<MangaTrackingData> mangas,
final AnimeTrackingState animeFilterState,
final MangaTrackingState mangaFilterState,
final TrackingMediumType trackingType}) = _$_AnimeListState; final TrackingMediumType trackingType}) = _$_AnimeListState;
@override @override
List<AnimeTrackingData> get animes; List<AnimeTrackingData> get animes;
@override @override
AnimeTrackingState get filterState; List<MangaTrackingData> get mangas;
@override
AnimeTrackingState get animeFilterState;
@override
MangaTrackingState get mangaFilterState;
@override @override
TrackingMediumType get trackingType; TrackingMediumType get trackingType;
@override @override

View File

@ -26,7 +26,7 @@ class AnimeAddedEvent extends AnimeListEvent {
/// Triggered when animes are to be loaded from the database /// Triggered when animes are to be loaded from the database
class AnimesLoadedEvent extends AnimeListEvent {} class AnimesLoadedEvent extends AnimeListEvent {}
/// Triggered when the filter is changed /// Triggered when the anime filter is changed
class AnimeFilterChangedEvent extends AnimeListEvent { class AnimeFilterChangedEvent extends AnimeListEvent {
AnimeFilterChangedEvent(this.filterState); AnimeFilterChangedEvent(this.filterState);
@ -41,3 +41,32 @@ class AnimeTrackingTypeChanged extends AnimeListEvent {
/// The type we switched to /// The type we switched to
final TrackingMediumType type; final TrackingMediumType type;
} }
class MangaAddedEvent extends AnimeListEvent {
MangaAddedEvent(this.data);
/// The manga to add.
final MangaTrackingData data;
}
/// Triggered when the manga filter is changed
class MangaFilterChangedEvent extends AnimeListEvent {
MangaFilterChangedEvent(this.filterState);
/// The state to filter
final MangaTrackingState filterState;
}
class MangaChapterIncrementedEvent extends AnimeListEvent {
MangaChapterIncrementedEvent(this.id);
/// The ID of the anime
final String id;
}
class MangaChapterDecrementedEvent extends AnimeListEvent {
MangaChapterDecrementedEvent(this.id);
/// The ID of the anime
final String id;
}

View File

@ -4,7 +4,9 @@ part of 'anime_list_bloc.dart';
class AnimeListState with _$AnimeListState { class AnimeListState with _$AnimeListState {
factory AnimeListState({ factory AnimeListState({
@Default([]) List<AnimeTrackingData> animes, @Default([]) List<AnimeTrackingData> animes,
@Default(AnimeTrackingState.watching) AnimeTrackingState filterState, @Default([]) List<MangaTrackingData> mangas,
@Default(AnimeTrackingState.watching) AnimeTrackingState animeFilterState,
@Default(MangaTrackingState.reading) MangaTrackingState mangaFilterState,
@Default(TrackingMediumType.anime) TrackingMediumType trackingType, @Default(TrackingMediumType.anime) TrackingMediumType trackingType,
}) = _AnimeListState; }) = _AnimeListState;
} }

View File

@ -1,5 +1,7 @@
import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/manga.dart';
import 'package:anitrack/src/data/search_result.dart'; import 'package:anitrack/src/data/search_result.dart';
import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart' as list; import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart' as list;
import 'package:anitrack/src/ui/bloc/navigation_bloc.dart'; import 'package:anitrack/src/ui/bloc/navigation_bloc.dart';
@ -17,7 +19,7 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
on<AnimeSearchRequestedEvent>(_onRequested); on<AnimeSearchRequestedEvent>(_onRequested);
on<SearchQueryChangedEvent>(_onQueryChanged); on<SearchQueryChangedEvent>(_onQueryChanged);
on<SearchQuerySubmittedEvent>(_onQuerySubmitted); on<SearchQuerySubmittedEvent>(_onQuerySubmitted);
on<AnimeAddedEvent>(_onAnimeAdded); on<ResultTappedEvent>(_onResultTapped);
} }
Future<void> _onRequested(AnimeSearchRequestedEvent event, Emitter<AnimeSearchState> emit) async { Future<void> _onRequested(AnimeSearchRequestedEvent event, Emitter<AnimeSearchState> emit) async {
@ -26,6 +28,7 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
searchQuery: '', searchQuery: '',
working: false, working: false,
searchResults: [], searchResults: [],
trackingType: event.type,
), ),
); );
@ -52,42 +55,70 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
working: true, working: true,
), ),
); );
final result = await Jikan().searchAnime(
query: state.searchQuery,
);
emit( if (state.trackingType == TrackingMediumType.anime) {
state.copyWith( // Anime
working: false, final result = await Jikan().searchAnime(
), query: state.searchQuery,
); );
emit( emit(
state.copyWith( state.copyWith(
searchResults: result.map((Anime anime) => AnimeSearchResult( working: false,
anime.title, searchResults: result.map((Anime anime) => SearchResult(
anime.malId.toString(), anime.title,
anime.episodes, anime.malId.toString(),
anime.imageUrl, anime.episodes,
anime.synopsis ?? '', anime.imageUrl,
),).toList(), anime.synopsis ?? '',
), ),).toList(),
); ),
);
} else {
// Manga
final result = await Jikan().searchManga(
query: state.searchQuery,
);
emit(
state.copyWith(
working: false,
searchResults: result.map((Manga manga) => SearchResult(
manga.title,
manga.malId.toString(),
manga.chapters,
manga.imageUrl,
manga.synopsis ?? '',
),).toList(),
),
);
}
} }
Future<void> _onAnimeAdded(AnimeAddedEvent event, Emitter<AnimeSearchState> emit) async { Future<void> _onResultTapped(ResultTappedEvent event, Emitter<AnimeSearchState> emit) async {
GetIt.I.get<list.AnimeListBloc>().add( GetIt.I.get<list.AnimeListBloc>().add(
list.AnimeAddedEvent( state.trackingType == TrackingMediumType.anime ?
AnimeTrackingData( list.AnimeAddedEvent(
event.result.id, AnimeTrackingData(
AnimeTrackingState.watching, event.result.id,
event.result.title, AnimeTrackingState.watching,
0, event.result.title,
event.result.episodesTotal, 0,
event.result.thumbnailUrl, event.result.total,
), event.result.thumbnailUrl,
), ),
) :
list.MangaAddedEvent(
MangaTrackingData(
event.result.id,
MangaTrackingState.reading,
event.result.title,
0,
0,
event.result.total,
event.result.thumbnailUrl,
),
)
); );
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(

View File

@ -16,10 +16,10 @@ final _privateConstructorUsedError = UnsupportedError(
/// @nodoc /// @nodoc
mixin _$AnimeSearchState { mixin _$AnimeSearchState {
TrackingMediumType get trackingType => throw _privateConstructorUsedError;
String get searchQuery => throw _privateConstructorUsedError; String get searchQuery => throw _privateConstructorUsedError;
bool get working => throw _privateConstructorUsedError; bool get working => throw _privateConstructorUsedError;
List<AnimeSearchResult> get searchResults => List<SearchResult> get searchResults => throw _privateConstructorUsedError;
throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
$AnimeSearchStateCopyWith<AnimeSearchState> get copyWith => $AnimeSearchStateCopyWith<AnimeSearchState> get copyWith =>
@ -32,9 +32,10 @@ abstract class $AnimeSearchStateCopyWith<$Res> {
AnimeSearchState value, $Res Function(AnimeSearchState) then) = AnimeSearchState value, $Res Function(AnimeSearchState) then) =
_$AnimeSearchStateCopyWithImpl<$Res>; _$AnimeSearchStateCopyWithImpl<$Res>;
$Res call( $Res call(
{String searchQuery, {TrackingMediumType trackingType,
String searchQuery,
bool working, bool working,
List<AnimeSearchResult> searchResults}); List<SearchResult> searchResults});
} }
/// @nodoc /// @nodoc
@ -48,11 +49,16 @@ class _$AnimeSearchStateCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? trackingType = freezed,
Object? searchQuery = freezed, Object? searchQuery = freezed,
Object? working = freezed, Object? working = freezed,
Object? searchResults = freezed, Object? searchResults = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
trackingType: trackingType == freezed
? _value.trackingType
: trackingType // ignore: cast_nullable_to_non_nullable
as TrackingMediumType,
searchQuery: searchQuery == freezed searchQuery: searchQuery == freezed
? _value.searchQuery ? _value.searchQuery
: searchQuery // ignore: cast_nullable_to_non_nullable : searchQuery // ignore: cast_nullable_to_non_nullable
@ -64,7 +70,7 @@ class _$AnimeSearchStateCopyWithImpl<$Res>
searchResults: searchResults == freezed searchResults: searchResults == freezed
? _value.searchResults ? _value.searchResults
: searchResults // ignore: cast_nullable_to_non_nullable : searchResults // ignore: cast_nullable_to_non_nullable
as List<AnimeSearchResult>, as List<SearchResult>,
)); ));
} }
} }
@ -77,9 +83,10 @@ abstract class _$$_AnimeSearchStateCopyWith<$Res>
__$$_AnimeSearchStateCopyWithImpl<$Res>; __$$_AnimeSearchStateCopyWithImpl<$Res>;
@override @override
$Res call( $Res call(
{String searchQuery, {TrackingMediumType trackingType,
String searchQuery,
bool working, bool working,
List<AnimeSearchResult> searchResults}); List<SearchResult> searchResults});
} }
/// @nodoc /// @nodoc
@ -95,11 +102,16 @@ class __$$_AnimeSearchStateCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? trackingType = freezed,
Object? searchQuery = freezed, Object? searchQuery = freezed,
Object? working = freezed, Object? working = freezed,
Object? searchResults = freezed, Object? searchResults = freezed,
}) { }) {
return _then(_$_AnimeSearchState( return _then(_$_AnimeSearchState(
trackingType: trackingType == freezed
? _value.trackingType
: trackingType // ignore: cast_nullable_to_non_nullable
as TrackingMediumType,
searchQuery: searchQuery == freezed searchQuery: searchQuery == freezed
? _value.searchQuery ? _value.searchQuery
: searchQuery // ignore: cast_nullable_to_non_nullable : searchQuery // ignore: cast_nullable_to_non_nullable
@ -111,7 +123,7 @@ class __$$_AnimeSearchStateCopyWithImpl<$Res>
searchResults: searchResults == freezed searchResults: searchResults == freezed
? _value._searchResults ? _value._searchResults
: searchResults // ignore: cast_nullable_to_non_nullable : searchResults // ignore: cast_nullable_to_non_nullable
as List<AnimeSearchResult>, as List<SearchResult>,
)); ));
} }
} }
@ -120,28 +132,32 @@ class __$$_AnimeSearchStateCopyWithImpl<$Res>
class _$_AnimeSearchState implements _AnimeSearchState { class _$_AnimeSearchState implements _AnimeSearchState {
_$_AnimeSearchState( _$_AnimeSearchState(
{this.searchQuery = '', {this.trackingType = TrackingMediumType.anime,
this.searchQuery = '',
this.working = false, this.working = false,
final List<AnimeSearchResult> searchResults = const []}) final List<SearchResult> searchResults = const []})
: _searchResults = searchResults; : _searchResults = searchResults;
@override
@JsonKey()
final TrackingMediumType trackingType;
@override @override
@JsonKey() @JsonKey()
final String searchQuery; final String searchQuery;
@override @override
@JsonKey() @JsonKey()
final bool working; final bool working;
final List<AnimeSearchResult> _searchResults; final List<SearchResult> _searchResults;
@override @override
@JsonKey() @JsonKey()
List<AnimeSearchResult> get searchResults { List<SearchResult> get searchResults {
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_searchResults); return EqualUnmodifiableListView(_searchResults);
} }
@override @override
String toString() { String toString() {
return 'AnimeSearchState(searchQuery: $searchQuery, working: $working, searchResults: $searchResults)'; return 'AnimeSearchState(trackingType: $trackingType, searchQuery: $searchQuery, working: $working, searchResults: $searchResults)';
} }
@override @override
@ -149,6 +165,8 @@ class _$_AnimeSearchState implements _AnimeSearchState {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$_AnimeSearchState && other is _$_AnimeSearchState &&
const DeepCollectionEquality()
.equals(other.trackingType, trackingType) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.searchQuery, searchQuery) && .equals(other.searchQuery, searchQuery) &&
const DeepCollectionEquality().equals(other.working, working) && const DeepCollectionEquality().equals(other.working, working) &&
@ -159,6 +177,7 @@ class _$_AnimeSearchState implements _AnimeSearchState {
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
const DeepCollectionEquality().hash(trackingType),
const DeepCollectionEquality().hash(searchQuery), const DeepCollectionEquality().hash(searchQuery),
const DeepCollectionEquality().hash(working), const DeepCollectionEquality().hash(working),
const DeepCollectionEquality().hash(_searchResults)); const DeepCollectionEquality().hash(_searchResults));
@ -171,16 +190,19 @@ class _$_AnimeSearchState implements _AnimeSearchState {
abstract class _AnimeSearchState implements AnimeSearchState { abstract class _AnimeSearchState implements AnimeSearchState {
factory _AnimeSearchState( factory _AnimeSearchState(
{final String searchQuery, {final TrackingMediumType trackingType,
final String searchQuery,
final bool working, final bool working,
final List<AnimeSearchResult> searchResults}) = _$_AnimeSearchState; final List<SearchResult> searchResults}) = _$_AnimeSearchState;
@override
TrackingMediumType get trackingType;
@override @override
String get searchQuery; String get searchQuery;
@override @override
bool get working; bool get working;
@override @override
List<AnimeSearchResult> get searchResults; List<SearchResult> get searchResults;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_AnimeSearchStateCopyWith<_$_AnimeSearchState> get copyWith => _$$_AnimeSearchStateCopyWith<_$_AnimeSearchState> get copyWith =>

View File

@ -2,7 +2,12 @@ part of 'anime_search_bloc.dart';
abstract class AnimeSearchEvent {} abstract class AnimeSearchEvent {}
class AnimeSearchRequestedEvent extends AnimeSearchEvent {} class AnimeSearchRequestedEvent extends AnimeSearchEvent {
AnimeSearchRequestedEvent(this.type);
/// The tracking type for which we want to search
TrackingMediumType type;
}
/// Triggered when the search query is changed. /// Triggered when the search query is changed.
class SearchQueryChangedEvent extends AnimeSearchEvent { class SearchQueryChangedEvent extends AnimeSearchEvent {
@ -16,9 +21,9 @@ class SearchQueryChangedEvent extends AnimeSearchEvent {
class SearchQuerySubmittedEvent extends AnimeSearchEvent {} class SearchQuerySubmittedEvent extends AnimeSearchEvent {}
/// Triggered when an anime is added to the tracking list /// Triggered when an anime is added to the tracking list
class AnimeAddedEvent extends AnimeSearchEvent { class ResultTappedEvent extends AnimeSearchEvent {
AnimeAddedEvent(this.result); ResultTappedEvent(this.result);
/// The search result to add. /// The search result to add.
final AnimeSearchResult result; final SearchResult result;
} }

View File

@ -3,8 +3,9 @@ part of 'anime_search_bloc.dart';
@freezed @freezed
class AnimeSearchState with _$AnimeSearchState { class AnimeSearchState with _$AnimeSearchState {
factory AnimeSearchState({ factory AnimeSearchState({
@Default(TrackingMediumType.anime) TrackingMediumType trackingType,
@Default('') String searchQuery, @Default('') String searchQuery,
@Default(false) bool working, @Default(false) bool working,
@Default([]) List<AnimeSearchResult> searchResults, @Default([]) List<SearchResult> searchResults,
}) = _AnimeSearchState; }) = _AnimeSearchState;
} }

View File

@ -1,4 +1,5 @@
import 'package:anitrack/src/data/anime.dart'; 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/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';
@ -24,6 +25,79 @@ class AnimeListPage extends StatelessWidget {
case TrackingMediumType.manga: return 'Manga'; case TrackingMediumType.manga: return 'Manga';
} }
} }
Widget _getPopupButton(BuildContext context, AnimeListState state) {
switch (state.trackingType) {
case TrackingMediumType.anime:
return PopupMenuButton(
icon: Icon(
Icons.filter_list,
),
initialValue: state.animeFilterState,
onSelected: (filterState) {
context.read<AnimeListBloc>().add(
AnimeFilterChangedEvent(filterState),
);
},
itemBuilder: (_) => [
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.watching,
child: Text('Watching'),
),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.completed,
child: Text('Completed'),
),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.planToWatch,
child: Text('Plan to watch'),
),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.dropped,
child: Text('Dropped'),
),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.all,
child: Text('All'),
),
],
);
case TrackingMediumType.manga:
return PopupMenuButton(
icon: Icon(
Icons.filter_list,
),
initialValue: state.mangaFilterState,
onSelected: (filterState) {
context.read<AnimeListBloc>().add(
MangaFilterChangedEvent(filterState),
);
},
itemBuilder: (_) => [
const PopupMenuItem<MangaTrackingState>(
value: MangaTrackingState.reading,
child: Text('Reading'),
),
const PopupMenuItem<MangaTrackingState>(
value: MangaTrackingState.completed,
child: Text('Completed'),
),
const PopupMenuItem<MangaTrackingState>(
value: MangaTrackingState.planToWatch,
child: Text('Plan to watch'),
),
const PopupMenuItem<MangaTrackingState>(
value: MangaTrackingState.dropped,
child: Text('Dropped'),
),
const PopupMenuItem<MangaTrackingState>(
value: MangaTrackingState.all,
child: Text('All'),
),
],
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -35,39 +109,7 @@ class AnimeListPage extends StatelessWidget {
_getPageTitle(state.trackingType) _getPageTitle(state.trackingType)
), ),
actions: [ actions: [
PopupMenuButton( _getPopupButton(context, state),
icon: Icon(
Icons.filter_list,
),
initialValue: state.filterState,
onSelected: (filterState) {
context.read<AnimeListBloc>().add(
AnimeFilterChangedEvent(filterState),
);
},
itemBuilder: (_) => [
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.watching,
child: Text('Watching'),
),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.completed,
child: Text('Completed'),
),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.planToWatch,
child: Text('Plan to watch'),
),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.dropped,
child: Text('Dropped'),
),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.all,
child: Text('All'),
),
],
),
], ],
), ),
body: PageView( body: PageView(
@ -77,8 +119,8 @@ class AnimeListPage extends StatelessWidget {
itemCount: state.animes.length, itemCount: state.animes.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final anime = state.animes[index]; final anime = state.animes[index];
if (state.filterState != AnimeTrackingState.all) { if (state.animeFilterState != AnimeTrackingState.all) {
if (anime.state != state.filterState) return Container(); if (anime.state != state.animeFilterState) return Container();
} }
return ListItem( return ListItem(
@ -103,13 +145,42 @@ class AnimeListPage extends StatelessWidget {
); );
}, },
), ),
Placeholder(), ListView.builder(
itemCount: state.mangas.length,
itemBuilder: (context, index) {
final manga = state.mangas[index];
if (state.mangaFilterState != MangaTrackingState.all) {
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<AnimeListBloc>().add(
MangaChapterDecrementedEvent(state.mangas[index].id),
);
},
onRightSwipe: () {
context.read<AnimeListBloc>().add(
MangaChapterIncrementedEvent(state.mangas[index].id),
);
},
);
},
),
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () { onPressed: () {
context.read<AnimeSearchBloc>().add( context.read<AnimeSearchBloc>().add(
AnimeSearchRequestedEvent(), AnimeSearchRequestedEvent(state.trackingType),
); );
}, },
tooltip: 'Increment', tooltip: 'Increment',
@ -120,7 +191,6 @@ class AnimeListPage extends StatelessWidget {
0 : 0 :
1, 1,
onTap: (int index) { onTap: (int index) {
_controller.jumpToPage(index);
context.read<AnimeListBloc>().add( context.read<AnimeListBloc>().add(
AnimeTrackingTypeChanged( AnimeTrackingTypeChanged(
index == 0 ? index == 0 ?
@ -128,6 +198,8 @@ class AnimeListPage extends StatelessWidget {
TrackingMediumType.manga, TrackingMediumType.manga,
), ),
); );
_controller.jumpToPage(index);
}, },
items: <BottomBarItem>[ items: <BottomBarItem>[
BottomBarItem( BottomBarItem(

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/type.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/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';
@ -20,7 +21,11 @@ class AnimeSearchPage extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Anime Search'), title: Text(
state.trackingType == TrackingMediumType.anime ?
'Anime Search' :
'Manga Search',
),
), ),
body: Column( body: Column(
children: [ children: [
@ -60,7 +65,7 @@ class AnimeSearchPage extends StatelessWidget {
return InkWell( return InkWell(
onTap: () { onTap: () {
context.read<AnimeSearchBloc>().add( context.read<AnimeSearchBloc>().add(
AnimeAddedEvent(item), ResultTappedEvent(item),
); );
}, },
child: ListItem( child: ListItem(