fix: Fix routing

This commit is contained in:
2026-07-26 15:53:36 +02:00
parent 34b3817e6d
commit d034875e16
17 changed files with 602 additions and 500 deletions

View File

@@ -14,13 +14,13 @@ 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/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 +33,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 +68,68 @@ 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: true,
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.go(state.destination);
} else if (state is GoNavigationState) {
_router.push(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 +141,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;
},
); );
} }
} }

View File

@@ -6,6 +6,7 @@ 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:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/cupertino.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';
@@ -41,6 +42,9 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
growable: true, growable: true,
); );
/// Controller for the bottom navigation bar
final pageController = PageController();
List<AnimeTrackingData> get unfilteredAnime => _animes; List<AnimeTrackingData> get unfilteredAnime => _animes;
List<AnimeTrackingData> _getFilteredAnime({ List<AnimeTrackingData> _getFilteredAnime({
@@ -372,10 +376,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
), ),
); );
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PushedNamedAndRemoveUntilEvent( GoNavigationEvent(animeListRoute),
const NavigationDestination(animeListRoute),
(_) => false,
),
); );
} }
} }

View File

@@ -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),
); );
} }
} }

View File

@@ -44,9 +44,7 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
); );
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent( GoNavigationEvent(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),
),
); );
} }
@@ -126,7 +122,7 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
} }
// Navigate back // Navigate back
GetIt.I.get<NavigationBloc>().add(PoppedRouteEvent()); GetIt.I.get<NavigationBloc>().add(PopNavigationEvent());
} }
Future<void> _onAnimeWatcherAdded( Future<void> _onAnimeWatcherAdded(

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ 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: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 +25,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);
}, },
), ),
], ],

View File

@@ -3,6 +3,7 @@ import 'package:anitrack/licenses.g.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package: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,6 +23,12 @@ 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: ossLicenses.length + 1,

View File

