Compare commits

...

30 Commits

Author SHA1 Message Date
e489cae228 release: Release 0.2.2 2026-07-26 22:15:25 +02:00
06da4dbb16 chore: Apply dart analyze fixes 2026-07-26 22:14:24 +02:00
18f0d55e00 feat: Improve a bit the padding on about 2026-07-26 22:10:18 +02:00
d7566c9414 fix: Disable go_router debug logging in debug mode 2026-07-26 22:07:47 +02:00
c5c7711611 fix: Fix licenses on the about page 2026-07-26 22:05:40 +02:00
84594b42cf fix: Fix hardcoded text 2026-07-26 22:02:03 +02:00
437d76fddc fix: Fix PageView not working after adding an entry 2026-07-26 21:59:51 +02:00
3a6ab1e825 fix: Navigation back from the details view 2026-07-26 19:00:09 +02:00
39cc9b8d7c feat: Fix framework assertion when adding an item 2026-07-26 18:55:42 +02:00
58ea71e694 feat: Also update total episode count on refresh 2026-07-26 18:42:32 +02:00
04d64c3178 feat: Fix calendar view using a stack 2026-07-26 18:37:03 +02:00
69553707a9 fix: Use ON DELETE CASCADE for the anime watcher join table 2026-07-26 18:14:39 +02:00
d034875e16 fix: Fix routing 2026-07-26 15:53:36 +02:00
34b3817e6d release: Release 0.2.1 2026-07-26 00:06:29 +02:00
a17bebffe4 fix: Fix database due to forgotten new attribute 2026-07-26 00:06:02 +02:00
87fc1566f2 fix: Put the database somewhere more sensible on Linux 2026-07-25 23:48:27 +02:00
85df1e702a chore: Update dependencies 2026-07-25 23:40:42 +02:00
cfe0c0fb94 fix: Fix database entry creation due to missing other titles 2026-07-25 23:38:13 +02:00
2f5ddb6ea4 feat: Use large cover images instead of medium ones 2026-07-25 23:11:24 +02:00
60c09fcffd fix: Fix a weird interaction when switching back to the anime list 2026-07-25 23:05:16 +02:00
0e61f9f757 fix: Fix TextField look when adding a watcher 2026-07-25 22:52:26 +02:00
90a7531d72 fix: Fix hero transition for images 2026-07-25 20:57:14 +02:00
8a31f2b72d chore: Move details.dart into details/ 2026-07-25 20:49:54 +02:00
c47c98e65e fix: Fix weird borders in the calendar view 2026-07-25 20:46:13 +02:00
ca796ab9f9 fix: Fix anime watchers creation 2026-07-25 20:45:16 +02:00
a8805b0cba release: Release 0.2.0 2026-07-25 17:37:17 +02:00
e203fbfd75 fix: Fix anime watcher migration 2026-07-25 17:37:17 +02:00
1bf4c93628 chore: Bump to Flutter 3.44 2026-07-25 17:16:56 +02:00
9ef699be3c feat: Bump dependencies + make AniList link work 2026-07-25 16:48:30 +02:00
6c2af9fc57 feat: Integrate the AniList API since Jikan is shutting down 2026-07-25 16:34:14 +02:00
54 changed files with 41874 additions and 6472 deletions

View File

@@ -1,6 +1,6 @@
# anitrack # anitrack
A simple Anime and Manga tracker that uses the [Jikan API](https://jikan.moe/) as A simple Anime and Manga tracker that uses the [AniList API](https://docs.anilist.co/guide/introduction) as
its data source. its data source.
## Screenshots ## Screenshots

View File

@@ -1,10 +1,15 @@
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
} }
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
android { android {
namespace = "me.polynom.anitrack" namespace = "me.polynom.anitrack"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
@@ -15,10 +20,6 @@ android {
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "me.polynom.anitrack" applicationId = "me.polynom.anitrack"

View File

@@ -1,4 +1,5 @@
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
android.newDsl=false android.newDsl=false
android.builtInKotlin=false

View File

@@ -47,6 +47,7 @@
"episodes": "Folgen", "episodes": "Folgen",
"chapters": "Kapitel", "chapters": "Kapitel",
"mal": "MAL", "mal": "MAL",
"anilist": "AniList",
"watchingWith": { "watchingWith": {
"title": "Zusammen schauen mit", "title": "Zusammen schauen mit",
"add": "Hinzufügen", "add": "Hinzufügen",

View File

@@ -50,6 +50,7 @@
"chapters": "Chapters", "chapters": "Chapters",
"volumesOwned": "Volumes owned", "volumesOwned": "Volumes owned",
"mal": "MAL", "mal": "MAL",
"anilist": "AniList",
"watchingWith": { "watchingWith": {
"title": "Watching with", "title": "Watching with",
"add": "Add", "add": "Add",

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import 'package:anitrack/i18n/strings.g.dart'; 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/service/database.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart'; import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart'; import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart';
@@ -11,15 +12,16 @@ import 'package:anitrack/src/ui/pages/about.dart';
import 'package:anitrack/src/ui/pages/anime_list.dart'; import 'package:anitrack/src/ui/pages/anime_list.dart';
import 'package:anitrack/src/ui/pages/anime_search.dart'; import 'package:anitrack/src/ui/pages/anime_search.dart';
import 'package:anitrack/src/ui/pages/calendar.dart'; import 'package:anitrack/src/ui/pages/calendar.dart';
import 'package:anitrack/src/ui/pages/details.dart'; import 'package:anitrack/src/ui/pages/details/details.dart';
import 'package:anitrack/src/ui/pages/settings.dart'; import 'package:anitrack/src/ui/pages/settings.dart';
import 'package:anitrack/src/ui/widgets/shell_wrapper.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
void main() async { void main() async {
final navKey = GlobalKey<NavigatorState>();
// Initialize the widgets binding for sqflite // Initialize the widgets binding for sqflite
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@@ -32,9 +34,10 @@ void main() async {
GetIt.I.registerSingleton<AnimeListBloc>(AnimeListBloc()); GetIt.I.registerSingleton<AnimeListBloc>(AnimeListBloc());
GetIt.I.registerSingleton<AnimeSearchBloc>(AnimeSearchBloc()); GetIt.I.registerSingleton<AnimeSearchBloc>(AnimeSearchBloc());
GetIt.I.registerSingleton<DetailsBloc>(DetailsBloc()); GetIt.I.registerSingleton<DetailsBloc>(DetailsBloc());
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navKey)); GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc());
GetIt.I.registerSingleton<SettingsBloc>(SettingsBloc()); GetIt.I.registerSingleton<SettingsBloc>(SettingsBloc());
GetIt.I.registerSingleton<CalendarBloc>(CalendarBloc()); GetIt.I.registerSingleton<CalendarBloc>(CalendarBloc());
GetIt.I.registerSingleton<AniListClient>(AniListClient());
// Load animes // Load animes
GetIt.I.get<AnimeListBloc>().add( GetIt.I.get<AnimeListBloc>().add(
@@ -66,53 +69,85 @@ void main() async {
create: (_) => GetIt.I.get<CalendarBloc>(), create: (_) => GetIt.I.get<CalendarBloc>(),
), ),
], ],
child: MyApp(navKey), child: MyApp(),
), ),
); );
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp( MyApp({super.key});
this.navKey, {
super.key,
});
final GlobalKey<NavigatorState> navKey; /// The router to attach to the main app.
final _router = GoRouter(
initialLocation: animeListRoute,
debugLogDiagnostics: kDebugMode,
routes: [
ShellRoute(
builder: (context, state, child) {
return ShellWrapper(state: state, child: child);
},
routes: [
GoRoute(
path: animeListRoute,
builder: (context, state) => const AnimeListPage(),
),
GoRoute(
path: calendarRoute,
builder: (context, state) => const CalendarPage(),
),
],
),
GoRoute(
path: animeSearchRoute,
builder: (context, state) => const AnimeSearchPage(),
),
GoRoute(
path: detailsRoute,
builder: (context, state) => const DetailsPage(),
),
GoRoute(
path: aboutRoute,
builder: (context, state) => const AboutPage(),
),
GoRoute(
path: settingsRoute,
builder: (context, state) => const SettingsPage(),
),
],
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return BlocListener<NavigationBloc, NavigationState>(
title: 'AniTrack', listener: (context, state) {
theme: ThemeData( if (state is NoopNavigationState) {
brightness: Brightness.light, // NOOP
primarySwatch: Colors.blue, } else if (state is PushNavigationState) {
useMaterial3: true, _router.push(state.destination);
), } else if (state is GoNavigationState) {
darkTheme: ThemeData( if (_router.canPop()) {
brightness: Brightness.dark, _router.replace(state.destination);
primarySwatch: Colors.blue, } else {
useMaterial3: true, _router.go(state.destination);
), }
navigatorKey: navKey, } else if (state is PoppedNavigationState) {
onGenerateRoute: (settings) { _router.pop();
switch (settings.name) {
case '/':
case animeListRoute:
return AnimeListPage.route;
case animeSearchRoute:
return AnimeSearchPage.route;
case calendarRoute:
return CalendarPage.route;
case detailsRoute:
return DetailsPage.route;
case aboutRoute:
return AboutPage.route;
case settingsRoute:
return SettingsPage.route;
} }
return null;
}, },
child: MaterialApp.router(
title: 'AniTrack',
theme: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.blue,
useMaterial3: true,
),
darkTheme: ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.blue,
useMaterial3: true,
),
routerConfig: _router,
),
); );
} }
} }

39112
lib/oss_licenses.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,6 @@
import 'package:anitrack/src/data/data_base.dart';
import 'package:anitrack/src/data/other_titles.dart';
import 'package:anitrack/src/data/source.dart';
import 'package:anitrack/src/data/type.dart'; import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/service/database.dart'; import 'package:anitrack/src/service/database.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@@ -17,7 +20,9 @@ class BoolConverter implements JsonConverter<bool, int> {
/// Data about a tracked anime /// Data about a tracked anime
@freezed @freezed
abstract class AnimeTrackingData with _$AnimeTrackingData, TrackingMedium { abstract class AnimeTrackingData
with _$AnimeTrackingData, TrackingMedium
implements TrackingDataBase {
factory AnimeTrackingData( factory AnimeTrackingData(
/// The ID of the anime /// The ID of the anime
String id, String id,
@@ -42,6 +47,12 @@ abstract class AnimeTrackingData with _$AnimeTrackingData, TrackingMedium {
/// The day of the week the anime is airing /// The day of the week the anime is airing
String? broadcastDay, String? broadcastDay,
/// Other titles
@OtherTitlesConverter() OtherTitles otherTitles,
/// The source where we got the data from.
@TrackingDataSourceConverter() TrackingDataSource source,
) = _AnimeTrackingData; ) = _AnimeTrackingData;
AnimeTrackingData._(); AnimeTrackingData._();

View File

@@ -23,7 +23,9 @@ mixin _$AnimeTrackingData {
int? get episodesTotal;/// URL to the thumbnail/cover art for the anime. int? get episodesTotal;/// URL to the thumbnail/cover art for the anime.
String get thumbnailUrl;/// Flag whether the anime is airing String get thumbnailUrl;/// Flag whether the anime is airing
@BoolConverter() bool get airing;/// The day of the week the anime is airing @BoolConverter() bool get airing;/// The day of the week the anime is airing
String? get broadcastDay; String? get broadcastDay;/// Other titles
@OtherTitlesConverter() OtherTitles get otherTitles;/// The source where we got the data from.
@TrackingDataSourceConverter() TrackingDataSource get source;
/// Create a copy of AnimeTrackingData /// Create a copy of AnimeTrackingData
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -36,16 +38,16 @@ $AnimeTrackingDataCopyWith<AnimeTrackingData> get copyWith => _$AnimeTrackingDat
@override @override
bool operator ==(Object other) { 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.otherTitles, otherTitles) || other.otherTitles == otherTitles)&&(identical(other.source, source) || other.source == source));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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,otherTitles,source);
@override @override
String toString() { 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, otherTitles: $otherTitles, source: $source)';
} }
@@ -56,11 +58,11 @@ abstract mixin class $AnimeTrackingDataCopyWith<$Res> {
factory $AnimeTrackingDataCopyWith(AnimeTrackingData value, $Res Function(AnimeTrackingData) _then) = _$AnimeTrackingDataCopyWithImpl; factory $AnimeTrackingDataCopyWith(AnimeTrackingData value, $Res Function(AnimeTrackingData) _then) = _$AnimeTrackingDataCopyWithImpl;
@useResult @useResult
$Res call({ $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,@OtherTitlesConverter() OtherTitles otherTitles,@TrackingDataSourceConverter() TrackingDataSource source
}); });
$OtherTitlesCopyWith<$Res> get otherTitles;
} }
/// @nodoc /// @nodoc
@@ -73,7 +75,7 @@ class _$AnimeTrackingDataCopyWithImpl<$Res>
/// Create a copy of AnimeTrackingData /// Create a copy of AnimeTrackingData
/// with the given fields replaced by the non-null parameter values. /// 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? otherTitles = null,Object? source = null,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 as String,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
@@ -83,10 +85,21 @@ as int,episodesTotal: freezed == episodesTotal ? _self.episodesTotal : episodesT
as int?,thumbnailUrl: null == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable 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 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 bool,broadcastDay: freezed == broadcastDay ? _self.broadcastDay : broadcastDay // ignore: cast_nullable_to_non_nullable
as String?, as String?,otherTitles: null == otherTitles ? _self.otherTitles : otherTitles // ignore: cast_nullable_to_non_nullable
as OtherTitles,source: null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable
as TrackingDataSource,
)); ));
} }
/// Create a copy of AnimeTrackingData
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$OtherTitlesCopyWith<$Res> get otherTitles {
return $OtherTitlesCopyWith<$Res>(_self.otherTitles, (value) {
return _then(_self.copyWith(otherTitles: value));
});
}
} }
@@ -168,10 +181,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, @OtherTitlesConverter() OtherTitles otherTitles, @TrackingDataSourceConverter() TrackingDataSource source)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _AnimeTrackingData() when $default != null: 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.otherTitles,_that.source);case _:
return orElse(); return orElse();
} }
@@ -189,10 +202,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, @OtherTitlesConverter() OtherTitles otherTitles, @TrackingDataSourceConverter() TrackingDataSource source) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _AnimeTrackingData(): 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.otherTitles,_that.source);case _:
throw StateError('Unexpected subclass'); throw StateError('Unexpected subclass');
} }
@@ -209,10 +222,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, @OtherTitlesConverter() OtherTitles otherTitles, @TrackingDataSourceConverter() TrackingDataSource source)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _AnimeTrackingData() when $default != null: 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.otherTitles,_that.source);case _:
return null; return null;
} }
@@ -224,7 +237,7 @@ return $default(_that.id,_that.state,_that.title,_that.episodesWatched,_that.epi
@JsonSerializable() @JsonSerializable()
class _AnimeTrackingData extends AnimeTrackingData { 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, @OtherTitlesConverter() this.otherTitles, @TrackingDataSourceConverter() this.source): super._();
factory _AnimeTrackingData.fromJson(Map<String, dynamic> json) => _$AnimeTrackingDataFromJson(json); factory _AnimeTrackingData.fromJson(Map<String, dynamic> json) => _$AnimeTrackingDataFromJson(json);
/// The ID of the anime /// The ID of the anime
@@ -243,6 +256,10 @@ class _AnimeTrackingData extends AnimeTrackingData {
@override@BoolConverter() final bool airing; @override@BoolConverter() final bool airing;
/// The day of the week the anime is airing /// The day of the week the anime is airing
@override final String? broadcastDay; @override final String? broadcastDay;
/// Other titles
@override@OtherTitlesConverter() final OtherTitles otherTitles;
/// The source where we got the data from.
@override@TrackingDataSourceConverter() final TrackingDataSource source;
/// Create a copy of AnimeTrackingData /// Create a copy of AnimeTrackingData
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -257,16 +274,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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.otherTitles, otherTitles) || other.otherTitles == otherTitles)&&(identical(other.source, source) || other.source == source));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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,otherTitles,source);
@override @override
String toString() { 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, otherTitles: $otherTitles, source: $source)';
} }
@@ -277,11 +294,11 @@ abstract mixin class _$AnimeTrackingDataCopyWith<$Res> implements $AnimeTracking
factory _$AnimeTrackingDataCopyWith(_AnimeTrackingData value, $Res Function(_AnimeTrackingData) _then) = __$AnimeTrackingDataCopyWithImpl; factory _$AnimeTrackingDataCopyWith(_AnimeTrackingData value, $Res Function(_AnimeTrackingData) _then) = __$AnimeTrackingDataCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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,@OtherTitlesConverter() OtherTitles otherTitles,@TrackingDataSourceConverter() TrackingDataSource source
}); });
@override $OtherTitlesCopyWith<$Res> get otherTitles;
} }
/// @nodoc /// @nodoc
@@ -294,7 +311,7 @@ class __$AnimeTrackingDataCopyWithImpl<$Res>
/// Create a copy of AnimeTrackingData /// Create a copy of AnimeTrackingData
/// with the given fields replaced by the non-null parameter values. /// 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? otherTitles = null,Object? source = null,}) {
return _then(_AnimeTrackingData( return _then(_AnimeTrackingData(
null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable as String,null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
@@ -304,11 +321,22 @@ as int,freezed == episodesTotal ? _self.episodesTotal : episodesTotal // ignore:
as int?,null == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable 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 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 bool,freezed == broadcastDay ? _self.broadcastDay : broadcastDay // ignore: cast_nullable_to_non_nullable
as String?, as String?,null == otherTitles ? _self.otherTitles : otherTitles // ignore: cast_nullable_to_non_nullable
as OtherTitles,null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable
as TrackingDataSource,
)); ));
} }
/// Create a copy of AnimeTrackingData
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$OtherTitlesCopyWith<$Res> get otherTitles {
return $OtherTitlesCopyWith<$Res>(_self.otherTitles, (value) {
return _then(_self.copyWith(otherTitles: value));
});
}
} }
// dart format on // dart format on

