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
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.
## Screenshots

View File

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

View File

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

View File

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

View File

@@ -50,6 +50,7 @@
"chapters": "Chapters",
"volumesOwned": "Volumes owned",
"mal": "MAL",
"anilist": "AniList",
"watchingWith": {
"title": "Watching with",
"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/src/service/anilist/anilist_client.dart';
import 'package:anitrack/src/service/database.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart';
@@ -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_search.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/widgets/shell_wrapper.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
void main() async {
final navKey = GlobalKey<NavigatorState>();
// Initialize the widgets binding for sqflite
WidgetsFlutterBinding.ensureInitialized();
@@ -32,9 +34,10 @@ void main() async {
GetIt.I.registerSingleton<AnimeListBloc>(AnimeListBloc());
GetIt.I.registerSingleton<AnimeSearchBloc>(AnimeSearchBloc());
GetIt.I.registerSingleton<DetailsBloc>(DetailsBloc());
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navKey));
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc());
GetIt.I.registerSingleton<SettingsBloc>(SettingsBloc());
GetIt.I.registerSingleton<CalendarBloc>(CalendarBloc());
GetIt.I.registerSingleton<AniListClient>(AniListClient());
// Load animes
GetIt.I.get<AnimeListBloc>().add(
@@ -66,53 +69,85 @@ void main() async {
create: (_) => GetIt.I.get<CalendarBloc>(),
),
],
child: MyApp(navKey),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp(
this.navKey, {
super.key,
});
MyApp({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
Widget build(BuildContext context) {
return MaterialApp(
title: 'AniTrack',
theme: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.blue,
useMaterial3: true,
),
darkTheme: ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.blue,
useMaterial3: true,
),
navigatorKey: navKey,
onGenerateRoute: (settings) {
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 BlocListener<NavigationBloc, NavigationState>(
listener: (context, state) {
if (state is NoopNavigationState) {
// NOOP
} else if (state is PushNavigationState) {
_router.push(state.destination);
} else if (state is GoNavigationState) {
if (_router.canPop()) {
_router.replace(state.destination);
} else {
_router.go(state.destination);
}
} else if (state is PoppedNavigationState) {
_router.pop();
}
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/service/database.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -17,7 +20,9 @@ class BoolConverter implements JsonConverter<bool, int> {
/// Data about a tracked anime
@freezed
abstract class AnimeTrackingData with _$AnimeTrackingData, TrackingMedium {
abstract class AnimeTrackingData
with _$AnimeTrackingData, TrackingMedium
implements TrackingDataBase {
factory AnimeTrackingData(
/// The ID of the anime
String id,
@@ -42,6 +47,12 @@ abstract class AnimeTrackingData with _$AnimeTrackingData, TrackingMedium {
/// The day of the week the anime is airing
String? broadcastDay,
/// Other titles
@OtherTitlesConverter() OtherTitles otherTitles,
/// The source where we got the data from.
@TrackingDataSourceConverter() TrackingDataSource source,
) = _AnimeTrackingData;
AnimeTrackingData._();

View File

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

View File

@@ -18,6 +18,8 @@ _AnimeTrackingData _$AnimeTrackingDataFromJson(Map<String, dynamic> json) =>
json['thumbnailUrl'] as String,
const BoolConverter().fromJson((json['airing'] as num).toInt()),
json['broadcastDay'] as String?,
const OtherTitlesConverter().fromJson(json['otherTitles'] as String),
const TrackingDataSourceConverter().fromJson(json['source'] as String),
);
Map<String, dynamic> _$AnimeTrackingDataToJson(_AnimeTrackingData instance) =>
@@ -30,4 +32,6 @@ Map<String, dynamic> _$AnimeTrackingDataToJson(_AnimeTrackingData instance) =>
'thumbnailUrl': instance.thumbnailUrl,
'airing': const BoolConverter().toJson(instance.airing),
'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:freezed_annotation/freezed_annotation.dart';
@@ -6,7 +9,9 @@ part 'manga.g.dart';
/// Data about a tracked anime
@freezed
abstract class MangaTrackingData with _$MangaTrackingData, TrackingMedium {
abstract class MangaTrackingData
with _$MangaTrackingData, TrackingMedium
implements TrackingDataBase {
factory MangaTrackingData(
/// The ID of the manga
String id,
@@ -28,6 +33,12 @@ abstract class MangaTrackingData with _$MangaTrackingData, TrackingMedium {
/// URL to the thumbnail/cover art for the manga.
String thumbnailUrl,
/// Other titles the manga can have.
@OtherTitlesConverter() OtherTitles otherTitles,
/// The source where we got the data from.
@TrackingDataSourceConverter() TrackingDataSource source,
) = _MangaTrackingData;
MangaTrackingData._();

View File

@@ -22,7 +22,9 @@ mixin _$MangaTrackingData {
int get chaptersRead;/// Chapters read.
int get volumesOwned;/// Episodes watched.
int? get chaptersTotal;/// URL to the thumbnail/cover art for the manga.
String get thumbnailUrl;
String get thumbnailUrl;/// 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
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -35,16 +37,16 @@ $MangaTrackingDataCopyWith<MangaTrackingData> get copyWith => _$MangaTrackingDat
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is MangaTrackingData&&(identical(other.id, id) || other.id == id)&&(identical(other.state, state) || other.state == state)&&(identical(other.title, title) || other.title == title)&&(identical(other.chaptersRead, chaptersRead) || other.chaptersRead == chaptersRead)&&(identical(other.volumesOwned, volumesOwned) || other.volumesOwned == volumesOwned)&&(identical(other.chaptersTotal, chaptersTotal) || other.chaptersTotal == chaptersTotal)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl));
return identical(this, other) || (other.runtimeType == runtimeType&&other is MangaTrackingData&&(identical(other.id, id) || other.id == id)&&(identical(other.state, state) || other.state == state)&&(identical(other.title, title) || other.title == title)&&(identical(other.chaptersRead, chaptersRead) || other.chaptersRead == chaptersRead)&&(identical(other.volumesOwned, volumesOwned) || other.volumesOwned == volumesOwned)&&(identical(other.chaptersTotal, chaptersTotal) || other.chaptersTotal == chaptersTotal)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl)&&(identical(other.otherTitles, otherTitles) || other.otherTitles == otherTitles)&&(identical(other.source, source) || other.source == source));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,state,title,chaptersRead,volumesOwned,chaptersTotal,thumbnailUrl);
int get hashCode => Object.hash(runtimeType,id,state,title,chaptersRead,volumesOwned,chaptersTotal,thumbnailUrl,otherTitles,source);
@override
String toString() {
return 'MangaTrackingData(id: $id, state: $state, title: $title, chaptersRead: $chaptersRead, volumesOwned: $volumesOwned, chaptersTotal: $chaptersTotal, thumbnailUrl: $thumbnailUrl)';
return 'MangaTrackingData(id: $id, state: $state, title: $title, chaptersRead: $chaptersRead, volumesOwned: $volumesOwned, chaptersTotal: $chaptersTotal, thumbnailUrl: $thumbnailUrl, otherTitles: $otherTitles, source: $source)';
}
@@ -55,11 +57,11 @@ abstract mixin class $MangaTrackingDataCopyWith<$Res> {
factory $MangaTrackingDataCopyWith(MangaTrackingData value, $Res Function(MangaTrackingData) _then) = _$MangaTrackingDataCopyWithImpl;
@useResult
$Res call({
String id,@MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl
String id,@MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl,@OtherTitlesConverter() OtherTitles otherTitles,@TrackingDataSourceConverter() TrackingDataSource source
});
$OtherTitlesCopyWith<$Res> get otherTitles;
}
/// @nodoc
@@ -72,7 +74,7 @@ class _$MangaTrackingDataCopyWithImpl<$Res>
/// Create a copy of MangaTrackingData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? state = null,Object? title = null,Object? chaptersRead = null,Object? volumesOwned = null,Object? chaptersTotal = freezed,Object? thumbnailUrl = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? state = null,Object? title = null,Object? chaptersRead = null,Object? volumesOwned = null,Object? chaptersTotal = freezed,Object? thumbnailUrl = null,Object? otherTitles = null,Object? source = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
@@ -81,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,chaptersTotal: freezed == chaptersTotal ? _self.chaptersTotal : chaptersTotal // ignore: cast_nullable_to_non_nullable
as int?,thumbnailUrl: null == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable
as String,
as String,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) {
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();
}
@@ -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) {
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');
}
@@ -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) {
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;
}
@@ -222,7 +235,7 @@ return $default(_that.id,_that.state,_that.title,_that.chaptersRead,_that.volume
@JsonSerializable()
class _MangaTrackingData extends MangaTrackingData {
_MangaTrackingData(this.id, @MediumTrackingStateConverter() this.state, this.title, this.chaptersRead, this.volumesOwned, this.chaptersTotal, this.thumbnailUrl): super._();
_MangaTrackingData(this.id, @MediumTrackingStateConverter() this.state, this.title, this.chaptersRead, this.volumesOwned, this.chaptersTotal, this.thumbnailUrl, @OtherTitlesConverter() this.otherTitles, @TrackingDataSourceConverter() this.source): super._();
factory _MangaTrackingData.fromJson(Map<String, dynamic> json) => _$MangaTrackingDataFromJson(json);
/// The ID of the manga
@@ -239,6 +252,10 @@ class _MangaTrackingData extends MangaTrackingData {
@override final int? chaptersTotal;
/// URL to the thumbnail/cover art for the manga.
@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
/// with the given fields replaced by the non-null parameter values.
@@ -253,16 +270,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MangaTrackingData&&(identical(other.id, id) || other.id == id)&&(identical(other.state, state) || other.state == state)&&(identical(other.title, title) || other.title == title)&&(identical(other.chaptersRead, chaptersRead) || other.chaptersRead == chaptersRead)&&(identical(other.volumesOwned, volumesOwned) || other.volumesOwned == volumesOwned)&&(identical(other.chaptersTotal, chaptersTotal) || other.chaptersTotal == chaptersTotal)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MangaTrackingData&&(identical(other.id, id) || other.id == id)&&(identical(other.state, state) || other.state == state)&&(identical(other.title, title) || other.title == title)&&(identical(other.chaptersRead, chaptersRead) || other.chaptersRead == chaptersRead)&&(identical(other.volumesOwned, volumesOwned) || other.volumesOwned == volumesOwned)&&(identical(other.chaptersTotal, chaptersTotal) || other.chaptersTotal == chaptersTotal)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl)&&(identical(other.otherTitles, otherTitles) || other.otherTitles == otherTitles)&&(identical(other.source, source) || other.source == source));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,state,title,chaptersRead,volumesOwned,chaptersTotal,thumbnailUrl);
int get hashCode => Object.hash(runtimeType,id,state,title,chaptersRead,volumesOwned,chaptersTotal,thumbnailUrl,otherTitles,source);
@override
String toString() {
return 'MangaTrackingData(id: $id, state: $state, title: $title, chaptersRead: $chaptersRead, volumesOwned: $volumesOwned, chaptersTotal: $chaptersTotal, thumbnailUrl: $thumbnailUrl)';
return 'MangaTrackingData(id: $id, state: $state, title: $title, chaptersRead: $chaptersRead, volumesOwned: $volumesOwned, chaptersTotal: $chaptersTotal, thumbnailUrl: $thumbnailUrl, otherTitles: $otherTitles, source: $source)';
}
@@ -273,11 +290,11 @@ abstract mixin class _$MangaTrackingDataCopyWith<$Res> implements $MangaTracking
factory _$MangaTrackingDataCopyWith(_MangaTrackingData value, $Res Function(_MangaTrackingData) _then) = __$MangaTrackingDataCopyWithImpl;
@override @useResult
$Res call({
String id,@MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl
String id,@MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, int? chaptersTotal, String thumbnailUrl,@OtherTitlesConverter() OtherTitles otherTitles,@TrackingDataSourceConverter() TrackingDataSource source
});
@override $OtherTitlesCopyWith<$Res> get otherTitles;
}
/// @nodoc
@@ -290,7 +307,7 @@ class __$MangaTrackingDataCopyWithImpl<$Res>
/// Create a copy of MangaTrackingData
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? state = null,Object? title = null,Object? chaptersRead = null,Object? volumesOwned = null,Object? chaptersTotal = freezed,Object? thumbnailUrl = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? state = null,Object? title = null,Object? chaptersRead = null,Object? volumesOwned = null,Object? chaptersTotal = freezed,Object? thumbnailUrl = null,Object? otherTitles = null,Object? source = null,}) {
return _then(_MangaTrackingData(
null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
@@ -299,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,freezed == chaptersTotal ? _self.chaptersTotal : chaptersTotal // ignore: cast_nullable_to_non_nullable
as int?,null == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable
as String,
as String,null == 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

View File

@@ -17,6 +17,8 @@ _MangaTrackingData _$MangaTrackingDataFromJson(Map<String, dynamic> json) =>
(json['volumesOwned'] as num).toInt(),
(json['chaptersTotal'] as num?)?.toInt(),
json['thumbnailUrl'] as String,
const OtherTitlesConverter().fromJson(json['otherTitles'] as String),
const TrackingDataSourceConverter().fromJson(json['source'] as String),
);
Map<String, dynamic> _$MangaTrackingDataToJson(_MangaTrackingData instance) =>
@@ -28,4 +30,6 @@ Map<String, dynamic> _$MangaTrackingDataToJson(_MangaTrackingData instance) =>
'volumesOwned': instance.volumesOwned,
'chaptersTotal': instance.chaptersTotal,
'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 {
const SearchResult(
this.title,
this.titleEnglish,
this.titleJapanese,
this.id,
this.total,
this.thumbnailUrl,
@@ -12,6 +14,12 @@ class SearchResult {
/// The title of the anime.
final String title;
/// The English title.
final String? titleEnglish;
/// The original Japanese title.
final String? titleJapanese;
/// The id of the anime.
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_score.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';
const animeTable = 'Anime';
@@ -38,7 +43,8 @@ Future<void> _createDatabase(Database db, int version) async {
otherTitles TEXT NOT NULL,
score INTEGER,
airing INTEGER NOT NULL,
broadcastDay TEXT
broadcastDay TEXT,
source TEXT NOT NULL
)''',
);
await db.execute(
@@ -52,13 +58,14 @@ Future<void> _createDatabase(Database db, int version) async {
thumbnailUrl TEXT NOT NULL,
title TEXT NOT NULL,
otherTitles TEXT NOT NULL,
score INTEGER
score INTEGER,
source TEXT NOT NULL
)''',
);
await db.execute(
'''
CREATE TABLE $animeWatcherTable(
name TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL PRIMARY KEY
)''',
);
await db.execute(
@@ -67,8 +74,8 @@ Future<void> _createDatabase(Database db, int version) async {
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),
CONSTRAINT fk_anime FOREIGN KEY (anime) REFERENCES $animeTable(id)
CONSTRAINT fk_watcher FOREIGN KEY (name) REFERENCES $animeWatcherTable(name) ON DELETE CASCADE,
CONSTRAINT fk_anime FOREIGN KEY (anime) REFERENCES $animeTable(id) ON DELETE CASCADE
)''',
);
}
@@ -80,6 +87,15 @@ class DatabaseService {
/// Cached AnimeWatchers.
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 {
// Allow initializing the database on Windows and Linux as well.
if (Platform.isLinux || Platform.isWindows) {
@@ -87,9 +103,11 @@ class DatabaseService {
databaseFactory = databaseFactoryFfi;
}
final databasePath = await _getDatabasePath();
print('Opening database at $databasePath');
_db = await openDatabase(
'anitrack.db',
version: 4,
databasePath,
version: 7,
onConfigure: (db) async {
// In order to do schema changes during database upgrades, we disable foreign
// keys in the onConfigure phase, but re-enable them here.
@@ -111,9 +129,17 @@ class DatabaseService {
if (oldVersion < 4) {
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 {
@@ -138,7 +164,6 @@ class DatabaseService {
await _db.insert(
animeTable,
data.toJson(),
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}

View File

@@ -15,7 +15,7 @@ Future<void> migrateFromV3ToV4(Database db) async {
anime TEXT NOT NULL,
CONSTRAINT pk_watcher_join_table PRIMARY KEY(name, anime),
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/type.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:collection/collection.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -28,6 +30,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
on<AnimeRemovedEvent>(_onAnimeRemoved);
on<MangaRemovedEvent>(_onMangaRemoved);
on<AddButtonVisibilitySetEvent>(_onButtonVisibilityToggled);
on<AnimeListRequestedEvent>(_onAnimeListSummoned);
}
/// Internal anime state
@@ -122,8 +125,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
final anime = state.animes[index];
if (anime.episodesTotal != null &&
anime.episodesWatched + 1 > anime.episodesTotal!)
anime.episodesWatched + 1 > anime.episodesTotal!) {
return;
}
final newAnime = await GetIt.I
.get<DatabaseService>()
@@ -223,8 +227,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
final manga = state.mangas[index];
if (manga.chaptersTotal != null &&
manga.chaptersRead + 1 > manga.chaptersTotal!)
manga.chaptersRead + 1 > manga.chaptersTotal!) {
return;
}
final newManga = await GetIt.I
.get<DatabaseService>()
@@ -317,9 +322,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
);
// Update the cache
final cacheIndex = _mangas.indexWhere((m) => m.id == event.id);
final cacheIndex = _animes.indexWhere((m) => m.id == event.id);
assert(cacheIndex != -1, 'The anime must exist');
_mangas.removeAt(cacheIndex);
_animes.removeAt(cacheIndex);
// Update the database
await GetIt.I.get<DatabaseService>().deleteAnime(event.id);
@@ -338,9 +343,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
);
// Update the cache
final cacheIndex = _animes.indexWhere((a) => a.id == event.id);
final cacheIndex = _mangas.indexWhere((a) => a.id == event.id);
assert(cacheIndex != -1, 'The manga must exist');
_animes.removeAt(cacheIndex);
_mangas.removeAt(cacheIndex);
// Update the database
await GetIt.I.get<DatabaseService>().deleteManga(event.id);
@@ -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
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/manga.dart';
import 'package:anitrack/src/data/other_titles.dart';
import 'package:anitrack/src/data/search_result.dart';
import 'package:anitrack/src/data/source.dart';
import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/service/anilist/anilist_client.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart' as list;
import 'package:anitrack/src/ui/bloc/navigation_bloc.dart';
import 'package:anitrack/src/ui/constants.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get_it/get_it.dart';
import 'package:jikan_api/jikan_api.dart';
part 'anime_search_state.dart';
part 'anime_search_event.dart';
@@ -22,6 +24,9 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
on<ResultTappedEvent>(_onResultTapped);
}
// The client to use for AniList GraphQL queries.
final _anilistClient = AniListClient();
Future<void> _onRequested(
AnimeSearchRequestedEvent event,
Emitter<AnimeSearchState> emit,
@@ -36,9 +41,7 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
);
GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent(
const NavigationDestination(animeSearchRoute),
),
PushNavigationEvent(animeSearchRoute),
);
}
@@ -67,23 +70,23 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
if (state.trackingType == TrackingMediumType.anime) {
// Anime
final result = await Jikan().searchAnime(
query: state.searchQuery,
);
final result = await _anilistClient.searchAnime(state.searchQuery);
emit(
state.copyWith(
working: false,
searchResults: result
.map(
(Anime anime) => SearchResult(
(anime) => SearchResult(
anime.title,
anime.malId.toString(),
anime.titleEnglish,
anime.titleJapanese,
anime.id,
anime.episodes,
anime.imageUrl,
anime.synopsis ?? '',
anime.airing,
anime.broadcast?.split(' ').first,
anime.description ?? '',
anime.isAiring,
anime.broadcastDay,
),
)
.toList(),
@@ -91,21 +94,21 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
);
} else {
// Manga
final result = await Jikan().searchManga(
query: state.searchQuery,
);
final result = await _anilistClient.searchManga(state.searchQuery);
emit(
state.copyWith(
working: false,
searchResults: result
.map(
(Manga manga) => SearchResult(
(manga) => SearchResult(
manga.title,
manga.malId.toString(),
null,
null,
manga.id,
manga.chapters,
manga.imageUrl,
manga.synopsis ?? '',
manga.description ?? '',
// TODO(Unknown): Implement for Manga
false,
null,
@@ -133,6 +136,11 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
event.result.thumbnailUrl,
event.result.isAiring,
event.result.broadcastDay,
OtherTitles(
english: event.result.titleEnglish,
japanese: event.result.titleJapanese,
),
TrackingDataSource.anilist,
),
)
: list.MangaAddedEvent(
@@ -144,12 +152,17 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
0,
event.result.total,
event.result.thumbnailUrl,
OtherTitles(
english: event.result.titleEnglish,
japanese: event.result.titleJapanese,
),
TrackingDataSource.anilist,
),
),
);
GetIt.I.get<NavigationBloc>().add(
PoppedRouteEvent(),
GoNavigationEvent(animeListRoute),
);
}
}

View File

@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:anitrack/src/data/source.dart';
import 'package:anitrack/src/service/anilist/anilist_client.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -42,20 +44,36 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
String? broadcastDay;
bool airing;
int? episodes;
try {
final apiData = await Jikan().getAnime(int.parse(anime.id));
airing = apiData.airing;
broadcastDay = apiData.broadcast?.split(' ').first;
switch (anime.source) {
case TrackingDataSource.mal:
final apiData = await Jikan().getAnime(int.parse(anime.id));
airing = apiData.airing;
broadcastDay = apiData.broadcast?.split(' ').first;
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) {
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) {
al.add(
AnimeUpdatedEvent(
anime.copyWith(airing: false, broadcastDay: null),
anime.copyWith(
airing: false,
broadcastDay: null,
episodesTotal: episodes,
),
commit: true,
),
);
@@ -63,13 +81,17 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
print('Updating Anime "${anime.title}": broadcastDay=$broadcastDay');
al.add(
AnimeUpdatedEvent(
anime.copyWith(airing: true, broadcastDay: broadcastDay),
anime.copyWith(
airing: true,
broadcastDay: broadcastDay,
episodesTotal: episodes,
),
commit: true,
),
);
}
// Prevent hammering Jikan
// Prevent hammering Jikan/AniList
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(
PushedNamedEvent(
const NavigationDestination(detailsRoute),
),
PushNavigationEvent(detailsRoute),
);
}
@@ -57,14 +55,13 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
emit(
state.copyWith(
trackingType: TrackingMediumType.manga,
heroImagePrefix: event.heroImagePrefix,
data: event.manga,
),
);
GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent(
const NavigationDestination(detailsRoute),
),
PushNavigationEvent(detailsRoute),
);
}
@@ -118,14 +115,12 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
switch (event.trackingType) {
case TrackingMediumType.anime:
bloc.add(AnimeRemovedEvent(event.id));
break;
case TrackingMediumType.manga:
bloc.add(MangaRemovedEvent(event.id));
break;
}
// Navigate back
GetIt.I.get<NavigationBloc>().add(PoppedRouteEvent());
GetIt.I.get<NavigationBloc>().add(PopNavigationEvent());
}
Future<void> _onAnimeWatcherAdded(

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,19 @@
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/src/data/anime.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/service/database.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
true,
null,
OtherTitles(),
TrackingDataSource.mal,
),
);
}
@@ -207,6 +211,8 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
// 0 means that MAL does not know
totalChapters == 0 ? null : totalChapters,
data.imageUrl,
OtherTitles(),
TrackingDataSource.mal,
),
);
}
@@ -244,7 +250,7 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
Emitter<SettingsState> emit,
) async {
final al = GetIt.I.get<AnimeListBloc>();
final exportArchive = archive.GZipDecoder().decodeBytes(
final exportArchive = const archive.GZipDecoder().decodeBytes(
await File(event.path).readAsBytes(),
);
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/src/ui/constants.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
Widget getDrawer(BuildContext context) {
return Drawer(
@@ -22,34 +23,30 @@ Widget getDrawer(BuildContext context) {
leading: const Icon(Icons.list),
title: Text(t.content.list),
onTap: () {
Navigator.of(context).pushNamedAndRemoveUntil(
animeListRoute,
(_) => false,
);
GoRouter.of(context).go(animeListRoute);
Navigator.of(context).pop();
},
),
ListTile(
leading: const Icon(Icons.calendar_today),
title: Text(t.calendar.calendar),
onTap: () {
Navigator.of(context).pushNamedAndRemoveUntil(
calendarRoute,
(_) => false,
);
GoRouter.of(context).go(calendarRoute);
Navigator.of(context).pop();
},
),
ListTile(
leading: const Icon(Icons.settings),
title: Text(t.settings.title),
onTap: () {
Navigator.of(context).pushNamed(settingsRoute);
GoRouter.of(context).push(settingsRoute);
},
),
ListTile(
leading: const Icon(Icons.info),
title: Text(t.about.title),
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/licenses.g.dart';
import 'package:anitrack/oss_licenses.dart';
import 'package:anitrack/src/ui/constants.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutPage extends StatelessWidget {
@@ -22,9 +23,15 @@ class AboutPage extends StatelessWidget {
return Scaffold(
appBar: AppBar(
title: Text(t.about.title),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
},
),
),
body: ListView.builder(
itemCount: ossLicenses.length + 1,
itemCount: dependencies.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
@@ -41,7 +48,11 @@ class AboutPage extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.only(
top: 8,
left: 4,
right: 4,
),
child: ElevatedButton(
onPressed: () async {
await launchUrl(
@@ -55,7 +66,11 @@ class AboutPage extends StatelessWidget {
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.only(
top: 8,
left: 4,
right: 4,
),
child: ElevatedButton(
onPressed: () async {
final licenseText = await rootBundle.loadString(
@@ -90,9 +105,7 @@ class AboutPage extends StatelessWidget {
);
}
final dep = ossLicenses[index - 1];
if (!dep.isDirectDependency) return Container();
final dep = dependencies[index - 1];
return ListTile(
title: Text(dep.name),
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/details_bloc.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/image.dart';
import 'package:bottom_bar/bottom_bar.dart';
@@ -24,40 +23,7 @@ class AnimeListPage extends StatefulWidget {
),
);
@override
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) {
static String getPageTitle(TrackingMediumType type) {
switch (type) {
case TrackingMediumType.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,
) {
return [
@@ -90,14 +56,14 @@ class AnimeListPageState extends State<AnimeListPage> {
value: MediumTrackingState.paused,
child: Text(MediumTrackingState.paused.getName(type)),
),
const PopupMenuItem<MediumTrackingState>(
PopupMenuItem<MediumTrackingState>(
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) {
case TrackingMediumType.anime:
return PopupMenuButton(
@@ -110,7 +76,8 @@ class AnimeListPageState extends State<AnimeListPage> {
AnimeFilterChangedEvent(filterState),
);
},
itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.anime),
itemBuilder: (_) =>
AnimeListPage.getPopupButtonItems(TrackingMediumType.anime),
);
case TrackingMediumType.manga:
return PopupMenuButton(
@@ -123,183 +90,224 @@ class AnimeListPageState extends State<AnimeListPage> {
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
Widget build(BuildContext context) {
return BlocBuilder<AnimeListBloc, AnimeListState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(
title: Text(
_getPageTitle(state.trackingType),
return IndexedStack(
index: state.trackingType == TrackingMediumType.anime ? 0 : 1,
children: [
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: [
_getPopupButton(context, state),
],
),
drawer: getDrawer(context),
body: PageView(
// Prevent swiping between pages
// (https://github.com/flutter/flutter/issues/37510#issuecomment-612663656)
physics: const NeverScrollableScrollPhysics(),
controller: _controller,
children: [
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,
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: 'grid_${manga.id}',
url: manga.thumbnailUrl,
onTap: () {
context.read<DetailsBloc>().add(
MangaDetailsRequestedEvent(
manga,
heroImagePrefix: 'grid_',
),
);
},
plusCallback: () {
context.read<AnimeListBloc>().add(
AnimeEpisodeIncrementedEvent(
anime.id,
),
);
},
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,
),
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,
),
),
),
);
},
),
),
);
},
),
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
Widget build(BuildContext context) {
return BlocBuilder<AnimeSearchBloc, AnimeSearchState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(
title: Text(
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),
);
},
),
return BlocListener<AnimeSearchBloc, AnimeSearchState>(
listener: (context, state) {},
child: BlocBuilder<AnimeSearchBloc, AnimeSearchState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(
title: Text(
state.trackingType == TrackingMediumType.anime
? t.search.anime
: t.search.manga,
),
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,
),
),
],
),
),
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(
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/details_bloc.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/image.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
CalendarPageState createState() => CalendarPageState();
}
class CalendarPageState extends State<CalendarPage> {
/// State for the "refreshing" overlay.
OverlayEntry? _overlayEntry;
List<Widget> _renderWeekdayList(
BuildContext context,
Weekday day,
@@ -122,8 +140,7 @@ class CalendarPageState extends State<CalendarPage> {
];
}
@override
Widget build(BuildContext context) {
Map<Weekday, List<AnimeTrackingData>> _constructAiringMap() {
final airingAnimeMap = <Weekday, List<AnimeTrackingData>>{};
for (final anime in GetIt.I.get<AnimeListBloc>().unfilteredAnime) {
if (!anime.airing) continue;
@@ -132,192 +149,153 @@ class CalendarPageState extends State<CalendarPage> {
switch (anime.broadcastDay) {
case 'Mondays':
day = Weekday.monday;
break;
case 'Tuesdays':
day = Weekday.tuesday;
break;
case 'Wednesdays':
day = Weekday.wednesday;
break;
case 'Thursdays':
day = Weekday.thursday;
break;
case 'Fridays':
day = Weekday.friday;
break;
case 'Saturdays':
day = Weekday.saturday;
break;
case 'Sundays':
day = Weekday.sunday;
break;
default:
day = Weekday.unknown;
break;
}
airingAnimeMap.addOrSet(day, anime);
}
return airingAnimeMap;
}
return BlocListener<CalendarBloc, CalendarState>(
listenWhen: (previous, current) =>
previous.refreshing != current.refreshing,
listener: (context, state) {
// Force an update
if (!state.refreshing) {
setState(() {});
}
},
child: WillPopScope(
onWillPop: () async => !context.read<CalendarBloc>().state.refreshing,
child: Stack(
children: [
Positioned(
left: 8,
right: 8,
top: 0,
bottom: 0,
child: Scaffold(
appBar: AppBar(
title: Text(t.calendar.calendar),
actions: [
IconButton(
onPressed: () {
context.read<CalendarBloc>().add(
RefreshPerformedEvent(),
);
},
icon: const Icon(Icons.refresh),
),
],
),
drawer: getDrawer(context),
body: Padding(
padding: const EdgeInsetsGeometry.symmetric(
horizontal: 12,
),
child: CustomScrollView(
slivers: [
// Render all available weekdays
..._renderWeekdayList(
context,
Weekday.unknown,
airingAnimeMap,
@override
Widget build(BuildContext context) {
return BlocBuilder<AnimeListBloc, AnimeListState>(
builder: (context, animeListState) {
final airingAnimeMap = _constructAiringMap();
return BlocListener<CalendarBloc, CalendarState>(
listenWhen: (previous, current) =>
previous.refreshing != current.refreshing,
listener: (context, state) {
if (!state.refreshing) {
_overlayEntry?.remove();
_overlayEntry?.dispose();
_overlayEntry = null;
} else {
_overlayEntry = OverlayEntry(
builder: (context) => SafeArea(
child: Stack(
children: [
const ModalBarrier(
dismissible: false,
color: Colors.black54,
),
..._renderWeekdayList(
context,
Weekday.monday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.tuesday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.wednesday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.thursday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.friday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.saturday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.sunday,
airingAnimeMap,
),
// Provide a nice bottom padding, while keeping the elastic effect attached
// to the bottom-most edge.
const SliverToBoxAdapter(
Center(
child: SizedBox(
height: 16,
width: 150,
height: 150,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.grey.shade800,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.all(25),
child: CircularProgressIndicator(),
),
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,
),
),
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: BlocBuilder<CalendarBloc, CalendarState>(
buildWhen: (previous, current) =>
previous.refreshing != current.refreshing,
builder: (context, state) {
if (!state.refreshing) {
return const SizedBox();
}
child: CustomScrollView(
slivers: [
// Render all available weekdays
..._renderWeekdayList(
context,
Weekday.unknown,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.monday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.tuesday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.wednesday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.thursday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.friday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.saturday,
airingAnimeMap,
),
..._renderWeekdayList(
context,
Weekday.sunday,
airingAnimeMap,
),
return const ModalBarrier(
dismissible: false,
color: Colors.black54,
);
},
),
),
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: BlocBuilder<CalendarBloc, CalendarState>(
builder: (context, state) {
if (!state.refreshing) {
return const SizedBox();
}
return Center(
// Provide a nice bottom padding, while keeping the elastic effect attached
// to the bottom-most edge.
const SliverToBoxAdapter(
child: SizedBox(
width: 150,
height: 150,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.grey.shade800,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.all(25),
child: CircularProgressIndicator(),
),
Text(
t.settings.importIndicator(
current: state.refreshingCount,
total: state.refreshingTotal,
),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
height: 16,
),
);
},
),
],
),
),
],
),
),
),
);
},
);
}
}

View File

@@ -1,7 +1,9 @@
import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/src/data/anime.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/source.dart';
import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/ui/bloc/details_bloc.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:fluttertoast/fluttertoast.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
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
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(t.details.title),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
},
),
),
body: BlocBuilder<DetailsBloc, DetailsState>(
builder: (context, state) {
@@ -246,20 +280,30 @@ class DetailsPage extends StatelessWidget {
children: [
ElevatedButton(
onPressed: () async {
final url = switch (state
.trackingType) {
TrackingMediumType.anime =>
'https://myanimelist.net/anime/${state.data!.id}',
TrackingMediumType.manga =>
'https://myanimelist.net/manga/${state.data!.id}',
};
final url =
switch (state.trackingType) {
TrackingMediumType.anime =>
_animeUrl(
state.data!
as AnimeTrackingData,
),
TrackingMediumType.manga =>
_mangaUrl(
state.data!
as MangaTrackingData,
),
};
await launchUrl(
Uri.parse(url),
mode: LaunchMode
.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(
controller: _controller,
decoration: InputDecoration(
border: const OutlineInputBorder(),
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_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
class SettingsPage extends StatelessWidget {
@@ -35,6 +36,12 @@ class SettingsPage extends StatelessWidget {
child: Scaffold(
appBar: AppBar(
title: Text(t.settings.title),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
},
),
),
body: ListView(
children: [
@@ -43,7 +50,7 @@ class SettingsPage extends StatelessWidget {
subtitle: Text(t.settings.importAnimeDesc),
onTap: () async {
// Pick the file
final result = await FilePicker.platform.pickFiles();
final result = await FilePicker.pickFiles();
if (result == null) return;
if (!result.files.first.path!.endsWith('.xml.gz')) {
@@ -70,7 +77,7 @@ class SettingsPage extends StatelessWidget {
subtitle: Text(t.settings.importMangaDesc),
onTap: () async {
// Pick the file
final result = await FilePicker.platform.pickFiles();
final result = await FilePicker.pickFiles();
if (result == null) return;
if (!result.files.first.path!.endsWith('.xml.gz')) {
@@ -96,14 +103,14 @@ class SettingsPage extends StatelessWidget {
title: Text(t.settings.exportData),
onTap: () async {
// Pick the file
final result = await FilePicker.platform
.getDirectoryPath();
final result = await FilePicker.getDirectoryPath();
if (result == null) return;
if (!(await Permission.manageExternalStorage
.request())
.isGranted)
.isGranted) {
return;
}
GetIt.I.get<SettingsBloc>().add(
DataExportedEvent(
@@ -116,7 +123,7 @@ class SettingsPage extends StatelessWidget {
title: Text(t.settings.importData),
onTap: () async {
// Pick the file
final result = await FilePicker.platform.pickFiles();
final result = await FilePicker.pickFiles();
if (result == null) return;
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';
class SelectorItem<T> {

View File

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
class AnimeCoverImage extends StatelessWidget {
const AnimeCoverImage({
required this.url,
required this.hero,
this.hero,
this.cached = true,
this.extra,
this.onTap,
@@ -18,7 +18,7 @@ class AnimeCoverImage extends StatelessWidget {
final bool cached;
/// The hero tag of the image.
final String hero;
final String? hero;
/// An extra widget with a translucent backdrop.
final Widget? extra;
@@ -28,53 +28,59 @@ class AnimeCoverImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Hero(
tag: hero,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Material(
child: SizedBox(
height: 100 * (16 / 9),
width: 120,
child: InkWell(
onTap: onTap ?? () {},
child: Stack(
children: [
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: cached
? CachedNetworkImageProvider(url) as ImageProvider
: NetworkImage(url),
fit: BoxFit.cover,
),
final image = ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Material(
child: SizedBox(
height: 100 * (16 / 9),
width: 120,
child: InkWell(
onTap: onTap ?? () {},
child: Stack(
children: [
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: cached
? CachedNetworkImageProvider(url) as ImageProvider
: NetworkImage(url),
fit: BoxFit.cover,
),
),
),
if (extra != null)
Positioned(
left: 0,
bottom: 0,
right: 0,
child: SizedBox(
height: 40,
child: ColoredBox(
color: Colors.black54,
child: extra,
),
),
if (extra != null)
Positioned(
left: 0,
bottom: 0,
right: 0,
child: SizedBox(
height: 40,
child: ColoredBox(
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
key: UniqueKey(),
backgroundBuilder: (_, direction, __) {
backgroundBuilder: (_, direction, _) {
if (direction == SwipeDirection.endToStart) {
return const Align(
alignment: Alignment.centerRight,
@@ -76,8 +76,6 @@ class ListItem extends StatelessWidget {
children: [
AnimeCoverImage(
cached: cached,
// TODO(Unknown): Have the ID here
hero: thumbnailUrl,
extra: imageExtra,
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
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)

View File

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

View File

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