@@ -4,13 +4,13 @@ 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';
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';
class AnimeListPage extends StatefulWidget { class AnimeListPage extends StatefulWidget {
const AnimeListPage({ const AnimeListPage({
@@ -24,6 +24,147 @@ class AnimeListPage extends StatefulWidget {
), ),
); );
static String getPageTitle(TrackingMediumType type) {
switch (type) {
case TrackingMediumType.anime:
return t.content.anime;
case TrackingMediumType.manga:
return t.content.manga;
}
}
static List<PopupMenuItem<MediumTrackingState>> getPopupButtonItems(
TrackingMediumType type,
) {
return [
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.ongoing,
child: Text(MediumTrackingState.ongoing.getName(type)),
),
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.completed,
child: Text(MediumTrackingState.completed.getName(type)),
),
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.planned,
child: Text(MediumTrackingState.planned.getName(type)),
),
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.dropped,
child: Text(MediumTrackingState.dropped.getName(type)),
),
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.paused,
child: Text(MediumTrackingState.paused.getName(type)),
),
const PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.all,
child: Text('All'),
),
];
}
static Widget getPopupButton(BuildContext context, AnimeListState state) {
switch (state.trackingType) {
case TrackingMediumType.anime:
return PopupMenuButton(
icon: const Icon(
Icons.filter_list,
),
initialValue: state.animeFilterState,
onSelected: (filterState) {
context.read<AnimeListBloc>().add(
AnimeFilterChangedEvent(filterState),
);
},
itemBuilder: (_) =>
AnimeListPage.getPopupButtonItems(TrackingMediumType.anime),
);
case TrackingMediumType.manga:
return PopupMenuButton(
icon: const Icon(
Icons.filter_list,
),
initialValue: state.mangaFilterState,
onSelected: (filterState) {
context.read<AnimeListBloc>().add(
MangaFilterChangedEvent(filterState),
);
},
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: [
BlocBuilder<AnimeListBloc, AnimeListState>(
builder: (context, state) =>
AnimeListPage.getPopupButton(context, state),
),
],
);
}
static Widget buildBottomNavigationBar(BuildContext context) {
final pageController = context.read<AnimeListBloc>().pageController;
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,
),
);
pageController.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,
),
],
),
);
}
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.push(animeSearchRoute);
},
tooltip: t.tooltips.addNewItem,
child: const Icon(Icons.add),
),
);
},
);
}
@override @override
AnimeListPageState createState() => AnimeListPageState(); AnimeListPageState createState() => AnimeListPageState();
} }
@@ -57,92 +198,11 @@ class AnimeListPageState extends State<AnimeListPage> {
} }
} }
String _getPageTitle(TrackingMediumType type) {
switch (type) {
case TrackingMediumType.anime:
return t.content.anime;
case TrackingMediumType.manga:
return t.content.manga;
}
}
List<PopupMenuItem<MediumTrackingState>> _getPopupButtonItems(
TrackingMediumType type,
) {
return [
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.ongoing,
child: Text(MediumTrackingState.ongoing.getName(type)),
),
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.completed,
child: Text(MediumTrackingState.completed.getName(type)),
),
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.planned,
child: Text(MediumTrackingState.planned.getName(type)),
),
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.dropped,
child: Text(MediumTrackingState.dropped.getName(type)),
),
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.paused,
child: Text(MediumTrackingState.paused.getName(type)),
),
const PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.all,
child: Text('All'),
),
];
}
Widget _getPopupButton(BuildContext context, AnimeListState state) {
switch (state.trackingType) {
case TrackingMediumType.anime:
return PopupMenuButton(
icon: const Icon(
Icons.filter_list,
),
initialValue: state.animeFilterState,
onSelected: (filterState) {
context.read<AnimeListBloc>().add(
AnimeFilterChangedEvent(filterState),
);
},
itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.anime),
);
case TrackingMediumType.manga:
return PopupMenuButton(
icon: const Icon(
Icons.filter_list,
),
initialValue: state.mangaFilterState,
onSelected: (filterState) {
context.read<AnimeListBloc>().add(
MangaFilterChangedEvent(filterState),
);
},
itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.manga),
);
}
}
@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 PageView(
appBar: AppBar(
title: Text(
_getPageTitle(state.trackingType),
),
actions: [
_getPopupButton(context, state),
],
),
drawer: getDrawer(context),
body: PageView(
// Prevent swiping between pages // Prevent swiping between pages
// (https://github.com/flutter/flutter/issues/37510#issuecomment-612663656) // (https://github.com/flutter/flutter/issues/37510#issuecomment-612663656)
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@@ -256,56 +316,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,
),
],
),
); );
}, },
); );

View File

@@ -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 {
), ),
); );
}, },
),
); );
} }
} }

View File

@@ -63,6 +63,22 @@ 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();
} }
@@ -131,28 +147,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);
@@ -184,22 +192,7 @@ class CalendarPageState extends State<CalendarPage> {
right: 0, right: 0,
top: 0, top: 0,
bottom: 0, bottom: 0,
child: Scaffold( child: Padding(
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,7 +251,6 @@ class CalendarPageState extends State<CalendarPage> {
), ),
), ),
), ),
),
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,

View File

@@ -0,0 +1,14 @@
import 'package:anitrack/src/ui/helpers.dart';
import 'package:flutter/material.dart';
/// A page that renders the anime list and the calendar as nested views.
class MainPage extends StatelessWidget {
const MainPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: getDrawer(context),
);
}
}

View File

@@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
class SettingsPage extends StatelessWidget { class SettingsPage extends StatelessWidget {
@@ -35,6 +36,12 @@ class SettingsPage extends StatelessWidget {
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(t.settings.title), title: Text(t.settings.title),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
},
),
), ),
body: ListView( body: ListView(
children: [ children: [

View File

@@ -0,0 +1,49 @@
import 'package:anitrack/i18n/strings.g.dart';
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

@@ -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:

View File

@@ -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