View File

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

View File

@@ -0,0 +1,5 @@
import 'package:anitrack/src/data/source.dart';
abstract class TrackingDataBase {
TrackingDataSource get source;
}

View File

@@ -1,3 +1,6 @@
import 'package:anitrack/src/data/data_base.dart';
import 'package:anitrack/src/data/other_titles.dart';
import 'package:anitrack/src/data/source.dart';
import 'package:anitrack/src/data/type.dart'; import 'package:anitrack/src/data/type.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@@ -6,7 +9,9 @@ part 'manga.g.dart';
/// Data about a tracked anime /// Data about a tracked anime
@freezed @freezed
abstract class MangaTrackingData with _$MangaTrackingData, TrackingMedium { abstract class MangaTrackingData
with _$MangaTrackingData, TrackingMedium
implements TrackingDataBase {
factory MangaTrackingData( factory MangaTrackingData(
/// The ID of the manga /// The ID of the manga
String id, String id,
@@ -28,6 +33,12 @@ abstract class MangaTrackingData with _$MangaTrackingData, TrackingMedium {
/// URL to the thumbnail/cover art for the manga. /// URL to the thumbnail/cover art for the manga.
String thumbnailUrl, String thumbnailUrl,
/// Other titles the manga can have.
@OtherTitlesConverter() OtherTitles otherTitles,
/// The source where we got the data from.
@TrackingDataSourceConverter() TrackingDataSource source,
) = _MangaTrackingData; ) = _MangaTrackingData;
MangaTrackingData._(); MangaTrackingData._();

View File

@@ -22,7 +22,9 @@ mixin _$MangaTrackingData {
int get chaptersRead;/// Chapters read. int get chaptersRead;/// Chapters read.
int get volumesOwned;/// Episodes watched. int get volumesOwned;/// Episodes watched.
int? get chaptersTotal;/// URL to the thumbnail/cover art for the manga. int? get chaptersTotal;/// URL to the thumbnail/cover art for the manga.
String get thumbnailUrl; String get thumbnailUrl;/// Other titles the manga can have.
@OtherTitlesConverter() OtherTitles get otherTitles;/// The source where we got the data from.
@TrackingDataSourceConverter() TrackingDataSource get source;
/// Create a copy of MangaTrackingData /// Create a copy of MangaTrackingData
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -35,16 +37,16 @@ $MangaTrackingDataCopyWith<MangaTrackingData> get copyWith => _$MangaTrackingDat
@override @override
bool operator ==(Object other) { 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.otherTitles, otherTitles) || other.otherTitles == otherTitles)&&(identical(other.source, source) || other.source == source));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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,otherTitles,source);
@override @override
String toString() { 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, otherTitles: $otherTitles, source: $source)';
} }
@@ -55,11 +57,11 @@ abstract mixin class $MangaTrackingDataCopyWith<$Res> {
factory $MangaTrackingDataCopyWith(MangaTrackingData value, $Res Function(MangaTrackingData) _then) = _$MangaTrackingDataCopyWithImpl; factory $MangaTrackingDataCopyWith(MangaTrackingData value, $Res Function(MangaTrackingData) _then) = _$MangaTrackingDataCopyWithImpl;
@useResult @useResult
$Res call({ $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,@OtherTitlesConverter() OtherTitles otherTitles,@TrackingDataSourceConverter() TrackingDataSource source
}); });
$OtherTitlesCopyWith<$Res> get otherTitles;
} }
/// @nodoc /// @nodoc
@@ -72,7 +74,7 @@ class _$MangaTrackingDataCopyWithImpl<$Res>
/// Create a copy of MangaTrackingData /// Create a copy of MangaTrackingData
/// with the given fields replaced by the non-null parameter values. /// 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? otherTitles = null,Object? source = null,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 as String,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
@@ -81,10 +83,21 @@ as String,chaptersRead: null == chaptersRead ? _self.chaptersRead : chaptersRead
as int,volumesOwned: null == volumesOwned ? _self.volumesOwned : volumesOwned // ignore: cast_nullable_to_non_nullable 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,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 int?,thumbnailUrl: null == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable
as String, as String,otherTitles: null == otherTitles ? _self.otherTitles : otherTitles // ignore: cast_nullable_to_non_nullable
as OtherTitles,source: null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable
as TrackingDataSource,
)); ));
} }
/// Create a copy of MangaTrackingData
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$OtherTitlesCopyWith<$Res> get otherTitles {
return $OtherTitlesCopyWith<$Res>(_self.otherTitles, (value) {
return _then(_self.copyWith(otherTitles: value));
});
}
} }
@@ -166,10 +179,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, @OtherTitlesConverter() OtherTitles otherTitles, @TrackingDataSourceConverter() TrackingDataSource source)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _MangaTrackingData() when $default != null: 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.otherTitles,_that.source);case _:
return orElse(); return orElse();
} }
@@ -187,10 +200,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, @OtherTitlesConverter() OtherTitles otherTitles, @TrackingDataSourceConverter() TrackingDataSource source) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _MangaTrackingData(): 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.otherTitles,_that.source);case _:
throw StateError('Unexpected subclass'); throw StateError('Unexpected subclass');
} }
@@ -207,10 +220,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, @OtherTitlesConverter() OtherTitles otherTitles, @TrackingDataSourceConverter() TrackingDataSource source)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _MangaTrackingData() when $default != null: 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.otherTitles,_that.source);case _:
return null; return null;
} }
@@ -222,7 +235,7 @@ return $default(_that.id,_that.state,_that.title,_that.chaptersRead,_that.volume
@JsonSerializable() @JsonSerializable()
class _MangaTrackingData extends MangaTrackingData { 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, @OtherTitlesConverter() this.otherTitles, @TrackingDataSourceConverter() this.source): super._();
factory _MangaTrackingData.fromJson(Map<String, dynamic> json) => _$MangaTrackingDataFromJson(json); factory _MangaTrackingData.fromJson(Map<String, dynamic> json) => _$MangaTrackingDataFromJson(json);
/// The ID of the manga /// The ID of the manga
@@ -239,6 +252,10 @@ class _MangaTrackingData extends MangaTrackingData {
@override final int? chaptersTotal; @override final int? chaptersTotal;
/// URL to the thumbnail/cover art for the manga. /// URL to the thumbnail/cover art for the manga.
@override final String thumbnailUrl; @override final String thumbnailUrl;
/// Other titles the manga can have.
@override@OtherTitlesConverter() final OtherTitles otherTitles;
/// The source where we got the data from.
@override@TrackingDataSourceConverter() final TrackingDataSource source;
/// Create a copy of MangaTrackingData /// Create a copy of MangaTrackingData
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -253,16 +270,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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.otherTitles, otherTitles) || other.otherTitles == otherTitles)&&(identical(other.source, source) || other.source == source));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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,otherTitles,source);
@override @override
String toString() { 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, otherTitles: $otherTitles, source: $source)';
} }
@@ -273,11 +290,11 @@ abstract mixin class _$MangaTrackingDataCopyWith<$Res> implements $MangaTracking
factory _$MangaTrackingDataCopyWith(_MangaTrackingData value, $Res Function(_MangaTrackingData) _then) = __$MangaTrackingDataCopyWithImpl; factory _$MangaTrackingDataCopyWith(_MangaTrackingData value, $Res Function(_MangaTrackingData) _then) = __$MangaTrackingDataCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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,@OtherTitlesConverter() OtherTitles otherTitles,@TrackingDataSourceConverter() TrackingDataSource source
}); });
@override $OtherTitlesCopyWith<$Res> get otherTitles;
} }
/// @nodoc /// @nodoc
@@ -290,7 +307,7 @@ class __$MangaTrackingDataCopyWithImpl<$Res>
/// Create a copy of MangaTrackingData /// Create a copy of MangaTrackingData
/// with the given fields replaced by the non-null parameter values. /// 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? otherTitles = null,Object? source = null,}) {
return _then(_MangaTrackingData( return _then(_MangaTrackingData(
null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable as String,null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
@@ -299,11 +316,22 @@ as String,null == chaptersRead ? _self.chaptersRead : chaptersRead // ignore: ca
as int,null == volumesOwned ? _self.volumesOwned : volumesOwned // ignore: cast_nullable_to_non_nullable 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,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 int?,null == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable
as String, as String,null == otherTitles ? _self.otherTitles : otherTitles // ignore: cast_nullable_to_non_nullable
as OtherTitles,null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable
as TrackingDataSource,
)); ));
} }
/// Create a copy of MangaTrackingData
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$OtherTitlesCopyWith<$Res> get otherTitles {
return $OtherTitlesCopyWith<$Res>(_self.otherTitles, (value) {
return _then(_self.copyWith(otherTitles: value));
});
}
} }
// dart format on // dart format on

View File

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

View File

@@ -0,0 +1,30 @@
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'other_titles.freezed.dart';
part 'other_titles.g.dart';
class OtherTitlesConverter implements JsonConverter<OtherTitles, String> {
const OtherTitlesConverter();
@override
OtherTitles fromJson(String json) =>
OtherTitles.fromJson(jsonDecode(json) as Map<String, dynamic>);
@override
String toJson(OtherTitles object) => jsonEncode(object.toJson());
}
@freezed
abstract class OtherTitles with _$OtherTitles {
factory OtherTitles({
String? english,
String? japanese,
@Default([]) List<String> others,
}) = _OtherTitles;
/// JSON
factory OtherTitles.fromJson(Map<String, dynamic> json) =>
_$OtherTitlesFromJson(json);
}

View File

