feat: Integrate the AniList API since Jikan is shutting down

This commit is contained in:
2026-07-25 16:34:14 +02:00
parent 0dba2a9909
commit 6c2af9fc57
23 changed files with 40865 additions and 218 deletions

View File

@@ -1,4 +1,5 @@
import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/src/service/anilist/anilist_client.dart';
import 'package:anitrack/src/service/database.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart';
@@ -35,6 +36,7 @@ void main() async {
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navKey));
GetIt.I.registerSingleton<SettingsBloc>(SettingsBloc());
GetIt.I.registerSingleton<CalendarBloc>(CalendarBloc());
GetIt.I.registerSingleton<AniListClient>(AniListClient());
// Load animes
GetIt.I.get<AnimeListBloc>().add(

39416
lib/oss_licenses.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
import 'package:anitrack/src/data/source.dart';
import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/service/database.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -42,6 +43,9 @@ abstract class AnimeTrackingData with _$AnimeTrackingData, TrackingMedium {
/// The day of the week the anime is airing
String? broadcastDay,
/// The source where we got the data from.
@TrackingDataSourceConverter() TrackingDataSource source,
) = _AnimeTrackingData;
AnimeTrackingData._();

View File

@@ -23,7 +23,8 @@ mixin _$AnimeTrackingData {
int? get episodesTotal;/// URL to the thumbnail/cover art for the anime.
String get thumbnailUrl;/// Flag whether the anime is airing
@BoolConverter() bool get airing;/// The day of the week the anime is airing
String? get broadcastDay;
String? get broadcastDay;/// The source where we got the data from.
@TrackingDataSourceConverter() TrackingDataSource get source;
/// Create a copy of AnimeTrackingData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -36,16 +37,16 @@ $AnimeTrackingDataCopyWith<AnimeTrackingData> get copyWith => _$AnimeTrackingDat
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AnimeTrackingData&&(identical(other.id, id) || other.id == id)&&(identical(other.state, state) || other.state == state)&&(identical(other.title, title) || other.title == title)&&(identical(other.episodesWatched, episodesWatched) || other.episodesWatched == episodesWatched)&&(identical(other.episodesTotal, episodesTotal) || other.episodesTotal == episodesTotal)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl)&&(identical(other.airing, airing) || other.airing == airing)&&(identical(other.broadcastDay, broadcastDay) || other.broadcastDay == broadcastDay));
return identical(this, other) || (other.runtimeType == runtimeType&&other is AnimeTrackingData&&(identical(other.id, id) || other.id == id)&&(identical(other.state, state) || other.state == state)&&(identical(other.title, title) || other.title == title)&&(identical(other.episodesWatched, episodesWatched) || other.episodesWatched == episodesWatched)&&(identical(other.episodesTotal, episodesTotal) || other.episodesTotal == episodesTotal)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl)&&(identical(other.airing, airing) || other.airing == airing)&&(identical(other.broadcastDay, broadcastDay) || other.broadcastDay == broadcastDay)&&(identical(other.source, source) || other.source == source));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,state,title,episodesWatched,episodesTotal,thumbnailUrl,airing,broadcastDay);
int get hashCode => Object.hash(runtimeType,id,state,title,episodesWatched,episodesTotal,thumbnailUrl,airing,broadcastDay,source);
@override
String toString() {
return 'AnimeTrackingData(id: $id, state: $state, title: $title, episodesWatched: $episodesWatched, episodesTotal: $episodesTotal, thumbnailUrl: $thumbnailUrl, airing: $airing, broadcastDay: $broadcastDay)';
return 'AnimeTrackingData(id: $id, state: $state, title: $title, episodesWatched: $episodesWatched, episodesTotal: $episodesTotal, thumbnailUrl: $thumbnailUrl, airing: $airing, broadcastDay: $broadcastDay, source: $source)';
}
@@ -56,7 +57,7 @@ abstract mixin class $AnimeTrackingDataCopyWith<$Res> {
factory $AnimeTrackingDataCopyWith(AnimeTrackingData value, $Res Function(AnimeTrackingData) _then) = _$AnimeTrackingDataCopyWithImpl;
@useResult
$Res call({
String id,@MediumTrackingStateConverter() MediumTrackingState state, String title, int episodesWatched, int? episodesTotal, String thumbnailUrl,@BoolConverter() bool airing, String? broadcastDay
String id,@MediumTrackingStateConverter() MediumTrackingState state, String title, int episodesWatched, int? episodesTotal, String thumbnailUrl,@BoolConverter() bool airing, String? broadcastDay,@TrackingDataSourceConverter() TrackingDataSource source
});
@@ -73,7 +74,7 @@ class _$AnimeTrackingDataCopyWithImpl<$Res>
/// Create a copy of AnimeTrackingData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? state = null,Object? title = null,Object? episodesWatched = null,Object? episodesTotal = freezed,Object? thumbnailUrl = null,Object? airing = null,Object? broadcastDay = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? state = null,Object? title = null,Object? episodesWatched = null,Object? episodesTotal = freezed,Object? thumbnailUrl = null,Object? airing = null,Object? broadcastDay = freezed,Object? source = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
@@ -83,7 +84,8 @@ as int,episodesTotal: freezed == episodesTotal ? _self.episodesTotal : episodesT
as int?,thumbnailUrl: null == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable
as String,airing: null == airing ? _self.airing : airing // ignore: cast_nullable_to_non_nullable
as bool,broadcastDay: freezed == broadcastDay ? _self.broadcastDay : broadcastDay // ignore: cast_nullable_to_non_nullable
as String?,
as String?,source: null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable
as TrackingDataSource,
));
}
@@ -168,10 +170,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, @MediumTrackingStateConverter() MediumTrackingState state, String title, int episodesWatched, int? episodesTotal, String thumbnailUrl, @BoolConverter() bool airing, String? broadcastDay)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, @MediumTrackingStateConverter() MediumTrackingState state, String title, int episodesWatched, int? episodesTotal, String thumbnailUrl, @BoolConverter() bool airing, String? broadcastDay, @TrackingDataSourceConverter() TrackingDataSource source)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AnimeTrackingData() when $default != null:
return $default(_that.id,_that.state,_that.title,_that.episodesWatched,_that.episodesTotal,_that.thumbnailUrl,_that.airing,_that.broadcastDay);case _:
return $default(_that.id,_that.state,_that.title,_that.episodesWatched,_that.episodesTotal,_that.thumbnailUrl,_that.airing,_that.broadcastDay,_that.source);case _:
return orElse();
}
@@ -189,10 +191,10 @@ return $default(_that.id,_that.state,_that.title,_that.episodesWatched,_that.epi
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, @MediumTrackingStateConverter() MediumTrackingState state, String title, int episodesWatched, int? episodesTotal, String thumbnailUrl, @BoolConverter() bool airing, String? broadcastDay) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, @MediumTrackingStateConverter() MediumTrackingState state, String title, int episodesWatched, int? episodesTotal, String thumbnailUrl, @BoolConverter() bool airing, String? broadcastDay, @TrackingDataSourceConverter() TrackingDataSource source) $default,) {final _that = this;
switch (_that) {
case _AnimeTrackingData():
return $default(_that.id,_that.state,_that.title,_that.episodesWatched,_that.episodesTotal,_that.thumbnailUrl,_that.airing,_that.broadcastDay);case _:
return $default(_that.id,_that.state,_that.title,_that.episodesWatched,_that.episodesTotal,_that.thumbnailUrl,_that.airing,_that.broadcastDay,_that.source);case _:
throw StateError('Unexpected subclass');
}
@@ -209,10 +211,10 @@ return $default(_that.id,_that.state,_that.title,_that.episodesWatched,_that.epi
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, @MediumTrackingStateConverter() MediumTrackingState state, String title, int episodesWatched, int? episodesTotal, String thumbnailUrl, @BoolConverter() bool airing, String? broadcastDay)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, @MediumTrackingStateConverter() MediumTrackingState state, String title, int episodesWatched, int? episodesTotal, String thumbnailUrl, @BoolConverter() bool airing, String? broadcastDay, @TrackingDataSourceConverter() TrackingDataSource source)? $default,) {final _that = this;
switch (_that) {
case _AnimeTrackingData() when $default != null:
return $default(_that.id,_that.state,_that.title,_that.episodesWatched,_that.episodesTotal,_that.thumbnailUrl,_that.airing,_that.broadcastDay);case _:
return $default(_that.id,_that.state,_that.title,_that.episodesWatched,_that.episodesTotal,_that.thumbnailUrl,_that.airing,_that.broadcastDay,_that.source);case _:
return null;
}
@@ -224,7 +226,7 @@ return $default(_that.id,_that.state,_that.title,_that.episodesWatched,_that.epi
@JsonSerializable()
class _AnimeTrackingData extends AnimeTrackingData {
_AnimeTrackingData(this.id, @MediumTrackingStateConverter() this.state, this.title, this.episodesWatched, this.episodesTotal, this.thumbnailUrl, @BoolConverter() this.airing, this.broadcastDay): super._();
_AnimeTrackingData(this.id, @MediumTrackingStateConverter() this.state, this.title, this.episodesWatched, this.episodesTotal, this.thumbnailUrl, @BoolConverter() this.airing, this.broadcastDay, @TrackingDataSourceConverter() this.source): super._();
factory _AnimeTrackingData.fromJson(Map<String, dynamic> json) => _$AnimeTrackingDataFromJson(json);
/// The ID of the anime
@@ -243,6 +245,8 @@ class _AnimeTrackingData extends AnimeTrackingData {
@override@BoolConverter() final bool airing;
/// The day of the week the anime is airing
@override final String? broadcastDay;
/// The source where we got the data from.
@override@TrackingDataSourceConverter() final TrackingDataSource source;
/// Create a copy of AnimeTrackingData
/// with the given fields replaced by the non-null parameter values.
@@ -257,16 +261,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AnimeTrackingData&&(identical(other.id, id) || other.id == id)&&(identical(other.state, state) || other.state == state)&&(identical(other.title, title) || other.title == title)&&(identical(other.episodesWatched, episodesWatched) || other.episodesWatched == episodesWatched)&&(identical(other.episodesTotal, episodesTotal) || other.episodesTotal == episodesTotal)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl)&&(identical(other.airing, airing) || other.airing == airing)&&(identical(other.broadcastDay, broadcastDay) || other.broadcastDay == broadcastDay));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AnimeTrackingData&&(identical(other.id, id) || other.id == id)&&(identical(other.state, state) || other.state == state)&&(identical(other.title, title) || other.title == title)&&(identical(other.episodesWatched, episodesWatched) || other.episodesWatched == episodesWatched)&&(identical(other.episodesTotal, episodesTotal) || other.episodesTotal == episodesTotal)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl)&&(identical(other.airing, airing) || other.airing == airing)&&(identical(other.broadcastDay, broadcastDay) || other.broadcastDay == broadcastDay)&&(identical(other.source, source) || other.source == source));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,state,title,episodesWatched,episodesTotal,thumbnailUrl,airing,broadcastDay);
int get hashCode => Object.hash(runtimeType,id,state,title,episodesWatched,episodesTotal,thumbnailUrl,airing,broadcastDay,source);
@override
String toString() {
return 'AnimeTrackingData(id: $id, state: $state, title: $title, episodesWatched: $episodesWatched, episodesTotal: $episodesTotal, thumbnailUrl: $thumbnailUrl, airing: $airing, broadcastDay: $broadcastDay)';
return 'AnimeTrackingData(id: $id, state: $state, title: $title, episodesWatched: $episodesWatched, episodesTotal: $episodesTotal, thumbnailUrl: $thumbnailUrl, airing: $airing, broadcastDay: $broadcastDay, source: $source)';
}
@@ -277,7 +281,7 @@ abstract mixin class _$AnimeTrackingDataCopyWith<$Res> implements $AnimeTracking
factory _$AnimeTrackingDataCopyWith(_AnimeTrackingData value, $Res Function(_AnimeTrackingData) _then) = __$AnimeTrackingDataCopyWithImpl;
@override @useResult
$Res call({
String id,@MediumTrackingStateConverter() MediumTrackingState state, String title, int episodesWatched, int? episodesTotal, String thumbnailUrl,@BoolConverter() bool airing, String? broadcastDay
String id,@MediumTrackingStateConverter() MediumTrackingState state, String title, int episodesWatched, int? episodesTotal, String thumbnailUrl,@BoolConverter() bool airing, String? broadcastDay,@TrackingDataSourceConverter() TrackingDataSource source
});
@@ -294,7 +298,7 @@ class __$AnimeTrackingDataCopyWithImpl<$Res>
/// Create a copy of AnimeTrackingData
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? state = null,Object? title = null,Object? episodesWatched = null,Object? episodesTotal = freezed,Object? thumbnailUrl = null,Object? airing = null,Object? broadcastDay = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? state = null,Object? title = null,Object? episodesWatched = null,Object? episodesTotal = freezed,Object? thumbnailUrl = null,Object? airing = null,Object? broadcastDay = freezed,Object? source = null,}) {
return _then(_AnimeTrackingData(
null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
@@ -304,7 +308,8 @@ as int,freezed == episodesTotal ? _self.episodesTotal : episodesTotal // ignore:
as int?,null == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable
as String,null == airing ? _self.airing : airing // ignore: cast_nullable_to_non_nullable
as bool,freezed == broadcastDay ? _self.broadcastDay : broadcastDay // ignore: cast_nullable_to_non_nullable
as String?,
as String?,null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable
as TrackingDataSource,
));
}

