Refactor onboarding to single page

bottom navigation bar
This commit is contained in:
Dr-Blank 2024-05-10 17:49:47 -04:00
parent d9345cad2b
commit 5e152a0baf
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
10 changed files with 285 additions and 101 deletions

View file

@ -2,9 +2,10 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:whispering_pages/api/authenticated_user_provider.dart'; import 'package:whispering_pages/api/authenticated_user_provider.dart';
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart' as model;
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/db/storage.dart'; import 'package:whispering_pages/db/storage.dart';
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart'
as model;
part 'server_provider.g.dart'; part 'server_provider.g.dart';

View file

@ -20,13 +20,7 @@ void main() async {
); );
} }
// final _router = MyAppRouter(needOnboarding: _needAuth()); var routerConfig = const MyAppRouter().config;
// bool _needAuth() {
// final apiSettings = ApiSettings().readFromBoxOrCreate();
// final servers = AudiobookShelfServer().readFromBoxOrCreate();
// return apiSettings.activeUser == null || servers.isEmpty;
// }
class MyApp extends ConsumerWidget { class MyApp extends ConsumerWidget {
const MyApp({super.key}); const MyApp({super.key});
@ -36,16 +30,12 @@ class MyApp extends ConsumerWidget {
final servers = ref.watch(audiobookShelfServerProvider); final servers = ref.watch(audiobookShelfServerProvider);
final apiSettings = ref.watch(apiSettingsProvider); final apiSettings = ref.watch(apiSettingsProvider);
bool needOnboarding() { final needOnboarding = apiSettings.activeUser == null || servers.isEmpty;
return apiSettings.activeUser == null || servers.isEmpty;
if (needOnboarding) {
routerConfig.goNamed(Routes.onboarding.name);
} }
var routerConfig = MyAppRouter(needOnboarding: needOnboarding()).config;
// if (needOnboarding()) {
// routerConfig.goNamed(Routes.onboarding);
// }
return MaterialApp.router( return MaterialApp.router(
theme: lightTheme, theme: lightTheme,
darkTheme: darkTheme, darkTheme: darkTheme,
@ -53,7 +43,6 @@ class MyApp extends ConsumerWidget {
? ThemeMode.dark ? ThemeMode.dark
: ThemeMode.light, : ThemeMode.light,
routerConfig: routerConfig, routerConfig: routerConfig,
// routerConfig: _router.config,
); );
} }
} }

View file