@@ -0,0 +1,289 @@
// 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 'other_titles.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$OtherTitles {
String? get english; String? get japanese; List<String> get others;
/// Create a copy of OtherTitles
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$OtherTitlesCopyWith<OtherTitles> get copyWith => _$OtherTitlesCopyWithImpl<OtherTitles>(this as OtherTitles, _$identity);
/// Serializes this OtherTitles to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is OtherTitles&&(identical(other.english, english) || other.english == english)&&(identical(other.japanese, japanese) || other.japanese == japanese)&&const DeepCollectionEquality().equals(other.others, others));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,english,japanese,const DeepCollectionEquality().hash(others));
@override
String toString() {
return 'OtherTitles(english: $english, japanese: $japanese, others: $others)';
}
}
/// @nodoc
abstract mixin class $OtherTitlesCopyWith<$Res> {
factory $OtherTitlesCopyWith(OtherTitles value, $Res Function(OtherTitles) _then) = _$OtherTitlesCopyWithImpl;
@useResult
$Res call({
String? english, String? japanese, List<String> others
});
}
/// @nodoc
class _$OtherTitlesCopyWithImpl<$Res>
implements $OtherTitlesCopyWith<$Res> {
_$OtherTitlesCopyWithImpl(this._self, this._then);
final OtherTitles _self;
final $Res Function(OtherTitles) _then;
/// Create a copy of OtherTitles
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? english = freezed,Object? japanese = freezed,Object? others = null,}) {
return _then(_self.copyWith(
english: freezed == english ? _self.english : english // ignore: cast_nullable_to_non_nullable
as String?,japanese: freezed == japanese ? _self.japanese : japanese // ignore: cast_nullable_to_non_nullable
as String?,others: null == others ? _self.others : others // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
/// Adds pattern-matching-related methods to [OtherTitles].
extension OtherTitlesPatterns on OtherTitles {
/// 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( _OtherTitles value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _OtherTitles() 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( _OtherTitles value) $default,){
final _that = this;
switch (_that) {
case _OtherTitles():
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( _OtherTitles value)? $default,){
final _that = this;
switch (_that) {
case _OtherTitles() 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? english, String? japanese, List<String> others)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _OtherTitles() when $default != null:
return $default(_that.english,_that.japanese,_that.others);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? english, String? japanese, List<String> others) $default,) {final _that = this;
switch (_that) {
case _OtherTitles():
return $default(_that.english,_that.japanese,_that.others);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? english, String? japanese, List<String> others)? $default,) {final _that = this;
switch (_that) {
case _OtherTitles() when $default != null:
return $default(_that.english,_that.japanese,_that.others);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _OtherTitles implements OtherTitles {
_OtherTitles({this.english, this.japanese, final List<String> others = const []}): _others = others;
factory _OtherTitles.fromJson(Map<String, dynamic> json) => _$OtherTitlesFromJson(json);
@override final String? english;
@override final String? japanese;
final List<String> _others;
@override@JsonKey() List<String> get others {
if (_others is EqualUnmodifiableListView) return _others;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_others);
}
/// Create a copy of OtherTitles
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$OtherTitlesCopyWith<_OtherTitles> get copyWith => __$OtherTitlesCopyWithImpl<_OtherTitles>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$OtherTitlesToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _OtherTitles&&(identical(other.english, english) || other.english == english)&&(identical(other.japanese, japanese) || other.japanese == japanese)&&const DeepCollectionEquality().equals(other._others, _others));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,english,japanese,const DeepCollectionEquality().hash(_others));
@override
String toString() {
return 'OtherTitles(english: $english, japanese: $japanese, others: $others)';
}
}
/// @nodoc
abstract mixin class _$OtherTitlesCopyWith<$Res> implements $OtherTitlesCopyWith<$Res> {
factory _$OtherTitlesCopyWith(_OtherTitles value, $Res Function(_OtherTitles) _then) = __$OtherTitlesCopyWithImpl;
@override @useResult
$Res call({
String? english, String? japanese, List<String> others
});
}
/// @nodoc
class __$OtherTitlesCopyWithImpl<$Res>
implements _$OtherTitlesCopyWith<$Res> {
__$OtherTitlesCopyWithImpl(this._self, this._then);
final _OtherTitles _self;
final $Res Function(_OtherTitles) _then;
/// Create a copy of OtherTitles
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? english = freezed,Object? japanese = freezed,Object? others = null,}) {
return _then(_OtherTitles(
english: freezed == english ? _self.english : english // ignore: cast_nullable_to_non_nullable
as String?,japanese: freezed == japanese ? _self.japanese : japanese // ignore: cast_nullable_to_non_nullable
as String?,others: null == others ? _self._others : others // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
// dart format on

View File

@@ -0,0 +1,22 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'other_titles.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_OtherTitles _$OtherTitlesFromJson(Map<String, dynamic> json) => _OtherTitles(
english: json['english'] as String?,
japanese: json['japanese'] as String?,
others:
(json['others'] as List<dynamic>?)?.map((e) => e as String).toList() ??
const [],
);
Map<String, dynamic> _$OtherTitlesToJson(_OtherTitles instance) =>
<String, dynamic>{
'english': instance.english,
'japanese': instance.japanese,
'others': instance.others,
};

View File

@@ -1,6 +1,8 @@
class SearchResult { class SearchResult {
const SearchResult( const SearchResult(
this.title, this.title,
this.titleEnglish,
this.titleJapanese,
this.id, this.id,
this.total, this.total,
this.thumbnailUrl, this.thumbnailUrl,
@@ -12,6 +14,12 @@ class SearchResult {
/// The title of the anime. /// The title of the anime.
final String title; final String title;
/// The English title.
final String? titleEnglish;
/// The original Japanese title.
final String? titleJapanese;
/// The id of the anime. /// The id of the anime.
final String id; final String id;

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 {
large
}
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);
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 {
large
}
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);
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);
}
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,96 @@
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? titleEnglish,
required String? titleJapanese,
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,
titleEnglish:
(json['title']! as Map<String, dynamic>)['english'] as String?,
titleJapanese:
(json['title']! as Map<String, dynamic>)['native'] as String?,
id: (json['id']! as int).toString(),
episodes: json['episodes'] as int?,
imageUrl:
(json['coverImage']! as Map<String, dynamic>)['large']! 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? titleEnglish,
required String? titleJapanese,
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,
titleEnglish:
(json['title']! as Map<String, dynamic>)['english'] as String?,
titleJapanese:
(json['title']! as Map<String, dynamic>)['native'] as String?,
id: (json['id']! as int).toString(),
chapters: json['chapters'] as int?,
imageUrl:
(json['coverImage']! as Map<String, dynamic>)['large']! 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,
required int? episodes,
}) = _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',
episodes: json['episodes'] as int?,
broadcastDay: airingDayOfTheWeek,
);
}
}

View File

@@ -0,0 +1,836 @@
// 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 titleEnglish; String? get titleJapanese; 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.titleEnglish, titleEnglish) || other.titleEnglish == titleEnglish)&&(identical(other.titleJapanese, titleJapanese) || other.titleJapanese == titleJapanese)&&(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,titleEnglish,titleJapanese,id,episodes,imageUrl,description,isAiring,broadcastDay);
@override
String toString() {
return 'AnimeSearchResult(title: $title, titleEnglish: $titleEnglish, titleJapanese: $titleJapanese, 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? titleEnglish, String? titleJapanese, 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? titleEnglish = freezed,Object? titleJapanese = freezed,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,titleEnglish: freezed == titleEnglish ? _self.titleEnglish : titleEnglish // ignore: cast_nullable_to_non_nullable
as String?,titleJapanese: freezed == titleJapanese ? _self.titleJapanese : titleJapanese // 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? titleEnglish, String? titleJapanese, 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.titleEnglish,_that.titleJapanese,_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? titleEnglish, String? titleJapanese, 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.titleEnglish,_that.titleJapanese,_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? titleEnglish, String? titleJapanese, 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.titleEnglish,_that.titleJapanese,_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.titleEnglish, required this.titleJapanese, 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? titleEnglish;
@override final String? titleJapanese;
@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.titleEnglish, titleEnglish) || other.titleEnglish == titleEnglish)&&(identical(other.titleJapanese, titleJapanese) || other.titleJapanese == titleJapanese)&&(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,titleEnglish,titleJapanese,id,episodes,imageUrl,description,isAiring,broadcastDay);
@override
String toString() {
return 'AnimeSearchResult(title: $title, titleEnglish: $titleEnglish, titleJapanese: $titleJapanese, 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? titleEnglish, String? titleJapanese, 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? titleEnglish = freezed,Object? titleJapanese = freezed,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,titleEnglish: freezed == titleEnglish ? _self.titleEnglish : titleEnglish // ignore: cast_nullable_to_non_nullable
as String?,titleJapanese: freezed == titleJapanese ? _self.titleJapanese : titleJapanese // 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 titleEnglish; String? get titleJapanese; 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.titleEnglish, titleEnglish) || other.titleEnglish == titleEnglish)&&(identical(other.titleJapanese, titleJapanese) || other.titleJapanese == titleJapanese)&&(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,titleEnglish,titleJapanese,id,chapters,imageUrl,description);
@override
String toString() {
return 'MangaSearchResult(title: $title, titleEnglish: $titleEnglish, titleJapanese: $titleJapanese, 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? titleEnglish, String? titleJapanese, 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? titleEnglish = freezed,Object? titleJapanese = freezed,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,titleEnglish: freezed == titleEnglish ? _self.titleEnglish : titleEnglish // ignore: cast_nullable_to_non_nullable
as String?,titleJapanese: freezed == titleJapanese ? _self.titleJapanese : titleJapanese // 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? titleEnglish, String? titleJapanese, 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.titleEnglish,_that.titleJapanese,_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? titleEnglish, String? titleJapanese, String id, int? chapters, String imageUrl, String? description) $default,) {final _that = this;
switch (_that) {
case _MangaSearchResult():
return $default(_that.title,_that.titleEnglish,_that.titleJapanese,_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? titleEnglish, String? titleJapanese, String id, int? chapters, String imageUrl, String? description)? $default,) {final _that = this;
switch (_that) {
case _MangaSearchResult() when $default != null:
return $default(_that.title,_that.titleEnglish,_that.titleJapanese,_that.id,_that.chapters,_that.imageUrl,_that.description);case _:
return null;
}
}
}
/// @nodoc
class _MangaSearchResult implements MangaSearchResult {
const _MangaSearchResult({required this.title, required this.titleEnglish, required this.titleJapanese, required this.id, required this.chapters, required this.imageUrl, required this.description});
@override final String title;
@override final String? titleEnglish;
@override final String? titleJapanese;
@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.titleEnglish, titleEnglish) || other.titleEnglish == titleEnglish)&&(identical(other.titleJapanese, titleJapanese) || other.titleJapanese == titleJapanese)&&(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,titleEnglish,titleJapanese,id,chapters,imageUrl,description);
@override
String toString() {
return 'MangaSearchResult(title: $title, titleEnglish: $titleEnglish, titleJapanese: $titleJapanese, 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? titleEnglish, String? titleJapanese, 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? titleEnglish = freezed,Object? titleJapanese = freezed,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,titleEnglish: freezed == titleEnglish ? _self.titleEnglish : titleEnglish // ignore: cast_nullable_to_non_nullable
as String?,titleJapanese: freezed == titleJapanese ? _self.titleJapanese : titleJapanese // 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; int? get episodes;
/// 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)&&(identical(other.episodes, episodes) || other.episodes == episodes));
}
@override
int get hashCode => Object.hash(runtimeType,title,isAiring,broadcastDay,episodes);
@override
String toString() {
return 'Anime(title: $title, isAiring: $isAiring, broadcastDay: $broadcastDay, episodes: $episodes)';
}
}
/// @nodoc
abstract mixin class $AnimeCopyWith<$Res> {
factory $AnimeCopyWith(Anime value, $Res Function(Anime) _then) = _$AnimeCopyWithImpl;
@useResult
$Res call({
String title, bool isAiring, String? broadcastDay, int? episodes
});
}
/// @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,Object? episodes = 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?,episodes: freezed == episodes ? _self.episodes : episodes // ignore: cast_nullable_to_non_nullable
as int?,
));
}
}
/// 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, int? episodes)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _Anime() when $default != null:
return $default(_that.title,_that.isAiring,_that.broadcastDay,_that.episodes);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, int? episodes) $default,) {final _that = this;
switch (_that) {
case _Anime():
return $default(_that.title,_that.isAiring,_that.broadcastDay,_that.episodes);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, int? episodes)? $default,) {final _that = this;
switch (_that) {
case _Anime() when $default != null:
return $default(_that.title,_that.isAiring,_that.broadcastDay,_that.episodes);case _:
return null;
}
}
}
/// @nodoc
class _Anime implements Anime {
const _Anime({required this.title, required this.isAiring, required this.broadcastDay, required this.episodes});
@override final String title;
@override final bool isAiring;
@override final String? broadcastDay;
@override final int? episodes;
/// 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)&&(identical(other.episodes, episodes) || other.episodes == episodes));
}
@override
int get hashCode => Object.hash(runtimeType,title,isAiring,broadcastDay,episodes);
@override
String toString() {
return 'Anime(title: $title, isAiring: $isAiring, broadcastDay: $broadcastDay, episodes: $episodes)';
}
}
/// @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, int? episodes
});
}
/// @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,Object? episodes = 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?,episodes: freezed == episodes ? _self.episodes : episodes // ignore: cast_nullable_to_non_nullable
as int?,
));
}
}
// dart format on

View File