View File

@@ -18,6 +18,7 @@ _AnimeTrackingData _$AnimeTrackingDataFromJson(Map<String, dynamic> json) =>
json['thumbnailUrl'] as String,
const BoolConverter().fromJson((json['airing'] as num).toInt()),
json['broadcastDay'] as String?,
const TrackingDataSourceConverter().fromJson(json['source'] as String),
);
Map<String, dynamic> _$AnimeTrackingDataToJson(_AnimeTrackingData instance) =>
@@ -30,4 +31,5 @@ Map<String, dynamic> _$AnimeTrackingDataToJson(_AnimeTrackingData instance) =>
'thumbnailUrl': instance.thumbnailUrl,
'airing': const BoolConverter().toJson(instance.airing),
'broadcastDay': instance.broadcastDay,
'source': const TrackingDataSourceConverter().toJson(instance.source),
};

View File

@@ -1,3 +1,4 @@
import 'package:anitrack/src/data/source.dart';
import 'package:anitrack/src/data/type.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -28,6 +29,9 @@ abstract class MangaTrackingData with _$MangaTrackingData, TrackingMedium {
/// URL to the thumbnail/cover art for the manga.
String thumbnailUrl,
/// The source where we got the data from.
@TrackingDataSourceConverter() TrackingDataSource source,
) = _MangaTrackingData;
MangaTrackingData._();