@ -30,7 +30,6 @@ class OnboardingSinglePage extends HookConsumerWidget {
final usernameController = useTextEditingController(); final usernameController = useTextEditingController();
final passwordController = useTextEditingController(); final passwordController = useTextEditingController();
// reverse the animation if the user is not logged in
void addServer() { void addServer() {
var newServer = serverUriController.text.isEmpty var newServer = serverUriController.text.isEmpty
? null ? null

View file

@ -4,7 +4,10 @@ part of 'router.dart';
class Routes { class Routes {
static const home = 'home'; static const home = 'home';
static const onboarding = 'onboarding'; static const onboarding = _SimpleRoute(
pathName: 'login',
name: 'onboarding',
);
static const library = _SimpleRoute( static const library = _SimpleRoute(
pathName: 'library', pathName: 'library',
pathParamName: 'libraryId', pathParamName: 'libraryId',
@ -15,6 +18,10 @@ class Routes {
pathParamName: 'itemId', pathParamName: 'itemId',
name: 'libraryItem', name: 'libraryItem',
); );
static const settings = _SimpleRoute(
pathName: 'config',
name: 'settings',
);
} }
// a class to store path // a class to store path
@ -22,13 +29,14 @@ class Routes {
class _SimpleRoute { class _SimpleRoute {
const _SimpleRoute({ const _SimpleRoute({
required this.pathName, required this.pathName,
required this.pathParamName, this.pathParamName,
required this.name, required this.name,
}); });
final String pathName; final String pathName;
final String pathParamName; final String? pathParamName;
final String name; final String name;
String get path => '/$pathName/:$pathParamName'; String get path =>
'/$pathName${pathParamName != null ? '/:$pathParamName' : ''}';
} }

View file

@ -1,52 +1,100 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:whispering_pages/pages/app_settings.dart';
import 'package:whispering_pages/pages/home_page.dart'; import 'package:whispering_pages/pages/home_page.dart';
import 'package:whispering_pages/pages/library_item_page.dart'; import 'package:whispering_pages/pages/library_item_page.dart';
import 'package:whispering_pages/pages/library_page.dart'; import 'package:whispering_pages/pages/library_page.dart';
import 'package:whispering_pages/pages/onboarding/onboarding_single_page.dart'; import 'package:whispering_pages/pages/onboarding/onboarding_single_page.dart';
import 'scaffold_with_nav_bar.dart';
import 'transitions/slide.dart';
part 'constants.dart'; part 'constants.dart';
final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> _sectionANavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'sectionANav');
// GoRouter configuration // GoRouter configuration
class MyAppRouter { class MyAppRouter {
const MyAppRouter({required this.needOnboarding}); const MyAppRouter();
final bool needOnboarding;
// some static strings for named routes
GoRouter get config => GoRouter( GoRouter get config => GoRouter(
routes: [ routes: [
// sign in page
GoRoute( GoRoute(
path: '/login', path: Routes.onboarding.path,
name: Routes.onboarding, name: Routes.onboarding.name,
builder: (context, state) => const OnboardingSinglePage(), builder: (context, state) => const OnboardingSinglePage(),
), ),
StatefulShellRoute.indexedStack(
builder: (
BuildContext context,
GoRouterState state,
StatefulNavigationShell navigationShell,
) {
// Return the widget that implements the custom shell (in this case
// using a BottomNavigationBar). The StatefulNavigationShell is passed
// to be able access the state of the shell and to navigate to other
// branches in a stateful way.
return ScaffoldWithNavBar(navigationShell: navigationShell);
},
branches: <StatefulShellBranch>[
// The route branch for the first tab of the bottom navigation bar.
StatefulShellBranch(
navigatorKey: _sectionANavigatorKey,
routes: <RouteBase>[
GoRoute( GoRoute(
path: '/', path: '/',
name: Routes.home, name: Routes.home,
builder: (context, state) => const HomePage(), // builder: (context, state) => const HomePage(),
pageBuilder: defaultPageBuilder(const HomePage()),
), ),
// /library/:libraryId // /library/:libraryId
GoRoute( GoRoute(
path: Routes.library.path, path: Routes.library.path,
name: Routes.library.name, name: Routes.library.name,
builder: (context, state) => LibraryPage( builder: (context, state) => LibraryPage(
libraryId: state.pathParameters[Routes.library.pathParamName]!, libraryId:
state.pathParameters[Routes.library.pathParamName]!,
), ),
), ),
GoRoute( GoRoute(
path: Routes.libraryItem.path, path: Routes.libraryItem.path,
name: Routes.libraryItem.name, name: Routes.libraryItem.name,
builder: (context, state) { // builder: (context, state) {
final itemId = // final itemId = state
state.pathParameters[Routes.libraryItem.pathParamName]!; // .pathParameters[Routes.libraryItem.pathParamName]!;
return LibraryItemPage(itemId: itemId, extra: state.extra); // return LibraryItemPage(
// itemId: itemId, extra: state.extra);
// },
pageBuilder: (context, state) {
final itemId = state
.pathParameters[Routes.libraryItem.pathParamName]!;
final child =
LibraryItemPage(itemId: itemId, extra: state.extra);
return buildPageWithDefaultTransition(
context: context,
state: state,
child: child,
);
}, },
), ),
], ],
redirect: (context, state) { ),
if (needOnboarding) { StatefulShellBranch(
return config.namedLocation(Routes.onboarding); routes: <RouteBase>[
} GoRoute(
return null; path: Routes.settings.path,
}, name: Routes.settings.name,
// builder: (context, state) => const AppSettingsPage(),
pageBuilder: defaultPageBuilder(const AppSettingsPage()),
),
],
),
],
),
],
); );
} }

View file

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// Builds the "shell" for the app by building a Scaffold with a
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
class ScaffoldWithNavBar extends StatelessWidget {
/// Constructs an [ScaffoldWithNavBar].
const ScaffoldWithNavBar({
required this.navigationShell,
Key? key,
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
/// The navigation shell and container for the branch Navigators.
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
elevation: 0.0,
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
selectedFontSize: Theme.of(context).textTheme.labelMedium!.fontSize!,
unselectedFontSize: Theme.of(context).textTheme.labelMedium!.fontSize!,
// fixedColor: Theme.of(context).colorScheme.primary,
// type: BottomNavigationBarType.fixed,
// Here, the items of BottomNavigationBar are hard coded. In a real
// world scenario, the items would most likely be generated from the
// branches of the shell route, which can be fetched using
// `navigationShell.route.branches`.
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
label: 'Home',
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
),
BottomNavigationBarItem(
label: 'Settings',
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
),
],
currentIndex: navigationShell.currentIndex,
onTap: (int index) => _onTap(context, index),
),
);
}
/// Navigate to the current location of the branch at the provided index when
/// tapping an item in the BottomNavigationBar.
void _onTap(BuildContext context, int index) {
// When navigating to a new branch, it's recommended to use the goBranch
// method, as doing so makes sure the last navigation state of the
// Navigator for the branch is restored.
navigationShell.goBranch(
index,
// A common pattern when using bottom navigation bars is to support
// navigating to the initial location when tapping the item that is
// already active. This example demonstrates how to support this behavior,
// using the initialLocation parameter of goBranch.
initialLocation: index == navigationShell.currentIndex,
);
}
}

View file

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:go_router/go_router.dart';
// class CustomSlideTransition extends CustomTransitionPage<void> {
// CustomSlideTransition({super.key, required super.child})
// : super(
// transitionDuration: const Duration(milliseconds: 250),
// transitionsBuilder: (_, animation, __, child) {
// return SlideTransition(
// position: animation.drive(
// Tween(
// begin: const Offset(1.5, 0),
// end: Offset.zero,
// ).chain(
// CurveTween(curve: Curves.ease),
// ),
// ),
// child: child,
// );
// },
// );
// }
CustomTransitionPage buildPageWithDefaultTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) {
return CustomTransitionPage<T>(
key: state.pageKey,
transitionDuration: 250.ms,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(
opacity: animation,
child: SlideTransition(
position: animation.drive(
Tween(
begin: const Offset(0, 1.50),
end: Offset.zero,
).chain(
CurveTween(curve: Curves.easeOut),
),
),
child: child,
),
),
);
}
Page<dynamic> Function(BuildContext, GoRouterState) defaultPageBuilder<T>(
Widget child,
) =>
(BuildContext context, GoRouterState state) {
return buildPageWithDefaultTransition<T>(
context: context,
state: state,
child: child,
);
};