@@ -6,6 +6,11 @@ import 'package:anitrack/src/data/manga.dart';
import 'package:anitrack/src/service/migrations/0000_airing.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/0000_score.dart';
import 'package:anitrack/src/service/migrations/0001_anime_watcher.dart'; import 'package:anitrack/src/service/migrations/0001_anime_watcher.dart';
import 'package:anitrack/src/service/migrations/0002_anilist.dart';
import 'package:anitrack/src/service/migrations/0003_other_titles.dart';
import 'package:anitrack/src/service/migrations/0004_fix_constraints.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
const animeTable = 'Anime'; const animeTable = 'Anime';
@@ -38,7 +43,8 @@ Future<void> _createDatabase(Database db, int version) async {
otherTitles TEXT NOT NULL, otherTitles TEXT NOT NULL,
score INTEGER, score INTEGER,
airing INTEGER NOT NULL, airing INTEGER NOT NULL,
broadcastDay TEXT broadcastDay TEXT,
source TEXT NOT NULL
)''', )''',
); );
await db.execute( await db.execute(
@@ -52,13 +58,14 @@ Future<void> _createDatabase(Database db, int version) async {
thumbnailUrl TEXT NOT NULL, thumbnailUrl TEXT NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
otherTitles TEXT NOT NULL, otherTitles TEXT NOT NULL,
score INTEGER score INTEGER,
source TEXT NOT NULL
)''', )''',
); );
await db.execute( await db.execute(
''' '''
CREATE TABLE $animeWatcherTable( CREATE TABLE $animeWatcherTable(
name TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL PRIMARY KEY
)''', )''',
); );
await db.execute( await db.execute(
@@ -67,8 +74,8 @@ Future<void> _createDatabase(Database db, int version) async {
name TEXT NOT NULL, name TEXT NOT NULL,
anime TEXT NOT NULL, anime TEXT NOT NULL,
CONSTRAINT pk_watcher_join_table PRIMARY KEY(name, anime), CONSTRAINT pk_watcher_join_table PRIMARY KEY(name, anime),
CONSTRAINT fk_watcher FOREIGN KEY (name) REFERENCES $animeWatcherTable(name), CONSTRAINT fk_watcher FOREIGN KEY (name) REFERENCES $animeWatcherTable(name) ON DELETE CASCADE,
CONSTRAINT fk_anime FOREIGN KEY (anime) REFERENCES $animeTable(id) CONSTRAINT fk_anime FOREIGN KEY (anime) REFERENCES $animeTable(id) ON DELETE CASCADE
)''', )''',
); );
} }
@@ -80,6 +87,15 @@ class DatabaseService {
/// Cached AnimeWatchers. /// Cached AnimeWatchers.
List<AnimeWatcher>? _watcherCache; List<AnimeWatcher>? _watcherCache;
Future<String> _getDatabasePath() async {
if (Platform.isAndroid || Platform.isIOS) {
return 'anitrack.db';
}
final supportDir = await getApplicationSupportDirectory();
return p.join(supportDir.path, 'anitrack.db');
}
Future<void> initialize() async { Future<void> initialize() async {
// Allow initializing the database on Windows and Linux as well. // Allow initializing the database on Windows and Linux as well.
if (Platform.isLinux || Platform.isWindows) { if (Platform.isLinux || Platform.isWindows) {
@@ -87,9 +103,11 @@ class DatabaseService {
databaseFactory = databaseFactoryFfi; databaseFactory = databaseFactoryFfi;
} }
final databasePath = await _getDatabasePath();
print('Opening database at $databasePath');
_db = await openDatabase( _db = await openDatabase(
'anitrack.db', databasePath,
version: 4, version: 7,
onConfigure: (db) async { onConfigure: (db) async {
// In order to do schema changes during database upgrades, we disable foreign // In order to do schema changes during database upgrades, we disable foreign
// keys in the onConfigure phase, but re-enable them here. // keys in the onConfigure phase, but re-enable them here.
@@ -111,9 +129,17 @@ class DatabaseService {
if (oldVersion < 4) { if (oldVersion < 4) {
await migrateFromV3ToV4(db); await migrateFromV3ToV4(db);
} }
if (oldVersion < 5) {
await migrateFromV4ToV5(db);
}
if (oldVersion < 6) {
await migrateFromV5ToV6(db);
}
if (oldVersion < 7) {
await migrateFromV6ToV7(db);
}
}, },
); );
print(_db.path);
} }
Future<List<AnimeTrackingData>> loadAnimes() async { Future<List<AnimeTrackingData>> loadAnimes() async {
@@ -138,7 +164,6 @@ class DatabaseService {
await _db.insert( await _db.insert(
animeTable, animeTable,
data.toJson(), data.toJson(),
conflictAlgorithm: ConflictAlgorithm.ignore,
); );
} }

View File

@@ -15,7 +15,7 @@ Future<void> migrateFromV3ToV4(Database db) async {
anime TEXT NOT NULL, anime TEXT NOT NULL,
CONSTRAINT pk_watcher_join_table PRIMARY KEY(name, anime), CONSTRAINT pk_watcher_join_table PRIMARY KEY(name, anime),
CONSTRAINT fk_watcher FOREIGN KEY (name) REFERENCES $animeWatcherTable(name), CONSTRAINT fk_watcher FOREIGN KEY (name) REFERENCES $animeWatcherTable(name),
CONSTRAINT fk_anime FOREIGN KEY (anime) REFERENCES $animeTable(id), CONSTRAINT fk_anime FOREIGN KEY (anime) REFERENCES $animeTable(id)
)''', )''',
); );
} }

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

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

View File

@@ -0,0 +1,22 @@
import 'package:anitrack/src/service/database.dart';
import 'package:sqflite/sqflite.dart';
Future<void> migrateFromV6ToV7(Database db) async {
await db.execute(
'''
CREATE TABLE ${animeWatcherJoinTable}_2 (
name TEXT NOT NULL,
anime TEXT NOT NULL,
CONSTRAINT pk_watcher_join_table PRIMARY KEY(name, anime),
CONSTRAINT fk_watcher FOREIGN KEY (name) REFERENCES $animeWatcherTable(name) ON DELETE CASCADE,
CONSTRAINT fk_anime FOREIGN KEY (anime) REFERENCES $animeTable(id) ON DELETE CASCADE
);''',
);
await db.execute(
'INSERT INTO ${animeWatcherJoinTable}_2 SELECT * from $animeWatcherJoinTable;',
);
await db.execute('DROP TABLE $animeWatcherJoinTable');
await db.execute(
'ALTER TABLE ${animeWatcherJoinTable}_2 RENAME TO $animeWatcherJoinTable',
);
}

View File

@@ -2,6 +2,8 @@ import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/manga.dart'; import 'package:anitrack/src/data/manga.dart';
import 'package:anitrack/src/data/type.dart'; import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/service/database.dart'; import 'package:anitrack/src/service/database.dart';
import 'package:anitrack/src/ui/bloc/navigation_bloc.dart';
import 'package:anitrack/src/ui/constants.dart';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@@ -28,6 +30,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
on<AnimeRemovedEvent>(_onAnimeRemoved); on<AnimeRemovedEvent>(_onAnimeRemoved);
on<MangaRemovedEvent>(_onMangaRemoved); on<MangaRemovedEvent>(_onMangaRemoved);
on<AddButtonVisibilitySetEvent>(_onButtonVisibilityToggled); on<AddButtonVisibilitySetEvent>(_onButtonVisibilityToggled);
on<AnimeListRequestedEvent>(_onAnimeListSummoned);
} }
/// Internal anime state /// Internal anime state
@@ -122,8 +125,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
final anime = state.animes[index]; final anime = state.animes[index];
if (anime.episodesTotal != null && if (anime.episodesTotal != null &&
anime.episodesWatched + 1 > anime.episodesTotal!) anime.episodesWatched + 1 > anime.episodesTotal!) {
return; return;
}
final newAnime = await GetIt.I final newAnime = await GetIt.I
.get<DatabaseService>() .get<DatabaseService>()
@@ -223,8 +227,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
final manga = state.mangas[index]; final manga = state.mangas[index];
if (manga.chaptersTotal != null && if (manga.chaptersTotal != null &&
manga.chaptersRead + 1 > manga.chaptersTotal!) manga.chaptersRead + 1 > manga.chaptersTotal!) {
return; return;
}
final newManga = await GetIt.I final newManga = await GetIt.I
.get<DatabaseService>() .get<DatabaseService>()
@@ -317,9 +322,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
); );
// Update the cache // 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'); assert(cacheIndex != -1, 'The anime must exist');
_mangas.removeAt(cacheIndex); _animes.removeAt(cacheIndex);
// Update the database // Update the database
await GetIt.I.get<DatabaseService>().deleteAnime(event.id); await GetIt.I.get<DatabaseService>().deleteAnime(event.id);
@@ -338,9 +343,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
); );
// Update the cache // 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'); assert(cacheIndex != -1, 'The manga must exist');
_animes.removeAt(cacheIndex); _mangas.removeAt(cacheIndex);
// Update the database // Update the database
await GetIt.I.get<DatabaseService>().deleteManga(event.id); await GetIt.I.get<DatabaseService>().deleteManga(event.id);
@@ -356,4 +361,20 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
), ),
); );
} }
Future<void> _onAnimeListSummoned(
AnimeListRequestedEvent event,
Emitter<AnimeListState> emit,
) async {
emit(
state.copyWith(
trackingType: TrackingMediumType.anime,
animeFilterState: MediumTrackingState.ongoing,
mangaFilterState: MediumTrackingState.ongoing,
),
);
GetIt.I.get<NavigationBloc>().add(
GoNavigationEvent(animeListRoute),
);
}
} }

View File

@@ -114,3 +114,6 @@ class AddButtonVisibilitySetEvent extends AnimeListEvent {
/// The visibility of the button /// The visibility of the button
final bool state; final bool state;
} }
/// Triggered by the UI when the anime list is supposed to be summoned.
class AnimeListRequestedEvent extends AnimeListEvent {}

View File

@@ -1,14 +1,16 @@
import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/manga.dart'; import 'package:anitrack/src/data/manga.dart';
import 'package:anitrack/src/data/other_titles.dart';
import 'package:anitrack/src/data/search_result.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/data/type.dart';
import 'package:anitrack/src/service/anilist/anilist_client.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart' as list; import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart' as list;
import 'package:anitrack/src/ui/bloc/navigation_bloc.dart'; import 'package:anitrack/src/ui/bloc/navigation_bloc.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:jikan_api/jikan_api.dart';
part 'anime_search_state.dart'; part 'anime_search_state.dart';
part 'anime_search_event.dart'; part 'anime_search_event.dart';
@@ -22,6 +24,9 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
on<ResultTappedEvent>(_onResultTapped); on<ResultTappedEvent>(_onResultTapped);
} }
// The client to use for AniList GraphQL queries.
final _anilistClient = AniListClient();
Future<void> _onRequested( Future<void> _onRequested(
AnimeSearchRequestedEvent event, AnimeSearchRequestedEvent event,
Emitter<AnimeSearchState> emit, Emitter<AnimeSearchState> emit,
@@ -36,9 +41,7 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
); );
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent( PushNavigationEvent(animeSearchRoute),
const NavigationDestination(animeSearchRoute),
),
); );
} }
@@ -67,23 +70,23 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
if (state.trackingType == TrackingMediumType.anime) { if (state.trackingType == TrackingMediumType.anime) {
// Anime // Anime
final result = await Jikan().searchAnime( final result = await _anilistClient.searchAnime(state.searchQuery);
query: state.searchQuery,
);
emit( emit(
state.copyWith( state.copyWith(
working: false, working: false,
searchResults: result searchResults: result
.map( .map(
(Anime anime) => SearchResult( (anime) => SearchResult(
anime.title, anime.title,
anime.malId.toString(), anime.titleEnglish,
anime.titleJapanese,
anime.id,
anime.episodes, anime.episodes,
anime.imageUrl, anime.imageUrl,
anime.synopsis ?? '', anime.description ?? '',
anime.airing, anime.isAiring,
anime.broadcast?.split(' ').first, anime.broadcastDay,
), ),
) )
.toList(), .toList(),
@@ -91,21 +94,21 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
); );
} else { } else {
// Manga // Manga
final result = await Jikan().searchManga( final result = await _anilistClient.searchManga(state.searchQuery);
query: state.searchQuery,
);
emit( emit(
state.copyWith( state.copyWith(
working: false, working: false,
searchResults: result searchResults: result
.map( .map(
(Manga manga) => SearchResult( (manga) => SearchResult(
manga.title, manga.title,
manga.malId.toString(), null,
null,
manga.id,
manga.chapters, manga.chapters,
manga.imageUrl, manga.imageUrl,
manga.synopsis ?? '', manga.description ?? '',
// TODO(Unknown): Implement for Manga // TODO(Unknown): Implement for Manga
false, false,
null, null,
@@ -133,6 +136,11 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
event.result.thumbnailUrl, event.result.thumbnailUrl,
event.result.isAiring, event.result.isAiring,
event.result.broadcastDay, event.result.broadcastDay,
OtherTitles(
english: event.result.titleEnglish,
japanese: event.result.titleJapanese,
),
TrackingDataSource.anilist,
), ),
) )
: list.MangaAddedEvent( : list.MangaAddedEvent(
@@ -144,12 +152,17 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
0, 0,
event.result.total, event.result.total,
event.result.thumbnailUrl, event.result.thumbnailUrl,
OtherTitles(
english: event.result.titleEnglish,
japanese: event.result.titleJapanese,
),
TrackingDataSource.anilist,
), ),
), ),
); );
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PoppedRouteEvent(), GoNavigationEvent(animeListRoute),
); );
} }
} }

View File

@@ -1,5 +1,7 @@
import 'dart:async'; 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:anitrack/src/ui/bloc/anime_list_bloc.dart';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@@ -42,20 +44,36 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
String? broadcastDay; String? broadcastDay;
bool airing; bool airing;
int? episodes;
try { try {
final apiData = await Jikan().getAnime(int.parse(anime.id)); switch (anime.source) {
airing = apiData.airing; case TrackingDataSource.mal:
broadcastDay = apiData.broadcast?.split(' ').first; final apiData = await Jikan().getAnime(int.parse(anime.id));
airing = apiData.airing;
broadcastDay = apiData.broadcast?.split(' ').first;
episodes = apiData.episodes;
case TrackingDataSource.anilist:
final apiData = await GetIt.I.get<AniListClient>().getAnimeById(
anime.id,
);
airing = apiData.isAiring;
broadcastDay = apiData.broadcastDay;
episodes = apiData.episodes;
}
} catch (ex) { } catch (ex) {
print('API request for anime ${anime.id} failed: $ex'); print('API request for anime ${anime.id} failed: $ex');
airing = false; continue;
} }
print('Anime "${anime.title}": airing=$airing'); print('Anime "${anime.title}": airing=$airing; episodes=$episodes');
if (!airing) { if (!airing) {
al.add( al.add(
AnimeUpdatedEvent( AnimeUpdatedEvent(
anime.copyWith(airing: false, broadcastDay: null), anime.copyWith(
airing: false,
broadcastDay: null,
episodesTotal: episodes,
),
commit: true, commit: true,
), ),
); );
@@ -63,13 +81,17 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
print('Updating Anime "${anime.title}": broadcastDay=$broadcastDay'); print('Updating Anime "${anime.title}": broadcastDay=$broadcastDay');
al.add( al.add(
AnimeUpdatedEvent( AnimeUpdatedEvent(
anime.copyWith(airing: true, broadcastDay: broadcastDay), anime.copyWith(
airing: true,
broadcastDay: broadcastDay,
episodesTotal: episodes,
),
commit: true, commit: true,
), ),
); );
} }
// Prevent hammering Jikan // Prevent hammering Jikan/AniList
await Future<void>.delayed(const Duration(milliseconds: 500)); await Future<void>.delayed(const Duration(milliseconds: 500));
} }