View File

@@ -22,7 +22,8 @@ mixin _$MangaTrackingData {
int get chaptersRead;/// Chapters read.
int get volumesOwned;/// Episodes watched.
int? get chaptersTotal;/// URL to the thumbnail/cover art for the manga.
String get thumbnailUrl;
String get thumbnailUrl;/// The source where we got the data from.
@TrackingDataSourceConverter() TrackingDataSource get source;
/// Create a copy of MangaTrackingData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -35,16 +36,16 @@ $MangaTrackingDataCopyWith<MangaTrackingData> get copyWith => _$MangaTrackingDat
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is MangaTrackingData&&(identical(other.id, id) || other.id == id)&&(identical(other.state, state) || other.state == state)&&(identical(other.title, title) || other.title == title)&&(identical(other.chaptersRead, chaptersRead) || other.chaptersRead == chaptersRead)&&(identical(other.volumesOwned, volumesOwned) || other.volumesOwned == volumesOwned)&&(identical(other.chaptersTotal, chaptersTotal) || other.chaptersTotal == chaptersTotal)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl));
return identical(this, other) || (other.runtimeType == runtimeType&&other is MangaTrackingData&&(identical(other.id, id) || other.id == id)&&(identical(other.state, state) || other.state == state)&&(identical(other.title, title) || other.title == title)&&(identical(other.chaptersRead, chaptersRead) || other.chaptersRead == chaptersRead)&&(identical(other.volumesOwned, volumesOwned) || other.volumesOwned == volumesOwned)&&(identical(other.chaptersTotal, chaptersTotal) || other.chaptersTotal == chaptersTotal)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl)&&(identical(other.source, source) || other.source == source));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,state,title,chaptersRead,volumesOwned,chaptersTotal,thumbnailUrl);
int get hashCode => Object.hash(runtimeType,id,state,title,chaptersRead,volumesOwned,chaptersTotal,thumbnailUrl,source);
@override
String toString() {
return 'MangaTrackingData(id: $id, state: $state, title: $title, chaptersRead: $chaptersRead, volumesOwned: $volumesOwned, chaptersTotal: $chaptersTotal, thumbnailUrl: $thumbnailUrl)';
return 'MangaTrackingData(id: $id, state: $state, title: $title, chaptersRead: $chaptersRead, volumesOwned: $volumesOwned, chaptersTotal: $chaptersTotal, thumbnailUrl: $thumbnailUrl, source: $source)';
}
@@ -55,7 +56,7 @@ abstract mixin class $MangaTrackingDataCopyWith<$Res> {
factory $MangaTrackingDataCopyWith(MangaTrackingData value, $Res Function(MangaTrackingData) _then) = _$MangaTrackingDataCopyWithImpl;
@useResult
$Res call({
String id,@MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl
String id,@MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl,@TrackingDataSourceConverter() TrackingDataSource source
});
@@ -72,7 +73,7 @@ class _$MangaTrackingDataCopyWithImpl<$Res>
/// Create a copy of MangaTrackingData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? state = null,Object? title = null,Object? chaptersRead = null,Object? volumesOwned = null,Object? chaptersTotal = freezed,Object? thumbnailUrl = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? state = null,Object? title = null,Object? chaptersRead = null,Object? volumesOwned = null,Object? chaptersTotal = freezed,Object? thumbnailUrl = null,Object? source = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
@@ -81,7 +82,8 @@ as String,chaptersRead: null == chaptersRead ? _self.chaptersRead : chaptersRead
as int,volumesOwned: null == volumesOwned ? _self.volumesOwned : volumesOwned // ignore: cast_nullable_to_non_nullable
as int,chaptersTotal: freezed == chaptersTotal ? _self.chaptersTotal : chaptersTotal // ignore: cast_nullable_to_non_nullable
as int?,thumbnailUrl: null == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable
as String,
as String,source: null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable
as TrackingDataSource,
));
}
@@ -166,10 +168,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, @MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, @MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl, @TrackingDataSourceConverter() TrackingDataSource source)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _MangaTrackingData() when $default != null:
return $default(_that.id,_that.state,_that.title,_that.chaptersRead,_that.volumesOwned,_that.chaptersTotal,_that.thumbnailUrl);case _:
return $default(_that.id,_that.state,_that.title,_that.chaptersRead,_that.volumesOwned,_that.chaptersTotal,_that.thumbnailUrl,_that.source);case _:
return orElse();
}
@@ -187,10 +189,10 @@ return $default(_that.id,_that.state,_that.title,_that.chaptersRead,_that.volume
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, @MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, @MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl, @TrackingDataSourceConverter() TrackingDataSource source) $default,) {final _that = this;
switch (_that) {
case _MangaTrackingData():
return $default(_that.id,_that.state,_that.title,_that.chaptersRead,_that.volumesOwned,_that.chaptersTotal,_that.thumbnailUrl);case _:
return $default(_that.id,_that.state,_that.title,_that.chaptersRead,_that.volumesOwned,_that.chaptersTotal,_that.thumbnailUrl,_that.source);case _:
throw StateError('Unexpected subclass');
}
@@ -207,10 +209,10 @@ return $default(_that.id,_that.state,_that.title,_that.chaptersRead,_that.volume
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, @MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, @MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl, @TrackingDataSourceConverter() TrackingDataSource source)? $default,) {final _that = this;
switch (_that) {
case _MangaTrackingData() when $default != null:
return $default(_that.id,_that.state,_that.title,_that.chaptersRead,_that.volumesOwned,_that.chaptersTotal,_that.thumbnailUrl);case _:
return $default(_that.id,_that.state,_that.title,_that.chaptersRead,_that.volumesOwned,_that.chaptersTotal,_that.thumbnailUrl,_that.source);case _:
return null;
}
@@ -222,7 +224,7 @@ return $default(_that.id,_that.state,_that.title,_that.chaptersRead,_that.volume
@JsonSerializable()
class _MangaTrackingData extends MangaTrackingData {
_MangaTrackingData(this.id, @MediumTrackingStateConverter() this.state, this.title, this.chaptersRead, this.volumesOwned, this.chaptersTotal, this.thumbnailUrl): super._();
_MangaTrackingData(this.id, @MediumTrackingStateConverter() this.state, this.title, this.chaptersRead, this.volumesOwned, this.chaptersTotal, this.thumbnailUrl, @TrackingDataSourceConverter() this.source): super._();
factory _MangaTrackingData.fromJson(Map<String, dynamic> json) => _$MangaTrackingDataFromJson(json);
/// The ID of the manga
@@ -239,6 +241,8 @@ class _MangaTrackingData extends MangaTrackingData {
@override final int? chaptersTotal;
/// URL to the thumbnail/cover art for the manga.
@override final String thumbnailUrl;
/// The source where we got the data from.
@override@TrackingDataSourceConverter() final TrackingDataSource source;
/// Create a copy of MangaTrackingData
/// with the given fields replaced by the non-null parameter values.
@@ -253,16 +257,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MangaTrackingData&&(identical(other.id, id) || other.id == id)&&(identical(other.state, state) || other.state == state)&&(identical(other.title, title) || other.title == title)&&(identical(other.chaptersRead, chaptersRead) || other.chaptersRead == chaptersRead)&&(identical(other.volumesOwned, volumesOwned) || other.volumesOwned == volumesOwned)&&(identical(other.chaptersTotal, chaptersTotal) || other.chaptersTotal == chaptersTotal)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MangaTrackingData&&(identical(other.id, id) || other.id == id)&&(identical(other.state, state) || other.state == state)&&(identical(other.title, title) || other.title == title)&&(identical(other.chaptersRead, chaptersRead) || other.chaptersRead == chaptersRead)&&(identical(other.volumesOwned, volumesOwned) || other.volumesOwned == volumesOwned)&&(identical(other.chaptersTotal, chaptersTotal) || other.chaptersTotal == chaptersTotal)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl)&&(identical(other.source, source) || other.source == source));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,state,title,chaptersRead,volumesOwned,chaptersTotal,thumbnailUrl);
int get hashCode => Object.hash(runtimeType,id,state,title,chaptersRead,volumesOwned,chaptersTotal,thumbnailUrl,source);
@override
String toString() {
return 'MangaTrackingData(id: $id, state: $state, title: $title, chaptersRead: $chaptersRead, volumesOwned: $volumesOwned, chaptersTotal: $chaptersTotal, thumbnailUrl: $thumbnailUrl)';
return 'MangaTrackingData(id: $id, state: $state, title: $title, chaptersRead: $chaptersRead, volumesOwned: $volumesOwned, chaptersTotal: $chaptersTotal, thumbnailUrl: $thumbnailUrl, source: $source)';
}
@@ -273,7 +277,7 @@ abstract mixin class _$MangaTrackingDataCopyWith<$Res> implements $MangaTracking
factory _$MangaTrackingDataCopyWith(_MangaTrackingData value, $Res Function(_MangaTrackingData) _then) = __$MangaTrackingDataCopyWithImpl;
@override @useResult
$Res call({
String id,@MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl
String id,@MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl,@TrackingDataSourceConverter() TrackingDataSource source
});
@@ -290,7 +294,7 @@ class __$MangaTrackingDataCopyWithImpl<$Res>
/// Create a copy of MangaTrackingData
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? state = null,Object? title = null,Object? chaptersRead = null,Object? volumesOwned = null,Object? chaptersTotal = freezed,Object? thumbnailUrl = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? state = null,Object? title = null,Object? chaptersRead = null,Object? volumesOwned = null,Object? chaptersTotal = freezed,Object? thumbnailUrl = null,Object? source = null,}) {
return _then(_MangaTrackingData(
null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
@@ -299,7 +303,8 @@ as String,null == chaptersRead ? _self.chaptersRead : chaptersRead // ignore: ca
as int,null == volumesOwned ? _self.volumesOwned : volumesOwned // ignore: cast_nullable_to_non_nullable
as int,freezed == chaptersTotal ? _self.chaptersTotal : chaptersTotal // ignore: cast_nullable_to_non_nullable
as int?,null == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable
as String,
as String,null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable
as TrackingDataSource,
));
}

