Initial commit

This commit is contained in:
2023-02-03 23:33:21 +01:00
commit ee4458d613
147 changed files with 5860 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
import 'dart:math';
import 'package:anitrack/src/data/anime.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'anime_list_state.dart';
part 'anime_list_event.dart';
part 'anime_list_bloc.freezed.dart';
class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
AnimeListBloc() : super(AnimeListState()) {
on<AnimeAddedEvent>(_onAnimeAdded);
on<AnimeEpisodeIncrementedEvent>(_onIncremented);
on<AnimeEpisodeDecrementedEvent>(_onDecremented);
}
// TODO: Remove
Future<void> _onAnimeAdded(AnimeAddedEvent event, Emitter<AnimeListState> emit) async {
emit(
state.copyWith(
animes: List.from([
...state.animes,
event.data,
]),
),
);
}
Future<void> _onIncremented(AnimeEpisodeIncrementedEvent event, Emitter<AnimeListState> emit) async {
final index = state.animes.indexWhere((item) => item.id == event.id);
if (index == -1) return;
final anime = state.animes[index];
if (anime.episodesTotal != null && anime.episodesWatched + 1 > anime.episodesTotal!) return;
final newList = List<AnimeTrackingData>.from(state.animes);
newList[index] = anime.copyWith(
anime.episodesWatched + 1,
);
emit(
state.copyWith(
animes: newList,
),
);
print('${event.id} incremented');
}
Future<void> _onDecremented(AnimeEpisodeDecrementedEvent event, Emitter<AnimeListState> emit) async {
final index = state.animes.indexWhere((item) => item.id == event.id);
if (index == -1) return;
final anime = state.animes[index];
if (anime.episodesWatched - 1 < 0) return;
final newList = List<AnimeTrackingData>.from(state.animes);
newList[index] = anime.copyWith(
anime.episodesWatched - 1,
);
emit(
state.copyWith(
animes: newList,
),
);
print('${event.id} decremented');
}
}

View File