View File

@@ -44,9 +44,7 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
); );
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent( PushNavigationEvent(detailsRoute),
const NavigationDestination(detailsRoute),
),
); );
} }
@@ -57,14 +55,13 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
emit( emit(
state.copyWith( state.copyWith(
trackingType: TrackingMediumType.manga, trackingType: TrackingMediumType.manga,
heroImagePrefix: event.heroImagePrefix,
data: event.manga, data: event.manga,
), ),
); );
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent( PushNavigationEvent(detailsRoute),
const NavigationDestination(detailsRoute),
),
); );
} }
@@ -118,14 +115,12 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
switch (event.trackingType) { switch (event.trackingType) {
case TrackingMediumType.anime: case TrackingMediumType.anime:
bloc.add(AnimeRemovedEvent(event.id)); bloc.add(AnimeRemovedEvent(event.id));
break;
case TrackingMediumType.manga: case TrackingMediumType.manga:
bloc.add(MangaRemovedEvent(event.id)); bloc.add(MangaRemovedEvent(event.id));
break;
} }
// Navigate back // Navigate back
GetIt.I.get<NavigationBloc>().add(PoppedRouteEvent()); GetIt.I.get<NavigationBloc>().add(PopNavigationEvent());
} }
Future<void> _onAnimeWatcherAdded( Future<void> _onAnimeWatcherAdded(

View File

@@ -15,10 +15,15 @@ class AnimeDetailsRequestedEvent extends DetailsEvent {
} }
class MangaDetailsRequestedEvent extends DetailsEvent { class MangaDetailsRequestedEvent extends DetailsEvent {
MangaDetailsRequestedEvent(this.manga); MangaDetailsRequestedEvent(
this.manga, {
this.heroImagePrefix,
});
/// The manga to show details about /// The manga to show details about
final MangaTrackingData manga; final MangaTrackingData manga;
final String? heroImagePrefix;
} }
class DetailsUpdatedEvent extends DetailsEvent { class DetailsUpdatedEvent extends DetailsEvent {

View File

@@ -1,57 +1,39 @@
import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
part 'navigation_event.dart'; part 'navigation_event.dart';
part 'navigation_state.dart'; part 'navigation_state.dart';
class NavigationBloc extends Bloc<NavigationEvent, NavigationState> { class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
NavigationBloc(this.navigationKey) : super(NavigationState()) { NavigationBloc() : super(NoopNavigationState()) {
on<PushedNamedEvent>(_onPushedNamed); on<GoNavigationEvent>(_onGoEvent);
on<PushedNamedAndRemoveUntilEvent>(_onPushedNamedAndRemoveUntil); on<PushNavigationEvent>(_onPushEvent);
on<PushedNamedReplaceEvent>(_onPushedNamedReplaceEvent); on<PopNavigationEvent>(_onPopEvent);
on<PoppedRouteEvent>(_onPoppedRoute);
} }
final GlobalKey<NavigatorState> navigationKey;
Future<void> _onPushedNamed( Future<void> _onGoEvent(
PushedNamedEvent event, GoNavigationEvent event,
Emitter<NavigationState> emit, Emitter<NavigationState> emit,
) async { ) async {
await navigationKey.currentState!.pushNamed( emit(
event.destination.path, GoNavigationState(event.destination),
arguments: event.destination.arguments,
); );
} }
Future<void> _onPushedNamedAndRemoveUntil( Future<void> _onPushEvent(
PushedNamedAndRemoveUntilEvent event, PushNavigationEvent event,
Emitter<NavigationState> emit, Emitter<NavigationState> emit,
) async { ) async {
await navigationKey.currentState!.pushNamedAndRemoveUntil( emit(
event.destination.path, PushNavigationState(event.destination),
event.predicate,
arguments: event.destination.arguments,
); );
} }
Future<void> _onPushedNamedReplaceEvent( Future<void> _onPopEvent(
PushedNamedReplaceEvent event, PopNavigationEvent event,
Emitter<NavigationState> emit, Emitter<NavigationState> emit,
) async { ) async {
await navigationKey.currentState!.pushReplacementNamed( emit(PoppedNavigationState());
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

@@ -11,20 +11,14 @@ class NavigationDestination {
abstract class NavigationEvent {} abstract class NavigationEvent {}
class PushedNamedEvent extends NavigationEvent { class GoNavigationEvent extends NavigationEvent {
PushedNamedEvent(this.destination); GoNavigationEvent(this.destination);
final NavigationDestination destination; final String destination;
} }
class PushedNamedAndRemoveUntilEvent extends NavigationEvent { class PushNavigationEvent extends NavigationEvent {
PushedNamedAndRemoveUntilEvent(this.destination, this.predicate); PushNavigationEvent(this.destination);
final NavigationDestination destination; final String destination;
final RoutePredicate predicate;
} }
class PushedNamedReplaceEvent extends NavigationEvent { class PopNavigationEvent extends NavigationEvent {}
PushedNamedReplaceEvent(this.destination);
final NavigationDestination destination;
}
class PoppedRouteEvent extends NavigationEvent {}

View File

@@ -1,3 +1,19 @@
part of 'navigation_bloc.dart'; part of 'navigation_bloc.dart';
class NavigationState {} abstract class NavigationState {}
class NoopNavigationState extends NavigationState {}
class PoppedNavigationState extends NavigationState {}
class PushNavigationState extends NavigationState {
PushNavigationState(this.destination);
final String destination;
}
class GoNavigationState extends NavigationState {
GoNavigationState(this.destination);
final String destination;
}

View File

@@ -4,6 +4,8 @@ import 'dart:io';
import 'package:anitrack/i18n/strings.g.dart'; import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/manga.dart'; import 'package:anitrack/src/data/manga.dart';
import 'package:anitrack/src/data/other_titles.dart';
import 'package:anitrack/src/data/source.dart';
import 'package:anitrack/src/data/type.dart'; import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/service/database.dart'; import 'package:anitrack/src/service/database.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart'; import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
@@ -131,6 +133,8 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
// NOTE: When the calendar gets refreshed, this should also get cleared // NOTE: When the calendar gets refreshed, this should also get cleared
true, true,
null, null,
OtherTitles(),
TrackingDataSource.mal,
), ),
); );
} }
@@ -207,6 +211,8 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
// 0 means that MAL does not know // 0 means that MAL does not know
totalChapters == 0 ? null : totalChapters, totalChapters == 0 ? null : totalChapters,
data.imageUrl, data.imageUrl,
OtherTitles(),
TrackingDataSource.mal,
), ),
); );
} }
@@ -244,7 +250,7 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
Emitter<SettingsState> emit, Emitter<SettingsState> emit,
) async { ) async {
final al = GetIt.I.get<AnimeListBloc>(); final al = GetIt.I.get<AnimeListBloc>();
final exportArchive = archive.GZipDecoder().decodeBytes( final exportArchive = const archive.GZipDecoder().decodeBytes(
await File(event.path).readAsBytes(), await File(event.path).readAsBytes(),
); );
final json = jsonDecode(utf8.decode(exportArchive)) as Map<String, dynamic>; final json = jsonDecode(utf8.decode(exportArchive)) as Map<String, dynamic>;

View File

@@ -1,6 +1,7 @@
import 'package:anitrack/i18n/strings.g.dart'; import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
Widget getDrawer(BuildContext context) { Widget getDrawer(BuildContext context) {
return Drawer( return Drawer(
@@ -22,34 +23,30 @@ Widget getDrawer(BuildContext context) {
leading: const Icon(Icons.list), leading: const Icon(Icons.list),
title: Text(t.content.list), title: Text(t.content.list),
onTap: () { onTap: () {
Navigator.of(context).pushNamedAndRemoveUntil( GoRouter.of(context).go(animeListRoute);
animeListRoute, Navigator.of(context).pop();
(_) => false,
);
}, },
), ),
ListTile( ListTile(
leading: const Icon(Icons.calendar_today), leading: const Icon(Icons.calendar_today),
title: Text(t.calendar.calendar), title: Text(t.calendar.calendar),
onTap: () { onTap: () {
Navigator.of(context).pushNamedAndRemoveUntil( GoRouter.of(context).go(calendarRoute);
calendarRoute, Navigator.of(context).pop();
(_) => false,
);
}, },
), ),
ListTile( ListTile(
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
title: Text(t.settings.title), title: Text(t.settings.title),
onTap: () { onTap: () {
Navigator.of(context).pushNamed(settingsRoute); GoRouter.of(context).push(settingsRoute);
}, },
), ),
ListTile( ListTile(
leading: const Icon(Icons.info), leading: const Icon(Icons.info),
title: Text(t.about.title), title: Text(t.about.title),
onTap: () { onTap: () {
Navigator.of(context).pushNamed(aboutRoute); GoRouter.of(context).push(aboutRoute);
}, },
), ),
], ],

View File

@@ -1,8 +1,9 @@
import 'package:anitrack/i18n/strings.g.dart'; import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/licenses.g.dart'; import 'package:anitrack/oss_licenses.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class AboutPage extends StatelessWidget { class AboutPage extends StatelessWidget {
@@ -22,9 +23,15 @@ class AboutPage extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(t.about.title), title: Text(t.about.title),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
},
),
), ),
body: ListView.builder( body: ListView.builder(
itemCount: ossLicenses.length + 1, itemCount: dependencies.length + 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) { if (index == 0) {
return Padding( return Padding(
@@ -41,7 +48,11 @@ class AboutPage extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.only(
top: 8,
left: 4,
right: 4,
),
child: ElevatedButton( child: ElevatedButton(
onPressed: () async { onPressed: () async {
await launchUrl( await launchUrl(
@@ -55,7 +66,11 @@ class AboutPage extends StatelessWidget {
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.only(
top: 8,
left: 4,
right: 4,
),
child: ElevatedButton( child: ElevatedButton(
onPressed: () async { onPressed: () async {
final licenseText = await rootBundle.loadString( final licenseText = await rootBundle.loadString(
@@ -90,9 +105,7 @@ class AboutPage extends StatelessWidget {
); );
} }
final dep = ossLicenses[index - 1]; final dep = dependencies[index - 1];
if (!dep.isDirectDependency) return Container();
return ListTile( return ListTile(
title: Text(dep.name), title: Text(dep.name),
onTap: () { onTap: () {

View File

@@ -4,7 +4,6 @@ import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart'; import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart';
import 'package:anitrack/src/ui/bloc/details_bloc.dart'; import 'package:anitrack/src/ui/bloc/details_bloc.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
import 'package:anitrack/src/ui/helpers.dart';
import 'package:anitrack/src/ui/widgets/grid_item.dart'; import 'package:anitrack/src/ui/widgets/grid_item.dart';
import 'package:anitrack/src/ui/widgets/image.dart'; import 'package:anitrack/src/ui/widgets/image.dart';
import 'package:bottom_bar/bottom_bar.dart'; import 'package:bottom_bar/bottom_bar.dart';
@@ -24,40 +23,7 @@ class AnimeListPage extends StatefulWidget {
), ),
); );
@override static String getPageTitle(TrackingMediumType type) {
AnimeListPageState createState() => AnimeListPageState();
}
class AnimeListPageState extends State<AnimeListPage> {
final PageController _controller = PageController();
final ScrollController _animeScrollController = ScrollController();
@override
void initState() {
super.initState();
_animeScrollController.addListener(_onAnimeListScrolled);
}
void _onAnimeListScrolled() {
//print(_animeScrollController.position.maxScrollExtent);
final bloc = GetIt.I.get<AnimeListBloc>();
if (_animeScrollController.offset + 20 >=
_animeScrollController.position.maxScrollExtent) {
if (bloc.state.buttonVisibility) {
bloc.add(
AddButtonVisibilitySetEvent(false),
);
}
} else {
if (!bloc.state.buttonVisibility) {
bloc.add(
AddButtonVisibilitySetEvent(true),
);
}
}
}
String _getPageTitle(TrackingMediumType type) {
switch (type) { switch (type) {
case TrackingMediumType.anime: case TrackingMediumType.anime:
return t.content.anime; return t.content.anime;
@@ -66,7 +32,7 @@ class AnimeListPageState extends State<AnimeListPage> {
} }
} }
List<PopupMenuItem<MediumTrackingState>> _getPopupButtonItems( static List<PopupMenuItem<MediumTrackingState>> getPopupButtonItems(
TrackingMediumType type, TrackingMediumType type,
) { ) {
return [ return [
@@ -90,14 +56,14 @@ class AnimeListPageState extends State<AnimeListPage> {
value: MediumTrackingState.paused, value: MediumTrackingState.paused,
child: Text(MediumTrackingState.paused.getName(type)), child: Text(MediumTrackingState.paused.getName(type)),
), ),
const PopupMenuItem<MediumTrackingState>( PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.all, value: MediumTrackingState.all,
child: Text('All'), child: Text(t.data.all),
), ),
]; ];
} }
Widget _getPopupButton(BuildContext context, AnimeListState state) { static Widget getPopupButton(BuildContext context, AnimeListState state) {
switch (state.trackingType) { switch (state.trackingType) {
case TrackingMediumType.anime: case TrackingMediumType.anime:
return PopupMenuButton( return PopupMenuButton(
@@ -110,7 +76,8 @@ class AnimeListPageState extends State<AnimeListPage> {
AnimeFilterChangedEvent(filterState), AnimeFilterChangedEvent(filterState),
); );
}, },
itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.anime), itemBuilder: (_) =>
AnimeListPage.getPopupButtonItems(TrackingMediumType.anime),
); );
case TrackingMediumType.manga: case TrackingMediumType.manga:
return PopupMenuButton( return PopupMenuButton(
@@ -123,183 +90,224 @@ class AnimeListPageState extends State<AnimeListPage> {
MangaFilterChangedEvent(filterState), MangaFilterChangedEvent(filterState),
); );
}, },
itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.manga), itemBuilder: (_) =>
AnimeListPage.getPopupButtonItems(TrackingMediumType.manga),
); );
} }
} }
static AppBar buildAppBar(BuildContext context) {
return AppBar(
title: BlocBuilder<AnimeListBloc, AnimeListState>(
builder: (context, state) => Text(
AnimeListPage.getPageTitle(state.trackingType),
),
),
actions: const [
BlocBuilder<AnimeListBloc, AnimeListState>(
builder: AnimeListPage.getPopupButton,
),
],
);
}
static Widget buildBottomNavigationBar(BuildContext context) {
return BlocBuilder<AnimeListBloc, AnimeListState>(
builder: (context, state) => BottomBar(
selectedIndex: state.trackingType == TrackingMediumType.anime ? 0 : 1,
onTap: (index) {
context.read<AnimeListBloc>().add(
AnimeTrackingTypeChanged(
index == 0 ? TrackingMediumType.anime : TrackingMediumType.manga,
),
);
},
items: [
BottomBarItem(
icon: const Icon(Icons.tv),
title: Text(t.content.anime),
activeColor: Colors.blue,
),
BottomBarItem(
icon: const Icon(Icons.auto_stories),
title: Text(t.content.manga),
activeColor: Colors.red,
),
],
),
);
}
static Widget buildFab() {
return BlocBuilder<AnimeListBloc, AnimeListState>(
buildWhen: (prev, next) =>
prev.buttonVisibility != next.buttonVisibility ||
prev.trackingType != next.trackingType,
builder: (context, state) {
return AnimatedScale(
duration: const Duration(milliseconds: 250),
scale: state.buttonVisibility ? 1 : 0,
curve: Curves.easeInOutQuint,
child: FloatingActionButton(
onPressed: () {
context.read<AnimeSearchBloc>().add(
AnimeSearchRequestedEvent(state.trackingType),
);
},
tooltip: t.tooltips.addNewItem,
child: const Icon(Icons.add),
),
);
},
);
}
@override
AnimeListPageState createState() => AnimeListPageState();
}
class AnimeListPageState extends State<AnimeListPage> {
final ScrollController _animeScrollController = ScrollController();
@override
void initState() {
super.initState();
_animeScrollController.addListener(_onAnimeListScrolled);
}
void _onAnimeListScrolled() {
final bloc = GetIt.I.get<AnimeListBloc>();
if (_animeScrollController.offset + 20 >=
_animeScrollController.position.maxScrollExtent) {
if (bloc.state.buttonVisibility) {
bloc.add(
AddButtonVisibilitySetEvent(false),
);
}
} else {
if (!bloc.state.buttonVisibility) {
bloc.add(
AddButtonVisibilitySetEvent(true),
);
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<AnimeListBloc, AnimeListState>( return BlocBuilder<AnimeListBloc, AnimeListState>(
builder: (context, state) { builder: (context, state) {
return Scaffold( return IndexedStack(
appBar: AppBar( index: state.trackingType == TrackingMediumType.anime ? 0 : 1,
title: Text( children: [
_getPageTitle(state.trackingType), Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 120 / (100 * (16 / 9)),
),
itemCount: state.animes.length,
controller: _animeScrollController,
itemBuilder: (context, index) {
final anime = state.animes[index];
return GridItem(
minusCallback: () {
context.read<AnimeListBloc>().add(
AnimeEpisodeDecrementedEvent(
anime.id,
),
);
},
plusCallback: () {
context.read<AnimeListBloc>().add(
AnimeEpisodeIncrementedEvent(
anime.id,
),
);
},
child: AnimeCoverImage(
url: anime.thumbnailUrl,
hero: 'grid_${anime.id}',
onTap: () {
context.read<DetailsBloc>().add(
AnimeDetailsRequestedEvent(
anime,
heroImagePrefix: 'grid_',
),
);
},
extra: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(
'${anime.episodesWatched}/${anime.episodesTotal ?? "???"}',
style: Theme.of(context).textTheme.titleMedium,
),
),
),
),
);
},
),
), ),
actions: [ Padding(
_getPopupButton(context, state), padding: const EdgeInsets.symmetric(horizontal: 8),
], child: GridView.builder(
), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
drawer: getDrawer(context), crossAxisCount: 3,
body: PageView( mainAxisSpacing: 8,
// Prevent swiping between pages crossAxisSpacing: 8,
// (https://github.com/flutter/flutter/issues/37510#issuecomment-612663656) childAspectRatio: 120 / (100 * (16 / 9)),
physics: const NeverScrollableScrollPhysics(), ),
controller: _controller, itemCount: state.mangas.length,
children: [ itemBuilder: (context, index) {
Padding( final manga = state.mangas[index];
padding: const EdgeInsets.symmetric(horizontal: 8), return GridItem(
child: GridView.builder( minusCallback: () {
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( context.read<AnimeListBloc>().add(
crossAxisCount: 3, MangaChapterDecrementedEvent(
mainAxisSpacing: 8, manga.id,
crossAxisSpacing: 8, ),
childAspectRatio: 120 / (100 * (16 / 9)), );
), },
itemCount: state.animes.length, plusCallback: () {
controller: _animeScrollController, context.read<AnimeListBloc>().add(
itemBuilder: (context, index) { MangaChapterIncrementedEvent(
final anime = state.animes[index]; manga.id,
return GridItem( ),
minusCallback: () { );
context.read<AnimeListBloc>().add( },
AnimeEpisodeDecrementedEvent( child: AnimeCoverImage(
anime.id, hero: 'grid_${manga.id}',
url: manga.thumbnailUrl,
onTap: () {
context.read<DetailsBloc>().add(
MangaDetailsRequestedEvent(
manga,
heroImagePrefix: 'grid_',
), ),
); );
}, },
plusCallback: () { extra: Align(
context.read<AnimeListBloc>().add( alignment: Alignment.centerRight,
AnimeEpisodeIncrementedEvent( child: Padding(
anime.id, padding: const EdgeInsets.only(right: 8),
), child: Text(
); '${manga.chaptersRead}/${manga.chaptersTotal ?? "???"}',
}, style: Theme.of(context).textTheme.titleMedium,
child: AnimeCoverImage(
url: anime.thumbnailUrl,
hero: anime.id,
onTap: () {
context.read<DetailsBloc>().add(
AnimeDetailsRequestedEvent(anime),
);
},
extra: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(
'${anime.episodesWatched}/${anime.episodesTotal ?? "???"}',
style: Theme.of(context).textTheme.titleMedium,
),
), ),
), ),
), ),
); ),
}, );
), },
), ),
Padding( ),
padding: const EdgeInsets.symmetric(horizontal: 8), ],
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 120 / (100 * (16 / 9)),
),
itemCount: state.mangas.length,
itemBuilder: (context, index) {
final manga = state.mangas[index];
return GridItem(
minusCallback: () {
context.read<AnimeListBloc>().add(
MangaChapterDecrementedEvent(
manga.id,
),
);
},
plusCallback: () {
context.read<AnimeListBloc>().add(
MangaChapterIncrementedEvent(
manga.id,
),
);
},
child: AnimeCoverImage(
hero: manga.id,
url: manga.thumbnailUrl,
onTap: () {
context.read<DetailsBloc>().add(
MangaDetailsRequestedEvent(manga),
);
},
extra: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(
'${manga.chaptersRead}/${manga.chaptersTotal ?? "???"}',
style: Theme.of(context).textTheme.titleMedium,
),
),
),
),
);
},
),
),
],
),
floatingActionButton: BlocBuilder<AnimeListBloc, AnimeListState>(
buildWhen: (prev, next) =>
prev.buttonVisibility != next.buttonVisibility ||
prev.trackingType != next.trackingType,
builder: (context, state) {
return AnimatedScale(
duration: const Duration(milliseconds: 250),
scale: state.buttonVisibility ? 1 : 0,
curve: Curves.easeInOutQuint,
child: FloatingActionButton(
onPressed: () {
context.read<AnimeSearchBloc>().add(
AnimeSearchRequestedEvent(state.trackingType),
);
},
tooltip: t.tooltips.addNewItem,
child: const Icon(Icons.add),
),
);
},
),
bottomNavigationBar: BottomBar(
selectedIndex: state.trackingType == TrackingMediumType.anime
? 0
: 1,
onTap: (int index) {
context.read<AnimeListBloc>().add(
AnimeTrackingTypeChanged(
index == 0
? TrackingMediumType.anime
: TrackingMediumType.manga,
),
);
_controller.jumpToPage(index);
},
items: [
BottomBarItem(
icon: const Icon(Icons.tv),
title: Text(t.content.anime),
activeColor: Colors.blue,
),
BottomBarItem(
icon: const Icon(Icons.auto_stories),
title: Text(t.content.manga),
activeColor: Colors.red,
),
],
),
); );
}, },
); );