View file

@ -23,7 +23,14 @@ class ApiSettings extends _$ApiSettings {
model.ApiSettings readFromBoxOrCreate() { model.ApiSettings readFromBoxOrCreate() {
// see if the settings are already in the box // see if the settings are already in the box
if (_box.isNotEmpty) { if (_box.isNotEmpty) {
final foundSettings = _box.getAt(0); var foundSettings = _box.getAt(0);
// foundSettings.activeServer ??= foundSettings.activeUser?.server;
// foundSettings =foundSettings.copyWith(activeServer: foundSettings.activeUser?.server);
if (foundSettings.activeServer == null) {
foundSettings = foundSettings.copyWith(
activeServer: foundSettings.activeUser?.server,
);
}
debugPrint('found api settings in box: $foundSettings'); debugPrint('found api settings in box: $foundSettings');
return foundSettings; return foundSettings;
} else { } else {

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:whispering_pages/pages/app_settings.dart'; import 'package:go_router/go_router.dart';
import 'package:whispering_pages/pages/server_manager.dart'; import 'package:whispering_pages/pages/server_manager.dart';
import 'package:whispering_pages/router/router.dart';
class MyDrawer extends StatelessWidget { class MyDrawer extends StatelessWidget {
@ -35,11 +36,7 @@ class MyDrawer extends StatelessWidget {
ListTile( ListTile(
title: const Text('App Settings'), title: const Text('App Settings'),
onTap: () { onTap: () {
Navigator.of(context).push( context.goNamed(Routes.settings.name);
MaterialPageRoute(
builder: (context) => const AppSettingsPage(),
),
);
}, },
), ),
], ],

View file

@ -72,13 +72,14 @@ class BookOnShelf extends HookConsumerWidget {
// the cover image of the book // the cover image of the book
// take up remaining space // take up remaining space
Expanded( Expanded(
child: Center(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
// open the book // open the book
context.pushNamed( context.pushNamed(
Routes.libraryItem.name, Routes.libraryItem.name,
pathParameters: { pathParameters: {
Routes.libraryItem.pathParamName: item.id, Routes.libraryItem.pathParamName!: item.id,
}, },
extra: LibraryItemExtras( extra: LibraryItemExtras(
book: book, book: book,
@ -95,18 +96,25 @@ class BookOnShelf extends HookConsumerWidget {
if (image.isEmpty) { if (image.isEmpty) {
return const Icon(Icons.error); return const Icon(Icons.error);
} }
// cover 80% of parent height var imageWidget = Image.memory(
return Hero(
tag: HeroTagPrefixes.bookCover +
item.id +
heroTagSuffix,
child: Image.memory(
image, image,
fit: BoxFit.cover, fit: BoxFit.fill,
cacheWidth: (height * cacheWidth: (height *
1.2 * 1.2 *
MediaQuery.of(context).devicePixelRatio) MediaQuery.of(context).devicePixelRatio)
.round(), .round(),
);
return Hero(
tag: HeroTagPrefixes.bookCover +
item.id +
heroTagSuffix,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
child: imageWidget,
), ),
); );
}, },
@ -120,6 +128,7 @@ class BookOnShelf extends HookConsumerWidget {
), ),
), ),
), ),
),
// the title and author of the book // the title and author of the book
// AutoScrollText( // AutoScrollText(
Text( Text(