@@ -0,0 +1,137 @@
// 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 'anime_list_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$AnimeListState {
List<AnimeTrackingData> get animes => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$AnimeListStateCopyWith<AnimeListState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AnimeListStateCopyWith<$Res> {
factory $AnimeListStateCopyWith(
AnimeListState value, $Res Function(AnimeListState) then) =
_$AnimeListStateCopyWithImpl<$Res>;
$Res call({List<AnimeTrackingData> animes});
}
/// @nodoc
class _$AnimeListStateCopyWithImpl<$Res>
implements $AnimeListStateCopyWith<$Res> {
_$AnimeListStateCopyWithImpl(this._value, this._then);
final AnimeListState _value;
// ignore: unused_field
final $Res Function(AnimeListState) _then;
@override
$Res call({
Object? animes = freezed,
}) {
return _then(_value.copyWith(
animes: animes == freezed
? _value.animes
: animes // ignore: cast_nullable_to_non_nullable
as List<AnimeTrackingData>,
));
}
}
/// @nodoc
abstract class _$$_AnimeListStateCopyWith<$Res>
implements $AnimeListStateCopyWith<$Res> {
factory _$$_AnimeListStateCopyWith(
_$_AnimeListState value, $Res Function(_$_AnimeListState) then) =
__$$_AnimeListStateCopyWithImpl<$Res>;
@override
$Res call({List<AnimeTrackingData> animes});
}
/// @nodoc
class __$$_AnimeListStateCopyWithImpl<$Res>
extends _$AnimeListStateCopyWithImpl<$Res>
implements _$$_AnimeListStateCopyWith<$Res> {
__$$_AnimeListStateCopyWithImpl(
_$_AnimeListState _value, $Res Function(_$_AnimeListState) _then)
: super(_value, (v) => _then(v as _$_AnimeListState));
@override
_$_AnimeListState get _value => super._value as _$_AnimeListState;
@override
$Res call({
Object? animes = freezed,
}) {
return _then(_$_AnimeListState(
animes: animes == freezed
? _value._animes
: animes // ignore: cast_nullable_to_non_nullable
as List<AnimeTrackingData>,
));
}
}
/// @nodoc
class _$_AnimeListState implements _AnimeListState {
_$_AnimeListState({final List<AnimeTrackingData> animes = const []})
: _animes = animes;
final List<AnimeTrackingData> _animes;
@override
@JsonKey()
List<AnimeTrackingData> get animes {
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_animes);
}
@override
String toString() {
return 'AnimeListState(animes: $animes)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_AnimeListState &&
const DeepCollectionEquality().equals(other._animes, _animes));
}
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(_animes));
@JsonKey(ignore: true)
@override
_$$_AnimeListStateCopyWith<_$_AnimeListState> get copyWith =>
__$$_AnimeListStateCopyWithImpl<_$_AnimeListState>(this, _$identity);
}
abstract class _AnimeListState implements AnimeListState {
factory _AnimeListState({final List<AnimeTrackingData> animes}) =
_$_AnimeListState;
@override
List<AnimeTrackingData> get animes;
@override
@JsonKey(ignore: true)
_$$_AnimeListStateCopyWith<_$_AnimeListState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,24 @@
part of 'anime_list_bloc.dart';
abstract class AnimeListEvent {}
class AnimeEpisodeIncrementedEvent extends AnimeListEvent {
AnimeEpisodeIncrementedEvent(this.id);
/// The ID of the anime
final String id;
}
class AnimeEpisodeDecrementedEvent extends AnimeListEvent {
AnimeEpisodeDecrementedEvent(this.id);
/// The ID of the anime
final String id;
}
class AnimeAddedEvent extends AnimeListEvent {
AnimeAddedEvent(this.data);
/// The anime to add.
final AnimeTrackingData data;
}

View File

@@ -0,0 +1,8 @@
part of 'anime_list_bloc.dart';
@freezed
class AnimeListState with _$AnimeListState {
factory AnimeListState({
@Default([]) List<AnimeTrackingData> animes,
}) = _AnimeListState;
}

View File

@@ -0,0 +1,98 @@
import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/search_result.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/navigation_bloc.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get_it/get_it.dart';
import 'package:jikan_api/jikan_api.dart';
part 'anime_search_state.dart';
part 'anime_search_event.dart';
part 'anime_search_bloc.freezed.dart';
class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
AnimeSearchBloc() : super(AnimeSearchState()) {
on<AnimeSearchRequestedEvent>(_onRequested);
on<SearchQueryChangedEvent>(_onQueryChanged);
on<SearchQuerySubmittedEvent>(_onQuerySubmitted);
on<AnimeAddedEvent>(_onAnimeAdded);
}
Future<void> _onRequested(AnimeSearchRequestedEvent event, Emitter<AnimeSearchState> emit) async {
emit(
state.copyWith(
searchQuery: '',
working: false,
searchResults: [],
),
);
GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent(
NavigationDestination(animeSearchRoute),
),
);
}
Future<void> _onQueryChanged(SearchQueryChangedEvent event, Emitter<AnimeSearchState> emit) async {
emit(
state.copyWith(
searchQuery: event.query,
),
);
}
Future<void> _onQuerySubmitted(SearchQuerySubmittedEvent event, Emitter<AnimeSearchState> emit) async {
if (state.searchQuery.isEmpty) return;
emit(
state.copyWith(
working: true,
),
);
final result = await Jikan().searchAnime(
query: state.searchQuery,
);
emit(
state.copyWith(
working: false,
),
);
emit(
state.copyWith(
searchResults: result.map((Anime anime) => AnimeSearchResult(
anime.title,
anime.malId.toString(),
anime.episodes,
anime.imageUrl,
anime.synopsis ?? '',
),).toList(),
),
);
print(result);
}
Future<void> _onAnimeAdded(AnimeAddedEvent event, Emitter<AnimeSearchState> emit) async {
GetIt.I.get<list.AnimeListBloc>().add(
list.AnimeAddedEvent(
AnimeTrackingData(
event.result.id,
AnimeTrackingState.watching,
event.result.title,
0,
event.result.episodesTotal,
event.result.thumbnailUrl,
),
),
);
GetIt.I.get<NavigationBloc>().add(
PoppedRouteEvent(),
);
}
}

View File