View File

@@ -17,6 +17,7 @@ _MangaTrackingData _$MangaTrackingDataFromJson(Map<String, dynamic> json) =>
(json['volumesOwned'] as num).toInt(),
(json['chaptersTotal'] as num?)?.toInt(),
json['thumbnailUrl'] as String,
const TrackingDataSourceConverter().fromJson(json['source'] as String),
);
Map<String, dynamic> _$MangaTrackingDataToJson(_MangaTrackingData instance) =>
@@ -28,4 +29,5 @@ Map<String, dynamic> _$MangaTrackingDataToJson(_MangaTrackingData instance) =>
'volumesOwned': instance.volumesOwned,
'chaptersTotal': instance.chaptersTotal,
'thumbnailUrl': instance.thumbnailUrl,
'source': const TrackingDataSourceConverter().toJson(instance.source),
};

22
lib/src/data/source.dart Normal file
View File

@@ -0,0 +1,22 @@
import 'package:freezed_annotation/freezed_annotation.dart';
enum TrackingDataSource {
mal('mal'),
anilist('anilist');
const TrackingDataSource(this.value);
final String value;
}
class TrackingDataSourceConverter
implements JsonConverter<TrackingDataSource, String> {
const TrackingDataSourceConverter();
@override
TrackingDataSource fromJson(String json) =>
TrackingDataSource.values.where((val) => val.value == json).first;
@override
String toJson(TrackingDataSource source) => source.value;
}

View File

@@ -0,0 +1,132 @@
import 'package:anitrack/src/service/anilist/model.dart';
import 'package:graphql/client.dart';
class AniListClient {
/// The base GraphQL client for AniList
final _client = GraphQLClient(
link: HttpLink("https://graphql.anilist.co"),
cache: GraphQLCache(),
);
Future<List<AnimeSearchResult>> searchAnime(String q) async {
const query = r'''
query ($search: String!) {
Page {
media(search: $search, type: ANIME) {
id
status
coverImage {
medium
}
episodes
description
nextAiringEpisode {
airingAt
}
title {
romaji
english
native
}
}
}
}
''';
final result = await _client.query(
QueryOptions(
document: gql(query),
variables: {
'search': q,
},
),
);
if (result.hasException) {
// TODO: Handle this more elegantly
print(result.exception.toString());
return [];
}
return (result.data!["Page"]["media"] as List<Object?>)
.cast<Map<String, dynamic>>()
.map(AnimeSearchResult.fromJson)
.toList();
}
Future<List<MangaSearchResult>> searchManga(String q) async {
const query = r'''
query ($search: String!) {
Page {
media(search: $search, type: MANGA) {
id
status
coverImage {
medium
}
chapters
description
title {
romaji
english
native
}
}
}
}
''';
final result = await _client.query(
QueryOptions(
document: gql(query),
variables: {
'search': q,
},
),
);
if (result.hasException) {
// TODO: Handle this more elegantly
print(result.exception.toString());
return [];
}
return (result.data!["Page"]["media"] as List<Object?>)
.cast<Map<String, dynamic>>()
.map(MangaSearchResult.fromJson)
.toList();
}
Future<Anime> getAnimeById(String id) async {
const query = r'''
query ($id: Int) {
Media (id: $id) {
id
status
nextAiringEpisode {
airingAt
}
title {
romaji
english
native
}
}
}
''';
final result = await _client.query(
QueryOptions(
document: gql(query),
variables: {
'id': id,
},
),
);
if (result.hasException) {
print(result.exception.toString());
}
return Anime.fromJson(result.data!['Media'] as Map<String, dynamic>);
}
/*
Future<dynamic> getMangaById(String id) {
}*/
}

View File

@@ -0,0 +1,13 @@
const _weekdays = [
'Mondays',
'Tuesdays',
'Wednesdays',
'Thursdays',
'Fridays',
'Saturdays',
'Sundays',
];
String getAiringDay(int timestamp) {
return _weekdays[DateTime.fromMillisecondsSinceEpoch(timestamp).weekday];
}

View File