View File

@@ -20,81 +20,84 @@ class AnimeSearchPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<AnimeSearchBloc, AnimeSearchState>( return BlocListener<AnimeSearchBloc, AnimeSearchState>(
builder: (context, state) { listener: (context, state) {},
return Scaffold( child: BlocBuilder<AnimeSearchBloc, AnimeSearchState>(
appBar: AppBar( builder: (context, state) {
title: Text( return Scaffold(
state.trackingType == TrackingMediumType.anime appBar: AppBar(
? t.search.anime title: Text(
: t.search.manga, state.trackingType == TrackingMediumType.anime
), ? t.search.anime
), : t.search.manga,
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: t.search.query,
),
onSubmitted: (_) {
context.read<AnimeSearchBloc>().add(
SearchQuerySubmittedEvent(),
);
},
onChanged: (value) {
context.read<AnimeSearchBloc>().add(
SearchQueryChangedEvent(value),
);
},
),
), ),
if (state.working) ),
const Expanded( body: Column(
child: Align( children: [
child: CircularProgressIndicator(), Padding(
), padding: const EdgeInsets.all(8),
) child: TextField(
else decoration: InputDecoration(
Expanded( border: const OutlineInputBorder(),
child: ListView.builder( labelText: t.search.query,
itemCount: state.searchResults.length, ),
itemBuilder: (context, index) { onSubmitted: (_) {
final item = state.searchResults[index]; context.read<AnimeSearchBloc>().add(
return InkWell( SearchQuerySubmittedEvent(),
onTap: () { );
context.read<AnimeSearchBloc>().add( },
ResultTappedEvent(item), onChanged: (value) {
); context.read<AnimeSearchBloc>().add(
}, SearchQueryChangedEvent(value),
child: ListItem(
title: item.title,
thumbnailUrl: item.thumbnailUrl,
cached: false,
extra: [
Align(
alignment: Alignment.centerLeft,
child: Text(
item.description,
textAlign: TextAlign.justify,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 4,
softWrap: true,
overflow: TextOverflow.ellipsis,
),
),
],
),
); );
}, },
), ),
), ),
], if (state.working)
), const Expanded(
); child: Align(
}, child: CircularProgressIndicator(),
),
)
else
Expanded(
child: ListView.builder(
itemCount: state.searchResults.length,
itemBuilder: (context, index) {
final item = state.searchResults[index];
return InkWell(
onTap: () {
context.read<AnimeSearchBloc>().add(
ResultTappedEvent(item),
);
},
child: ListItem(
title: item.title,
thumbnailUrl: item.thumbnailUrl,
cached: false,
extra: [
Align(
alignment: Alignment.centerLeft,
child: Text(
item.description,
textAlign: TextAlign.justify,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 4,
softWrap: true,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
},
),
),
],
),
);
},
),
); );
} }
} }

View File