@@ -0,0 +1,188 @@
// 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 'anime_search_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$AnimeSearchState {
String get searchQuery => throw _privateConstructorUsedError;
bool get working => throw _privateConstructorUsedError;
List<AnimeSearchResult> get searchResults =>
throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$AnimeSearchStateCopyWith<AnimeSearchState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AnimeSearchStateCopyWith<$Res> {
factory $AnimeSearchStateCopyWith(
AnimeSearchState value, $Res Function(AnimeSearchState) then) =
_$AnimeSearchStateCopyWithImpl<$Res>;
$Res call(
{String searchQuery,
bool working,
List<AnimeSearchResult> searchResults});
}
/// @nodoc
class _$AnimeSearchStateCopyWithImpl<$Res>
implements $AnimeSearchStateCopyWith<$Res> {
_$AnimeSearchStateCopyWithImpl(this._value, this._then);
final AnimeSearchState _value;
// ignore: unused_field
final $Res Function(AnimeSearchState) _then;
@override
$Res call({
Object? searchQuery = freezed,
Object? working = freezed,
Object? searchResults = freezed,
}) {
return _then(_value.copyWith(
searchQuery: searchQuery == freezed
? _value.searchQuery
: searchQuery // ignore: cast_nullable_to_non_nullable
as String,
working: working == freezed
? _value.working
: working // ignore: cast_nullable_to_non_nullable
as bool,
searchResults: searchResults == freezed
? _value.searchResults
: searchResults // ignore: cast_nullable_to_non_nullable
as List<AnimeSearchResult>,
));
}
}
/// @nodoc
abstract class _$$_AnimeSearchStateCopyWith<$Res>
implements $AnimeSearchStateCopyWith<$Res> {
factory _$$_AnimeSearchStateCopyWith(
_$_AnimeSearchState value, $Res Function(_$_AnimeSearchState) then) =
__$$_AnimeSearchStateCopyWithImpl<$Res>;
@override
$Res call(
{String searchQuery,
bool working,
List<AnimeSearchResult> searchResults});
}
/// @nodoc
class __$$_AnimeSearchStateCopyWithImpl<$Res>
extends _$AnimeSearchStateCopyWithImpl<$Res>
implements _$$_AnimeSearchStateCopyWith<$Res> {
__$$_AnimeSearchStateCopyWithImpl(
_$_AnimeSearchState _value, $Res Function(_$_AnimeSearchState) _then)
: super(_value, (v) => _then(v as _$_AnimeSearchState));
@override
_$_AnimeSearchState get _value => super._value as _$_AnimeSearchState;
@override
$Res call({
Object? searchQuery = freezed,
Object? working = freezed,
Object? searchResults = freezed,
}) {
return _then(_$_AnimeSearchState(
searchQuery: searchQuery == freezed
? _value.searchQuery
: searchQuery // ignore: cast_nullable_to_non_nullable
as String,
working: working == freezed
? _value.working
: working // ignore: cast_nullable_to_non_nullable
as bool,
searchResults: searchResults == freezed
? _value._searchResults
: searchResults // ignore: cast_nullable_to_non_nullable
as List<AnimeSearchResult>,
));
}
}
/// @nodoc
class _$_AnimeSearchState implements _AnimeSearchState {
_$_AnimeSearchState(
{this.searchQuery = '',
this.working = false,
final List<AnimeSearchResult> searchResults = const []})
: _searchResults = searchResults;
@override
@JsonKey()
final String searchQuery;
@override
@JsonKey()
final bool working;
final List<AnimeSearchResult> _searchResults;
@override
@JsonKey()
List<AnimeSearchResult> get searchResults {
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_searchResults);
}
@override
String toString() {
return 'AnimeSearchState(searchQuery: $searchQuery, working: $working, searchResults: $searchResults)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_AnimeSearchState &&
const DeepCollectionEquality()
.equals(other.searchQuery, searchQuery) &&
const DeepCollectionEquality().equals(other.working, working) &&
const DeepCollectionEquality()
.equals(other._searchResults, _searchResults));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(searchQuery),
const DeepCollectionEquality().hash(working),
const DeepCollectionEquality().hash(_searchResults));
@JsonKey(ignore: true)
@override
_$$_AnimeSearchStateCopyWith<_$_AnimeSearchState> get copyWith =>
__$$_AnimeSearchStateCopyWithImpl<_$_AnimeSearchState>(this, _$identity);
}
abstract class _AnimeSearchState implements AnimeSearchState {
factory _AnimeSearchState(
{final String searchQuery,
final bool working,
final List<AnimeSearchResult> searchResults}) = _$_AnimeSearchState;
@override
String get searchQuery;
@override
bool get working;
@override
List<AnimeSearchResult> get searchResults;
@override
@JsonKey(ignore: true)
_$$_AnimeSearchStateCopyWith<_$_AnimeSearchState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,24 @@
part of 'anime_search_bloc.dart';
abstract class AnimeSearchEvent {}
class AnimeSearchRequestedEvent extends AnimeSearchEvent {}
/// Triggered when the search query is changed.
class SearchQueryChangedEvent extends AnimeSearchEvent {
SearchQueryChangedEvent(this.query);
/// The current value of the query
final String query;
}
/// Triggered when the search is submitted.
class SearchQuerySubmittedEvent extends AnimeSearchEvent {}
/// Triggered when an anime is added to the tracking list
class AnimeAddedEvent extends AnimeSearchEvent {
AnimeAddedEvent(this.result);
/// The search result to add.
final AnimeSearchResult result;
}

View File

@@ -0,0 +1,10 @@
part of 'anime_search_bloc.dart';
@freezed
class AnimeSearchState with _$AnimeSearchState {
factory AnimeSearchState({
@Default('') String searchQuery,
@Default(false) bool working,
@Default([]) List<AnimeSearchResult> searchResults,
}) = _AnimeSearchState;
}

View File

@@ -0,0 +1,46 @@
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'navigation_event.dart';
part 'navigation_state.dart';
class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
NavigationBloc(this.navigationKey) : super(NavigationState()) {
on<PushedNamedEvent>(_onPushedNamed);
on<PushedNamedAndRemoveUntilEvent>(_onPushedNamedAndRemoveUntil);
on<PushedNamedReplaceEvent>(_onPushedNamedReplaceEvent);
on<PoppedRouteEvent>(_onPoppedRoute);
}
final GlobalKey<NavigatorState> navigationKey;
Future<void> _onPushedNamed(PushedNamedEvent event, Emitter<NavigationState> emit) async {
await navigationKey.currentState!.pushNamed(
event.destination.path,
arguments: event.destination.arguments,
);
}
Future<void> _onPushedNamedAndRemoveUntil(PushedNamedAndRemoveUntilEvent event, Emitter<NavigationState> emit) async {
await navigationKey.currentState!.pushNamedAndRemoveUntil(
event.destination.path,
event.predicate,
arguments: event.destination.arguments,
);
}
Future<void> _onPushedNamedReplaceEvent(PushedNamedReplaceEvent event, Emitter<NavigationState> emit) async {
await navigationKey.currentState!.pushReplacementNamed(
event.destination.path,
arguments: event.destination.arguments,
);
}
Future<void> _onPoppedRoute(PoppedRouteEvent event, Emitter<NavigationState> emit) async {
navigationKey.currentState!.pop();
}
bool canPop() {
return navigationKey.currentState!.canPop();
}
}

View File

@@ -0,0 +1,32 @@
part of 'navigation_bloc.dart';
class NavigationDestination {
const NavigationDestination(
this.path,
{
this.arguments,
}
);
final String path;
final Object? arguments;
}
abstract class NavigationEvent {}
class PushedNamedEvent extends NavigationEvent {
PushedNamedEvent(this.destination);
final NavigationDestination destination;
}
class PushedNamedAndRemoveUntilEvent extends NavigationEvent {
PushedNamedAndRemoveUntilEvent(this.destination, this.predicate);
final NavigationDestination destination;
final RoutePredicate predicate;
}
class PushedNamedReplaceEvent extends NavigationEvent {
PushedNamedReplaceEvent(this.destination);
final NavigationDestination destination;
}
class PoppedRouteEvent extends NavigationEvent {}

View File

@@ -0,0 +1,3 @@
part of 'navigation_bloc.dart';
class NavigationState {}