Compare commits
13 Commits
34b3817e6d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e489cae228 | |||
| 06da4dbb16 | |||
| 18f0d55e00 | |||
| d7566c9414 | |||
| c5c7711611 | |||
| 84594b42cf | |||
| 437d76fddc | |||
| 3a6ab1e825 | |||
| 39cc9b8d7c | |||
| 58ea71e694 | |||
| 04d64c3178 | |||
| 69553707a9 | |||
| d034875e16 |
5632
lib/licenses.g.dart
5632
lib/licenses.g.dart
File diff suppressed because it is too large
Load Diff
@@ -14,13 +14,14 @@ import 'package:anitrack/src/ui/pages/anime_search.dart';
|
|||||||
import 'package:anitrack/src/ui/pages/calendar.dart';
|
import 'package:anitrack/src/ui/pages/calendar.dart';
|
||||||
import 'package:anitrack/src/ui/pages/details/details.dart';
|
import 'package:anitrack/src/ui/pages/details/details.dart';
|
||||||
import 'package:anitrack/src/ui/pages/settings.dart';
|
import 'package:anitrack/src/ui/pages/settings.dart';
|
||||||
|
import 'package:anitrack/src/ui/widgets/shell_wrapper.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
final navKey = GlobalKey<NavigatorState>();
|
|
||||||
|
|
||||||
// Initialize the widgets binding for sqflite
|
// Initialize the widgets binding for sqflite
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ void main() async {
|
|||||||
GetIt.I.registerSingleton<AnimeListBloc>(AnimeListBloc());
|
GetIt.I.registerSingleton<AnimeListBloc>(AnimeListBloc());
|
||||||
GetIt.I.registerSingleton<AnimeSearchBloc>(AnimeSearchBloc());
|
GetIt.I.registerSingleton<AnimeSearchBloc>(AnimeSearchBloc());
|
||||||
GetIt.I.registerSingleton<DetailsBloc>(DetailsBloc());
|
GetIt.I.registerSingleton<DetailsBloc>(DetailsBloc());
|
||||||
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navKey));
|
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc());
|
||||||
GetIt.I.registerSingleton<SettingsBloc>(SettingsBloc());
|
GetIt.I.registerSingleton<SettingsBloc>(SettingsBloc());
|
||||||
GetIt.I.registerSingleton<CalendarBloc>(CalendarBloc());
|
GetIt.I.registerSingleton<CalendarBloc>(CalendarBloc());
|
||||||
GetIt.I.registerSingleton<AniListClient>(AniListClient());
|
GetIt.I.registerSingleton<AniListClient>(AniListClient());
|
||||||
@@ -68,22 +69,72 @@ void main() async {
|
|||||||
create: (_) => GetIt.I.get<CalendarBloc>(),
|
create: (_) => GetIt.I.get<CalendarBloc>(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: MyApp(navKey),
|
child: MyApp(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp(
|
MyApp({super.key});
|
||||||
this.navKey, {
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final GlobalKey<NavigatorState> navKey;
|
/// The router to attach to the main app.
|
||||||
|
final _router = GoRouter(
|
||||||
|
initialLocation: animeListRoute,
|
||||||
|
debugLogDiagnostics: kDebugMode,
|
||||||
|
routes: [
|
||||||
|
ShellRoute(
|
||||||
|
builder: (context, state, child) {
|
||||||
|
return ShellWrapper(state: state, child: child);
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: animeListRoute,
|
||||||
|
builder: (context, state) => const AnimeListPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: calendarRoute,
|
||||||
|
builder: (context, state) => const CalendarPage(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: animeSearchRoute,
|
||||||
|
builder: (context, state) => const AnimeSearchPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: detailsRoute,
|
||||||
|
builder: (context, state) => const DetailsPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: aboutRoute,
|
||||||
|
builder: (context, state) => const AboutPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: settingsRoute,
|
||||||
|
builder: (context, state) => const SettingsPage(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return BlocListener<NavigationBloc, NavigationState>(
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: MaterialApp.router(
|
||||||
title: 'AniTrack',
|
title: 'AniTrack',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
@@ -95,26 +146,8 @@ class MyApp extends StatelessWidget {
|
|||||||
primarySwatch: Colors.blue,
|
primarySwatch: Colors.blue,
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
),
|
),
|
||||||
navigatorKey: navKey,
|
routerConfig: _router,
|
||||||
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 null;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ import 'package:graphql/client.dart';
|
|||||||
class AniListClient {
|
class AniListClient {
|
||||||
/// The base GraphQL client for AniList
|
/// The base GraphQL client for AniList
|
||||||
final _client = GraphQLClient(
|
final _client = GraphQLClient(
|
||||||
link: HttpLink("https://graphql.anilist.co"),
|
link: HttpLink('https://graphql.anilist.co'),
|
||||||
cache: GraphQLCache(),
|
cache: GraphQLCache(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -42,11 +42,11 @@ class AniListClient {
|
|||||||
);
|
);
|
||||||
if (result.hasException) {
|
if (result.hasException) {
|
||||||
// TODO: Handle this more elegantly
|
// TODO: Handle this more elegantly
|
||||||
print(result.exception.toString());
|
print(result.exception);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (result.data!["Page"]["media"] as List<Object?>)
|
return (result.data!['Page']['media'] as List<Object?>)
|
||||||
.cast<Map<String, dynamic>>()
|
.cast<Map<String, dynamic>>()
|
||||||
.map(AnimeSearchResult.fromJson)
|
.map(AnimeSearchResult.fromJson)
|
||||||
.toList();
|
.toList();
|
||||||
@@ -83,11 +83,11 @@ class AniListClient {
|
|||||||
);
|
);
|
||||||
if (result.hasException) {
|
if (result.hasException) {
|
||||||
// TODO: Handle this more elegantly
|
// TODO: Handle this more elegantly
|
||||||
print(result.exception.toString());
|
print(result.exception);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (result.data!["Page"]["media"] as List<Object?>)
|
return (result.data!['Page']['media'] as List<Object?>)
|
||||||
.cast<Map<String, dynamic>>()
|
.cast<Map<String, dynamic>>()
|
||||||
.map(MangaSearchResult.fromJson)
|
.map(MangaSearchResult.fromJson)
|
||||||
.toList();
|
.toList();
|
||||||
@@ -119,7 +119,7 @@ class AniListClient {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (result.hasException) {
|
if (result.hasException) {
|
||||||
print(result.exception.toString());
|
print(result.exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Anime.fromJson(result.data!['Media'] as Map<String, dynamic>);
|
return Anime.fromJson(result.data!['Media'] as Map<String, dynamic>);
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ abstract class Anime with _$Anime {
|
|||||||
required String title,
|
required String title,
|
||||||
required bool isAiring,
|
required bool isAiring,
|
||||||
required String? broadcastDay,
|
required String? broadcastDay,
|
||||||
|
required int? episodes,
|
||||||
}) = _Anime;
|
}) = _Anime;
|
||||||
|
|
||||||
factory Anime.fromJson(Map<String, Object?> json) {
|
factory Anime.fromJson(Map<String, Object?> json) {
|
||||||
@@ -88,6 +89,7 @@ abstract class Anime with _$Anime {
|
|||||||
return Anime(
|
return Anime(
|
||||||
title: (json['title']! as Map<String, dynamic>)['romaji']! as String,
|
title: (json['title']! as Map<String, dynamic>)['romaji']! as String,
|
||||||
isAiring: json['status'] == 'RELEASING',
|
isAiring: json['status'] == 'RELEASING',
|
||||||
|
episodes: json['episodes'] as int?,
|
||||||
broadcastDay: airingDayOfTheWeek,
|
broadcastDay: airingDayOfTheWeek,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -570,7 +570,7 @@ as String?,
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$Anime {
|
mixin _$Anime {
|
||||||
|
|
||||||
String get title; bool get isAiring; String? get broadcastDay;
|
String get title; bool get isAiring; String? get broadcastDay; int? get episodes;
|
||||||
/// Create a copy of Anime
|
/// Create a copy of Anime
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -581,16 +581,16 @@ $AnimeCopyWith<Anime> get copyWith => _$AnimeCopyWithImpl<Anime>(this as Anime,
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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));
|
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
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,title,isAiring,broadcastDay);
|
int get hashCode => Object.hash(runtimeType,title,isAiring,broadcastDay,episodes);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'Anime(title: $title, isAiring: $isAiring, broadcastDay: $broadcastDay)';
|
return 'Anime(title: $title, isAiring: $isAiring, broadcastDay: $broadcastDay, episodes: $episodes)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -601,7 +601,7 @@ abstract mixin class $AnimeCopyWith<$Res> {
|
|||||||
factory $AnimeCopyWith(Anime value, $Res Function(Anime) _then) = _$AnimeCopyWithImpl;
|
factory $AnimeCopyWith(Anime value, $Res Function(Anime) _then) = _$AnimeCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String title, bool isAiring, String? broadcastDay
|
String title, bool isAiring, String? broadcastDay, int? episodes
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -618,12 +618,13 @@ class _$AnimeCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of Anime
|
/// Create a copy of Anime
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? title = null,Object? isAiring = null,Object? broadcastDay = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? title = null,Object? isAiring = null,Object? broadcastDay = freezed,Object? episodes = freezed,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
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 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 bool,broadcastDay: freezed == broadcastDay ? _self.broadcastDay : broadcastDay // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
as String?,episodes: freezed == episodes ? _self.episodes : episodes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,10 +709,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String title, bool isAiring, String? broadcastDay)? $default,{required TResult orElse(),}) {final _that = this;
|
@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) {
|
switch (_that) {
|
||||||
case _Anime() when $default != null:
|
case _Anime() when $default != null:
|
||||||
return $default(_that.title,_that.isAiring,_that.broadcastDay);case _:
|
return $default(_that.title,_that.isAiring,_that.broadcastDay,_that.episodes);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -729,10 +730,10 @@ return $default(_that.title,_that.isAiring,_that.broadcastDay);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String title, bool isAiring, String? broadcastDay) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String title, bool isAiring, String? broadcastDay, int? episodes) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Anime():
|
case _Anime():
|
||||||
return $default(_that.title,_that.isAiring,_that.broadcastDay);case _:
|
return $default(_that.title,_that.isAiring,_that.broadcastDay,_that.episodes);case _:
|
||||||
throw StateError('Unexpected subclass');
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -749,10 +750,10 @@ return $default(_that.title,_that.isAiring,_that.broadcastDay);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String title, bool isAiring, String? broadcastDay)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String title, bool isAiring, String? broadcastDay, int? episodes)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Anime() when $default != null:
|
case _Anime() when $default != null:
|
||||||
return $default(_that.title,_that.isAiring,_that.broadcastDay);case _:
|
return $default(_that.title,_that.isAiring,_that.broadcastDay,_that.episodes);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -764,12 +765,13 @@ return $default(_that.title,_that.isAiring,_that.broadcastDay);case _:
|
|||||||
|
|
||||||
|
|
||||||
class _Anime implements Anime {
|
class _Anime implements Anime {
|
||||||
const _Anime({required this.title, required this.isAiring, required this.broadcastDay});
|
const _Anime({required this.title, required this.isAiring, required this.broadcastDay, required this.episodes});
|
||||||
|
|
||||||
|
|
||||||
@override final String title;
|
@override final String title;
|
||||||
@override final bool isAiring;
|
@override final bool isAiring;
|
||||||
@override final String? broadcastDay;
|
@override final String? broadcastDay;
|
||||||
|
@override final int? episodes;
|
||||||
|
|
||||||
/// Create a copy of Anime
|
/// Create a copy of Anime
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -781,16 +783,16 @@ _$AnimeCopyWith<_Anime> get copyWith => __$AnimeCopyWithImpl<_Anime>(this, _$ide
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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));
|
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
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,title,isAiring,broadcastDay);
|
int get hashCode => Object.hash(runtimeType,title,isAiring,broadcastDay,episodes);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'Anime(title: $title, isAiring: $isAiring, broadcastDay: $broadcastDay)';
|
return 'Anime(title: $title, isAiring: $isAiring, broadcastDay: $broadcastDay, episodes: $episodes)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -801,7 +803,7 @@ abstract mixin class _$AnimeCopyWith<$Res> implements $AnimeCopyWith<$Res> {
|
|||||||
factory _$AnimeCopyWith(_Anime value, $Res Function(_Anime) _then) = __$AnimeCopyWithImpl;
|
factory _$AnimeCopyWith(_Anime value, $Res Function(_Anime) _then) = __$AnimeCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String title, bool isAiring, String? broadcastDay
|
String title, bool isAiring, String? broadcastDay, int? episodes
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -818,12 +820,13 @@ class __$AnimeCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of Anime
|
/// Create a copy of Anime
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? title = null,Object? isAiring = null,Object? broadcastDay = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? title = null,Object? isAiring = null,Object? broadcastDay = freezed,Object? episodes = freezed,}) {
|
||||||
return _then(_Anime(
|
return _then(_Anime(
|
||||||
title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
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 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 bool,broadcastDay: freezed == broadcastDay ? _self.broadcastDay : broadcastDay // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
as String?,episodes: freezed == episodes ? _self.episodes : episodes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:anitrack/src/service/migrations/0000_score.dart';
|
|||||||
import 'package:anitrack/src/service/migrations/0001_anime_watcher.dart';
|
import 'package:anitrack/src/service/migrations/0001_anime_watcher.dart';
|
||||||
import 'package:anitrack/src/service/migrations/0002_anilist.dart';
|
import 'package:anitrack/src/service/migrations/0002_anilist.dart';
|
||||||
import 'package:anitrack/src/service/migrations/0003_other_titles.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/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
@@ -73,8 +74,8 @@ Future<void> _createDatabase(Database db, int version) async {
|
|||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
anime TEXT NOT NULL,
|
anime TEXT NOT NULL,
|
||||||
CONSTRAINT pk_watcher_join_table PRIMARY KEY(name, anime),
|
CONSTRAINT pk_watcher_join_table PRIMARY KEY(name, anime),
|
||||||
CONSTRAINT fk_watcher FOREIGN KEY (name) REFERENCES $animeWatcherTable(name),
|
CONSTRAINT fk_watcher FOREIGN KEY (name) REFERENCES $animeWatcherTable(name) ON DELETE CASCADE,
|
||||||
CONSTRAINT fk_anime FOREIGN KEY (anime) REFERENCES $animeTable(id)
|
CONSTRAINT fk_anime FOREIGN KEY (anime) REFERENCES $animeTable(id) ON DELETE CASCADE
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -106,7 +107,7 @@ class DatabaseService {
|
|||||||
print('Opening database at $databasePath');
|
print('Opening database at $databasePath');
|
||||||
_db = await openDatabase(
|
_db = await openDatabase(
|
||||||
databasePath,
|
databasePath,
|
||||||
version: 6,
|
version: 7,
|
||||||
onConfigure: (db) async {
|
onConfigure: (db) async {
|
||||||
// In order to do schema changes during database upgrades, we disable foreign
|
// In order to do schema changes during database upgrades, we disable foreign
|
||||||
// keys in the onConfigure phase, but re-enable them here.
|
// keys in the onConfigure phase, but re-enable them here.
|
||||||
@@ -134,6 +135,9 @@ class DatabaseService {
|
|||||||
if (oldVersion < 6) {
|
if (oldVersion < 6) {
|
||||||
await migrateFromV5ToV6(db);
|
await migrateFromV5ToV6(db);
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 7) {
|
||||||
|
await migrateFromV6ToV7(db);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
22
lib/src/service/migrations/0004_fix_constraints.dart
Normal file
22
lib/src/service/migrations/0004_fix_constraints.dart
Normal 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',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -125,8 +125,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
|
|
||||||
final anime = state.animes[index];
|
final anime = state.animes[index];
|
||||||
if (anime.episodesTotal != null &&
|
if (anime.episodesTotal != null &&
|
||||||
anime.episodesWatched + 1 > anime.episodesTotal!)
|
anime.episodesWatched + 1 > anime.episodesTotal!) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final newAnime = await GetIt.I
|
final newAnime = await GetIt.I
|
||||||
.get<DatabaseService>()
|
.get<DatabaseService>()
|
||||||
@@ -226,8 +227,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
|
|
||||||
final manga = state.mangas[index];
|
final manga = state.mangas[index];
|
||||||
if (manga.chaptersTotal != null &&
|
if (manga.chaptersTotal != null &&
|
||||||
manga.chaptersRead + 1 > manga.chaptersTotal!)
|
manga.chaptersRead + 1 > manga.chaptersTotal!) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final newManga = await GetIt.I
|
final newManga = await GetIt.I
|
||||||
.get<DatabaseService>()
|
.get<DatabaseService>()
|
||||||
@@ -372,10 +374,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
GetIt.I.get<NavigationBloc>().add(
|
GetIt.I.get<NavigationBloc>().add(
|
||||||
PushedNamedAndRemoveUntilEvent(
|
GoNavigationEvent(animeListRoute),
|
||||||
const NavigationDestination(animeListRoute),
|
|
||||||
(_) => false,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import 'package:anitrack/src/data/search_result.dart';
|
|||||||
import 'package:anitrack/src/data/source.dart';
|
import 'package:anitrack/src/data/source.dart';
|
||||||
import 'package:anitrack/src/data/type.dart';
|
import 'package:anitrack/src/data/type.dart';
|
||||||
import 'package:anitrack/src/service/anilist/anilist_client.dart';
|
import 'package:anitrack/src/service/anilist/anilist_client.dart';
|
||||||
import 'package:anitrack/src/service/anilist/model.dart';
|
|
||||||
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart' as list;
|
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart' as list;
|
||||||
import 'package:anitrack/src/ui/bloc/navigation_bloc.dart';
|
import 'package:anitrack/src/ui/bloc/navigation_bloc.dart';
|
||||||
import 'package:anitrack/src/ui/constants.dart';
|
import 'package:anitrack/src/ui/constants.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:jikan_api/jikan_api.dart';
|
|
||||||
|
|
||||||
part 'anime_search_state.dart';
|
part 'anime_search_state.dart';
|
||||||
part 'anime_search_event.dart';
|
part 'anime_search_event.dart';
|
||||||
@@ -43,9 +41,7 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
GetIt.I.get<NavigationBloc>().add(
|
GetIt.I.get<NavigationBloc>().add(
|
||||||
PushedNamedEvent(
|
PushNavigationEvent(animeSearchRoute),
|
||||||
const NavigationDestination(animeSearchRoute),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +162,7 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
GetIt.I.get<NavigationBloc>().add(
|
GetIt.I.get<NavigationBloc>().add(
|
||||||
PoppedRouteEvent(),
|
GoNavigationEvent(animeListRoute),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,29 +44,36 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
|
|
||||||
String? broadcastDay;
|
String? broadcastDay;
|
||||||
bool airing;
|
bool airing;
|
||||||
|
int? episodes;
|
||||||
try {
|
try {
|
||||||
switch (anime.source) {
|
switch (anime.source) {
|
||||||
case TrackingDataSource.mal:
|
case TrackingDataSource.mal:
|
||||||
final apiData = await Jikan().getAnime(int.parse(anime.id));
|
final apiData = await Jikan().getAnime(int.parse(anime.id));
|
||||||
airing = apiData.airing;
|
airing = apiData.airing;
|
||||||
broadcastDay = apiData.broadcast?.split(' ').first;
|
broadcastDay = apiData.broadcast?.split(' ').first;
|
||||||
|
episodes = apiData.episodes;
|
||||||
case TrackingDataSource.anilist:
|
case TrackingDataSource.anilist:
|
||||||
final apiData = await GetIt.I.get<AniListClient>().getAnimeById(
|
final apiData = await GetIt.I.get<AniListClient>().getAnimeById(
|
||||||
anime.id,
|
anime.id,
|
||||||
);
|
);
|
||||||
airing = apiData.isAiring;
|
airing = apiData.isAiring;
|
||||||
broadcastDay = apiData.broadcastDay;
|
broadcastDay = apiData.broadcastDay;
|
||||||
|
episodes = apiData.episodes;
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
print('API request for anime ${anime.id} failed: $ex');
|
print('API request for anime ${anime.id} failed: $ex');
|
||||||
airing = false;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
print('Anime "${anime.title}": airing=$airing');
|
print('Anime "${anime.title}": airing=$airing; episodes=$episodes');
|
||||||
if (!airing) {
|
if (!airing) {
|
||||||
al.add(
|
al.add(
|
||||||
AnimeUpdatedEvent(
|
AnimeUpdatedEvent(
|
||||||
anime.copyWith(airing: false, broadcastDay: null),
|
anime.copyWith(
|
||||||
|
airing: false,
|
||||||
|
broadcastDay: null,
|
||||||
|
episodesTotal: episodes,
|
||||||
|
),
|
||||||
commit: true,
|
commit: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -74,13 +81,17 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
print('Updating Anime "${anime.title}": broadcastDay=$broadcastDay');
|
print('Updating Anime "${anime.title}": broadcastDay=$broadcastDay');
|
||||||
al.add(
|
al.add(
|
||||||
AnimeUpdatedEvent(
|
AnimeUpdatedEvent(
|
||||||
anime.copyWith(airing: true, broadcastDay: broadcastDay),
|
anime.copyWith(
|
||||||
|
airing: true,
|
||||||
|
broadcastDay: broadcastDay,
|
||||||
|
episodesTotal: episodes,
|
||||||
|
),
|
||||||
commit: true,
|
commit: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent hammering Jikan
|
// Prevent hammering Jikan/AniList
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
GetIt.I.get<NavigationBloc>().add(
|
GetIt.I.get<NavigationBloc>().add(
|
||||||
PushedNamedEvent(
|
PushNavigationEvent(detailsRoute),
|
||||||
const NavigationDestination(detailsRoute),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,9 +61,7 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
GetIt.I.get<NavigationBloc>().add(
|
GetIt.I.get<NavigationBloc>().add(
|
||||||
PushedNamedEvent(
|
PushNavigationEvent(detailsRoute),
|
||||||
const NavigationDestination(detailsRoute),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,14 +115,12 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
|
|||||||
switch (event.trackingType) {
|
switch (event.trackingType) {
|
||||||
case TrackingMediumType.anime:
|
case TrackingMediumType.anime:
|
||||||
bloc.add(AnimeRemovedEvent(event.id));
|
bloc.add(AnimeRemovedEvent(event.id));
|
||||||
break;
|
|
||||||
case TrackingMediumType.manga:
|
case TrackingMediumType.manga:
|
||||||
bloc.add(MangaRemovedEvent(event.id));
|
bloc.add(MangaRemovedEvent(event.id));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate back
|
// Navigate back
|
||||||
GetIt.I.get<NavigationBloc>().add(PoppedRouteEvent());
|
GetIt.I.get<NavigationBloc>().add(PopNavigationEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAnimeWatcherAdded(
|
Future<void> _onAnimeWatcherAdded(
|
||||||
|
|||||||
@@ -1,57 +1,39 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
part 'navigation_event.dart';
|
part 'navigation_event.dart';
|
||||||
part 'navigation_state.dart';
|
part 'navigation_state.dart';
|
||||||
|
|
||||||
class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
|
class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
|
||||||
NavigationBloc(this.navigationKey) : super(NavigationState()) {
|
NavigationBloc() : super(NoopNavigationState()) {
|
||||||
on<PushedNamedEvent>(_onPushedNamed);
|
on<GoNavigationEvent>(_onGoEvent);
|
||||||
on<PushedNamedAndRemoveUntilEvent>(_onPushedNamedAndRemoveUntil);
|
on<PushNavigationEvent>(_onPushEvent);
|
||||||
on<PushedNamedReplaceEvent>(_onPushedNamedReplaceEvent);
|
on<PopNavigationEvent>(_onPopEvent);
|
||||||
on<PoppedRouteEvent>(_onPoppedRoute);
|
|
||||||
}
|
}
|
||||||
final GlobalKey<NavigatorState> navigationKey;
|
|
||||||
|
|
||||||
Future<void> _onPushedNamed(
|
Future<void> _onGoEvent(
|
||||||
PushedNamedEvent event,
|
GoNavigationEvent event,
|
||||||
Emitter<NavigationState> emit,
|
Emitter<NavigationState> emit,
|
||||||
) async {
|
) async {
|
||||||
await navigationKey.currentState!.pushNamed(
|
emit(
|
||||||
event.destination.path,
|
GoNavigationState(event.destination),
|
||||||
arguments: event.destination.arguments,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onPushedNamedAndRemoveUntil(
|
Future<void> _onPushEvent(
|
||||||
PushedNamedAndRemoveUntilEvent event,
|
PushNavigationEvent event,
|
||||||
Emitter<NavigationState> emit,
|
Emitter<NavigationState> emit,
|
||||||
) async {
|
) async {
|
||||||
await navigationKey.currentState!.pushNamedAndRemoveUntil(
|
emit(
|
||||||
event.destination.path,
|
PushNavigationState(event.destination),
|
||||||
event.predicate,
|
|
||||||
arguments: event.destination.arguments,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onPushedNamedReplaceEvent(
|
Future<void> _onPopEvent(
|
||||||
PushedNamedReplaceEvent event,
|
PopNavigationEvent event,
|
||||||
Emitter<NavigationState> emit,
|
Emitter<NavigationState> emit,
|
||||||
) async {
|
) async {
|
||||||
await navigationKey.currentState!.pushReplacementNamed(
|
emit(PoppedNavigationState());
|
||||||
event.destination.path,
|
|
||||||
arguments: event.destination.arguments,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onPoppedRoute(
|
|
||||||
PoppedRouteEvent event,
|
|
||||||
Emitter<NavigationState> emit,
|
|
||||||
) async {
|
|
||||||
navigationKey.currentState!.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool canPop() {
|
|
||||||
return navigationKey.currentState!.canPop();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,20 +11,14 @@ class NavigationDestination {
|
|||||||
|
|
||||||
abstract class NavigationEvent {}
|
abstract class NavigationEvent {}
|
||||||
|
|
||||||
class PushedNamedEvent extends NavigationEvent {
|
class GoNavigationEvent extends NavigationEvent {
|
||||||
PushedNamedEvent(this.destination);
|
GoNavigationEvent(this.destination);
|
||||||
final NavigationDestination destination;
|
final String destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PushedNamedAndRemoveUntilEvent extends NavigationEvent {
|
class PushNavigationEvent extends NavigationEvent {
|
||||||
PushedNamedAndRemoveUntilEvent(this.destination, this.predicate);
|
PushNavigationEvent(this.destination);
|
||||||
final NavigationDestination destination;
|
final String destination;
|
||||||
final RoutePredicate predicate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PushedNamedReplaceEvent extends NavigationEvent {
|
class PopNavigationEvent extends NavigationEvent {}
|
||||||
PushedNamedReplaceEvent(this.destination);
|
|
||||||
final NavigationDestination destination;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PoppedRouteEvent extends NavigationEvent {}
|
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
part of 'navigation_bloc.dart';
|
part of 'navigation_bloc.dart';
|
||||||
|
|
||||||
class NavigationState {}
|
abstract class NavigationState {}
|
||||||
|
|
||||||
|
class NoopNavigationState extends NavigationState {}
|
||||||
|
|
||||||
|
class PoppedNavigationState extends NavigationState {}
|
||||||
|
|
||||||
|
class PushNavigationState extends NavigationState {
|
||||||
|
PushNavigationState(this.destination);
|
||||||
|
|
||||||
|
final String destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GoNavigationState extends NavigationState {
|
||||||
|
GoNavigationState(this.destination);
|
||||||
|
|
||||||
|
final String destination;
|
||||||
|
}
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
|||||||
Emitter<SettingsState> emit,
|
Emitter<SettingsState> emit,
|
||||||
) async {
|
) async {
|
||||||
final al = GetIt.I.get<AnimeListBloc>();
|
final al = GetIt.I.get<AnimeListBloc>();
|
||||||
final exportArchive = archive.GZipDecoder().decodeBytes(
|
final exportArchive = const archive.GZipDecoder().decodeBytes(
|
||||||
await File(event.path).readAsBytes(),
|
await File(event.path).readAsBytes(),
|
||||||
);
|
);
|
||||||
final json = jsonDecode(utf8.decode(exportArchive)) as Map<String, dynamic>;
|
final json = jsonDecode(utf8.decode(exportArchive)) as Map<String, dynamic>;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'package:anitrack/i18n/strings.g.dart';
|
import 'package:anitrack/i18n/strings.g.dart';
|
||||||
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
|
|
||||||
import 'package:anitrack/src/ui/constants.dart';
|
import 'package:anitrack/src/ui/constants.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
Widget getDrawer(BuildContext context) {
|
Widget getDrawer(BuildContext context) {
|
||||||
return Drawer(
|
return Drawer(
|
||||||
@@ -24,33 +23,30 @@ Widget getDrawer(BuildContext context) {
|
|||||||
leading: const Icon(Icons.list),
|
leading: const Icon(Icons.list),
|
||||||
title: Text(t.content.list),
|
title: Text(t.content.list),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GetIt.I.get<AnimeListBloc>().add(
|
GoRouter.of(context).go(animeListRoute);
|
||||||
AnimeListRequestedEvent(),
|
Navigator.of(context).pop();
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.calendar_today),
|
leading: const Icon(Icons.calendar_today),
|
||||||
title: Text(t.calendar.calendar),
|
title: Text(t.calendar.calendar),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
GoRouter.of(context).go(calendarRoute);
|
||||||
calendarRoute,
|
Navigator.of(context).pop();
|
||||||
(_) => false,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.settings),
|
leading: const Icon(Icons.settings),
|
||||||
title: Text(t.settings.title),
|
title: Text(t.settings.title),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pushNamed(settingsRoute);
|
GoRouter.of(context).push(settingsRoute);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.info),
|
leading: const Icon(Icons.info),
|
||||||
title: Text(t.about.title),
|
title: Text(t.about.title),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pushNamed(aboutRoute);
|
GoRouter.of(context).push(aboutRoute);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'package:anitrack/i18n/strings.g.dart';
|
import 'package:anitrack/i18n/strings.g.dart';
|
||||||
import 'package:anitrack/licenses.g.dart';
|
import 'package:anitrack/oss_licenses.dart';
|
||||||
import 'package:anitrack/src/ui/constants.dart';
|
import 'package:anitrack/src/ui/constants.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class AboutPage extends StatelessWidget {
|
class AboutPage extends StatelessWidget {
|
||||||
@@ -22,9 +23,15 @@ class AboutPage extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(t.about.title),
|
title: Text(t.about.title),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
itemCount: ossLicenses.length + 1,
|
itemCount: dependencies.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -41,7 +48,11 @@ class AboutPage extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.only(
|
||||||
|
top: 8,
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
|
),
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await launchUrl(
|
await launchUrl(
|
||||||
@@ -55,7 +66,11 @@ class AboutPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.only(
|
||||||
|
top: 8,
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
|
),
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final licenseText = await rootBundle.loadString(
|
final licenseText = await rootBundle.loadString(
|
||||||
@@ -90,9 +105,7 @@ class AboutPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final dep = ossLicenses[index - 1];
|
final dep = dependencies[index - 1];
|
||||||
if (!dep.isDirectDependency) return Container();
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(dep.name),
|
title: Text(dep.name),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
|
|||||||
import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart';
|
import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart';
|
||||||
import 'package:anitrack/src/ui/bloc/details_bloc.dart';
|
import 'package:anitrack/src/ui/bloc/details_bloc.dart';
|
||||||
import 'package:anitrack/src/ui/constants.dart';
|
import 'package:anitrack/src/ui/constants.dart';
|
||||||
import 'package:anitrack/src/ui/helpers.dart';
|
|
||||||
import 'package:anitrack/src/ui/widgets/grid_item.dart';
|
import 'package:anitrack/src/ui/widgets/grid_item.dart';
|
||||||
import 'package:anitrack/src/ui/widgets/image.dart';
|
import 'package:anitrack/src/ui/widgets/image.dart';
|
||||||
import 'package:bottom_bar/bottom_bar.dart';
|
import 'package:bottom_bar/bottom_bar.dart';
|
||||||
@@ -24,40 +23,7 @@ class AnimeListPage extends StatefulWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
static String getPageTitle(TrackingMediumType type) {
|
||||||
AnimeListPageState createState() => AnimeListPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnimeListPageState extends State<AnimeListPage> {
|
|
||||||
final PageController _controller = PageController();
|
|
||||||
final ScrollController _animeScrollController = ScrollController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_animeScrollController.addListener(_onAnimeListScrolled);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onAnimeListScrolled() {
|
|
||||||
//print(_animeScrollController.position.maxScrollExtent);
|
|
||||||
final bloc = GetIt.I.get<AnimeListBloc>();
|
|
||||||
if (_animeScrollController.offset + 20 >=
|
|
||||||
_animeScrollController.position.maxScrollExtent) {
|
|
||||||
if (bloc.state.buttonVisibility) {
|
|
||||||
bloc.add(
|
|
||||||
AddButtonVisibilitySetEvent(false),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!bloc.state.buttonVisibility) {
|
|
||||||
bloc.add(
|
|
||||||
AddButtonVisibilitySetEvent(true),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getPageTitle(TrackingMediumType type) {
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TrackingMediumType.anime:
|
case TrackingMediumType.anime:
|
||||||
return t.content.anime;
|
return t.content.anime;
|
||||||
@@ -66,7 +32,7 @@ class AnimeListPageState extends State<AnimeListPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<PopupMenuItem<MediumTrackingState>> _getPopupButtonItems(
|
static List<PopupMenuItem<MediumTrackingState>> getPopupButtonItems(
|
||||||
TrackingMediumType type,
|
TrackingMediumType type,
|
||||||
) {
|
) {
|
||||||
return [
|
return [
|
||||||
@@ -90,14 +56,14 @@ class AnimeListPageState extends State<AnimeListPage> {
|
|||||||
value: MediumTrackingState.paused,
|
value: MediumTrackingState.paused,
|
||||||
child: Text(MediumTrackingState.paused.getName(type)),
|
child: Text(MediumTrackingState.paused.getName(type)),
|
||||||
),
|
),
|
||||||
const PopupMenuItem<MediumTrackingState>(
|
PopupMenuItem<MediumTrackingState>(
|
||||||
value: MediumTrackingState.all,
|
value: MediumTrackingState.all,
|
||||||
child: Text('All'),
|
child: Text(t.data.all),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _getPopupButton(BuildContext context, AnimeListState state) {
|
static Widget getPopupButton(BuildContext context, AnimeListState state) {
|
||||||
switch (state.trackingType) {
|
switch (state.trackingType) {
|
||||||
case TrackingMediumType.anime:
|
case TrackingMediumType.anime:
|
||||||
return PopupMenuButton(
|
return PopupMenuButton(
|
||||||
@@ -110,7 +76,8 @@ class AnimeListPageState extends State<AnimeListPage> {
|
|||||||
AnimeFilterChangedEvent(filterState),
|
AnimeFilterChangedEvent(filterState),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.anime),
|
itemBuilder: (_) =>
|
||||||
|
AnimeListPage.getPopupButtonItems(TrackingMediumType.anime),
|
||||||
);
|
);
|
||||||
case TrackingMediumType.manga:
|
case TrackingMediumType.manga:
|
||||||
return PopupMenuButton(
|
return PopupMenuButton(
|
||||||
@@ -123,30 +90,115 @@ class AnimeListPageState extends State<AnimeListPage> {
|
|||||||
MangaFilterChangedEvent(filterState),
|
MangaFilterChangedEvent(filterState),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.manga),
|
itemBuilder: (_) =>
|
||||||
|
AnimeListPage.getPopupButtonItems(TrackingMediumType.manga),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static AppBar buildAppBar(BuildContext context) {
|
||||||
|
return AppBar(
|
||||||
|
title: BlocBuilder<AnimeListBloc, AnimeListState>(
|
||||||
|
builder: (context, state) => Text(
|
||||||
|
AnimeListPage.getPageTitle(state.trackingType),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: const [
|
||||||
|
BlocBuilder<AnimeListBloc, AnimeListState>(
|
||||||
|
builder: AnimeListPage.getPopupButton,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget buildBottomNavigationBar(BuildContext context) {
|
||||||
|
return BlocBuilder<AnimeListBloc, AnimeListState>(
|
||||||
|
builder: (context, state) => BottomBar(
|
||||||
|
selectedIndex: state.trackingType == TrackingMediumType.anime ? 0 : 1,
|
||||||
|
onTap: (index) {
|
||||||
|
context.read<AnimeListBloc>().add(
|
||||||
|
AnimeTrackingTypeChanged(
|
||||||
|
index == 0 ? TrackingMediumType.anime : TrackingMediumType.manga,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
BottomBarItem(
|
||||||
|
icon: const Icon(Icons.tv),
|
||||||
|
title: Text(t.content.anime),
|
||||||
|
activeColor: Colors.blue,
|
||||||
|
),
|
||||||
|
BottomBarItem(
|
||||||
|
icon: const Icon(Icons.auto_stories),
|
||||||
|
title: Text(t.content.manga),
|
||||||
|
activeColor: Colors.red,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget buildFab() {
|
||||||
|
return BlocBuilder<AnimeListBloc, AnimeListState>(
|
||||||
|
buildWhen: (prev, next) =>
|
||||||
|
prev.buttonVisibility != next.buttonVisibility ||
|
||||||
|
prev.trackingType != next.trackingType,
|
||||||
|
builder: (context, state) {
|
||||||
|
return AnimatedScale(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
scale: state.buttonVisibility ? 1 : 0,
|
||||||
|
curve: Curves.easeInOutQuint,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.read<AnimeSearchBloc>().add(
|
||||||
|
AnimeSearchRequestedEvent(state.trackingType),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
tooltip: t.tooltips.addNewItem,
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AnimeListPageState createState() => AnimeListPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnimeListPageState extends State<AnimeListPage> {
|
||||||
|
final ScrollController _animeScrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_animeScrollController.addListener(_onAnimeListScrolled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAnimeListScrolled() {
|
||||||
|
final bloc = GetIt.I.get<AnimeListBloc>();
|
||||||
|
if (_animeScrollController.offset + 20 >=
|
||||||
|
_animeScrollController.position.maxScrollExtent) {
|
||||||
|
if (bloc.state.buttonVisibility) {
|
||||||
|
bloc.add(
|
||||||
|
AddButtonVisibilitySetEvent(false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!bloc.state.buttonVisibility) {
|
||||||
|
bloc.add(
|
||||||
|
AddButtonVisibilitySetEvent(true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<AnimeListBloc, AnimeListState>(
|
return BlocBuilder<AnimeListBloc, AnimeListState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Scaffold(
|
return IndexedStack(
|
||||||
appBar: AppBar(
|
index: state.trackingType == TrackingMediumType.anime ? 0 : 1,
|
||||||
title: Text(
|
|
||||||
_getPageTitle(state.trackingType),
|
|
||||||
),
|
|
||||||
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: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
@@ -256,56 +308,6 @@ class AnimeListPageState extends State<AnimeListPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ class AnimeSearchPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<AnimeSearchBloc, AnimeSearchState>(
|
return BlocListener<AnimeSearchBloc, AnimeSearchState>(
|
||||||
|
listener: (context, state) {},
|
||||||
|
child: BlocBuilder<AnimeSearchBloc, AnimeSearchState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -95,6 +97,7 @@ class AnimeSearchPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
|
|||||||
import 'package:anitrack/src/ui/bloc/calendar_bloc.dart';
|
import 'package:anitrack/src/ui/bloc/calendar_bloc.dart';
|
||||||
import 'package:anitrack/src/ui/bloc/details_bloc.dart';
|
import 'package:anitrack/src/ui/bloc/details_bloc.dart';
|
||||||
import 'package:anitrack/src/ui/constants.dart';
|
import 'package:anitrack/src/ui/constants.dart';
|
||||||
import 'package:anitrack/src/ui/helpers.dart';
|
|
||||||
import 'package:anitrack/src/ui/widgets/grid_item.dart';
|
import 'package:anitrack/src/ui/widgets/grid_item.dart';
|
||||||
import 'package:anitrack/src/ui/widgets/image.dart';
|
import 'package:anitrack/src/ui/widgets/image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -63,11 +62,30 @@ class CalendarPage extends StatefulWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static AppBar buildAppBar(BuildContext context) {
|
||||||
|
return AppBar(
|
||||||
|
title: Text(t.calendar.calendar),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.read<CalendarBloc>().add(
|
||||||
|
RefreshPerformedEvent(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CalendarPageState createState() => CalendarPageState();
|
CalendarPageState createState() => CalendarPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class CalendarPageState extends State<CalendarPage> {
|
class CalendarPageState extends State<CalendarPage> {
|
||||||
|
/// State for the "refreshing" overlay.
|
||||||
|
OverlayEntry? _overlayEntry;
|
||||||
|
|
||||||
List<Widget> _renderWeekdayList(
|
List<Widget> _renderWeekdayList(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Weekday day,
|
Weekday day,
|
||||||
@@ -131,28 +149,20 @@ class CalendarPageState extends State<CalendarPage> {
|
|||||||
switch (anime.broadcastDay) {
|
switch (anime.broadcastDay) {
|
||||||
case 'Mondays':
|
case 'Mondays':
|
||||||
day = Weekday.monday;
|
day = Weekday.monday;
|
||||||
break;
|
|
||||||
case 'Tuesdays':
|
case 'Tuesdays':
|
||||||
day = Weekday.tuesday;
|
day = Weekday.tuesday;
|
||||||
break;
|
|
||||||
case 'Wednesdays':
|
case 'Wednesdays':
|
||||||
day = Weekday.wednesday;
|
day = Weekday.wednesday;
|
||||||
break;
|
|
||||||
case 'Thursdays':
|
case 'Thursdays':
|
||||||
day = Weekday.thursday;
|
day = Weekday.thursday;
|
||||||
break;
|
|
||||||
case 'Fridays':
|
case 'Fridays':
|
||||||
day = Weekday.friday;
|
day = Weekday.friday;
|
||||||
break;
|
|
||||||
case 'Saturdays':
|
case 'Saturdays':
|
||||||
day = Weekday.saturday;
|
day = Weekday.saturday;
|
||||||
break;
|
|
||||||
case 'Sundays':
|
case 'Sundays':
|
||||||
day = Weekday.sunday;
|
day = Weekday.sunday;
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
day = Weekday.unknown;
|
day = Weekday.unknown;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
airingAnimeMap.addOrSet(day, anime);
|
airingAnimeMap.addOrSet(day, anime);
|
||||||
@@ -169,37 +179,63 @@ class CalendarPageState extends State<CalendarPage> {
|
|||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) =>
|
||||||
previous.refreshing != current.refreshing,
|
previous.refreshing != current.refreshing,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
// Force an update
|
|
||||||
if (!state.refreshing) {
|
if (!state.refreshing) {
|
||||||
setState(() {});
|
_overlayEntry?.remove();
|
||||||
|
_overlayEntry?.dispose();
|
||||||
|
_overlayEntry = null;
|
||||||
|
} else {
|
||||||
|
_overlayEntry = OverlayEntry(
|
||||||
|
builder: (context) => SafeArea(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
const ModalBarrier(
|
||||||
|
dismissible: false,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 150,
|
||||||
|
height: 150,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(25),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
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(
|
child: WillPopScope(
|
||||||
onWillPop: () async =>
|
onWillPop: () async =>
|
||||||
!context.read<CalendarBloc>().state.refreshing,
|
!context.read<CalendarBloc>().state.refreshing,
|
||||||
child: Stack(
|
child: Padding(
|
||||||
children: [
|
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
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(
|
padding: const EdgeInsetsGeometry.symmetric(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
),
|
),
|
||||||
@@ -258,73 +294,6 @@ class CalendarPageState extends State<CalendarPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
child: BlocBuilder<CalendarBloc, CalendarState>(
|
|
||||||
buildWhen: (previous, current) =>
|
|
||||||
previous.refreshing != current.refreshing,
|
|
||||||
builder: (context, state) {
|
|
||||||
if (!state.refreshing) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
return const ModalBarrier(
|
|
||||||
dismissible: false,
|
|
||||||
color: Colors.black54,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
child: BlocBuilder<CalendarBloc, CalendarState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
if (!state.refreshing) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Center(
|
|
||||||
child: SizedBox(
|
|
||||||
width: 150,
|
|
||||||
height: 150,
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
color: Colors.grey.shade800,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.all(25),
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
t.settings.importIndicator(
|
|
||||||
current: state.refreshingCount,
|
|
||||||
total: state.refreshingTotal,
|
|
||||||
),
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class DetailsPage extends StatelessWidget {
|
class DetailsPage extends StatelessWidget {
|
||||||
@@ -60,6 +61,12 @@ class DetailsPage extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(t.details.title),
|
title: Text(t.details.title),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: BlocBuilder<DetailsBloc, DetailsState>(
|
body: BlocBuilder<DetailsBloc, DetailsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
@@ -35,6 +36,12 @@ class SettingsPage extends StatelessWidget {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(t.settings.title),
|
title: Text(t.settings.title),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
@@ -101,8 +108,9 @@ class SettingsPage extends StatelessWidget {
|
|||||||
|
|
||||||
if (!(await Permission.manageExternalStorage
|
if (!(await Permission.manageExternalStorage
|
||||||
.request())
|
.request())
|
||||||
.isGranted)
|
.isGranted) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
GetIt.I.get<SettingsBloc>().add(
|
GetIt.I.get<SettingsBloc>().add(
|
||||||
DataExportedEvent(
|
DataExportedEvent(
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:anitrack/src/ui/widgets/list_item.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SelectorItem<T> {
|
class SelectorItem<T> {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
class AnimeCoverImage extends StatelessWidget {
|
class AnimeCoverImage extends StatelessWidget {
|
||||||
const AnimeCoverImage({
|
const AnimeCoverImage({
|
||||||
required this.url,
|
required this.url,
|
||||||
required this.hero,
|
this.hero,
|
||||||
this.cached = true,
|
this.cached = true,
|
||||||
this.extra,
|
this.extra,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
@@ -18,7 +18,7 @@ class AnimeCoverImage extends StatelessWidget {
|
|||||||
final bool cached;
|
final bool cached;
|
||||||
|
|
||||||
/// The hero tag of the image.
|
/// The hero tag of the image.
|
||||||
final String hero;
|
final String? hero;
|
||||||
|
|
||||||
/// An extra widget with a translucent backdrop.
|
/// An extra widget with a translucent backdrop.
|
||||||
final Widget? extra;
|
final Widget? extra;
|
||||||
@@ -28,9 +28,7 @@ class AnimeCoverImage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Hero(
|
final image = ClipRRect(
|
||||||
tag: hero,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Material(
|
child: Material(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@@ -74,7 +72,15 @@ class AnimeCoverImage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
|
||||||
|
if (hero != null) {
|
||||||
|
return Hero(
|
||||||
|
tag: hero!,
|
||||||
|
child: image,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class ListItem extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
// TODO(PapaTutuWawa): Fix
|
// TODO(PapaTutuWawa): Fix
|
||||||
key: UniqueKey(),
|
key: UniqueKey(),
|
||||||
backgroundBuilder: (_, direction, __) {
|
backgroundBuilder: (_, direction, _) {
|
||||||
if (direction == SwipeDirection.endToStart) {
|
if (direction == SwipeDirection.endToStart) {
|
||||||
return const Align(
|
return const Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
@@ -76,8 +76,6 @@ class ListItem extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
AnimeCoverImage(
|
AnimeCoverImage(
|
||||||
cached: cached,
|
cached: cached,
|
||||||
// TODO(Unknown): Have the ID here
|
|
||||||
hero: thumbnailUrl,
|
|
||||||
extra: imageExtra,
|
extra: imageExtra,
|
||||||
url: thumbnailUrl,
|
url: thumbnailUrl,
|
||||||
),
|
),
|
||||||
|
|||||||
48
lib/src/ui/widgets/shell_wrapper.dart
Normal file
48
lib/src/ui/widgets/shell_wrapper.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -413,6 +413,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
|
go_router:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: go_router
|
||||||
|
sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "17.3.0"
|
||||||
gql:
|
gql:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: anitrack
|
|||||||
description: An anime and manga tracker
|
description: An anime and manga tracker
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
|
|
||||||
version: 0.2.1+2018
|
version: 0.2.2+2019
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.0
|
sdk: ^3.8.0
|
||||||
@@ -21,6 +21,7 @@ dependencies:
|
|||||||
fluttertoast: ^9.0.0
|
fluttertoast: ^9.0.0
|
||||||
freezed_annotation: ^3.1.0
|
freezed_annotation: ^3.1.0
|
||||||
get_it: ^9.2.1
|
get_it: ^9.2.1
|
||||||
|
go_router: ^17.3.0
|
||||||
graphql: ^5.2.4
|
graphql: ^5.2.4
|
||||||
jikan_api: ^2.2.1
|
jikan_api: ^2.2.1
|
||||||
json_annotation: ^4.11.0
|
json_annotation: ^4.11.0
|
||||||
|
|||||||
Reference in New Issue
Block a user