@@ -0,0 +1,82 @@
import 'package:anitrack/src/service/anilist/helpers.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'model.freezed.dart';
@freezed
abstract class AnimeSearchResult with _$AnimeSearchResult {
const factory AnimeSearchResult({
required String title,
required String id,
required int? episodes,
required String imageUrl,
required String? description,
required bool isAiring,
required String? broadcastDay,
}) = _AnimeSearchResult;
factory AnimeSearchResult.fromJson(Map<String, Object?> json) {
final airingDayOfTheWeek = json['nextAiringEpisode'] == null
? null
: getAiringDay(
(json['nextAiringEpisode']! as Map<String, dynamic>)['airingAt']!
as int,
);
return AnimeSearchResult(
title: (json['title']! as Map<String, dynamic>)['romaji']! as String,
id: (json['id']! as int).toString(),
episodes: json['episodes'] as int?,
imageUrl:
(json['coverImage']! as Map<String, dynamic>)['medium']! as String,
description: json['description'] as String?,
isAiring: json['status'] == 'RELEASING',
broadcastDay: airingDayOfTheWeek,
);
}
}
@freezed
abstract class MangaSearchResult with _$MangaSearchResult {
const factory MangaSearchResult({
required String title,
required String id,
required int? chapters,
required String imageUrl,
required String? description,
}) = _MangaSearchResult;
factory MangaSearchResult.fromJson(Map<String, Object?> json) {
return MangaSearchResult(
title: (json['title']! as Map<String, dynamic>)['romaji']! as String,
id: (json['id']! as int).toString(),
chapters: json['chapters'] as int?,
imageUrl:
(json['coverImage']! as Map<String, dynamic>)['medium']! as String,
description: json['description'] as String?,
);
}
}
@freezed
abstract class Anime with _$Anime {
const factory Anime({
required String title,
required bool isAiring,
required String? broadcastDay,
}) = _Anime;
factory Anime.fromJson(Map<String, Object?> json) {
final airingDayOfTheWeek = json['nextAiringEpisode'] == null
? null
: getAiringDay(
(json['nextAiringEpisode']! as Map<String, dynamic>)['airingAt']!
as int,
);
return Anime(
title: (json['title']! as Map<String, dynamic>)['romaji']! as String,
isAiring: json['status'] == 'RELEASING',
broadcastDay: airingDayOfTheWeek,
);
}
}

View File

