chore(all): Format

This commit is contained in:
PapaTutuWawa 2023-04-12 16:16:58 +02:00
parent 67c1903bd1
commit 9dcc99ca36
22 changed files with 913 additions and 625 deletions

File diff suppressed because it is too large Load Diff

View File

@ -31,9 +31,9 @@ void main() async {
// Load animes // Load animes
GetIt.I.get<AnimeListBloc>().add( GetIt.I.get<AnimeListBloc>().add(
AnimesLoadedEvent(), AnimesLoadedEvent(),
); );
runApp( runApp(
MultiBlocProvider( MultiBlocProvider(
providers: [ providers: [
@ -62,7 +62,7 @@ class MyApp extends StatelessWidget {
}); });
final GlobalKey<NavigatorState> navKey; final GlobalKey<NavigatorState> navKey;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
@ -81,10 +81,14 @@ class MyApp extends StatelessWidget {
onGenerateRoute: (settings) { onGenerateRoute: (settings) {
switch (settings.name) { switch (settings.name) {
case '/': case '/':
case animeListRoute: return AnimeListPage.route; case animeListRoute:
case animeSearchRoute: return AnimeSearchPage.route; return AnimeListPage.route;
case detailsRoute: return DetailsPage.route; case animeSearchRoute:
case aboutRoute: return AboutPage.route; return AnimeSearchPage.route;
case detailsRoute:
return DetailsPage.route;
case aboutRoute:
return AboutPage.route;
} }
return null; return null;

View File

@ -10,18 +10,24 @@ class AnimeTrackingData with _$AnimeTrackingData, TrackingMedium {
factory AnimeTrackingData( factory AnimeTrackingData(
/// The ID of the anime /// The ID of the anime
String id, String id,
/// The state of the anime /// The state of the anime
@MediumTrackingStateConverter() MediumTrackingState state, @MediumTrackingStateConverter() MediumTrackingState state,
/// The title of the anime /// The title of the anime
String title, String title,
/// Episodes in total. /// Episodes in total.
int episodesWatched, int episodesWatched,
/// Episodes watched. /// Episodes watched.
int? episodesTotal, int? episodesTotal,
/// URL to the thumbnail/cover art for the anime. /// URL to the thumbnail/cover art for the anime.
String thumbnailUrl, String thumbnailUrl,
) = _AnimeTrackingData; ) = _AnimeTrackingData;
/// JSON /// JSON
factory AnimeTrackingData.fromJson(Map<String, dynamic> json) => _$AnimeTrackingDataFromJson(json); factory AnimeTrackingData.fromJson(Map<String, dynamic> json) =>
_$AnimeTrackingDataFromJson(json);
} }

View File

@ -10,20 +10,27 @@ class MangaTrackingData with _$MangaTrackingData, TrackingMedium {
factory MangaTrackingData( factory MangaTrackingData(
/// The ID of the manga /// The ID of the manga
String id, String id,
/// The state of the manga /// The state of the manga
@MediumTrackingStateConverter() MediumTrackingState state, @MediumTrackingStateConverter() MediumTrackingState state,
/// The title of the manga /// The title of the manga
String title, String title,
/// Chapters read. /// Chapters read.
int chaptersRead, int chaptersRead,
/// Chapters read. /// Chapters read.
int volumesOwned, int volumesOwned,
/// Episodes watched. /// Episodes watched.
int? chaptersTotal, int? chaptersTotal,
/// URL to the thumbnail/cover art for the manga. /// URL to the thumbnail/cover art for the manga.
String thumbnailUrl, String thumbnailUrl,
) = _MangaTrackingData; ) = _MangaTrackingData;
/// JSON /// JSON
factory MangaTrackingData.fromJson(Map<String, dynamic> json) => _$MangaTrackingDataFromJson(json); factory MangaTrackingData.fromJson(Map<String, dynamic> json) =>
_$MangaTrackingDataFromJson(json);
} }

View File

@ -32,52 +32,75 @@ abstract class TrackingMedium {
extension MediumStateExtension on MediumTrackingState { extension MediumStateExtension on MediumTrackingState {
int toInteger() { int toInteger() {
assert(this != MediumTrackingState.all, 'MediumTrackingState.all must not be serialized'); assert(
this != MediumTrackingState.all,
'MediumTrackingState.all must not be serialized',
);
switch (this) { switch (this) {
case MediumTrackingState.ongoing: return 0; case MediumTrackingState.ongoing:
case MediumTrackingState.completed: return 1; return 0;
case MediumTrackingState.planned: return 2; case MediumTrackingState.completed:
case MediumTrackingState.dropped: return 3; return 1;
case MediumTrackingState.all: return -1; case MediumTrackingState.planned:
return 2;
case MediumTrackingState.dropped:
return 3;
case MediumTrackingState.all:
return -1;
} }
} }
String toNameString(TrackingMediumType type) { String toNameString(TrackingMediumType type) {
assert(this != MediumTrackingState.all, 'MediumTrackingState.all must not be stringified'); assert(
this != MediumTrackingState.all,
'MediumTrackingState.all must not be stringified',
);
switch (this) { switch (this) {
case MediumTrackingState.ongoing: case MediumTrackingState.ongoing:
switch (type) { switch (type) {
case TrackingMediumType.anime: return 'Watching'; case TrackingMediumType.anime:
case TrackingMediumType.manga: return 'Reading'; return 'Watching';
case TrackingMediumType.manga:
return 'Reading';
} }
case MediumTrackingState.completed: return 'Completed'; case MediumTrackingState.completed:
return 'Completed';
case MediumTrackingState.planned: case MediumTrackingState.planned:
switch (type) { switch (type) {
case TrackingMediumType.anime: return 'Plan to watch'; case TrackingMediumType.anime:
case TrackingMediumType.manga: return 'Plan to read'; return 'Plan to watch';
case TrackingMediumType.manga:
return 'Plan to read';
} }
case MediumTrackingState.dropped: return 'Dropped'; case MediumTrackingState.dropped:
case MediumTrackingState.all: return 'All'; return 'Dropped';
case MediumTrackingState.all:
return 'All';
} }
} }
} }
class MediumTrackingStateConverter implements JsonConverter<MediumTrackingState, int> { class MediumTrackingStateConverter
implements JsonConverter<MediumTrackingState, int> {
const MediumTrackingStateConverter(); const MediumTrackingStateConverter();
@override @override
MediumTrackingState fromJson(int json) { MediumTrackingState fromJson(int json) {
switch (json) { switch (json) {
case 0: return MediumTrackingState.ongoing; case 0:
case 1: return MediumTrackingState.completed; return MediumTrackingState.ongoing;
case 2: return MediumTrackingState.planned; case 1:
case 3: return MediumTrackingState.dropped; return MediumTrackingState.completed;
case 2:
return MediumTrackingState.planned;
case 3:
return MediumTrackingState.dropped;
} }
return MediumTrackingState.planned; return MediumTrackingState.planned;
} }
@override @override
int toJson(MediumTrackingState state) => state.toInteger(); int toJson(MediumTrackingState state) => state.toInteger();
} }

View File

@ -46,20 +46,20 @@ class DatabaseService {
final animes = await _db.query(animeTable); final animes = await _db.query(animeTable);
return animes return animes
.cast<Map<String, dynamic>>() .cast<Map<String, dynamic>>()
.map(AnimeTrackingData.fromJson) .map(AnimeTrackingData.fromJson)
.toList(); .toList();
} }
Future<List<MangaTrackingData>> loadMangas() async { Future<List<MangaTrackingData>> loadMangas() async {
final mangas = await _db.query(mangaTable); final mangas = await _db.query(mangaTable);
return mangas return mangas
.cast<Map<String, dynamic>>() .cast<Map<String, dynamic>>()
.map(MangaTrackingData.fromJson) .map(MangaTrackingData.fromJson)
.toList(); .toList();
} }
Future<void> addAnime(AnimeTrackingData data) async { Future<void> addAnime(AnimeTrackingData data) async {
await _db.insert( await _db.insert(
animeTable, animeTable,
@ -83,7 +83,7 @@ class DatabaseService {
whereArgs: [id], whereArgs: [id],
); );
} }
Future<void> addManga(MangaTrackingData data) async { Future<void> addManga(MangaTrackingData data) async {
await _db.insert( await _db.insert(
mangaTable, mangaTable,

View File

@ -30,30 +30,35 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
} }
/// Internal anime state /// Internal anime state
final List<AnimeTrackingData> _animes = List<AnimeTrackingData>.empty(growable: true); final List<AnimeTrackingData> _animes =
final List<MangaTrackingData> _mangas = List<MangaTrackingData>.empty(growable: true); List<AnimeTrackingData>.empty(growable: true);
final List<MangaTrackingData> _mangas =
List<MangaTrackingData>.empty(growable: true);
List<AnimeTrackingData> _getFilteredAnime({MediumTrackingState? trackingState}) { List<AnimeTrackingData> _getFilteredAnime({
MediumTrackingState? trackingState,
}) {
final filterState = trackingState ?? state.animeFilterState; final filterState = trackingState ?? state.animeFilterState;
if (filterState == MediumTrackingState.all) return _animes; if (filterState == MediumTrackingState.all) return _animes;
return _animes return _animes.where((anime) => anime.state == filterState).toList();
.where((anime) => anime.state == filterState)
.toList();
} }
List<MangaTrackingData> _getFilteredManga({MediumTrackingState? trackingState}) { List<MangaTrackingData> _getFilteredManga({
MediumTrackingState? trackingState,
}) {
final filterState = trackingState ?? state.mangaFilterState; final filterState = trackingState ?? state.mangaFilterState;
if (state.mangaFilterState == MediumTrackingState.all) return _mangas; if (state.mangaFilterState == MediumTrackingState.all) return _mangas;
return _mangas return _mangas.where((manga) => manga.state == filterState).toList();
.where((manga) => manga.state == filterState)
.toList();
} }
Future<void> _onAnimeAdded(AnimeAddedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onAnimeAdded(
AnimeAddedEvent event,
Emitter<AnimeListState> emit,
) async {
// Add the anime to the database // Add the anime to the database
await GetIt.I.get<DatabaseService>().addAnime(event.data); await GetIt.I.get<DatabaseService>().addAnime(event.data);
@ -67,7 +72,10 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
); );
} }
Future<void> _onMangaAdded(MangaAddedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onMangaAdded(
MangaAddedEvent event,
Emitter<AnimeListState> emit,
) async {
// Add the manga to the database // Add the manga to the database
await GetIt.I.get<DatabaseService>().addManga(event.data); await GetIt.I.get<DatabaseService>().addManga(event.data);
@ -80,13 +88,17 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
), ),
); );
} }
Future<void> _onAnimeIncremented(AnimeEpisodeIncrementedEvent event, Emitter<AnimeListState> emit) async { 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;
final anime = state.animes[index]; final anime = state.animes[index];
if (anime.episodesTotal != null && anime.episodesWatched + 1 > anime.episodesTotal!) return; if (anime.episodesTotal != null &&
anime.episodesWatched + 1 > anime.episodesTotal!) return;
final newList = List<AnimeTrackingData>.from(state.animes); final newList = List<AnimeTrackingData>.from(state.animes);
final newAnime = anime.copyWith( final newAnime = anime.copyWith(
@ -103,7 +115,10 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
await GetIt.I.get<DatabaseService>().updateAnime(newAnime); await GetIt.I.get<DatabaseService>().updateAnime(newAnime);
} }
Future<void> _onAnimeDecremented(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;
@ -125,14 +140,17 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
await GetIt.I.get<DatabaseService>().updateAnime(newAnime); await GetIt.I.get<DatabaseService>().updateAnime(newAnime);
} }
Future<void> _onAnimesLoaded(AnimesLoadedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onAnimesLoaded(
AnimesLoadedEvent event,
Emitter<AnimeListState> emit,
) async {
_animes.addAll( _animes.addAll(
await GetIt.I.get<DatabaseService>().loadAnimes(), await GetIt.I.get<DatabaseService>().loadAnimes(),
); );
_mangas.addAll( _mangas.addAll(
await GetIt.I.get<DatabaseService>().loadMangas(), await GetIt.I.get<DatabaseService>().loadMangas(),
); );
emit( emit(
state.copyWith( state.copyWith(
animes: _getFilteredAnime(), animes: _getFilteredAnime(),
@ -141,7 +159,10 @@ 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(
animeFilterState: event.filterState, animeFilterState: event.filterState,
@ -150,7 +171,10 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
); );
} }
Future<void> _onMangasFiltered(MangaFilterChangedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onMangasFiltered(
MangaFilterChangedEvent event,
Emitter<AnimeListState> emit,
) async {
emit( emit(
state.copyWith( state.copyWith(
mangaFilterState: event.filterState, mangaFilterState: event.filterState,
@ -158,8 +182,11 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
), ),
); );
} }
Future<void> _onTrackingTypeChanged(AnimeTrackingTypeChanged event, Emitter<AnimeListState> emit) async { Future<void> _onTrackingTypeChanged(
AnimeTrackingTypeChanged event,
Emitter<AnimeListState> emit,
) async {
emit( emit(
state.copyWith( state.copyWith(
trackingType: event.type, trackingType: event.type,
@ -168,12 +195,16 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
); );
} }
Future<void> _onMangaIncremented(MangaChapterIncrementedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onMangaIncremented(
MangaChapterIncrementedEvent event,
Emitter<AnimeListState> emit,
) async {
final index = state.mangas.indexWhere((item) => item.id == event.id); final index = state.mangas.indexWhere((item) => item.id == event.id);
assert(index != -1, 'The manga must exist'); assert(index != -1, 'The manga must exist');
final manga = state.mangas[index]; final manga = state.mangas[index];
if (manga.chaptersTotal != null && manga.chaptersRead + 1 > manga.chaptersTotal!) return; if (manga.chaptersTotal != null &&
manga.chaptersRead + 1 > manga.chaptersTotal!) return;
final newList = List<MangaTrackingData>.from(state.mangas); final newList = List<MangaTrackingData>.from(state.mangas);
final newManga = manga.copyWith( final newManga = manga.copyWith(
@ -185,7 +216,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
final cacheIndex = _mangas.indexWhere((m) => m.id == event.id); final cacheIndex = _mangas.indexWhere((m) => m.id == event.id);
assert(cacheIndex != -1, 'The manga must exist'); assert(cacheIndex != -1, 'The manga must exist');
_mangas[cacheIndex] = newManga; _mangas[cacheIndex] = newManga;
emit( emit(
state.copyWith( state.copyWith(
mangas: newList, mangas: newList,
@ -195,7 +226,10 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
await GetIt.I.get<DatabaseService>().updateManga(newManga); await GetIt.I.get<DatabaseService>().updateManga(newManga);
} }
Future<void> _onMangaDecremented(MangaChapterDecrementedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onMangaDecremented(
MangaChapterDecrementedEvent event,
Emitter<AnimeListState> emit,
) async {
final index = state.mangas.indexWhere((item) => item.id == event.id); final index = state.mangas.indexWhere((item) => item.id == event.id);
if (index == -1) return; if (index == -1) return;
@ -212,7 +246,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
final cacheIndex = _mangas.indexWhere((m) => m.id == event.id); final cacheIndex = _mangas.indexWhere((m) => m.id == event.id);
assert(cacheIndex != -1, 'The manga must exist'); assert(cacheIndex != -1, 'The manga must exist');
_mangas[cacheIndex] = newManga; _mangas[cacheIndex] = newManga;
emit( emit(
state.copyWith( state.copyWith(
mangas: newList, mangas: newList,
@ -222,7 +256,10 @@ 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 { Future<void> _onAnimeUpdated(
AnimeUpdatedEvent event,
Emitter<AnimeListState> emit,
) async {
final index = _animes.indexWhere((anime) => anime.id == event.anime.id); final index = _animes.indexWhere((anime) => anime.id == event.anime.id);
assert(index != -1, 'The anime must exist'); assert(index != -1, 'The anime must exist');
@ -235,7 +272,10 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
); );
} }
Future<void> _onMangaUpdated(MangaUpdatedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onMangaUpdated(
MangaUpdatedEvent event,
Emitter<AnimeListState> emit,
) async {
final index = _mangas.indexWhere((manga) => manga.id == event.manga.id); final index = _mangas.indexWhere((manga) => manga.id == event.manga.id);
assert(index != -1, 'The manga must exist'); assert(index != -1, 'The manga must exist');
@ -248,7 +288,10 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
); );
} }
Future<void> _onAnimeRemoved(AnimeRemovedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onAnimeRemoved(
AnimeRemovedEvent event,
Emitter<AnimeListState> emit,
) async {
emit( emit(
state.copyWith( state.copyWith(
animes: List.from( animes: List.from(
@ -265,8 +308,11 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
// Update the database // Update the database
await GetIt.I.get<DatabaseService>().deleteAnime(event.id); await GetIt.I.get<DatabaseService>().deleteAnime(event.id);
} }
Future<void> _onMangaRemoved(MangaRemovedEvent event, Emitter<AnimeListState> emit) async { Future<void> _onMangaRemoved(
MangaRemovedEvent event,
Emitter<AnimeListState> emit,
) async {
emit( emit(
state.copyWith( state.copyWith(
mangas: List.from( mangas: List.from(
@ -279,12 +325,15 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
final cacheIndex = _animes.indexWhere((a) => a.id == event.id); final cacheIndex = _animes.indexWhere((a) => a.id == event.id);
assert(cacheIndex != -1, 'The manga must exist'); assert(cacheIndex != -1, 'The manga must exist');
_animes.removeAt(cacheIndex); _animes.removeAt(cacheIndex);
// Update the database // Update the database
await GetIt.I.get<DatabaseService>().deleteManga(event.id); await GetIt.I.get<DatabaseService>().deleteManga(event.id);
} }
Future<void> _onButtonVisibilityToggled(AddButtonVisibilitySetEvent event, Emitter<AnimeListState> emit) async { Future<void> _onButtonVisibilityToggled(
AddButtonVisibilitySetEvent event,
Emitter<AnimeListState> emit,
) async {
emit( emit(
state.copyWith( state.copyWith(
buttonVisibility: event.state, buttonVisibility: event.state,

View File

@ -18,7 +18,7 @@ class AnimeEpisodeDecrementedEvent extends AnimeListEvent {
class AnimeAddedEvent extends AnimeListEvent { class AnimeAddedEvent extends AnimeListEvent {
AnimeAddedEvent(this.data); AnimeAddedEvent(this.data);
/// The anime to add. /// The anime to add.
final AnimeTrackingData data; final AnimeTrackingData data;
} }
@ -57,7 +57,7 @@ class AnimeRemovedEvent extends AnimeListEvent {
class MangaAddedEvent extends AnimeListEvent { class MangaAddedEvent extends AnimeListEvent {
MangaAddedEvent(this.data); MangaAddedEvent(this.data);
/// The manga to add. /// The manga to add.
final MangaTrackingData data; final MangaTrackingData data;
} }

View File

@ -22,7 +22,10 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
on<ResultTappedEvent>(_onResultTapped); on<ResultTappedEvent>(_onResultTapped);
} }
Future<void> _onRequested(AnimeSearchRequestedEvent event, Emitter<AnimeSearchState> emit) async { Future<void> _onRequested(
AnimeSearchRequestedEvent event,
Emitter<AnimeSearchState> emit,
) async {
emit( emit(
state.copyWith( state.copyWith(
searchQuery: '', searchQuery: '',
@ -33,21 +36,27 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
); );
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent( PushedNamedEvent(
const NavigationDestination(animeSearchRoute), const NavigationDestination(animeSearchRoute),
), ),
); );
} }
Future<void> _onQueryChanged(SearchQueryChangedEvent event, Emitter<AnimeSearchState> emit) async { Future<void> _onQueryChanged(
SearchQueryChangedEvent event,
Emitter<AnimeSearchState> emit,
) async {
emit( emit(
state.copyWith( state.copyWith(
searchQuery: event.query, searchQuery: event.query,
), ),
); );
} }
Future<void> _onQuerySubmitted(SearchQuerySubmittedEvent event, Emitter<AnimeSearchState> emit) async { Future<void> _onQuerySubmitted(
SearchQuerySubmittedEvent event,
Emitter<AnimeSearchState> emit,
) async {
if (state.searchQuery.isEmpty) return; if (state.searchQuery.isEmpty) return;
emit( emit(
@ -65,13 +74,17 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
emit( emit(
state.copyWith( state.copyWith(
working: false, working: false,
searchResults: result.map((Anime anime) => SearchResult( searchResults: result
anime.title, .map(
anime.malId.toString(), (Anime anime) => SearchResult(
anime.episodes, anime.title,
anime.imageUrl, anime.malId.toString(),
anime.synopsis ?? '', anime.episodes,
),).toList(), anime.imageUrl,
anime.synopsis ?? '',
),
)
.toList(),
), ),
); );
} else { } else {
@ -83,46 +96,53 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
emit( emit(
state.copyWith( state.copyWith(
working: false, working: false,
searchResults: result.map((Manga manga) => SearchResult( searchResults: result
manga.title, .map(
manga.malId.toString(), (Manga manga) => SearchResult(
manga.chapters, manga.title,
manga.imageUrl, manga.malId.toString(),
manga.synopsis ?? '', manga.chapters,
),).toList(), manga.imageUrl,
manga.synopsis ?? '',
),
)
.toList(),
), ),
); );
} }
} }
Future<void> _onResultTapped(ResultTappedEvent 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(
state.trackingType == TrackingMediumType.anime ? state.trackingType == TrackingMediumType.anime
list.AnimeAddedEvent( ? list.AnimeAddedEvent(
AnimeTrackingData( AnimeTrackingData(
event.result.id, event.result.id,
MediumTrackingState.ongoing, MediumTrackingState.ongoing,
event.result.title, event.result.title,
0, 0,
event.result.total, event.result.total,
event.result.thumbnailUrl, event.result.thumbnailUrl,
), ),
) : )
list.MangaAddedEvent( : list.MangaAddedEvent(
MangaTrackingData( MangaTrackingData(
event.result.id, event.result.id,
MediumTrackingState.ongoing, MediumTrackingState.ongoing,
event.result.title, event.result.title,
0, 0,
0, 0,
event.result.total, event.result.total,
event.result.thumbnailUrl, event.result.thumbnailUrl,
), ),
), ),
); );
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PoppedRouteEvent(), PoppedRouteEvent(),
); );
} }
} }

View File

@ -12,7 +12,7 @@ class AnimeSearchRequestedEvent extends AnimeSearchEvent {
/// Triggered when the search query is changed. /// Triggered when the search query is changed.
class SearchQueryChangedEvent extends AnimeSearchEvent { class SearchQueryChangedEvent extends AnimeSearchEvent {
SearchQueryChangedEvent(this.query); SearchQueryChangedEvent(this.query);
/// The current value of the query /// The current value of the query
final String query; final String query;
} }

View File

@ -21,7 +21,10 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
on<ItemRemovedEvent>(_onItemRemoved); on<ItemRemovedEvent>(_onItemRemoved);
} }
Future<void> _onAnimeRequested(AnimeDetailsRequestedEvent event, Emitter<DetailsState> emit) async { Future<void> _onAnimeRequested(
AnimeDetailsRequestedEvent event,
Emitter<DetailsState> emit,
) async {
emit( emit(
state.copyWith( state.copyWith(
trackingType: TrackingMediumType.anime, trackingType: TrackingMediumType.anime,
@ -30,13 +33,16 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
); );
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent( PushedNamedEvent(
const NavigationDestination(detailsRoute), const NavigationDestination(detailsRoute),
), ),
); );
} }
Future<void> _onMangaRequested(MangaDetailsRequestedEvent event, Emitter<DetailsState> emit) async { Future<void> _onMangaRequested(
MangaDetailsRequestedEvent event,
Emitter<DetailsState> emit,
) async {
emit( emit(
state.copyWith( state.copyWith(
trackingType: TrackingMediumType.manga, trackingType: TrackingMediumType.manga,
@ -45,13 +51,16 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
); );
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent( PushedNamedEvent(
const NavigationDestination(detailsRoute), const NavigationDestination(detailsRoute),
), ),
); );
} }
Future<void> _onDetailsUpdated(DetailsUpdatedEvent event, Emitter<DetailsState> emit) async { Future<void> _onDetailsUpdated(
DetailsUpdatedEvent event,
Emitter<DetailsState> emit,
) async {
if (state.trackingType == TrackingMediumType.anime) { if (state.trackingType == TrackingMediumType.anime) {
emit( emit(
state.copyWith( state.copyWith(
@ -59,11 +68,13 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
), ),
); );
await GetIt.I.get<DatabaseService>().updateAnime(event.data as AnimeTrackingData); await GetIt.I
.get<DatabaseService>()
.updateAnime(event.data as AnimeTrackingData);
GetIt.I.get<AnimeListBloc>().add( GetIt.I.get<AnimeListBloc>().add(
AnimeUpdatedEvent(event.data as AnimeTrackingData), AnimeUpdatedEvent(event.data as AnimeTrackingData),
); );
} else { } else {
emit( emit(
state.copyWith( state.copyWith(
@ -71,15 +82,20 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
), ),
); );
await GetIt.I.get<DatabaseService>().updateManga(event.data as MangaTrackingData); await GetIt.I
.get<DatabaseService>()
.updateManga(event.data as MangaTrackingData);
GetIt.I.get<AnimeListBloc>().add( GetIt.I.get<AnimeListBloc>().add(
MangaUpdatedEvent(event.data as MangaTrackingData), MangaUpdatedEvent(event.data as MangaTrackingData),
); );
} }
} }
Future<void> _onItemRemoved(ItemRemovedEvent event, Emitter<DetailsState> emit) async { Future<void> _onItemRemoved(
ItemRemovedEvent event,
Emitter<DetailsState> emit,
) async {
emit( emit(
state.copyWith( state.copyWith(
data: null, data: null,

View File

@ -13,14 +13,20 @@ class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
} }
final GlobalKey<NavigatorState> navigationKey; final GlobalKey<NavigatorState> navigationKey;
Future<void> _onPushedNamed(PushedNamedEvent event, Emitter<NavigationState> emit) async { Future<void> _onPushedNamed(
PushedNamedEvent event,
Emitter<NavigationState> emit,
) async {
await navigationKey.currentState!.pushNamed( await navigationKey.currentState!.pushNamed(
event.destination.path, event.destination.path,
arguments: event.destination.arguments, arguments: event.destination.arguments,
); );
} }
Future<void> _onPushedNamedAndRemoveUntil(PushedNamedAndRemoveUntilEvent event, Emitter<NavigationState> emit) async { Future<void> _onPushedNamedAndRemoveUntil(
PushedNamedAndRemoveUntilEvent event,
Emitter<NavigationState> emit,
) async {
await navigationKey.currentState!.pushNamedAndRemoveUntil( await navigationKey.currentState!.pushNamedAndRemoveUntil(
event.destination.path, event.destination.path,
event.predicate, event.predicate,
@ -28,14 +34,20 @@ class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
); );
} }
Future<void> _onPushedNamedReplaceEvent(PushedNamedReplaceEvent event, Emitter<NavigationState> emit) async { Future<void> _onPushedNamedReplaceEvent(
PushedNamedReplaceEvent event,
Emitter<NavigationState> emit,
) async {
await navigationKey.currentState!.pushReplacementNamed( await navigationKey.currentState!.pushReplacementNamed(
event.destination.path, event.destination.path,
arguments: event.destination.arguments, arguments: event.destination.arguments,
); );
} }
Future<void> _onPoppedRoute(PoppedRouteEvent event, Emitter<NavigationState> emit) async { Future<void> _onPoppedRoute(
PoppedRouteEvent event,
Emitter<NavigationState> emit,
) async {
navigationKey.currentState!.pop(); navigationKey.currentState!.pop();
} }

View File

@ -2,11 +2,9 @@ part of 'navigation_bloc.dart';
class NavigationDestination { class NavigationDestination {
const NavigationDestination( const NavigationDestination(
this.path, this.path, {
{ this.arguments,
this.arguments, });
}
);
final String path; final String path;
final Object? arguments; final Object? arguments;
} }

View File

@ -9,11 +9,11 @@ class AboutPage extends StatelessWidget {
}); });
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>( static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
builder: (_) => const AboutPage(), builder: (_) => const AboutPage(),
settings: const RouteSettings( settings: const RouteSettings(
name: aboutRoute, name: aboutRoute,
), ),
); );
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -34,7 +34,6 @@ class AboutPage extends StatelessWidget {
'AniTrack', 'AniTrack',
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
await launchUrl( await launchUrl(

View File

@ -16,11 +16,11 @@ class AnimeListPage extends StatefulWidget {
}); });
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>( static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
builder: (_) => AnimeListPage(), builder: (_) => const AnimeListPage(),
settings: const RouteSettings( settings: const RouteSettings(
name: animeListRoute, name: animeListRoute,
), ),
); );
@override @override
AnimeListPageState createState() => AnimeListPageState(); AnimeListPageState createState() => AnimeListPageState();
@ -35,11 +35,12 @@ class AnimeListPageState extends State<AnimeListPage> {
super.initState(); super.initState();
_animeScrollController.addListener(_onAnimeListScrolled); _animeScrollController.addListener(_onAnimeListScrolled);
} }
void _onAnimeListScrolled() { void _onAnimeListScrolled() {
//print(_animeScrollController.position.maxScrollExtent); //print(_animeScrollController.position.maxScrollExtent);
final bloc = GetIt.I.get<AnimeListBloc>(); final bloc = GetIt.I.get<AnimeListBloc>();
if (_animeScrollController.offset + 20 >= _animeScrollController.position.maxScrollExtent) { if (_animeScrollController.offset + 20 >=
_animeScrollController.position.maxScrollExtent) {
if (bloc.state.buttonVisibility) { if (bloc.state.buttonVisibility) {
bloc.add( bloc.add(
AddButtonVisibilitySetEvent(false), AddButtonVisibilitySetEvent(false),
@ -53,15 +54,19 @@ class AnimeListPageState extends State<AnimeListPage> {
} }
} }
} }
String _getPageTitle(TrackingMediumType type) { String _getPageTitle(TrackingMediumType type) {
switch (type) { switch (type) {
case TrackingMediumType.anime: return 'Anime'; case TrackingMediumType.anime:
case TrackingMediumType.manga: return 'Manga'; return 'Anime';
case TrackingMediumType.manga:
return 'Manga';
} }
} }
List<PopupMenuItem<MediumTrackingState>> _getPopupButtonItems(TrackingMediumType type) { List<PopupMenuItem<MediumTrackingState>> _getPopupButtonItems(
TrackingMediumType type,
) {
return [ return [
PopupMenuItem<MediumTrackingState>( PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.ongoing, value: MediumTrackingState.ongoing,
@ -85,7 +90,7 @@ class AnimeListPageState extends State<AnimeListPage> {
), ),
]; ];
} }
Widget _getPopupButton(BuildContext context, AnimeListState state) { Widget _getPopupButton(BuildContext context, AnimeListState state) {
switch (state.trackingType) { switch (state.trackingType) {
case TrackingMediumType.anime: case TrackingMediumType.anime:
@ -96,8 +101,8 @@ class AnimeListPageState extends State<AnimeListPage> {
initialValue: state.animeFilterState, initialValue: state.animeFilterState,
onSelected: (filterState) { onSelected: (filterState) {
context.read<AnimeListBloc>().add( context.read<AnimeListBloc>().add(
AnimeFilterChangedEvent(filterState), AnimeFilterChangedEvent(filterState),
); );
}, },
itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.anime), itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.anime),
); );
@ -109,14 +114,14 @@ class AnimeListPageState extends State<AnimeListPage> {
initialValue: state.mangaFilterState, initialValue: state.mangaFilterState,
onSelected: (filterState) { onSelected: (filterState) {
context.read<AnimeListBloc>().add( context.read<AnimeListBloc>().add(
MangaFilterChangedEvent(filterState), MangaFilterChangedEvent(filterState),
); );
}, },
itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.manga), itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.manga),
); );
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<AnimeListBloc, AnimeListState>( return BlocBuilder<AnimeListBloc, AnimeListState>(
@ -145,7 +150,6 @@ class AnimeListPageState extends State<AnimeListPage> {
), ),
), ),
), ),
ListTile( ListTile(
leading: const Icon(Icons.info), leading: const Icon(Icons.info),
title: const Text('About'), title: const Text('About'),
@ -178,37 +182,37 @@ class AnimeListPageState extends State<AnimeListPage> {
return GridItem( return GridItem(
minusCallback: () { minusCallback: () {
context.read<AnimeListBloc>().add( context.read<AnimeListBloc>().add(
AnimeEpisodeDecrementedEvent( AnimeEpisodeDecrementedEvent(
anime.id, anime.id,
), ),
); );
}, },
plusCallback: () { plusCallback: () {
context.read<AnimeListBloc>().add( context.read<AnimeListBloc>().add(
AnimeEpisodeIncrementedEvent( AnimeEpisodeIncrementedEvent(
anime.id, anime.id,
), ),
); );
}, },
child: AnimeCoverImage( child: AnimeCoverImage(
url: anime.thumbnailUrl, url: anime.thumbnailUrl,
hero: anime.id, hero: anime.id,
onTap: () { onTap: () {
context.read<DetailsBloc>().add( context.read<DetailsBloc>().add(
AnimeDetailsRequestedEvent(anime), AnimeDetailsRequestedEvent(anime),
); );
}, },
extra: Align( extra: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: Text( child: Text(
'${anime.episodesWatched}/${anime.episodesTotal ?? "???"}', '${anime.episodesWatched}/${anime.episodesTotal ?? "???"}',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
),
), ),
), ),
), ),
),
); );
}, },
), ),
@ -228,37 +232,37 @@ class AnimeListPageState extends State<AnimeListPage> {
return GridItem( return GridItem(
minusCallback: () { minusCallback: () {
context.read<AnimeListBloc>().add( context.read<AnimeListBloc>().add(
MangaChapterDecrementedEvent( MangaChapterDecrementedEvent(
manga.id, manga.id,
), ),
); );
}, },
plusCallback: () { plusCallback: () {
context.read<AnimeListBloc>().add( context.read<AnimeListBloc>().add(
MangaChapterIncrementedEvent( MangaChapterIncrementedEvent(
manga.id, manga.id,
), ),
); );
}, },
child: AnimeCoverImage( child: AnimeCoverImage(
hero: manga.id, hero: manga.id,
url: manga.thumbnailUrl, url: manga.thumbnailUrl,
onTap: () { onTap: () {
context.read<DetailsBloc>().add( context.read<DetailsBloc>().add(
MangaDetailsRequestedEvent(manga), MangaDetailsRequestedEvent(manga),
); );
}, },
extra: Align( extra: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: Text( child: Text(
'${manga.chaptersRead}/${manga.chaptersTotal ?? "???"}', '${manga.chaptersRead}/${manga.chaptersTotal ?? "???"}',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
),
), ),
), ),
), ),
),
); );
}, },
), ),
@ -266,19 +270,18 @@ class AnimeListPageState extends State<AnimeListPage> {
], ],
), ),
floatingActionButton: BlocBuilder<AnimeListBloc, AnimeListState>( floatingActionButton: BlocBuilder<AnimeListBloc, AnimeListState>(
buildWhen: (prev, next) => prev.buttonVisibility != next.buttonVisibility, buildWhen: (prev, next) =>
prev.buttonVisibility != next.buttonVisibility,
builder: (context, state) { builder: (context, state) {
return AnimatedScale( return AnimatedScale(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
scale: state.buttonVisibility ? scale: state.buttonVisibility ? 1 : 0,
1 :
0,
curve: Curves.easeInOutQuint, curve: Curves.easeInOutQuint,
child: FloatingActionButton( child: FloatingActionButton(
onPressed: () { onPressed: () {
context.read<AnimeSearchBloc>().add( context.read<AnimeSearchBloc>().add(
AnimeSearchRequestedEvent(state.trackingType), AnimeSearchRequestedEvent(state.trackingType),
); );
}, },
tooltip: 'Add new item', tooltip: 'Add new item',
child: const Icon(Icons.add), child: const Icon(Icons.add),
@ -287,17 +290,16 @@ class AnimeListPageState extends State<AnimeListPage> {
}, },
), ),
bottomNavigationBar: BottomBar( bottomNavigationBar: BottomBar(
selectedIndex: state.trackingType == TrackingMediumType.anime ? selectedIndex:
0 : state.trackingType == TrackingMediumType.anime ? 0 : 1,
1,
onTap: (int index) { onTap: (int index) {
context.read<AnimeListBloc>().add( context.read<AnimeListBloc>().add(
AnimeTrackingTypeChanged( AnimeTrackingTypeChanged(
index == 0 ? index == 0
TrackingMediumType.anime : ? TrackingMediumType.anime
TrackingMediumType.manga, : TrackingMediumType.manga,
), ),
); );
_controller.jumpToPage(index); _controller.jumpToPage(index);
}, },

View File

@ -11,12 +11,12 @@ class AnimeSearchPage extends StatelessWidget {
}); });
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>( static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
builder: (_) => const AnimeSearchPage(), builder: (_) => const AnimeSearchPage(),
settings: const RouteSettings( settings: const RouteSettings(
name: animeSearchRoute, name: animeSearchRoute,
), ),
); );
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<AnimeSearchBloc, AnimeSearchState>( return BlocBuilder<AnimeSearchBloc, AnimeSearchState>(
@ -24,9 +24,9 @@ class AnimeSearchPage extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
state.trackingType == TrackingMediumType.anime ? state.trackingType == TrackingMediumType.anime
'Anime Search' : ? 'Anime Search'
'Manga Search', : 'Manga Search',
), ),
), ),
body: Column( body: Column(
@ -40,17 +40,16 @@ class AnimeSearchPage extends StatelessWidget {
), ),
onSubmitted: (_) { onSubmitted: (_) {
context.read<AnimeSearchBloc>().add( context.read<AnimeSearchBloc>().add(
SearchQuerySubmittedEvent(), SearchQuerySubmittedEvent(),
); );
}, },
onChanged: (value) { onChanged: (value) {
context.read<AnimeSearchBloc>().add( context.read<AnimeSearchBloc>().add(
SearchQueryChangedEvent(value), SearchQueryChangedEvent(value),
); );
}, },
), ),
), ),
if (state.working) if (state.working)
const Expanded( const Expanded(
child: Align( child: Align(
@ -66,8 +65,8 @@ class AnimeSearchPage extends StatelessWidget {
return InkWell( return InkWell(
onTap: () { onTap: () {
context.read<AnimeSearchBloc>().add( context.read<AnimeSearchBloc>().add(
ResultTappedEvent(item), ResultTappedEvent(item),
); );
}, },
child: ListItem( child: ListItem(
title: item.title, title: item.title,

View File

@ -16,11 +16,11 @@ class DetailsPage extends StatelessWidget {
}); });
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>( static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
builder: (_) => const DetailsPage(), builder: (_) => const DetailsPage(),
settings: const RouteSettings( settings: const RouteSettings(
name: detailsRoute, name: detailsRoute,
), ),
); );
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -30,195 +30,213 @@ class DetailsPage extends StatelessWidget {
), ),
body: BlocBuilder<DetailsBloc, DetailsState>( body: BlocBuilder<DetailsBloc, DetailsState>(
builder: (context, state) { builder: (context, state) {
return state.data == null ? return state.data == null
Container() : ? Container()
Padding( : Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: ListView( child: ListView(
children: [ children: [
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [AnimeCoverImage( children: [
url: state.data!.thumbnailUrl, AnimeCoverImage(
hero: state.data!.id, url: state.data!.thumbnailUrl,
), hero: state.data!.id,
),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 8, left: 8,
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
state.data!.title, state.data!.title,
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: Theme.of(context).textTheme.titleLarge, style:
maxLines: 2, Theme.of(context).textTheme.titleLarge,
softWrap: true, maxLines: 2,
overflow: TextOverflow.ellipsis, softWrap: true,
), overflow: TextOverflow.ellipsis,
ElevatedButton(
onPressed: () async {
final result = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Remove "${state.data!.title}"?'),
content: Text('Are you sure you want to remove "${state.data!.title}" from the list?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
child: const Text('Remove'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: const Text('Cancel'),
),
],
);
},
);
if (result != true) return;
// ignore: use_build_context_synchronously
context.read<DetailsBloc>().add(
ItemRemovedEvent(
state.data!.id,
state.trackingType,
), ),
); ElevatedButton(
}, onPressed: () async {
child: const Icon(Icons.delete), final result = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text(
'Remove "${state.data!.title}"?',
),
content: Text(
'Are you sure you want to remove "${state.data!.title}" from the list?',
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context)
.pop(true);
},
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
child: const Text('Remove'),
),
TextButton(
onPressed: () {
Navigator.of(context)
.pop(false);
},
child: const Text('Cancel'),
),
],
);
},
);
if (result != true) return;
// ignore: use_build_context_synchronously
context.read<DetailsBloc>().add(
ItemRemovedEvent(
state.data!.id,
state.trackingType,
),
);
},
child: const Icon(Icons.delete),
),
],
),
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
),
child: DropdownSelector<MediumTrackingState>(
title: state.trackingType == TrackingMediumType.anime
? 'Watch state'
: 'Read state',
onChanged: (MediumTrackingState newState) {
if (state.trackingType ==
TrackingMediumType.anime) {
context.read<DetailsBloc>().add(
DetailsUpdatedEvent(
(state.data! as AnimeTrackingData)
.copyWith(
state: newState,
),
),
);
} else if (state.trackingType ==
TrackingMediumType.manga) {
context.read<DetailsBloc>().add(
DetailsUpdatedEvent(
(state.data! as MangaTrackingData)
.copyWith(
state: newState,
),
),
);
}
},
values: [
SelectorItem(
MediumTrackingState.ongoing,
MediumTrackingState.ongoing
.toNameString(state.trackingType),
),
SelectorItem(
MediumTrackingState.completed,
MediumTrackingState.completed
.toNameString(state.trackingType),
),
SelectorItem(
MediumTrackingState.planned,
MediumTrackingState.planned
.toNameString(state.trackingType),
),
SelectorItem(
MediumTrackingState.dropped,
MediumTrackingState.dropped
.toNameString(state.trackingType),
), ),
], ],
initialValue: state.data!.state,
), ),
), ),
), Padding(
], padding: const EdgeInsets.symmetric(
), vertical: 8,
),
Padding( child: IntegerInput(
padding: const EdgeInsets.symmetric( labelText:
vertical: 8, state.trackingType == TrackingMediumType.anime
), ? 'Episodes'
child: DropdownSelector<MediumTrackingState>( : 'Chapters',
title: state.trackingType == TrackingMediumType.anime ? onChanged: (value) {
'Watch state' : switch (state.trackingType) {
'Read state', case TrackingMediumType.anime:
onChanged: (MediumTrackingState newState) { final data = state.data! as AnimeTrackingData;
if (state.trackingType == TrackingMediumType.anime) { context.read<DetailsBloc>().add(
context.read<DetailsBloc>().add( DetailsUpdatedEvent(
DetailsUpdatedEvent( data.copyWith(
(state.data! as AnimeTrackingData).copyWith( episodesWatched: value,
state: newState, ),
), ),
);
break;
case TrackingMediumType.manga:
final data = state.data! as MangaTrackingData;
context.read<DetailsBloc>().add(
DetailsUpdatedEvent(
data.copyWith(
chaptersRead: value,
),
),
);
break;
}
},
initialValue: state.trackingType ==
TrackingMediumType.anime
? (state.data! as AnimeTrackingData)
.episodesWatched
: (state.data! as MangaTrackingData).chaptersRead,
),
),
if (state.trackingType == TrackingMediumType.manga)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
), ),
); child: IntegerInput(
} else if (state.trackingType == TrackingMediumType.manga) { labelText: 'Volumes owned',
context.read<DetailsBloc>().add( onChanged: (value) {
DetailsUpdatedEvent( final data = state.data! as MangaTrackingData;
(state.data! as MangaTrackingData).copyWith( context.read<DetailsBloc>().add(
state: newState, DetailsUpdatedEvent(
), data.copyWith(
volumesOwned: value,
),
),
);
},
initialValue: (GetIt.I
.get<DetailsBloc>()
.state
.data! as MangaTrackingData)
.volumesOwned,
), ),
); ),
}
},
values: [
SelectorItem(
MediumTrackingState.ongoing,
MediumTrackingState.ongoing.toNameString(state.trackingType),
),
SelectorItem(
MediumTrackingState.completed,
MediumTrackingState.completed.toNameString(state.trackingType),
),
SelectorItem(
MediumTrackingState.planned,
MediumTrackingState.planned.toNameString(state.trackingType),
),
SelectorItem(
MediumTrackingState.dropped,
MediumTrackingState.dropped.toNameString(state.trackingType),
),
], ],
initialValue: state.data!.state,
), ),
), );
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
),
child: IntegerInput(
labelText: state.trackingType == TrackingMediumType.anime ?
'Episodes' :
'Chapters',
onChanged: (value) {
switch (state.trackingType) {
case TrackingMediumType.anime:
final data = state.data! as AnimeTrackingData;
context.read<DetailsBloc>().add(
DetailsUpdatedEvent(
data.copyWith(
episodesWatched: value,
),
),
);
break;
case TrackingMediumType.manga:
final data = state.data! as MangaTrackingData;
context.read<DetailsBloc>().add(
DetailsUpdatedEvent(
data.copyWith(
chaptersRead: value,
),
),
);
break;
}
},
initialValue: state.trackingType == TrackingMediumType.anime ?
(state.data! as AnimeTrackingData).episodesWatched :
(state.data! as MangaTrackingData).chaptersRead,
),
),
if (state.trackingType == TrackingMediumType.manga)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
),
child: IntegerInput(
labelText: 'Volumes owned',
onChanged: (value) {
final data = state.data! as MangaTrackingData;
context.read<DetailsBloc>().add(
DetailsUpdatedEvent(
data.copyWith(
volumesOwned: value,
),
),
);
},
initialValue: (GetIt.I.get<DetailsBloc>().state.data! as MangaTrackingData).volumesOwned,
),
),
],
),
);
}, },
), ),
); );