@@ -4,7 +4,6 @@ import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
import 'package:anitrack/src/ui/bloc/calendar_bloc.dart'; import 'package:anitrack/src/ui/bloc/calendar_bloc.dart';
import 'package:anitrack/src/ui/bloc/details_bloc.dart'; import 'package:anitrack/src/ui/bloc/details_bloc.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
import 'package:anitrack/src/ui/helpers.dart';
import 'package:anitrack/src/ui/widgets/grid_item.dart'; import 'package:anitrack/src/ui/widgets/grid_item.dart';
import 'package:anitrack/src/ui/widgets/image.dart'; import 'package:anitrack/src/ui/widgets/image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -63,11 +62,30 @@ class CalendarPage extends StatefulWidget {
), ),
); );
static AppBar buildAppBar(BuildContext context) {
return AppBar(
title: Text(t.calendar.calendar),
actions: [
IconButton(
onPressed: () {
context.read<CalendarBloc>().add(
RefreshPerformedEvent(),
);
},
icon: const Icon(Icons.refresh),
),
],
);
}
@override @override
CalendarPageState createState() => CalendarPageState(); CalendarPageState createState() => CalendarPageState();
} }
class CalendarPageState extends State<CalendarPage> { class CalendarPageState extends State<CalendarPage> {
/// State for the "refreshing" overlay.
OverlayEntry? _overlayEntry;
List<Widget> _renderWeekdayList( List<Widget> _renderWeekdayList(
BuildContext context, BuildContext context,
Weekday day, Weekday day,
@@ -122,8 +140,7 @@ class CalendarPageState extends State<CalendarPage> {
]; ];
} }
@override Map<Weekday, List<AnimeTrackingData>> _constructAiringMap() {
Widget build(BuildContext context) {
final airingAnimeMap = <Weekday, List<AnimeTrackingData>>{}; final airingAnimeMap = <Weekday, List<AnimeTrackingData>>{};
for (final anime in GetIt.I.get<AnimeListBloc>().unfilteredAnime) { for (final anime in GetIt.I.get<AnimeListBloc>().unfilteredAnime) {
if (!anime.airing) continue; if (!anime.airing) continue;
@@ -132,192 +149,153 @@ class CalendarPageState extends State<CalendarPage> {
switch (anime.broadcastDay) { switch (anime.broadcastDay) {
case 'Mondays': case 'Mondays':
day = Weekday.monday; day = Weekday.monday;
break;
case 'Tuesdays': case 'Tuesdays':
day = Weekday.tuesday; day = Weekday.tuesday;
break;
case 'Wednesdays': case 'Wednesdays':
day = Weekday.wednesday; day = Weekday.wednesday;
break;
case 'Thursdays': case 'Thursdays':
day = Weekday.thursday; day = Weekday.thursday;
break;
case 'Fridays': case 'Fridays':
day = Weekday.friday; day = Weekday.friday;
break;
case 'Saturdays': case 'Saturdays':
day = Weekday.saturday; day = Weekday.saturday;
break;
case 'Sundays': case 'Sundays':
day = Weekday.sunday; day = Weekday.sunday;
break;
default: default:
day = Weekday.unknown; day = Weekday.unknown;
break;
} }
airingAnimeMap.addOrSet(day, anime); airingAnimeMap.addOrSet(day, anime);
} }
return airingAnimeMap;
}
return BlocListener<CalendarBloc, CalendarState>( @override
listenWhen: (previous, current) => Widget build(BuildContext context) {
previous.refreshing != current.refreshing, return BlocBuilder<AnimeListBloc, AnimeListState>(
listener: (context, state) { builder: (context, animeListState) {
// Force an update final airingAnimeMap = _constructAiringMap();
if (!state.refreshing) { return BlocListener<CalendarBloc, CalendarState>(
setState(() {}); listenWhen: (previous, current) =>
} previous.refreshing != current.refreshing,
}, listener: (context, state) {
child: WillPopScope( if (!state.refreshing) {
onWillPop: () async => !context.read<CalendarBloc>().state.refreshing, _overlayEntry?.remove();
child: Stack( _overlayEntry?.dispose();
children: [ _overlayEntry = null;
Positioned( } else {
left: 8, _overlayEntry = OverlayEntry(
right: 8, builder: (context) => SafeArea(
top: 0, child: Stack(
bottom: 0, children: [
child: Scaffold( const ModalBarrier(
appBar: AppBar( dismissible: false,
title: Text(t.calendar.calendar), color: Colors.black54,
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( Center(
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(
child: SizedBox( 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(),
),
BlocBuilder<CalendarBloc, CalendarState>(
builder: (context, state) => Text(
t.settings.importIndicator(
current: state.refreshingCount,
total: state.refreshingTotal,
),
style: Theme.of(
context,
).textTheme.bodyMedium,
),
),
],
),
),
), ),
), ),
], ],
), ),
), ),
);
Overlay.of(context).insert(_overlayEntry!);
}
},
child: WillPopScope(
onWillPop: () async =>
!context.read<CalendarBloc>().state.refreshing,
child: Padding(
padding: const EdgeInsetsGeometry.symmetric(
horizontal: 12,
), ),
), child: CustomScrollView(
Positioned( slivers: [
left: 0, // Render all available weekdays
right: 0, ..._renderWeekdayList(
top: 0, context,
bottom: 0, Weekday.unknown,
child: BlocBuilder<CalendarBloc, CalendarState>( airingAnimeMap,
buildWhen: (previous, current) => ),
previous.refreshing != current.refreshing, ..._renderWeekdayList(
builder: (context, state) { context,
if (!state.refreshing) { Weekday.monday,
return const SizedBox(); 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,
),
return const ModalBarrier( // Provide a nice bottom padding, while keeping the elastic effect attached
dismissible: false, // to the bottom-most edge.
color: Colors.black54, const SliverToBoxAdapter(
);
},
),
),
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( child: SizedBox(
width: 150, height: 16,
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,
),
],
),
),
), ),
); ),
}, ],
), ),
), ),
], ),
), );
), },
); );
} }
} }

View File

@@ -1,7 +1,9 @@
import 'package:anitrack/i18n/strings.g.dart'; import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/anime_watcher.dart'; import 'package:anitrack/src/data/anime_watcher.dart';
import 'package:anitrack/src/data/data_base.dart';
import 'package:anitrack/src/data/manga.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/data/type.dart';
import 'package:anitrack/src/ui/bloc/details_bloc.dart'; import 'package:anitrack/src/ui/bloc/details_bloc.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
@@ -14,6 +16,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class DetailsPage extends StatelessWidget { class DetailsPage extends StatelessWidget {
@@ -28,11 +31,42 @@ class DetailsPage extends StatelessWidget {
), ),
); );
String _mangaUrl(MangaTrackingData data) {
switch (data.source) {
case TrackingDataSource.mal:
return 'https://myanimelist.net/manga/${data.id}';
case TrackingDataSource.anilist:
return 'https://anilist.co/manga/${data.id}';
}
}
String _animeUrl(AnimeTrackingData data) {
switch (data.source) {
case TrackingDataSource.mal:
return 'https://myanimelist.net/anime/${data.id}';
case TrackingDataSource.anilist:
return 'https://anilist.co/anime/${data.id}';
}
}
String _sourceButtonName(TrackingDataBase data) {
return switch (data.source) {
TrackingDataSource.mal => t.details.mal,
TrackingDataSource.anilist => t.details.anilist,
};
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(t.details.title), title: Text(t.details.title),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
},
),
), ),
body: BlocBuilder<DetailsBloc, DetailsState>( body: BlocBuilder<DetailsBloc, DetailsState>(
builder: (context, state) { builder: (context, state) {
@@ -246,20 +280,30 @@ class DetailsPage extends StatelessWidget {
children: [ children: [
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
final url = switch (state final url =
.trackingType) { switch (state.trackingType) {
TrackingMediumType.anime => TrackingMediumType.anime =>
'https://myanimelist.net/anime/${state.data!.id}', _animeUrl(
TrackingMediumType.manga => state.data!
'https://myanimelist.net/manga/${state.data!.id}', as AnimeTrackingData,
}; ),
TrackingMediumType.manga =>
_mangaUrl(
state.data!
as MangaTrackingData,
),
};
await launchUrl( await launchUrl(
Uri.parse(url), Uri.parse(url),
mode: LaunchMode mode: LaunchMode
.externalApplication, .externalApplication,
); );
}, },
child: Text(t.details.mal), child: Text(
_sourceButtonName(
state.data! as TrackingDataBase,
),
),
), ),
], ],
), ),

View File

@@ -80,6 +80,7 @@ class DetailsWatcherSheetState extends State<DetailsWatcherSheet> {
child: TextField( child: TextField(
controller: _controller, controller: _controller,
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: t.details.watchingWith.name, labelText: t.details.watchingWith.name,
), ),
), ),

View File

