diff --git a/assets/translations/de-DE.json b/assets/translations/de-DE.json index fab7f4b..e6c06b5 100644 --- a/assets/translations/de-DE.json +++ b/assets/translations/de-DE.json @@ -1,5 +1,15 @@ { "add": "Hinzufügen", + "navigation": { + "library": "Meine Bücher", + "stats": "Statistiken", + "timeline": "Timeline", + "wishlist": "Wunschliste", + "recommendations": "Vorschläge", + "book-keeping": "Verwaltung", + "settings": "Einstellungen" + }, + "anonymous-user": "Anonymer Bücherwurm", "account_creation_failed": "Dein Konto konnte nicht angelegt werden.", "add_book": { "manual": "Manuell eingeben", diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 8889053..35776c3 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -1,5 +1,15 @@ { "add": "Add", + "navigation": { + "library": "My Library", + "stats": "Statistics", + "timeline": "Timeline", + "wishlist": "Wishlist", + "recommendations": "Recommendations", + "book-keeping": "Book Keeping", + "settings": "Settings" + }, + "anonymous-user": "Anonymous Bookworm", "account_creation_failed": "Failed to create account", "add_book": { "manual": "Add manual", diff --git a/lib/src/providers/app_router.dart b/lib/src/providers/app_router.dart index 0b1d1e9..38ae65f 100644 --- a/lib/src/providers/app_router.dart +++ b/lib/src/providers/app_router.dart @@ -2,13 +2,20 @@ import 'package:dantex/src/providers/authentication.dart'; import 'package:dantex/src/ui/add/scan_book_page.dart'; import 'package:dantex/src/ui/book/book_detail_page.dart'; import 'package:dantex/src/ui/boot_page.dart'; +import 'package:dantex/src/ui/core/dante_page_scaffold.dart'; import 'package:dantex/src/ui/login/email_login_page.dart'; import 'package:dantex/src/ui/login/login_page.dart'; import 'package:dantex/src/ui/main/main_page.dart'; +import 'package:dantex/src/ui/management/book_management_page.dart'; import 'package:dantex/src/ui/profile/profile_page.dart'; +import 'package:dantex/src/ui/recommendations/recommendations_page.dart'; import 'package:dantex/src/ui/search/search_page.dart'; import 'package:dantex/src/ui/settings/contributors_page.dart'; import 'package:dantex/src/ui/settings/settings_page.dart'; +import 'package:dantex/src/ui/stats/stats_page.dart'; +import 'package:dantex/src/ui/timeline/timeline_page.dart'; +import 'package:dantex/src/ui/wishlist/wishlist_page.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -34,7 +41,7 @@ GoRouter goRouter(GoRouterRef ref) { final isSplash = state.uri.toString() == DanteRoute.boot.navigationUrl; if (isSplash) { return isAuth - ? DanteRoute.dashboard.navigationUrl + ? DanteRoute.library.navigationUrl : DanteRoute.login.navigationUrl; } @@ -42,7 +49,7 @@ GoRouter goRouter(GoRouterRef ref) { state.uri.toString() == DanteRoute.login.navigationUrl || state.uri.toString() == DanteRoute.emailLogin.navigationUrl; if (isLoggingIn) { - return isAuth ? DanteRoute.dashboard.navigationUrl : null; + return isAuth ? DanteRoute.library.navigationUrl : null; } return isAuth ? null : DanteRoute.boot.navigationUrl; @@ -65,101 +72,194 @@ GoRouter goRouter(GoRouterRef ref) { ), ], ), + _buildMainRoutes(), + ], + ); +} + +/// TODO Explain difference of web and mobile routing +RouteBase _buildMainRoutes() { + return kIsWeb ? _buildWebMainRoute() : _buildMobileMainRoute(); +} + +RouteBase _buildMobileMainRoute() { + return GoRoute( + path: DanteRoute.library.url, + builder: (BuildContext context, GoRouterState state) => const MainPage(), + routes: _mainRoutes, + ); +} + +RouteBase _buildWebMainRoute() { + return ShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + return DantePageScaffold(content: child); + }, + routes: [ GoRoute( - path: DanteRoute.dashboard.url, + path: DanteRoute.library.url, builder: (BuildContext context, GoRouterState state) => const MainPage(), - routes: [ - GoRoute( - path: DanteRoute.search.url, - builder: (BuildContext context, GoRouterState state) => - const SearchPage(), - ), - GoRoute( - path: DanteRoute.settings.url, - builder: (BuildContext context, GoRouterState state) => - const SettingsPage(), - routes: [ - GoRoute( - path: DanteRoute.contributors.url, - builder: (BuildContext context, GoRouterState state) => - const ContributorsPage(), - ), - ], - ), - GoRoute( - path: DanteRoute.profile.url, - builder: (BuildContext context, GoRouterState state) => - const ProfilePage(), - ), - GoRoute( - path: DanteRoute.scanBook.url, - builder: (BuildContext context, GoRouterState state) => - const ScanBookPage(), - ), - GoRoute( - path: DanteRoute.bookDetail.url, - builder: (context, state) { - final bookId = state.pathParameters['bookId'] ?? ''; - return BookDetailPage(id: bookId); - }, - ), - ], ), + ..._mainRoutes, ], ); } +List _mainRoutes = [ + GoRoute( + path: DanteRoute.settings.url, + builder: (BuildContext context, GoRouterState state) => + const SettingsPage(), + routes: [ + GoRoute( + path: DanteRoute.contributors.url, + builder: (BuildContext context, GoRouterState state) => + const ContributorsPage(), + ), + ], + ), + GoRoute( + path: DanteRoute.profile.url, + builder: (BuildContext context, GoRouterState state) => const ProfilePage(), + ), + GoRoute( + path: DanteRoute.statistics.url, + builder: (BuildContext context, GoRouterState state) => const StatsPage(), + ), + GoRoute( + path: DanteRoute.search.url, + builder: (BuildContext context, GoRouterState state) => const SearchPage(), + ), + GoRoute( + path: DanteRoute.timeline.url, + builder: (BuildContext context, GoRouterState state) => + const TimelinePage(), + ), + GoRoute( + path: DanteRoute.wishlist.url, + builder: (BuildContext context, GoRouterState state) => + const WishlistPage(), + ), + GoRoute( + path: DanteRoute.recommendations.url, + builder: (BuildContext context, GoRouterState state) => + const RecommendationsPage(), + ), + GoRoute( + path: DanteRoute.bookManagement.url, + builder: (BuildContext context, GoRouterState state) => + const BookManagementPage(), + ), + GoRoute( + path: DanteRoute.scanBook.url, + builder: (BuildContext context, GoRouterState state) => + const ScanBookPage(), + ), + GoRoute( + path: DanteRoute.bookDetail.url, + builder: (context, state) { + final bookId = state.pathParameters['bookId'] ?? ''; + return BookDetailPage(id: bookId); + }, + ), +]; + enum DanteRoute { boot( - url: '/boot', + webUrl: '/boot', + mobileUrl: '/boot', navigationUrl: '/boot', ), login( - url: '/login', + webUrl: '/login', + mobileUrl: '/login', navigationUrl: '/login', ), emailLogin( - url: 'email', + webUrl: 'email', + mobileUrl: 'email', navigationUrl: '/login/email', ), - dashboard( - url: '/', + library( + webUrl: '/', + mobileUrl: '/', navigationUrl: '/', ), scanBook( - url: 'scan', + webUrl: '/scan', + mobileUrl: 'scan', navigationUrl: '/scan', ), settings( - url: 'settings', + webUrl: '/settings', + mobileUrl: 'settings', navigationUrl: '/settings', ), search( - url: 'search', + webUrl: '/search', + mobileUrl: 'search', navigationUrl: '/search', ), contributors( - url: 'contributors', + webUrl: 'contributors', + mobileUrl: 'contributors', navigationUrl: '/settings/contributors', ), profile( - url: 'profile', + webUrl: '/profile', + mobileUrl: 'profile', navigationUrl: '/profile', ), + statistics( + webUrl: '/statistics', + mobileUrl: 'statistics', + navigationUrl: '/statistics', + ), + timeline( + webUrl: '/timeline', + mobileUrl: 'timeline', + navigationUrl: '/timeline', + ), + wishlist( + webUrl: '/wishlist', + mobileUrl: 'wishlist', + navigationUrl: '/wishlist', + ), + recommendations( + webUrl: '/recommendations', + mobileUrl: 'recommendations', + navigationUrl: '/recommendations', + ), + bookManagement( + webUrl: '/management', + mobileUrl: 'management', + navigationUrl: '/management', + ), bookDetail( - url: 'book/:bookId', + webUrl: '/book/:bookId', + mobileUrl: 'book/:bookId', navigationUrl: '/book/:bookId', ); - /// Url used for registering the route in the [_router] field. - final String url; + /// Url used for registering the route in the [_router] field for Web. + final String _webUrl; + + /// Url used for registering the route in the [_router] field for Mobile. + final String _mobileUrl; /// Used for navigating to another screen, when calling context.go() - final String navigationUrl; + final String _navigationUrl; + + String get url => kIsWeb ? _webUrl : _mobileUrl; + + String get navigationUrl => _navigationUrl; const DanteRoute({ - required this.url, - required this.navigationUrl, - }); + required String webUrl, + required String mobileUrl, + required String navigationUrl, + }) : _webUrl = webUrl, + _mobileUrl = mobileUrl, + _navigationUrl = navigationUrl; } diff --git a/lib/src/ui/core/dante_app_bar.dart b/lib/src/ui/core/dante_app_bar.dart index bfee8fd..5d4b2e2 100644 --- a/lib/src/ui/core/dante_app_bar.dart +++ b/lib/src/ui/core/dante_app_bar.dart @@ -8,6 +8,7 @@ import 'package:dantex/src/ui/add/add_book_widget.dart'; import 'package:dantex/src/ui/core/dante_components.dart'; import 'package:dantex/src/ui/core/platform_components.dart'; import 'package:dantex/src/ui/search/dante_search_bar.dart'; +import 'package:dantex/src/util/layout_utils.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -32,56 +33,14 @@ class DanteAppBar extends ConsumerWidget implements PreferredSizeWidget { horizontal: 16.0, vertical: 8.0, ), - child: Row( - children: [ - PopupMenuButton( - padding: const EdgeInsets.all(0), - icon: Icon( - Icons.add, - size: 32, - color: Theme.of(context).colorScheme.primary, - ), - onSelected: (AddBookAction action) async => - _handleAddBookAction(context, action), - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: AddBookAction.scan, - child: _AddActionItem( - text: 'add_book.scan'.tr(), - iconData: Icons.camera_alt_outlined, - color: Theme.of(context).colorScheme.secondary, - ), - ), - PopupMenuItem( - value: AddBookAction.query, - child: _AddActionItem( - text: 'add_book.query'.tr(), - iconData: Icons.search, - color: Theme.of(context).colorScheme.secondary, - ), - ), - PopupMenuItem( - value: AddBookAction.manual, - child: _AddActionItem( - text: 'add_book.manual'.tr(), - iconData: Icons.edit_outlined, - color: Theme.of(context).colorScheme.secondary, - ), - ), - ], - ), - const SizedBox(width: 12), - const Expanded( - child: DanteSearchBar(), - ), - const SizedBox(width: 32), - InkWell( - onTap: () => _openBottomSheet(context), - child: UserAvatar(user: user), - ), - const SizedBox(width: 16), - ], + child: LayoutBuilder( + builder: (context, constraints) { + if (isDesktop(constraints)) { + return _buildDesktopView(context); + } else { + return _buildMobileView(context, user); + } + }, ), ), ); @@ -92,13 +51,101 @@ class DanteAppBar extends ConsumerWidget implements PreferredSizeWidget { ); } + Widget _buildDesktopView(BuildContext context) { + return Row( + children: [ + const Spacer(), + const SizedBox( + width: 600, + child: DanteSearchBar(), + ), + const Spacer(), + TextButton( + onPressed: () async { + await _handleQueryAction(context); + }, + child: _AddActionItem( + text: 'add_book.query'.tr(), + iconData: Icons.search, + color: Theme.of(context).colorScheme.secondary, + ), + ), + const SizedBox(width: 16), + TextButton( + onPressed: () { + // TODO Support manually adding books + }, + child: _AddActionItem( + text: 'add_book.manual'.tr(), + iconData: Icons.edit_outlined, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ], + ); + } + + Widget _buildMobileView(BuildContext context, DanteUser? user) { + return Row( + children: [ + PopupMenuButton( + padding: const EdgeInsets.all(0), + icon: Icon( + Icons.add, + size: 32, + color: Theme.of(context).colorScheme.primary, + ), + onSelected: (AddBookAction action) async => + _handleAddBookAction(context, action), + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + value: AddBookAction.scan, + child: _AddActionItem( + text: 'add_book.scan'.tr(), + iconData: Icons.camera_alt_outlined, + color: Theme.of(context).colorScheme.secondary, + ), + ), + PopupMenuItem( + value: AddBookAction.query, + child: _AddActionItem( + text: 'add_book.query'.tr(), + iconData: Icons.search, + color: Theme.of(context).colorScheme.secondary, + ), + ), + PopupMenuItem( + value: AddBookAction.manual, + child: _AddActionItem( + text: 'add_book.manual'.tr(), + iconData: Icons.edit_outlined, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ], + ), + const SizedBox(width: 12), + const Expanded( + child: DanteSearchBar(), + ), + const SizedBox(width: 32), + InkWell( + onTap: () => _openBottomSheet(context), + child: UserAvatar(user: user), + ), + const SizedBox(width: 16), + ], + ); + } + _handleAddBookAction(BuildContext context, AddBookAction action) async { switch (action) { case AddBookAction.scan: context.go(DanteRoute.scanBook.navigationUrl); break; case AddBookAction.query: - await _handleQueryAction(context, action); + await _handleQueryAction(context); break; case AddBookAction.manual: // TODO Support manually adding books @@ -106,7 +153,7 @@ class DanteAppBar extends ConsumerWidget implements PreferredSizeWidget { } } - _handleQueryAction(BuildContext context, AddBookAction action) async { + _handleQueryAction(BuildContext context) async { final controller = TextEditingController(); await showDanteInputDialog( context, @@ -116,12 +163,11 @@ class DanteAppBar extends ConsumerWidget implements PreferredSizeWidget { actions: [ DanteDialogAction( name: 'cancel'.tr(), - action: (BuildContext context) => Navigator.of(context).pop(), + action: (BuildContext context) {}, ), DanteDialogAction( - name: 'search'.tr(), + name: 'search.search'.tr(), action: (BuildContext context) async { - Navigator.of(context).pop(); await openAddBookSheet( context, query: controller.text, @@ -137,70 +183,7 @@ class DanteAppBar extends ConsumerWidget implements PreferredSizeWidget { await showModalBottomSheet( context: context, barrierColor: Colors.transparent, - builder: (context) => Container( - color: Colors.transparent, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).bottomSheetTheme.backgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - ), - ), - height: 280, - child: Column( - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 12), - child: UserTag(), - ), - const DanteDivider(), - GridView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - mainAxisSpacing: 32, - childAspectRatio: 2, - ), - children: [ - _MenuItem( - text: 'Statistics', - icon: Icons.pie_chart_outline, - onItemClicked: () {}, - ), - _MenuItem( - text: 'Timeline', - icon: Icons.linear_scale, - onItemClicked: () {}, - ), - _MenuItem( - text: 'Wishlist', - icon: Icons.article, - onItemClicked: () {}, - ), - _MenuItem( - text: 'Recommendations', - icon: Icons.whatshot_outlined, - onItemClicked: () {}, - ), - _MenuItem( - text: 'Book keeping', - icon: Icons.all_inbox_outlined, - onItemClicked: () {}, - ), - _MenuItem( - text: 'Settings', - icon: Icons.settings_outlined, - onItemClicked: () => - context.go(DanteRoute.settings.navigationUrl), - ), - ], - ), - ], - ), - ), - ), + builder: (context) => const DanteBottomSheet(), ); } } @@ -260,7 +243,7 @@ class _MenuItem extends StatelessWidget { ), const SizedBox(height: 4), Text( - text, + text.tr(), textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context).colorScheme.onTertiaryContainer, @@ -273,7 +256,12 @@ class _MenuItem extends StatelessWidget { } class UserTag extends ConsumerWidget { - const UserTag({super.key}); + final bool useMobileLayout; + + const UserTag({ + required this.useMobileLayout, + super.key, + }); @override Widget build(BuildContext context, WidgetRef ref) { @@ -281,25 +269,11 @@ class UserTag extends ConsumerWidget { return user.when( data: (user) { - return Row( - children: [ - IconButton( - onPressed: () => context.go(DanteRoute.profile.navigationUrl), - icon: UserAvatar(user: user), - ), - const SizedBox(width: 4), - Expanded( - child: _getUserHeading(user), - ), - DanteOutlinedButton( - onPressed: () async => _handleLogout(context, ref, user), - child: const Text( - 'Logout', - textAlign: TextAlign.center, - ), - ), - ], - ); + if (useMobileLayout) { + return _buildMobileView(context, user, ref); + } else { + return _buildDesktopView(context, user, ref); + } }, loading: () { return const CircularProgressIndicator.adaptive(); @@ -311,17 +285,85 @@ class UserTag extends ConsumerWidget { ); } + Widget _buildDesktopView( + BuildContext context, + DanteUser? user, + WidgetRef ref, + ) { + return Column( + children: [ + IconButton( + onPressed: () => context.go(DanteRoute.profile.navigationUrl), + icon: UserAvatar(user: user), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: _getUserHeading(user), + ), + TextButton( + onPressed: () async => _handleLogout(context, ref, user), + child: Text( + 'logout'.tr(), + textAlign: TextAlign.center, + ), + ), + const DanteDivider(), + ], + ); + } + + Widget _buildMobileView( + BuildContext context, + DanteUser? user, + WidgetRef ref, + ) { + return Row( + children: [ + IconButton( + onPressed: () => context.go(DanteRoute.profile.navigationUrl), + icon: UserAvatar(user: user), + ), + const SizedBox(width: 4), + Expanded( + child: _getUserHeading(user), + ), + DanteOutlinedButton( + onPressed: () async => _handleLogout(context, ref, user), + child: Text( + 'logout'.tr(), + textAlign: TextAlign.center, + ), + ), + ], + ); + } + Widget _getUserHeading(DanteUser? user) { final String? name = user?.displayName; final String? email = user?.email; if (user?.source == AuthenticationSource.anonymous) { - return const Text('Anonymous Bookworm'); + return Text( + 'anonymous-user'.tr(), + textAlign: useMobileLayout ? TextAlign.start : TextAlign.center, + ); } return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: useMobileLayout + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, children: [ - name != null ? Text(name) : const SizedBox.shrink(), - email != null ? Text(email) : const SizedBox.shrink(), + name != null + ? Text( + name, + textAlign: TextAlign.center, + ) + : const SizedBox.shrink(), + email != null + ? Text( + email, + textAlign: TextAlign.center, + ) + : const SizedBox.shrink(), ], ); } @@ -387,8 +429,8 @@ class UserAvatar extends ConsumerWidget { } } -class BottomSheet extends ConsumerWidget { - const BottomSheet({super.key}); +class DanteBottomSheet extends ConsumerWidget { + const DanteBottomSheet({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -409,7 +451,7 @@ class BottomSheet extends ConsumerWidget { children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 12), - child: UserTag(), + child: UserTag(useMobileLayout: true), ), const DanteDivider(), GridView( @@ -422,32 +464,41 @@ class BottomSheet extends ConsumerWidget { ), children: [ _MenuItem( - text: 'Statistics', + text: 'navigation.stats', icon: Icons.pie_chart_outline, - onItemClicked: () {}, + onItemClicked: () => context.go( + DanteRoute.statistics.navigationUrl, + ), ), _MenuItem( - text: 'Timeline', + text: 'navigation.timeline', icon: Icons.linear_scale, - onItemClicked: () {}, + onItemClicked: () => context.go( + DanteRoute.timeline.navigationUrl, + ), ), _MenuItem( - text: 'Wishlist', + text: 'navigation.wishlist', icon: Icons.article, - onItemClicked: () {}, + onItemClicked: () => context.go( + DanteRoute.wishlist.navigationUrl, + ), ), _MenuItem( - text: 'Recommendations', + text: 'navigation.recommendations', icon: Icons.whatshot_outlined, - onItemClicked: () {}, + onItemClicked: () => context.go( + DanteRoute.recommendations.navigationUrl, + ), ), _MenuItem( - text: 'Book keeping', + text: 'navigation.book-keeping', icon: Icons.all_inbox_outlined, - onItemClicked: () {}, + onItemClicked: () => + context.go(DanteRoute.bookManagement.navigationUrl), ), _MenuItem( - text: 'Settings', + text: 'navigation.settings', icon: Icons.settings_outlined, onItemClicked: () => context.go(DanteRoute.settings.navigationUrl), diff --git a/lib/src/ui/core/dante_page_scaffold.dart b/lib/src/ui/core/dante_page_scaffold.dart new file mode 100644 index 0000000..dbe1193 --- /dev/null +++ b/lib/src/ui/core/dante_page_scaffold.dart @@ -0,0 +1,128 @@ +import 'package:dantex/src/providers/app_router.dart'; +import 'package:dantex/src/ui/core/dante_app_bar.dart'; +import 'package:dantex/src/util/layout_utils.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class DantePageScaffold extends StatefulWidget { + final Widget content; + + const DantePageScaffold({ + required this.content, + super.key, + }); + + @override + State createState() => _DantePageScaffoldState(); +} + +class _DantePageScaffoldState extends State { + + int _selectedIndex = 0; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + if (isDesktop(constraints)) { + return _buildDesktopView(context); + } else { + return _buildMobileView(context); + } + }, + ); + } + + Widget _buildDesktopView(BuildContext context) { + return Row( + children: [ + _buildNavigationRail(context), + const VerticalDivider( + thickness: 1, + width: 1, + ), + Expanded( + child: widget.content, + ), + ], + ); + } + + Widget _buildNavigationRail(BuildContext context) { + return SizedBox( + width: 160, + child: NavigationRail( + leading: const UserTag(useMobileLayout: false), + useIndicator: true, + onDestinationSelected: _onDestinationSelected, + labelType: NavigationRailLabelType.all, + destinations: [ + NavigationRailDestination( + icon: const Icon(Icons.book_outlined), + selectedIcon: const Icon(Icons.book), + label: Text('navigation.library'.tr()), + ), + NavigationRailDestination( + icon: const Icon(Icons.pie_chart_outline), + selectedIcon: const Icon(Icons.pie_chart), + label: Text('navigation.stats'.tr()), + ), + NavigationRailDestination( + icon: const Icon(Icons.linear_scale_outlined), + selectedIcon: const Icon(Icons.linear_scale), + label: Text('navigation.timeline'.tr()), + ), + NavigationRailDestination( + icon: const Icon(Icons.article_outlined), + selectedIcon: const Icon(Icons.article), + label: Text('navigation.wishlist'.tr()), + ), + NavigationRailDestination( + icon: const Icon(Icons.whatshot_outlined), + selectedIcon: const Icon(Icons.whatshot), + label: Text('navigation.recommendations'.tr()), + ), + NavigationRailDestination( + icon: const Icon(Icons.all_inbox_outlined), + selectedIcon: const Icon(Icons.all_inbox), + label: Text('navigation.book-keeping'.tr()), + ), + NavigationRailDestination( + icon: const Icon(Icons.settings_outlined), + selectedIcon: const Icon(Icons.settings), + label: Text('navigation.settings'.tr()), + ), + ], + selectedIndex: _selectedIndex, + ), + ); + } + + // For mobile, we are not using a dedicated layout + Widget _buildMobileView(BuildContext context) { + return widget.content; + } + + void _onDestinationSelected(int destinationIndex) { + + final DanteRoute? route = switch (destinationIndex) { + 0 => DanteRoute.library, + 1 => DanteRoute.statistics, + 2 => DanteRoute.timeline, + 3 => DanteRoute.wishlist, + 4 => DanteRoute.recommendations, + 5 => DanteRoute.bookManagement, + 6 => DanteRoute.settings, + int() => null, + }; + + if (route != null) { + context.go(route.navigationUrl); + } + + setState(() { + _selectedIndex = destinationIndex; + }); + } +} diff --git a/lib/src/ui/core/platform_components.dart b/lib/src/ui/core/platform_components.dart index eff837f..0b49696 100644 --- a/lib/src/ui/core/platform_components.dart +++ b/lib/src/ui/core/platform_components.dart @@ -58,6 +58,7 @@ Future showDanteInputDialog( }) { return showPlatformDialog( context: context, + useRootNavigator: false, builder: (_) => PlatformAlertDialog( title: _buildDialogTitle(title, leading), content: PlatformTextField( @@ -80,7 +81,10 @@ Future showDanteInputDialog( : Theme.of(context).colorScheme.onSurface, ), ), - onPressed: () => action.action(context), + onPressed: () { + Navigator.of(context).pop(); + action.action(context); + }, ), ) .toList(), diff --git a/lib/src/ui/management/book_management_page.dart b/lib/src/ui/management/book_management_page.dart new file mode 100644 index 0000000..99e8bed --- /dev/null +++ b/lib/src/ui/management/book_management_page.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class BookManagementPage extends StatelessWidget { + const BookManagementPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Material( + child: Center( + child: Text('TODO Implement BookManagement'), + ), + ); + } +} diff --git a/lib/src/ui/profile/profile_page.dart b/lib/src/ui/profile/profile_page.dart index 100e064..62e99bb 100644 --- a/lib/src/ui/profile/profile_page.dart +++ b/lib/src/ui/profile/profile_page.dart @@ -27,12 +27,6 @@ class ProfilePageSate extends ConsumerState { Widget build(BuildContext context) { return Scaffold( appBar: ThemedAppBar( - leading: InkWell( - onTap: () => Navigator.of(context).pop(), - child: const Icon( - Icons.arrow_back, - ), - ), title: Text( 'profile'.tr(), style: const TextStyle( diff --git a/lib/src/ui/recommendations/recommendations_page.dart b/lib/src/ui/recommendations/recommendations_page.dart new file mode 100644 index 0000000..8f2dae0 --- /dev/null +++ b/lib/src/ui/recommendations/recommendations_page.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class RecommendationsPage extends StatelessWidget { + const RecommendationsPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Material( + child: Center( + child: Text('TODO Implement Recommendations'), + ), + ); + } +} diff --git a/lib/src/ui/search/dante_search_bar.dart b/lib/src/ui/search/dante_search_bar.dart index 57a27b6..3ce2968 100644 --- a/lib/src/ui/search/dante_search_bar.dart +++ b/lib/src/ui/search/dante_search_bar.dart @@ -22,12 +22,15 @@ class DanteSearchBar extends StatelessWidget { color: Theme.of(context).colorScheme.onTertiaryContainer, ), const SizedBox(width: 4), - Center( - child: Text( - 'search.hint'.tr(), - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.onTertiaryContainer, + Expanded( + child: Center( + child: Text( + 'search.hint'.tr(), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).colorScheme.onTertiaryContainer, + ), ), ), ), diff --git a/lib/src/ui/stats/stats_page.dart b/lib/src/ui/stats/stats_page.dart new file mode 100644 index 0000000..4dd85ea --- /dev/null +++ b/lib/src/ui/stats/stats_page.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class StatsPage extends StatelessWidget { + const StatsPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Material( + child: Center( + child: Text('TODO Implement Statistics'), + ), + ); + } +} diff --git a/lib/src/ui/timeline/timeline_page.dart b/lib/src/ui/timeline/timeline_page.dart new file mode 100644 index 0000000..6e1fc6a --- /dev/null +++ b/lib/src/ui/timeline/timeline_page.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class TimelinePage extends StatelessWidget { + const TimelinePage({super.key}); + + @override + Widget build(BuildContext context) { + return const Material( + child: Center( + child: Text('TODO Implement Timeline'), + ), + ); + } +} diff --git a/lib/src/ui/wishlist/wishlist_page.dart b/lib/src/ui/wishlist/wishlist_page.dart new file mode 100644 index 0000000..d3c9ce8 --- /dev/null +++ b/lib/src/ui/wishlist/wishlist_page.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class WishlistPage extends StatelessWidget { + const WishlistPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Material( + child: Center( + child: Text('TODO Implement Wishlist'), + ), + ); + } +} diff --git a/lib/src/util/layout_utils.dart b/lib/src/util/layout_utils.dart new file mode 100644 index 0000000..aed00f5 --- /dev/null +++ b/lib/src/util/layout_utils.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +/// Helper function when we are only interested in desktop devices +bool isDesktop(BoxConstraints constraints) { + return getDeviceFormFactor(constraints) == DeviceFormFactor.desktop; +} + +DeviceFormFactor getDeviceFormFactor(BoxConstraints constraints) { + return switch (constraints.maxWidth) { + < 768 => DeviceFormFactor.phone, + < 1200 => DeviceFormFactor.tablet, + double() => DeviceFormFactor.desktop, + }; +} + +enum DeviceFormFactor { + desktop, + tablet, + phone, +} diff --git a/web/index.html b/web/index.html index 77dce7c..a7b929e 100644 --- a/web/index.html +++ b/web/index.html @@ -29,7 +29,7 @@ - dantex + Dante