View File

@ -23,10 +23,10 @@ class DropdownSelector<T> extends StatefulWidget {
final List<SelectorItem<T>> values; final List<SelectorItem<T>> values;
final T initialValue; final T initialValue;
/// Called when the selection has changed /// Called when the selection has changed
final void Function(T) onChanged; final void Function(T) onChanged;
@override @override
DropdownSelectorState<T> createState() => DropdownSelectorState<T>(); DropdownSelectorState<T> createState() => DropdownSelectorState<T>();
} }
@ -76,54 +76,56 @@ class DropdownSelectorState<T> extends State<DropdownSelector<T>> {
void initState() { void initState() {
super.initState(); super.initState();
index = widget.values.indexWhere((item) => item.value == widget.initialValue); index =
widget.values.indexWhere((item) => item.value == widget.initialValue);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
Expanded( Expanded(
child: Card( child: Card(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: InkWell( child: InkWell(
onTap: () async { onTap: () async {
final result = await showDialog<T>( final result = await showDialog<T>(
context: context, context: context,
builder: (context) => DropdownSelectorDialog<T>( builder: (context) => DropdownSelectorDialog<T>(
values: widget.values.cast<SelectorItem<T>>(), values: widget.values.cast<SelectorItem<T>>(),
),
);
if (result == null) return;
if (result == widget.values[index].value) return;
setState(() {
index = widget.values.indexWhere((item) => item.value == result);
});
widget.onChanged(result);
},
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: Theme.of(context).textTheme.titleSmall,
),
Text(
widget.values[index].text,
style: Theme.of(context).textTheme.bodyLarge,
),
],
), ),
);
if (result == null) return;
if (result == widget.values[index].value) return;
setState(() {
index =
widget.values.indexWhere((item) => item.value == result);
});
widget.onChanged(result);
},
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: Theme.of(context).textTheme.titleSmall,
),
Text(
widget.values[index].text,
style: Theme.of(context).textTheme.bodyLarge,
),
],
), ),
), ),
), ),
), ),
], ),
],
); );
} }
} }