@@ -0,0 +1,821 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// 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, unnecessary_question_mark
part of 'model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AnimeSearchResult {
String get title; String get id; int? get episodes; String get imageUrl; String? get description; bool get isAiring; String? get broadcastDay;
/// Create a copy of AnimeSearchResult
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AnimeSearchResultCopyWith<AnimeSearchResult> get copyWith => _$AnimeSearchResultCopyWithImpl<AnimeSearchResult>(this as AnimeSearchResult, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AnimeSearchResult&&(identical(other.title, title) || other.title == title)&&(identical(other.id, id) || other.id == id)&&(identical(other.episodes, episodes) || other.episodes == episodes)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.description, description) || other.description == description)&&(identical(other.isAiring, isAiring) || other.isAiring == isAiring)&&(identical(other.broadcastDay, broadcastDay) || other.broadcastDay == broadcastDay));
}
@override
int get hashCode => Object.hash(runtimeType,title,id,episodes,imageUrl,description,isAiring,broadcastDay);
@override
String toString() {
return 'AnimeSearchResult(title: $title, id: $id, episodes: $episodes, imageUrl: $imageUrl, description: $description, isAiring: $isAiring, broadcastDay: $broadcastDay)';
}
}
/// @nodoc
abstract mixin class $AnimeSearchResultCopyWith<$Res> {
factory $AnimeSearchResultCopyWith(AnimeSearchResult value, $Res Function(AnimeSearchResult) _then) = _$AnimeSearchResultCopyWithImpl;
@useResult
$Res call({
String title, String id, int? episodes, String imageUrl, String? description, bool isAiring, String? broadcastDay
});
}
/// @nodoc
class _$AnimeSearchResultCopyWithImpl<$Res>
implements $AnimeSearchResultCopyWith<$Res> {
_$AnimeSearchResultCopyWithImpl(this._self, this._then);
final AnimeSearchResult _self;
final $Res Function(AnimeSearchResult) _then;
/// Create a copy of AnimeSearchResult
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? title = null,Object? id = null,Object? episodes = freezed,Object? imageUrl = null,Object? description = freezed,Object? isAiring = null,Object? broadcastDay = freezed,}) {
return _then(_self.copyWith(
title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,episodes: freezed == episodes ? _self.episodes : episodes // ignore: cast_nullable_to_non_nullable
as int?,imageUrl: null == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,isAiring: null == isAiring ? _self.isAiring : isAiring // ignore: cast_nullable_to_non_nullable
as bool,broadcastDay: freezed == broadcastDay ? _self.broadcastDay : broadcastDay // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [AnimeSearchResult].
extension AnimeSearchResultPatterns on AnimeSearchResult {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _AnimeSearchResult value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _AnimeSearchResult() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _AnimeSearchResult value) $default,){
final _that = this;
switch (_that) {
case _AnimeSearchResult():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _AnimeSearchResult value)? $default,){
final _that = this;
switch (_that) {
case _AnimeSearchResult() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String title, String id, int? episodes, String imageUrl, String? description, bool isAiring, String? broadcastDay)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AnimeSearchResult() when $default != null:
return $default(_that.title,_that.id,_that.episodes,_that.imageUrl,_that.description,_that.isAiring,_that.broadcastDay);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String title, String id, int? episodes, String imageUrl, String? description, bool isAiring, String? broadcastDay) $default,) {final _that = this;
switch (_that) {
case _AnimeSearchResult():
return $default(_that.title,_that.id,_that.episodes,_that.imageUrl,_that.description,_that.isAiring,_that.broadcastDay);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String title, String id, int? episodes, String imageUrl, String? description, bool isAiring, String? broadcastDay)? $default,) {final _that = this;
switch (_that) {
case _AnimeSearchResult() when $default != null:
return $default(_that.title,_that.id,_that.episodes,_that.imageUrl,_that.description,_that.isAiring,_that.broadcastDay);case _:
return null;
}
}
}
/// @nodoc
class _AnimeSearchResult implements AnimeSearchResult {
const _AnimeSearchResult({required this.title, required this.id, required this.episodes, required this.imageUrl, required this.description, required this.isAiring, required this.broadcastDay});
@override final String title;
@override final String id;
@override final int? episodes;
@override final String imageUrl;
@override final String? description;
@override final bool isAiring;
@override final String? broadcastDay;
/// Create a copy of AnimeSearchResult
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$AnimeSearchResultCopyWith<_AnimeSearchResult> get copyWith => __$AnimeSearchResultCopyWithImpl<_AnimeSearchResult>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AnimeSearchResult&&(identical(other.title, title) || other.title == title)&&(identical(other.id, id) || other.id == id)&&(identical(other.episodes, episodes) || other.episodes == episodes)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.description, description) || other.description == description)&&(identical(other.isAiring, isAiring) || other.isAiring == isAiring)&&(identical(other.broadcastDay, broadcastDay) || other.broadcastDay == broadcastDay));
}
@override
int get hashCode => Object.hash(runtimeType,title,id,episodes,imageUrl,description,isAiring,broadcastDay);
@override
String toString() {
return 'AnimeSearchResult(title: $title, id: $id, episodes: $episodes, imageUrl: $imageUrl, description: $description, isAiring: $isAiring, broadcastDay: $broadcastDay)';
}
}
/// @nodoc
abstract mixin class _$AnimeSearchResultCopyWith<$Res> implements $AnimeSearchResultCopyWith<$Res> {
factory _$AnimeSearchResultCopyWith(_AnimeSearchResult value, $Res Function(_AnimeSearchResult) _then) = __$AnimeSearchResultCopyWithImpl;
@override @useResult
$Res call({
String title, String id, int? episodes, String imageUrl, String? description, bool isAiring, String? broadcastDay
});
}
/// @nodoc
class __$AnimeSearchResultCopyWithImpl<$Res>
implements _$AnimeSearchResultCopyWith<$Res> {
__$AnimeSearchResultCopyWithImpl(this._self, this._then);
final _AnimeSearchResult _self;
final $Res Function(_AnimeSearchResult) _then;
/// Create a copy of AnimeSearchResult
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? title = null,Object? id = null,Object? episodes = freezed,Object? imageUrl = null,Object? description = freezed,Object? isAiring = null,Object? broadcastDay = freezed,}) {
return _then(_AnimeSearchResult(
title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,episodes: freezed == episodes ? _self.episodes : episodes // ignore: cast_nullable_to_non_nullable
as int?,imageUrl: null == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,isAiring: null == isAiring ? _self.isAiring : isAiring // ignore: cast_nullable_to_non_nullable
as bool,broadcastDay: freezed == broadcastDay ? _self.broadcastDay : broadcastDay // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
mixin _$MangaSearchResult {
String get title; String get id; int? get chapters; String get imageUrl; String? get description;
/// Create a copy of MangaSearchResult
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$MangaSearchResultCopyWith<MangaSearchResult> get copyWith => _$MangaSearchResultCopyWithImpl<MangaSearchResult>(this as MangaSearchResult, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is MangaSearchResult&&(identical(other.title, title) || other.title == title)&&(identical(other.id, id) || other.id == id)&&(identical(other.chapters, chapters) || other.chapters == chapters)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.description, description) || other.description == description));
}
@override
int get hashCode => Object.hash(runtimeType,title,id,chapters,imageUrl,description);
@override
String toString() {
return 'MangaSearchResult(title: $title, id: $id, chapters: $chapters, imageUrl: $imageUrl, description: $description)';
}
}
/// @nodoc
abstract mixin class $MangaSearchResultCopyWith<$Res> {
factory $MangaSearchResultCopyWith(MangaSearchResult value, $Res Function(MangaSearchResult) _then) = _$MangaSearchResultCopyWithImpl;
@useResult
$Res call({
String title, String id, int? chapters, String imageUrl, String? description
});
}
/// @nodoc
class _$MangaSearchResultCopyWithImpl<$Res>
implements $MangaSearchResultCopyWith<$Res> {
_$MangaSearchResultCopyWithImpl(this._self, this._then);
final MangaSearchResult _self;
final $Res Function(MangaSearchResult) _then;
/// Create a copy of MangaSearchResult
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? title = null,Object? id = null,Object? chapters = freezed,Object? imageUrl = null,Object? description = freezed,}) {
return _then(_self.copyWith(
title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,chapters: freezed == chapters ? _self.chapters : chapters // ignore: cast_nullable_to_non_nullable
as int?,imageUrl: null == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [MangaSearchResult].
extension MangaSearchResultPatterns on MangaSearchResult {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _MangaSearchResult value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _MangaSearchResult() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _MangaSearchResult value) $default,){
final _that = this;
switch (_that) {
case _MangaSearchResult():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _MangaSearchResult value)? $default,){
final _that = this;
switch (_that) {
case _MangaSearchResult() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String title, String id, int? chapters, String imageUrl, String? description)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _MangaSearchResult() when $default != null:
return $default(_that.title,_that.id,_that.chapters,_that.imageUrl,_that.description);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String title, String id, int? chapters, String imageUrl, String? description) $default,) {final _that = this;
switch (_that) {
case _MangaSearchResult():
return $default(_that.title,_that.id,_that.chapters,_that.imageUrl,_that.description);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String title, String id, int? chapters, String imageUrl, String? description)? $default,) {final _that = this;
switch (_that) {
case _MangaSearchResult() when $default != null:
return $default(_that.title,_that.id,_that.chapters,_that.imageUrl,_that.description);case _:
return null;
}
}
}
/// @nodoc
class _MangaSearchResult implements MangaSearchResult {
const _MangaSearchResult({required this.title, required this.id, required this.chapters, required this.imageUrl, required this.description});
@override final String title;
@override final String id;
@override final int? chapters;
@override final String imageUrl;
@override final String? description;
/// Create a copy of MangaSearchResult
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$MangaSearchResultCopyWith<_MangaSearchResult> get copyWith => __$MangaSearchResultCopyWithImpl<_MangaSearchResult>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MangaSearchResult&&(identical(other.title, title) || other.title == title)&&(identical(other.id, id) || other.id == id)&&(identical(other.chapters, chapters) || other.chapters == chapters)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.description, description) || other.description == description));
}
@override
int get hashCode => Object.hash(runtimeType,title,id,chapters,imageUrl,description);
@override
String toString() {
return 'MangaSearchResult(title: $title, id: $id, chapters: $chapters, imageUrl: $imageUrl, description: $description)';
}
}
/// @nodoc
abstract mixin class _$MangaSearchResultCopyWith<$Res> implements $MangaSearchResultCopyWith<$Res> {
factory _$MangaSearchResultCopyWith(_MangaSearchResult value, $Res Function(_MangaSearchResult) _then) = __$MangaSearchResultCopyWithImpl;
@override @useResult
$Res call({
String title, String id, int? chapters, String imageUrl, String? description
});
}
/// @nodoc
class __$MangaSearchResultCopyWithImpl<$Res>
implements _$MangaSearchResultCopyWith<$Res> {
__$MangaSearchResultCopyWithImpl(this._self, this._then);
final _MangaSearchResult _self;
final $Res Function(_MangaSearchResult) _then;
/// Create a copy of MangaSearchResult
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? title = null,Object? id = null,Object? chapters = freezed,Object? imageUrl = null,Object? description = freezed,}) {
return _then(_MangaSearchResult(
title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,chapters: freezed == chapters ? _self.chapters : chapters // ignore: cast_nullable_to_non_nullable
as int?,imageUrl: null == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
mixin _$Anime {
String get title; bool get isAiring; String? get broadcastDay;
/// Create a copy of Anime
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AnimeCopyWith<Anime> get copyWith => _$AnimeCopyWithImpl<Anime>(this as Anime, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is Anime&&(identical(other.title, title) || other.title == title)&&(identical(other.isAiring, isAiring) || other.isAiring == isAiring)&&(identical(other.broadcastDay, broadcastDay) || other.broadcastDay == broadcastDay));
}
@override
int get hashCode => Object.hash(runtimeType,title,isAiring,broadcastDay);
@override
String toString() {
return 'Anime(title: $title, isAiring: $isAiring, broadcastDay: $broadcastDay)';
}
}
/// @nodoc
abstract mixin class $AnimeCopyWith<$Res> {
factory $AnimeCopyWith(Anime value, $Res Function(Anime) _then) = _$AnimeCopyWithImpl;
@useResult
$Res call({
String title, bool isAiring, String? broadcastDay
});
}
/// @nodoc
class _$AnimeCopyWithImpl<$Res>
implements $AnimeCopyWith<$Res> {
_$AnimeCopyWithImpl(this._self, this._then);
final Anime _self;
final $Res Function(Anime) _then;
/// Create a copy of Anime
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? title = null,Object? isAiring = null,Object? broadcastDay = freezed,}) {
return _then(_self.copyWith(
title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,isAiring: null == isAiring ? _self.isAiring : isAiring // ignore: cast_nullable_to_non_nullable
as bool,broadcastDay: freezed == broadcastDay ? _self.broadcastDay : broadcastDay // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [Anime].
extension AnimePatterns on Anime {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Anime value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _Anime() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Anime value) $default,){
final _that = this;
switch (_that) {
case _Anime():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Anime value)? $default,){
final _that = this;
switch (_that) {
case _Anime() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String title, bool isAiring, String? broadcastDay)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _Anime() when $default != null:
return $default(_that.title,_that.isAiring,_that.broadcastDay);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String title, bool isAiring, String? broadcastDay) $default,) {final _that = this;
switch (_that) {
case _Anime():
return $default(_that.title,_that.isAiring,_that.broadcastDay);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String title, bool isAiring, String? broadcastDay)? $default,) {final _that = this;
switch (_that) {
case _Anime() when $default != null:
return $default(_that.title,_that.isAiring,_that.broadcastDay);case _:
return null;
}
}
}
/// @nodoc
class _Anime implements Anime {
const _Anime({required this.title, required this.isAiring, required this.broadcastDay});
@override final String title;
@override final bool isAiring;
@override final String? broadcastDay;
/// Create a copy of Anime
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$AnimeCopyWith<_Anime> get copyWith => __$AnimeCopyWithImpl<_Anime>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Anime&&(identical(other.title, title) || other.title == title)&&(identical(other.isAiring, isAiring) || other.isAiring == isAiring)&&(identical(other.broadcastDay, broadcastDay) || other.broadcastDay == broadcastDay));
}
@override
int get hashCode => Object.hash(runtimeType,title,isAiring,broadcastDay);
@override
String toString() {
return 'Anime(title: $title, isAiring: $isAiring, broadcastDay: $broadcastDay)';
}
}
/// @nodoc
abstract mixin class _$AnimeCopyWith<$Res> implements $AnimeCopyWith<$Res> {
factory _$AnimeCopyWith(_Anime value, $Res Function(_Anime) _then) = __$AnimeCopyWithImpl;
@override @useResult
$Res call({
String title, bool isAiring, String? broadcastDay
});
}
/// @nodoc
class __$AnimeCopyWithImpl<$Res>
implements _$AnimeCopyWith<$Res> {
__$AnimeCopyWithImpl(this._self, this._then);
final _Anime _self;
final $Res Function(_Anime) _then;
/// Create a copy of Anime
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? title = null,Object? isAiring = null,Object? broadcastDay = freezed,}) {
return _then(_Anime(
title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,isAiring: null == isAiring ? _self.isAiring : isAiring // ignore: cast_nullable_to_non_nullable
as bool,broadcastDay: freezed == broadcastDay ? _self.broadcastDay : broadcastDay // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on

View File

@@ -6,6 +6,7 @@ import 'package:anitrack/src/data/manga.dart';
import 'package:anitrack/src/service/migrations/0000_airing.dart';
import 'package:anitrack/src/service/migrations/0000_score.dart';
import 'package:anitrack/src/service/migrations/0001_anime_watcher.dart';
import 'package:anitrack/src/service/migrations/0002_anilist.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
const animeTable = 'Anime';
@@ -38,7 +39,8 @@ Future<void> _createDatabase(Database db, int version) async {
otherTitles TEXT NOT NULL,
score INTEGER,
airing INTEGER NOT NULL,
broadcastDay TEXT
broadcastDay TEXT,
source TEXT NOT NULL
)''',
);
await db.execute(
@@ -52,7 +54,8 @@ Future<void> _createDatabase(Database db, int version) async {
thumbnailUrl TEXT NOT NULL,
title TEXT NOT NULL,
otherTitles TEXT NOT NULL,
score INTEGER
score INTEGER,
source TEXT NOT NULL
)''',
);
await db.execute(
@@ -89,7 +92,7 @@ class DatabaseService {
_db = await openDatabase(
'anitrack.db',
version: 4,
version: 5,
onConfigure: (db) async {
// In order to do schema changes during database upgrades, we disable foreign
// keys in the onConfigure phase, but re-enable them here.
@@ -111,6 +114,9 @@ class DatabaseService {
if (oldVersion < 4) {
await migrateFromV3ToV4(db);
}
if (oldVersion < 5) {
await migrateFromV4ToV5(db);
}
},
);
print(_db.path);

View File

@@ -0,0 +1,15 @@
import 'package:anitrack/src/service/database.dart';
import 'package:sqflite/sqflite.dart';
Future<void> migrateFromV4ToV5(Database db) async {
await db.execute(
'''
ALTER TABLE $animeTable ADD COLUMN source TEXT NOT NULL DEFAULT 'mal';
''',
);
await db.execute(
'''
ALTER TABLE $mangaTable ADD COLUMN source TEXT NOT NULL DEFAULT 'mal';
''',
);
}

View File

@@ -317,9 +317,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
);
// Update the cache
final cacheIndex = _mangas.indexWhere((m) => m.id == event.id);
final cacheIndex = _animes.indexWhere((m) => m.id == event.id);
assert(cacheIndex != -1, 'The anime must exist');
_mangas.removeAt(cacheIndex);
_animes.removeAt(cacheIndex);
// Update the database
await GetIt.I.get<DatabaseService>().deleteAnime(event.id);
@@ -338,9 +338,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
);
// Update the cache
final cacheIndex = _animes.indexWhere((a) => a.id == event.id);
final cacheIndex = _mangas.indexWhere((a) => a.id == event.id);
assert(cacheIndex != -1, 'The manga must exist');
_animes.removeAt(cacheIndex);
_mangas.removeAt(cacheIndex);
// Update the database
await GetIt.I.get<DatabaseService>().deleteManga(event.id);

View File

@@ -1,7 +1,10 @@
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/source.dart';
import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/service/anilist/anilist_client.dart';
import 'package:anitrack/src/service/anilist/model.dart';
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/constants.dart';
@@ -22,6 +25,9 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
on<ResultTappedEvent>(_onResultTapped);
}
// The client to use for AniList GraphQL queries.
final _anilistClient = AniListClient();
Future<void> _onRequested(
AnimeSearchRequestedEvent event,
Emitter<AnimeSearchState> emit,
@@ -67,23 +73,21 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
if (state.trackingType == TrackingMediumType.anime) {
// Anime
final result = await Jikan().searchAnime(
query: state.searchQuery,
);
final result = await _anilistClient.searchAnime(state.searchQuery);
emit(
state.copyWith(
working: false,
searchResults: result
.map(
(Anime anime) => SearchResult(
(anime) => SearchResult(
anime.title,
anime.malId.toString(),
anime.id,
anime.episodes,
anime.imageUrl,
anime.synopsis ?? '',
anime.airing,
anime.broadcast?.split(' ').first,
anime.description ?? '',
anime.isAiring,
anime.broadcastDay,
),
)
.toList(),
@@ -91,21 +95,19 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
);
} else {
// Manga
final result = await Jikan().searchManga(
query: state.searchQuery,
);
final result = await _anilistClient.searchManga(state.searchQuery);
emit(
state.copyWith(
working: false,
searchResults: result
.map(
(Manga manga) => SearchResult(
(manga) => SearchResult(
manga.title,
manga.malId.toString(),
manga.id,
manga.chapters,
manga.imageUrl,
manga.synopsis ?? '',
manga.description ?? '',
// TODO(Unknown): Implement for Manga
false,
null,
@@ -133,6 +135,7 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
event.result.thumbnailUrl,
event.result.isAiring,
event.result.broadcastDay,
TrackingDataSource.anilist,
),
)
: list.MangaAddedEvent(
@@ -144,6 +147,7 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
0,
event.result.total,
event.result.thumbnailUrl,
TrackingDataSource.anilist,
),
),
);

View File

@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:anitrack/src/data/source.dart';
import 'package:anitrack/src/service/anilist/anilist_client.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -43,9 +45,18 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
String? broadcastDay;
bool airing;
try {
final apiData = await Jikan().getAnime(int.parse(anime.id));
airing = apiData.airing;
broadcastDay = apiData.broadcast?.split(' ').first;
switch (anime.source) {
case TrackingDataSource.mal:
final apiData = await Jikan().getAnime(int.parse(anime.id));
airing = apiData.airing;
broadcastDay = apiData.broadcast?.split(' ').first;
case TrackingDataSource.anilist:
final apiData = await GetIt.I.get<AniListClient>().getAnimeById(
anime.id,
);
airing = apiData.isAiring;
broadcastDay = apiData.broadcastDay;
}
} catch (ex) {
print('API request for anime ${anime.id} failed: $ex');
airing = false;

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/manga.dart';
import 'package:anitrack/src/data/source.dart';
import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/service/database.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
@@ -131,6 +132,7 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
// NOTE: When the calendar gets refreshed, this should also get cleared
true,
null,
TrackingDataSource.mal,
),
);
}
@@ -207,6 +209,7 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
// 0 means that MAL does not know
totalChapters == 0 ? null : totalChapters,
data.imageUrl,
TrackingDataSource.mal,
),
);
}

View File

@@ -122,8 +122,7 @@ class CalendarPageState extends State<CalendarPage> {
];
}
@override
Widget build(BuildContext context) {
Map<Weekday, List<AnimeTrackingData>> _constructAiringMap() {
final airingAnimeMap = <Weekday, List<AnimeTrackingData>>{};
for (final anime in GetIt.I.get<AnimeListBloc>().unfilteredAnime) {
if (!anime.airing) continue;
@@ -158,166 +157,176 @@ class CalendarPageState extends State<CalendarPage> {
airingAnimeMap.addOrSet(day, anime);
}
return airingAnimeMap;
}
return BlocListener<CalendarBloc, CalendarState>(
listenWhen: (previous, current) =>
previous.refreshing != current.refreshing,
listener: (context, state) {
// Force an update
if (!state.refreshing) {
setState(() {});
}
},
child: WillPopScope(
onWillPop: () async => !context.read<CalendarBloc>().state.refreshing,
child: Stack(
children: [
Positioned(
left: 8,
right: 8,
top: 0,
bottom: 0,
child: Scaffold(
appBar: AppBar(
title: Text(t.calendar.calendar),
actions: [
IconButton(
onPressed: () {
context.read<CalendarBloc>().add(
RefreshPerformedEvent(),
);
},
icon: const Icon(Icons.refresh),
@override
Widget build(BuildContext context) {
return BlocBuilder<AnimeListBloc, AnimeListState>(
builder: (context, animeListState) {
final airingAnimeMap = _constructAiringMap();
return BlocListener<CalendarBloc, CalendarState>(
listenWhen: (previous, current) =>
previous.refreshing != current.refreshing,
listener: (context, state) {
// Force an update
if (!state.refreshing) {
setState(() {});
}
},
child: WillPopScope(
onWillPop: () async =>
!context.read<CalendarBloc>().state.refreshing,
child: Stack(
children: [
Positioned(
left: 8,
right: 8,
top: 0,
bottom: 0,
child: Scaffold(
appBar: AppBar(
title: Text(t.calendar.calendar),
actions: [
IconButton(
onPressed: () {
context.read<CalendarBloc>().add(
RefreshPerformedEvent(),
);
},
icon: const Icon(Icons.refresh),
),
],
),
],
),
drawer: getDrawer(context),
body: Padding(
padding: const EdgeInsetsGeometry.symmetric(
horizontal: 12,
),
child: CustomScrollView(
slivers: [
// Render all available weekdays
..._renderWeekdayList(
context,
Weekday.unknown,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.monday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.tuesday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.wednesday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.thursday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.friday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.saturday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.sunday,
airingAnimeMap,
drawer: getDrawer(context),
body: Padding(
padding: const EdgeInsetsGeometry.symmetric(
horizontal: 12,
),
child: CustomScrollView(
slivers: [
// Render all available weekdays
..._renderWeekdayList(
context,
Weekday.unknown,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.monday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.tuesday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.wednesday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.thursday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.friday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.saturday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.sunday,
airingAnimeMap,
),
// Provide a nice bottom padding, while keeping the elastic effect attached
// to the bottom-most edge.
const SliverToBoxAdapter(
// Provide a nice bottom padding, while keeping the elastic effect attached
// to the bottom-most edge.
const SliverToBoxAdapter(
child: SizedBox(
height: 16,
),
),
],
),
),
),
),
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: BlocBuilder<CalendarBloc, CalendarState>(
buildWhen: (previous, current) =>
previous.refreshing != current.refreshing,
builder: (context, state) {
if (!state.refreshing) {
return const SizedBox();
}
return const ModalBarrier(
dismissible: false,
color: Colors.black54,
);
},
),
),
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: BlocBuilder<CalendarBloc, CalendarState>(
builder: (context, state) {
if (!state.refreshing) {
return const SizedBox();
}
return Center(
child: SizedBox(
height: 16,
width: 150,
height: 150,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.grey.shade800,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.all(25),
child: CircularProgressIndicator(),
),
Text(
t.settings.importIndicator(
current: state.refreshingCount,
total: state.refreshingTotal,
),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
),
],
);
},
),
),
),
],
),
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: BlocBuilder<CalendarBloc, CalendarState>(
buildWhen: (previous, current) =>
previous.refreshing != current.refreshing,
builder: (context, state) {
if (!state.refreshing) {
return const SizedBox();
}
return const ModalBarrier(
dismissible: false,
color: Colors.black54,
);
},
),
),
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: BlocBuilder<CalendarBloc, CalendarState>(
builder: (context, state) {
if (!state.refreshing) {
return const SizedBox();
}
return Center(
child: SizedBox(
width: 150,
height: 150,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.grey.shade800,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.all(25),
child: CircularProgressIndicator(),
),
Text(
t.settings.importIndicator(
current: state.refreshingCount,
total: state.refreshingTotal,
),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
);
},
),
),
],
),
),
),
);
},
);
}
}