@@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
class SettingsPage extends StatelessWidget { class SettingsPage extends StatelessWidget {
@@ -35,6 +36,12 @@ class SettingsPage extends StatelessWidget {
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(t.settings.title), title: Text(t.settings.title),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
},
),
), ),
body: ListView( body: ListView(
children: [ children: [
@@ -43,7 +50,7 @@ class SettingsPage extends StatelessWidget {
subtitle: Text(t.settings.importAnimeDesc), subtitle: Text(t.settings.importAnimeDesc),
onTap: () async { onTap: () async {
// Pick the file // Pick the file
final result = await FilePicker.platform.pickFiles(); final result = await FilePicker.pickFiles();
if (result == null) return; if (result == null) return;
if (!result.files.first.path!.endsWith('.xml.gz')) { if (!result.files.first.path!.endsWith('.xml.gz')) {
@@ -70,7 +77,7 @@ class SettingsPage extends StatelessWidget {
subtitle: Text(t.settings.importMangaDesc), subtitle: Text(t.settings.importMangaDesc),
onTap: () async { onTap: () async {
// Pick the file // Pick the file
final result = await FilePicker.platform.pickFiles(); final result = await FilePicker.pickFiles();
if (result == null) return; if (result == null) return;
if (!result.files.first.path!.endsWith('.xml.gz')) { if (!result.files.first.path!.endsWith('.xml.gz')) {
@@ -96,14 +103,14 @@ class SettingsPage extends StatelessWidget {
title: Text(t.settings.exportData), title: Text(t.settings.exportData),
onTap: () async { onTap: () async {
// Pick the file // Pick the file
final result = await FilePicker.platform final result = await FilePicker.getDirectoryPath();
.getDirectoryPath();
if (result == null) return; if (result == null) return;
if (!(await Permission.manageExternalStorage if (!(await Permission.manageExternalStorage
.request()) .request())
.isGranted) .isGranted) {
return; return;
}
GetIt.I.get<SettingsBloc>().add( GetIt.I.get<SettingsBloc>().add(
DataExportedEvent( DataExportedEvent(
@@ -116,7 +123,7 @@ class SettingsPage extends StatelessWidget {
title: Text(t.settings.importData), title: Text(t.settings.importData),
onTap: () async { onTap: () async {
// Pick the file // Pick the file
final result = await FilePicker.platform.pickFiles(); final result = await FilePicker.pickFiles();
if (result == null) return; if (result == null) return;
if (!result.files.first.path!.endsWith('.json.gz')) { if (!result.files.first.path!.endsWith('.json.gz')) {

View File

@@ -1,4 +1,3 @@
import 'package:anitrack/src/ui/widgets/list_item.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class SelectorItem<T> { class SelectorItem<T> {

View File

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
class AnimeCoverImage extends StatelessWidget { class AnimeCoverImage extends StatelessWidget {
const AnimeCoverImage({ const AnimeCoverImage({
required this.url, required this.url,
required this.hero, this.hero,
this.cached = true, this.cached = true,
this.extra, this.extra,
this.onTap, this.onTap,
@@ -18,7 +18,7 @@ class AnimeCoverImage extends StatelessWidget {
final bool cached; final bool cached;
/// The hero tag of the image. /// The hero tag of the image.
final String hero; final String? hero;
/// An extra widget with a translucent backdrop. /// An extra widget with a translucent backdrop.
final Widget? extra; final Widget? extra;
@@ -28,53 +28,59 @@ class AnimeCoverImage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Hero( final image = ClipRRect(
tag: hero, borderRadius: BorderRadius.circular(8),
child: ClipRRect( child: Material(
borderRadius: BorderRadius.circular(8), child: SizedBox(
child: Material( height: 100 * (16 / 9),
child: SizedBox( width: 120,
height: 100 * (16 / 9), child: InkWell(
width: 120, onTap: onTap ?? () {},
child: InkWell( child: Stack(
onTap: onTap ?? () {}, children: [
child: Stack( Positioned(
children: [ left: 0,
Positioned( right: 0,
left: 0, top: 0,
right: 0, bottom: 0,
top: 0, child: DecoratedBox(
bottom: 0, decoration: BoxDecoration(
child: DecoratedBox( image: DecorationImage(
decoration: BoxDecoration( image: cached
image: DecorationImage( ? CachedNetworkImageProvider(url) as ImageProvider
image: cached : NetworkImage(url),
? CachedNetworkImageProvider(url) as ImageProvider fit: BoxFit.cover,
: NetworkImage(url),
fit: BoxFit.cover,
),
), ),
), ),
), ),
if (extra != null) ),
Positioned( if (extra != null)
left: 0, Positioned(
bottom: 0, left: 0,
right: 0, bottom: 0,
child: SizedBox( right: 0,
height: 40, child: SizedBox(
child: ColoredBox( height: 40,
color: Colors.black54, child: ColoredBox(
child: extra, color: Colors.black54,
), child: extra,
), ),
), ),
], ),
), ],
), ),
), ),
), ),
), ),
); );
if (hero != null) {
return Hero(
tag: hero!,
child: image,
);
}
return image;
} }
} }

View File

@@ -46,7 +46,7 @@ class ListItem extends StatelessWidget {
}, },
// TODO(PapaTutuWawa): Fix // TODO(PapaTutuWawa): Fix
key: UniqueKey(), key: UniqueKey(),
backgroundBuilder: (_, direction, __) { backgroundBuilder: (_, direction, _) {
if (direction == SwipeDirection.endToStart) { if (direction == SwipeDirection.endToStart) {
return const Align( return const Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
@@ -76,8 +76,6 @@ class ListItem extends StatelessWidget {
children: [ children: [
AnimeCoverImage( AnimeCoverImage(
cached: cached, cached: cached,
// TODO(Unknown): Have the ID here
hero: thumbnailUrl,
extra: imageExtra, extra: imageExtra,
url: thumbnailUrl, url: thumbnailUrl,
), ),

View File

@@ -0,0 +1,48 @@
import 'package:anitrack/src/ui/constants.dart';
import 'package:anitrack/src/ui/helpers.dart';
import 'package:anitrack/src/ui/pages/anime_list.dart';
import 'package:anitrack/src/ui/pages/calendar.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class ShellWrapper extends StatelessWidget {
const ShellWrapper({
required this.state,
required this.child,
super.key,
});
/// The current router state.
final GoRouterState state;
/// The child to show.
final Widget child;
@override
Widget build(BuildContext context) {
final currentPath = GoRouterState.of(context).uri.toString();
AppBar? appBar;
Widget? drawer;
Widget? fab;
Widget? bottomNavigationBar;
switch (currentPath) {
case animeListRoute:
drawer = getDrawer(context);
appBar = AnimeListPage.buildAppBar(context);
bottomNavigationBar = AnimeListPage.buildBottomNavigationBar(context);
fab = AnimeListPage.buildFab();
case calendarRoute:
drawer = getDrawer(context);
appBar = CalendarPage.buildAppBar(context);
}
return Scaffold(
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
drawer: drawer,
floatingActionButton: fab,
body: child,
);
}
}

View File

@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

View File

@@ -5,18 +5,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" sha256: "3b19a47f6ea7c2632760777c78174f47f6aec1e05f0cd611380d4593b8af1dbc"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "93.0.0" version: "96.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b sha256: "0c516bc4ad36a1a75759e54d5047cb9d15cded4459df01aa35a0b5ec7db2c2a0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.1" version: "10.2.0"
archive: archive:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -37,18 +37,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.1"
bloc: bloc:
dependency: "direct main" dependency: "direct main"
description: description:
name: bloc name: bloc
sha256: a48653a82055a900b88cd35f92429f068c5a8057ae9b136d197b3d56c57efb81 sha256: e03b235924e4f509c27b5d6b2f949200e0a91149a9818b4f65eeb56662b75413
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.2.0" version: "9.2.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -69,34 +69,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build name: build
sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.4" version: "4.0.7"
build_config: build_config:
dependency: transitive dependency: transitive
description: description:
name: build_config name: build_config
sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.1"
build_daemon: build_daemon:
dependency: transitive dependency: transitive
description: description:
name: build_daemon name: build_daemon
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.1" version: "4.1.2"
build_runner: build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: build_runner name: build_runner
sha256: "7981eb922842c77033026eb4341d5af651562008cdb116bdfa31fc46516b6462" sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.12.2" version: "2.15.1"
built_collection: built_collection:
dependency: transitive dependency: transitive
description: description:
@@ -109,10 +109,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: built_value name: built_value
sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.12.4" version: "8.12.6"
cached_network_image: cached_network_image:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -181,18 +181,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: code_assets name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.2.1"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
url: "https://pub.dev"
source: hosted
version: "4.11.1"
collection: collection:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -213,18 +205,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: coverage name: coverage
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.15.0" version: "1.15.1"
cross_file: cross_file:
dependency: transitive dependency: transitive
description: description:
name: cross_file name: cross_file
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.5+2" version: "0.3.5+4"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@@ -245,12 +237,12 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "1.0.9"
dart_pubspec_licenses: dart_pubspec_licenses:
dependency: transitive dependency: "direct dev"
description: description:
name: dart_pubspec_licenses name: dart_pubspec_licenses
sha256: "3d579e1aa3ad3b6519f08fce6980799c0a8375bf41e0b8d58ca21f1be64032c9" sha256: "3d579e1aa3ad3b6519f08fce6980799c0a8375bf41e0b8d58ca21f1be64032c9"
@@ -269,10 +261,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dbus name: dbus
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.12" version: "0.7.13"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -301,10 +293,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.3.10" version: "11.0.2"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@@ -330,10 +322,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_cache_manager name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" sha256: "1de7849213b4c73c85aca7e0ac687a9a5d82ccdb594366b9dcc26cb6a2189cd2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.1" version: "3.4.2"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -355,22 +347,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_oss_licenses:
dependency: "direct dev"
description:
name: flutter_oss_licenses
sha256: "80f5d879f1760f90020692db0d4b58b5a475df23573df567e12757a71ce59628"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.33" version: "2.0.35"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -385,10 +369,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fluttertoast name: fluttertoast
sha256: "144ddd74d49c865eba47abe31cbc746c7b311c82d6c32e571fd73c4264b740e2" sha256: "7903c9d5339173497bfecbc23bc4212f5a87e0edfac2e1693fb74465ea67da7e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.0.0" version: "9.1.0"
freezed: freezed:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -429,6 +413,78 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a"
url: "https://pub.dev"
source: hosted
version: "17.3.0"
gql:
dependency: transitive
description:
name: gql
sha256: "67c32325eb55c15f526f0f5e7d8b38a463dbff2ec3c2e046be4a1a95f0dc93d1"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
gql_dedupe_link:
dependency: transitive
description:
name: gql_dedupe_link
sha256: "10bee0564d67c24e0c8bd08bd56e0682b64a135e58afabbeed30d85d5e9fea96"
url: "https://pub.dev"
source: hosted
version: "2.0.4-alpha+1715521079596"
gql_error_link:
dependency: transitive
description:
name: gql_error_link
sha256: dd0f3fbfbcec848ea050507470cdb5d3dc47d29544ae11044a1c883cbe159ccc
url: "https://pub.dev"
source: hosted
version: "1.0.1"
gql_exec:
dependency: transitive
description:
name: gql_exec
sha256: "394944626fae900f1d34343ecf2d62e44eb984826189c8979d305f0ae5846e38"
url: "https://pub.dev"
source: hosted
version: "1.1.1-alpha+1699813812660"
gql_http_link:
dependency: transitive
description:
name: gql_http_link
sha256: "07635e85a4f313836904961904417fd27844fe8f68f77b410a4e6b81d8e9202e"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
gql_link:
dependency: transitive
description:
name: gql_link
sha256: "0730276ce3a6a0ced073194ff923a8d99b3c78e442cbf096eb54fd0c3fa9f974"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
gql_transform_link:
dependency: transitive
description:
name: gql_transform_link
sha256: b3bb06a6991bc5c9d877e2757455f80e2c14dc684b8327bedae4f4ee67afae8b
url: "https://pub.dev"
source: hosted
version: "1.0.1"
graphql:
dependency: "direct main"
description:
name: graphql
sha256: a7cb0b5e8719546bf8d4edf5f57c3690ddf0fcce379c0d9d2287fdab73481090
url: "https://pub.dev"
source: hosted
version: "5.2.4"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@@ -437,14 +493,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
hive_ce:
dependency: transitive
description:
name: hive_ce
sha256: "8e9980e68643afb1e765d3af32b47996552a64e190d03faf622cea07c1294418"
url: "https://pub.dev"
source: hosted
version: "2.19.3"
hooks: hooks:
dependency: transitive dependency: transitive
description: description:
name: hooks name: hooks
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "2.0.2"
html: html:
dependency: transitive dependency: transitive
description: description:
@@ -481,10 +545,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.0" version: "4.9.1"
intl: intl:
dependency: transitive dependency: transitive
description: description:
@@ -501,6 +565,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
isolate_channel:
dependency: transitive
description:
name: isolate_channel
sha256: a9d3d620695bc984244dafae00b95e4319d6974b2d77f4b9e1eb4f2efe099094
url: "https://pub.dev"
source: hosted
version: "0.6.1"
jikan_api: jikan_api:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -509,22 +581,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
json_annotation: json_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
name: json_annotation name: json_annotation
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.11.0" version: "4.12.0"
json_serializable: json_serializable:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: json_serializable name: json_serializable
sha256: "44729f5c45748e6748f6b9a57ab8f7e4336edc8ae41fc295070e3814e616a6c0" sha256: ffcd10cde35a93b2abbbcc26bd9971f4ca93763e8abe78d855e3c4177797e501
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.13.0" version: "6.14.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -569,10 +657,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: markdown name: markdown
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.3.0" version: "7.3.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -593,10 +681,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.18.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -609,10 +697,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: native_toolchain_c name: native_toolchain_c
sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f" sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.17.5" version: "0.19.2"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@@ -629,14 +717,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "2.0.2"
normalize:
dependency: transitive
description:
name: normalize
sha256: "703f0af9e6f43a5a71536e977b945238bc89f1a941347e7ba467865a20cc1a9f"
url: "https://pub.dev"
source: hosted
version: "0.10.0"
objective_c: objective_c:
dependency: transitive dependency: transitive
description: description:
name: objective_c name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.3.0" version: "9.4.1"
octo_image: octo_image:
dependency: transitive dependency: transitive
description: description:
@@ -657,10 +753,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: pana name: pana
sha256: d7bd85f18a14909f0baab69a13895dc4ff163be51c9884f73a506982adeac57d sha256: "847ee5df6ac13fdc6c53d641095de1b25886c973d0b0f0469c73521ebe4602fa"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.23.10" version: "0.23.12"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -670,21 +766,21 @@ packages:
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider: path_provider:
dependency: transitive dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.5" version: "2.1.6"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.22" version: "2.3.1"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
@@ -697,18 +793,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.2.2"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.3"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
@@ -721,10 +817,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: permission_handler name: permission_handler
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.0.1" version: "12.0.3"
permission_handler_android: permission_handler_android:
dependency: transitive dependency: transitive
description: description:
@@ -737,10 +833,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_apple name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.4.7" version: "9.4.10"
permission_handler_html: permission_handler_html:
dependency: transitive dependency: transitive
description: description:
@@ -801,10 +897,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: posix name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.5.0" version: "6.5.2"
provider: provider:
dependency: transitive dependency: transitive
description: description:
@@ -829,6 +925,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
retry: retry:
dependency: transitive dependency: transitive
description: description:
@@ -902,42 +1006,42 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: slang name: slang
sha256: "27f3490c7a9eb2908715ed7be6b11cc2cc35a51365f33041b561ded79636f136" sha256: "76dbf1f8e87ed1c417026e93a57512f6b6b597104ce72eed99b24a3ed08f0ba2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.13.0" version: "4.18.0"
slang_build_runner: slang_build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: slang_build_runner name: slang_build_runner
sha256: "137c359f6b692f3cfd69832f010b1473783389e88a88b7420ddb12d1a4387432" sha256: e24f5ab540c2078db20a1e3292b9bae11ef4ed07bef1c235ae000b0a6113267e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.13.0" version: "4.18.0"
slang_flutter: slang_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: slang_flutter name: slang_flutter
sha256: "2200626c34540af594c31a052bc55a59febc353228feaa1232d60d6143a99680" sha256: "7523a9389b44210eabcfd58412d9131f284b9b3c22d731614f0016e9582ee996"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.13.0" version: "4.18.0"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:
name: source_gen name: source_gen
sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17" sha256: a603f1fb984a7391ae5978d1b92bfaaa08b350dca5c825256f925818f7943bf5
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.0" version: "4.2.4"
source_helper: source_helper:
dependency: transitive dependency: transitive
description: description:
name: source_helper name: source_helper
sha256: "4a85e90b50694e652075cbe4575665539d253e6ec10e46e76b45368ab5e3caae" sha256: "4227d54ceefd0bb8ca4c8fcb96e1719dc53f1ee1b6e2ca9d7a6069da160e4eae"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.10" version: "1.3.12"
source_map_stack_trace: source_map_stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -966,58 +1070,58 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqflite name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 sha256: "58a799e6ac17dd32fbab93813d39ed835a75ccc0f8f85b8955fe318c6712b082"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.4.3"
sqflite_android: sqflite_android:
dependency: transitive dependency: transitive
description: description:
name: sqflite_android name: sqflite_android
sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 sha256: d0548f9d7422a2dae99ec6f8b0a3074463b132d216fa5ba0d230eeefc901983b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2+2" version: "2.4.3"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" sha256: "5bf6a55c166e73bf651ba7ec3ed486e577620e3dc8f3a9c6a258a8031b624590"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.6" version: "2.5.11"
sqflite_common_ffi: sqflite_common_ffi:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqflite_common_ffi name: sqflite_common_ffi
sha256: c59fcdc143839a77581f7a7c4de018e53682408903a0a0800b95ef2dc4033eff sha256: "5ccd38136edb9beb3213f6927775d52db70dfdadcdb28dad1f625ca9f2b9824f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0+2" version: "2.4.2"
sqflite_darwin: sqflite_darwin:
dependency: transitive dependency: transitive
description: description:
name: sqflite_darwin name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" sha256: c86ca18b8f666bbf903924687fe21cc16fc385d086005067e26619ca530bef9f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.4.3+1"
sqflite_platform_interface: sqflite_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: sqflite_platform_interface name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" sha256: f84939f84350d92d04416f8bc4dc52d3896aec7716cc9e80cf0146342139dc50
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.1"
sqlite3: sqlite3:
dependency: transitive dependency: transitive
description: description:
name: sqlite3 name: sqlite3
sha256: b7cf6b37667f6a921281797d2499ffc60fb878b161058d422064f0ddc78f6aa6 sha256: c73fd75df1332d76a6257f4823ae4df9c791f522b97e4a60cbcad214de1becf4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.6" version: "3.5.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -1062,10 +1166,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: synchronized name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 sha256: "61894a1956de6b4fc1aefd0892e109514a1a706cbece3ac59decd90ff5a7a423"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.0" version: "3.4.1+1"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -1078,26 +1182,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test name: test
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.30.0" version: "1.31.0"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.11"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.16" version: "0.6.17"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -1118,10 +1222,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.28" version: "6.3.32"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
@@ -1158,10 +1262,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.4.3"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
@@ -1174,10 +1278,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: uuid name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.3" version: "4.6.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -1190,18 +1294,18 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: very_good_analysis name: very_good_analysis
sha256: d1cb1d66a5aae2c702d68caca6c8347306d35e728fd94555fa21fa0448a972e0 sha256: "481af67ab5877af20325251dc215a4ebac7666a1c8cf09198ffd457bc612b33d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.2.0" version: "10.3.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" version: "15.2.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@@ -1262,10 +1366,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: xml name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.6.1" version: "7.0.1"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@@ -1275,5 +1379,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.11.0 <4.0.0" dart: ">=3.12.0 <4.0.0"
flutter: ">=3.38.4" flutter: ">=3.44.0"

View File

@@ -2,7 +2,7 @@ name: anitrack
description: An anime and manga tracker description: An anime and manga tracker
publish_to: "none" publish_to: "none"
version: 0.1.4+2014 version: 0.2.2+2019
environment: environment:
sdk: ^3.8.0 sdk: ^3.8.0
@@ -14,16 +14,19 @@ dependencies:
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
collection: ^1.18.0 collection: ^1.18.0
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
file_picker: ^10.3.10 file_picker: ^11.0.2
flutter: flutter:
sdk: flutter sdk: flutter
flutter_bloc: ^9.1.1 flutter_bloc: ^9.1.1
fluttertoast: ^9.0.0 fluttertoast: ^9.0.0
freezed_annotation: ^3.1.0 freezed_annotation: ^3.1.0
get_it: ^9.2.1 get_it: ^9.2.1
go_router: ^17.3.0
graphql: ^5.2.4
jikan_api: ^2.2.1 jikan_api: ^2.2.1
json_annotation: ^4.11.0 json_annotation: ^4.11.0
path: ^1.9.0 path: ^1.9.1
path_provider: ^2.1.6
permission_handler: ^12.0.1 permission_handler: ^12.0.1
slang: ^4.13.0 slang: ^4.13.0
slang_flutter: ^4.13.0 slang_flutter: ^4.13.0
@@ -31,13 +34,13 @@ dependencies:
sqflite_common_ffi: sqflite_common_ffi:
swipeable_tile: ^2.0.1 swipeable_tile: ^2.0.1
url_launcher: ^6.3.0 url_launcher: ^6.3.0
xml: ^6.5.0 xml: ^7.0.1
dev_dependencies: dev_dependencies:
build_runner: ^2.4.12 build_runner: ^2.4.12
flutter_launcher_icons: ^0.14.1 flutter_launcher_icons: ^0.14.1
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
flutter_oss_licenses: ^3.0.2 dart_pubspec_licenses: ^3.2.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
freezed: ^3.2.5 freezed: ^3.2.5