View File

@ -13,7 +13,7 @@ class GridItem extends StatefulWidget {
final void Function() plusCallback; final void Function() plusCallback;
final void Function() minusCallback; final void Function() minusCallback;
@override @override
GridItemState createState() => GridItemState(); GridItemState createState() => GridItemState();
} }
@ -28,7 +28,7 @@ class GridItemState extends State<GridItem> {
onHorizontalDragUpdate: (details) { onHorizontalDragUpdate: (details) {
setState(() { setState(() {
_offset += details.delta.dx; _offset += details.delta.dx;
_translationX = 160 / (1 + exp(-1 * (1/30) * _offset)) - 80; _translationX = 160 / (1 + exp(-1 * (1 / 30) * _offset)) - 80;
}); });
}, },
onHorizontalDragEnd: (_) { onHorizontalDragEnd: (_) {

View File

@ -19,13 +19,13 @@ class AnimeCoverImage extends StatelessWidget {
/// The hero tag of the image. /// The hero tag of the image.
final String hero; final String hero;
/// An extra widget with a translucent backdrop. /// An extra widget with a translucent backdrop.
final Widget? extra; final Widget? extra;
/// Callback for when the image is tapped. /// Callback for when the image is tapped.
final void Function()? onTap; final void Function()? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Hero( return Hero(
@ -48,15 +48,14 @@ class AnimeCoverImage extends StatelessWidget {
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: cached ? image: cached
CachedNetworkImageProvider(url) as ImageProvider : ? CachedNetworkImageProvider(url) as ImageProvider
NetworkImage(url), : NetworkImage(url),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
), ),
), ),
if (extra != null) if (extra != null)
Positioned( Positioned(
left: 0, left: 0,

View File

@ -27,7 +27,7 @@ class IntegerInputState extends State<IntegerInput> {
/// The controller for the TextField. /// The controller for the TextField.
final TextEditingController _controller = TextEditingController(); final TextEditingController _controller = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -44,7 +44,7 @@ class IntegerInputState extends State<IntegerInput> {
_controller.text = '$_value'; _controller.text = '$_value';
widget.onChanged(_value); widget.onChanged(_value);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
@ -59,7 +59,6 @@ class IntegerInputState extends State<IntegerInput> {
}, },
child: const Icon(Icons.remove), child: const Icon(Icons.remove),
), ),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -84,7 +83,6 @@ class IntegerInputState extends State<IntegerInput> {
), ),
), ),
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
_value++; _value++;

View File

@ -15,7 +15,7 @@ class ListItem extends StatelessWidget {
this.imageExtra, this.imageExtra,
super.key, super.key,
}); });
/// URL for the cover image. /// URL for the cover image.
final String thumbnailUrl; final String thumbnailUrl;
@ -26,14 +26,14 @@ class ListItem extends StatelessWidget {
final List<Widget> extra; final List<Widget> extra;
final Widget? imageExtra; final Widget? imageExtra;
/// Callbacks for the swipe functionality. /// Callbacks for the swipe functionality.
final void Function()? onLeftSwipe; final void Function()? onLeftSwipe;
final void Function()? onRightSwipe; final void Function()? onRightSwipe;
/// Flag indicating whether the thumbnail image should be cached /// Flag indicating whether the thumbnail image should be cached
final bool cached; final bool cached;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SwipeableTile.swipeToTrigger( return SwipeableTile.swipeToTrigger(
@ -66,9 +66,9 @@ class ListItem extends StatelessWidget {
return Container(); return Container();
}, },
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
direction: onRightSwipe != null && onLeftSwipe != null ? direction: onRightSwipe != null && onLeftSwipe != null
SwipeDirection.horizontal : ? SwipeDirection.horizontal
SwipeDirection.none, : SwipeDirection.none,
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Row( child: Row(
@ -81,7 +81,6 @@ class ListItem extends StatelessWidget {
extra: imageExtra, extra: imageExtra,
url: thumbnailUrl, url: thumbnailUrl,
), ),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
@ -97,7 +96,6 @@ class ListItem extends StatelessWidget {
softWrap: true, softWrap: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
...extra, ...extra,
], ],
), ),