mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-07 11:39:29 +00:00
Refactor onboarding to single page
bottom navigation bar
This commit is contained in:
parent
d9345cad2b
commit
5e152a0baf
10 changed files with 285 additions and 101 deletions
|
|
@ -2,9 +2,10 @@ import 'package:collection/collection.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.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/settings/api_settings_provider.dart';
|
||||
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart'
|
||||
as model;
|
||||
|
||||
part 'server_provider.g.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -20,13 +20,7 @@ void main() async {
|
|||
);
|
||||
}
|
||||
|
||||
// final _router = MyAppRouter(needOnboarding: _needAuth());
|
||||
|
||||
// bool _needAuth() {
|
||||
// final apiSettings = ApiSettings().readFromBoxOrCreate();
|
||||
// final servers = AudiobookShelfServer().readFromBoxOrCreate();
|
||||
// return apiSettings.activeUser == null || servers.isEmpty;
|
||||
// }
|
||||
var routerConfig = const MyAppRouter().config;
|
||||
|
||||
class MyApp extends ConsumerWidget {
|
||||
const MyApp({super.key});
|
||||
|
|
@ -36,16 +30,12 @@ class MyApp extends ConsumerWidget {
|
|||
final servers = ref.watch(audiobookShelfServerProvider);
|
||||
final apiSettings = ref.watch(apiSettingsProvider);
|
||||
|
||||
bool needOnboarding() {
|
||||
return apiSettings.activeUser == null || servers.isEmpty;
|
||||
final needOnboarding = 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(
|
||||
theme: lightTheme,
|
||||
darkTheme: darkTheme,
|
||||
|
|
@ -53,7 +43,6 @@ class MyApp extends ConsumerWidget {
|
|||
? ThemeMode.dark
|
||||
: ThemeMode.light,
|
||||
routerConfig: routerConfig,
|
||||
// routerConfig: _router.config,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ class OnboardingSinglePage extends HookConsumerWidget {
|
|||
final usernameController = useTextEditingController();
|
||||
final passwordController = useTextEditingController();
|
||||
|
||||
// reverse the animation if the user is not logged in
|
||||
void addServer() {
|
||||
var newServer = serverUriController.text.isEmpty
|
||||
? null
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ part of 'router.dart';
|
|||
|
||||
class Routes {
|
||||
static const home = 'home';
|
||||
static const onboarding = 'onboarding';
|
||||
static const onboarding = _SimpleRoute(
|
||||
pathName: 'login',
|
||||
name: 'onboarding',
|
||||
);
|
||||
static const library = _SimpleRoute(
|
||||
pathName: 'library',
|
||||
pathParamName: 'libraryId',
|
||||
|
|
@ -15,6 +18,10 @@ class Routes {
|
|||
pathParamName: 'itemId',
|
||||
name: 'libraryItem',
|
||||
);
|
||||
static const settings = _SimpleRoute(
|
||||
pathName: 'config',
|
||||
name: 'settings',
|
||||
);
|
||||
}
|
||||
|
||||
// a class to store path
|
||||
|
|
@ -22,13 +29,14 @@ class Routes {
|
|||
class _SimpleRoute {
|
||||
const _SimpleRoute({
|
||||
required this.pathName,
|
||||
required this.pathParamName,
|
||||
this.pathParamName,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
final String pathName;
|
||||
final String pathParamName;
|
||||
final String? pathParamName;
|
||||
final String name;
|
||||
|
||||
String get path => '/$pathName/:$pathParamName';
|
||||
String get path =>
|
||||
'/$pathName${pathParamName != null ? '/:$pathParamName' : ''}';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +1,100 @@
|
|||
import 'package:flutter/material.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/library_item_page.dart';
|
||||
import 'package:whispering_pages/pages/library_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';
|
||||
|
||||
final GlobalKey<NavigatorState> _rootNavigatorKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
final GlobalKey<NavigatorState> _sectionANavigatorKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'sectionANav');
|
||||
|
||||
// GoRouter configuration
|
||||
class MyAppRouter {
|
||||
const MyAppRouter({required this.needOnboarding});
|
||||
final bool needOnboarding;
|
||||
// some static strings for named routes
|
||||
const MyAppRouter();
|
||||
|
||||
GoRouter get config => GoRouter(
|
||||
routes: [
|
||||
// sign in page
|
||||
GoRoute(
|
||||
path: '/login',
|
||||
name: Routes.onboarding,
|
||||
path: Routes.onboarding.path,
|
||||
name: Routes.onboarding.name,
|
||||
builder: (context, state) => const OnboardingSinglePage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/',
|
||||
name: Routes.home,
|
||||
builder: (context, state) => const HomePage(),
|
||||
),
|
||||
// /library/:libraryId
|
||||
GoRoute(
|
||||
path: Routes.library.path,
|
||||
name: Routes.library.name,
|
||||
builder: (context, state) => LibraryPage(
|
||||
libraryId: state.pathParameters[Routes.library.pathParamName]!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.libraryItem.path,
|
||||
name: Routes.libraryItem.name,
|
||||
builder: (context, state) {
|
||||
final itemId =
|
||||
state.pathParameters[Routes.libraryItem.pathParamName]!;
|
||||
return LibraryItemPage(itemId: itemId, extra: state.extra);
|
||||
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(
|
||||
path: '/',
|
||||
name: Routes.home,
|
||||
// builder: (context, state) => const HomePage(),
|
||||
pageBuilder: defaultPageBuilder(const HomePage()),
|
||||
),
|
||||
// /library/:libraryId
|
||||
GoRoute(
|
||||
path: Routes.library.path,
|
||||
name: Routes.library.name,
|
||||
builder: (context, state) => LibraryPage(
|
||||
libraryId:
|
||||
state.pathParameters[Routes.library.pathParamName]!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.libraryItem.path,
|
||||
name: Routes.libraryItem.name,
|
||||
// builder: (context, state) {
|
||||
// final itemId = state
|
||||
// .pathParameters[Routes.libraryItem.pathParamName]!;
|
||||
// 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: Routes.settings.path,
|
||||
name: Routes.settings.name,
|
||||
// builder: (context, state) => const AppSettingsPage(),
|
||||
pageBuilder: defaultPageBuilder(const AppSettingsPage()),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
redirect: (context, state) {
|
||||
if (needOnboarding) {
|
||||
return config.namedLocation(Routes.onboarding);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
65
lib/router/scaffold_with_nav_bar.dart
Normal file
65
lib/router/scaffold_with_nav_bar.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
61
lib/router/transitions/slide.dart
Normal file
61
lib/router/transitions/slide.dart
Normal 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,
|
||||
);
|
||||
};
|
||||
|
|
@ -23,7 +23,14 @@ class ApiSettings extends _$ApiSettings {
|
|||
model.ApiSettings readFromBoxOrCreate() {
|
||||
// see if the settings are already in the box
|
||||
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');
|
||||
return foundSettings;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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/router/router.dart';
|
||||
|
||||
|
||||
class MyDrawer extends StatelessWidget {
|
||||
|
|
@ -35,11 +36,7 @@ class MyDrawer extends StatelessWidget {
|
|||
ListTile(
|
||||
title: const Text('App Settings'),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AppSettingsPage(),
|
||||
),
|
||||
);
|
||||
context.goNamed(Routes.settings.name);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -72,50 +72,59 @@ class BookOnShelf extends HookConsumerWidget {
|
|||
// the cover image of the book
|
||||
// take up remaining space
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// open the book
|
||||
context.pushNamed(
|
||||
Routes.libraryItem.name,
|
||||
pathParameters: {
|
||||
Routes.libraryItem.pathParamName: item.id,
|
||||
},
|
||||
extra: LibraryItemExtras(
|
||||
book: book,
|
||||
heroTagSuffix: heroTagSuffix,
|
||||
coverImage: coverImage.valueOrNull,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: coverImage.when(
|
||||
data: (image) {
|
||||
// return const BookCoverSkeleton();
|
||||
if (image.isEmpty) {
|
||||
return const Icon(Icons.error);
|
||||
}
|
||||
// cover 80% of parent height
|
||||
return Hero(
|
||||
tag: HeroTagPrefixes.bookCover +
|
||||
item.id +
|
||||
heroTagSuffix,
|
||||
child: Image.memory(
|
||||
child: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// open the book
|
||||
context.pushNamed(
|
||||
Routes.libraryItem.name,
|
||||
pathParameters: {
|
||||
Routes.libraryItem.pathParamName!: item.id,
|
||||
},
|
||||
extra: LibraryItemExtras(
|
||||
book: book,
|
||||
heroTagSuffix: heroTagSuffix,
|
||||
coverImage: coverImage.valueOrNull,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: coverImage.when(
|
||||
data: (image) {
|
||||
// return const BookCoverSkeleton();
|
||||
if (image.isEmpty) {
|
||||
return const Icon(Icons.error);
|
||||
}
|
||||
var imageWidget = Image.memory(
|
||||
image,
|
||||
fit: BoxFit.cover,
|
||||
fit: BoxFit.fill,
|
||||
cacheWidth: (height *
|
||||
1.2 *
|
||||
1.2 *
|
||||
MediaQuery.of(context).devicePixelRatio)
|
||||
.round(),
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () {
|
||||
return const Center(child: BookCoverSkeleton());
|
||||
},
|
||||
error: (error, stack) {
|
||||
return const Icon(Icons.error);
|
||||
},
|
||||
);
|
||||
return Hero(
|
||||
tag: HeroTagPrefixes.bookCover +
|
||||
item.id +
|
||||
heroTagSuffix,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
child: imageWidget,
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () {
|
||||
return const Center(child: BookCoverSkeleton());
|
||||
},
|
||||
error: (error, stack) {
|
||||
return const Icon(Icons.error);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue