search for books

This commit is contained in:
Dr-Blank 2024-06-05 12:08:44 -04:00
parent a1e238fc25
commit d372a6b096
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
12 changed files with 963 additions and 88 deletions

View file

@ -17,7 +17,8 @@
"mocktail",
"riverpod",
"shelfsdk",
"tapable"
"tapable",
"unfocus"
],
"cmake.configureOnOpen": false
}

View file

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'search_controller.g.dart';
/// The controller for the search bar.
@Riverpod(keepAlive: true)
class GlobalSearchController extends _$GlobalSearchController {
@override
Raw<SearchController> build() {
final controller = SearchController();
// dispose the controller when the provider is disposed
ref.onDispose(controller.dispose);
return controller;
}
}

View file

@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'search_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$globalSearchControllerHash() =>
r'd854ace6f2e00a10fc33aba63051375f82ad1b10';
/// The controller for the search bar.
///
/// Copied from [GlobalSearchController].
@ProviderFor(GlobalSearchController)
final globalSearchControllerProvider =
NotifierProvider<GlobalSearchController, Raw<SearchController>>.internal(
GlobalSearchController.new,
name: r'globalSearchControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$globalSearchControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$GlobalSearchController = Notifier<Raw<SearchController>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -0,0 +1,23 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/api/api_provider.dart';
import 'package:whispering_pages/settings/api_settings_provider.dart';
part 'search_result_provider.g.dart';
/// The provider for the search result.
@riverpod
FutureOr<LibrarySearchResponse?> searchResult(
SearchResultRef ref,
String query, {
int limit = 25,
}) async {
final api = ref.watch(authenticatedApiProvider);
final apiSettings = ref.watch(apiSettingsProvider);
return await api.libraries.search(
libraryId: apiSettings.activeLibraryId!,
query: query,
limit: limit,
);
}

View file

@ -0,0 +1,189 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'search_result_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$searchResultHash() => r'9baa643cce24f3a5e022f42202e423373939ef95';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// The provider for the search result.
///
/// Copied from [searchResult].
@ProviderFor(searchResult)
const searchResultProvider = SearchResultFamily();
/// The provider for the search result.
///
/// Copied from [searchResult].
class SearchResultFamily extends Family<AsyncValue<LibrarySearchResponse?>> {
/// The provider for the search result.
///
/// Copied from [searchResult].
const SearchResultFamily();
/// The provider for the search result.
///
/// Copied from [searchResult].
SearchResultProvider call(
String query, {
int limit = 25,
}) {
return SearchResultProvider(
query,
limit: limit,
);
}
@override
SearchResultProvider getProviderOverride(
covariant SearchResultProvider provider,
) {
return call(
provider.query,
limit: provider.limit,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'searchResultProvider';
}
/// The provider for the search result.
///
/// Copied from [searchResult].
class SearchResultProvider
extends AutoDisposeFutureProvider<LibrarySearchResponse?> {
/// The provider for the search result.
///
/// Copied from [searchResult].
SearchResultProvider(
String query, {
int limit = 25,
}) : this._internal(
(ref) => searchResult(
ref as SearchResultRef,
query,
limit: limit,
),
from: searchResultProvider,
name: r'searchResultProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$searchResultHash,
dependencies: SearchResultFamily._dependencies,
allTransitiveDependencies:
SearchResultFamily._allTransitiveDependencies,
query: query,
limit: limit,
);
SearchResultProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.query,
required this.limit,
}) : super.internal();
final String query;
final int limit;
@override
Override overrideWith(
FutureOr<LibrarySearchResponse?> Function(SearchResultRef provider) create,
) {
return ProviderOverride(
origin: this,
override: SearchResultProvider._internal(
(ref) => create(ref as SearchResultRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
query: query,
limit: limit,
),
);
}
@override
AutoDisposeFutureProviderElement<LibrarySearchResponse?> createElement() {
return _SearchResultProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SearchResultProvider &&
other.query == query &&
other.limit == limit;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, query.hashCode);
hash = _SystemHash.combine(hash, limit.hashCode);
return _SystemHash.finish(hash);
}
}
mixin SearchResultRef on AutoDisposeFutureProviderRef<LibrarySearchResponse?> {
/// The parameter `query` of this provider.
String get query;
/// The parameter `limit` of this provider.
int get limit;
}
class _SearchResultProviderElement
extends AutoDisposeFutureProviderElement<LibrarySearchResponse?>
with SearchResultRef {
_SearchResultProviderElement(super.provider);
@override
String get query => (origin as SearchResultProvider).query;
@override
int get limit => (origin as SearchResultProvider).limit;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -0,0 +1,401 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/api/api_provider.dart';
import 'package:whispering_pages/api/image_provider.dart';
import 'package:whispering_pages/api/library_item_provider.dart';
import 'package:whispering_pages/constants/hero_tag_conventions.dart';
import 'package:whispering_pages/features/explore/providers/search_controller.dart';
import 'package:whispering_pages/features/explore/view/search_result_page.dart';
import 'package:whispering_pages/router/router.dart';
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/settings/app_settings_provider.dart';
import 'package:whispering_pages/shared/widgets/shelves/book_shelf.dart';
const Duration debounceDuration = Duration(milliseconds: 500);
class ExplorePage extends HookConsumerWidget {
const ExplorePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// hooks for the dark mode
final settings = ref.watch(appSettingsProvider);
final api = ref.watch(authenticatedApiProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Explore'),
backgroundColor: Colors.transparent,
),
body: const MySearchBar(),
);
}
}
class MySearchBar extends HookConsumerWidget {
const MySearchBar({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final searchController = ref.watch(globalSearchControllerProvider);
final searchBarFocusNode = useFocusNode();
final api = ref.watch(authenticatedApiProvider);
final settings = ref.watch(apiSettingsProvider);
// The query currently being searched for. If null, there is no pending
// request.
String? currentQuery;
// The most recent suggestions received from the API.
final lastOptions = useState(<Widget>[]);
// Calls the "remote" API to search with the given query. Returns null when
// the call has been made obsolete.
Future<LibrarySearchResponse?> search(String query) async {
currentQuery = query;
// In a real application, there should be some error handling here.
final options = await api.libraries
.search(libraryId: settings.activeLibraryId!, query: query, limit: 3);
// If another search happened after this one, throw away these options.
if (currentQuery != query) {
return null;
}
currentQuery = null;
return options;
}
final debouncedSearch = _debounce(search);
return SearchAnchor(
searchController: searchController,
textInputAction: TextInputAction.search,
dividerColor: Colors.transparent,
builder: (context, controller) {
return SearchBar(
controller: controller,
focusNode: searchBarFocusNode,
// "What's your next page-turner?"
// "Looking for a good read? Type it in!"
// "Your next adventure is a search away..."
// "Bookworms unite! What's your pick today?"
// "Let's find your next literary love..."
// "Type in your next escape..."
// "What's on your reading list today?"
// "Seek and you shall find... your next book!"
// "Let's uncover your next favorite book..."
// "Ready to dive into a new story?"
hintText: 'Seek and you shall discover...',
// opacity: 0.5 for the hint text
hintStyle: WidgetStatePropertyAll(
Theme.of(context).textTheme.bodyMedium!.copyWith(
color:
Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
),
),
textInputAction: TextInputAction.search,
onTapOutside: (_) {
searchBarFocusNode.unfocus();
},
padding: const WidgetStatePropertyAll(
EdgeInsets.symmetric(horizontal: 16.0),
),
onTap: () {
controller.openView();
},
onChanged: (_) {
controller.openView();
},
leading: const Icon(Icons.search),
);
},
viewOnSubmitted: (value) {
context.pushNamed(
Routes.search.name,
queryParameters: {
'q': value,
},
);
},
suggestionsBuilder: (context, controller) async {
// check if the search controller is empty
if (controller.text.isEmpty) {
// TODO: show recent searches
return <Widget>[];
}
final options = await debouncedSearch(controller.text);
// debugPrint('options: $options');
if (options == null) {
// TODO: show loading indicator or failure message
return <Widget>[const ListTile(title: Text('Loading...'))];
}
// see if BookLibrarySearchResponse or PodcastLibrarySearchResponse
if (options is BookLibrarySearchResponse) {
lastOptions.value = buildBookSearchResult(options, context);
} else if (options is PodcastLibrarySearchResponse) {
lastOptions.value = options.podcast
.map(
(result) => ListTile(
title: Text(result.libraryItem.id),
subtitle: Text(result.libraryItem.libraryId),
),
)
.toList();
}
return lastOptions.value;
},
viewBuilder: (suggestions) {
// return a container such that on tap other than the suggestions, the view is closed
return GestureDetector(
onTap: () {
searchController.closeView(searchController.text);
searchBarFocusNode.unfocus();
},
child: ListView.builder(
itemCount: suggestions.length,
itemBuilder: (context, index) {
return suggestions.toList()[index];
},
),
);
},
);
}
}
List<Widget> buildBookSearchResult(
BookLibrarySearchResponse options,
BuildContext context,
) {
// build sections for the search results
// 1. Books
// 2. Authors
// 3. Series
// 4. Tags
// 5. Narrators
// each section will have a title and a list of items
// only show the section if there are items in it
final sections = <Widget>[];
if (options.book.isNotEmpty) {
sections.add(
SearchResultMiniSection(
// title: 'Books',
category: SearchResultCategory.books,
options: options.book.map(
(result) {
// convert result to a book object
final book =
BookExpanded.fromJson(result.libraryItem.media.toJson());
final metadata = BookMetadataExpanded.fromJson(
book.metadata.toJson(),
);
return BookSearchResultMini(book: book, metadata: metadata);
},
),
),
);
}
if (options.authors.isNotEmpty) {
sections.add(
SearchResultMiniSection(
// title: 'Authors',
category: SearchResultCategory.authors,
options: options.authors.map(
(result) {
return ListTile(title: Text(result.name));
},
),
),
);
}
return sections;
}
/// A mini version of the book that is displayed in the search results.
class BookSearchResultMini extends HookConsumerWidget {
const BookSearchResultMini({
super.key,
required this.book,
required this.metadata,
});
final BookExpanded book;
final BookMetadataExpanded metadata;
@override
Widget build(BuildContext context, WidgetRef ref) {
final item = ref.watch(libraryItemProvider(book.libraryItemId)).valueOrNull;
final image = item == null
? const AsyncValue.loading()
: ref.watch(coverImageProvider(item));
return ListTile(
leading: SizedBox(
width: 50,
height: 50,
child: Hero(
tag: HeroTagPrefixes.bookCover + book.libraryItemId,
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: image.when(
data: (bytes) => Image.memory(
bytes,
fit: BoxFit.cover,
),
loading: () => const BookCoverSkeleton(),
error: (error, _) => const Icon(Icons.error),
),
),
),
),
title: Text(book.metadata.title ?? ''),
subtitle: Text(
maxLines: 1,
overflow: TextOverflow.ellipsis,
metadata.authors
.map(
(author) => author.name,
)
.join(', '),
),
onTap: () {
// navigate to the book details page
context.pushNamed(
Routes.libraryItem.name,
pathParameters: {
Routes.libraryItem.pathParamName!: book.libraryItemId,
},
);
},
trailing: IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
// TODO: show a menu with options for the book
},
),
);
}
}
class SearchResultMiniSection extends HookConsumerWidget {
const SearchResultMiniSection({
super.key,
required this.category,
required this.options,
this.onTap,
});
// final String title;
final SearchResultCategory category;
final Iterable<Widget> options;
/// A callback that is called when the section is tapped.
/// typically used to navigate to a search page for the section
final VoidCallback? onTap;
@override
Widget build(BuildContext context, WidgetRef ref) {
final searchController = ref.watch(globalSearchControllerProvider);
openSearch() {
final query = searchController.text;
context.pushNamed(
Routes.search.name,
queryParameters: {
'q': query,
'category': category.toString().split('.').last,
},
);
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// title for the section
// Heading and then a hr line
Row(
children: [
Text(
category.toString().split('.').last,
style: Theme.of(context).textTheme.headlineSmall,
),
const Spacer(),
IconButton(
icon: const Icon(Icons.arrow_forward_ios),
onPressed: onTap ?? openSearch,
),
],
),
// const Divider(
// height: 1,
// thickness: BorderSide.strokeAlignCenter,
// ),
...options,
],
),
);
}
}
typedef _Debounceable<S, T> = Future<S?> Function(T parameter);
/// Returns a new function that is a debounced version of the given function.
///
/// This means that the original function will be called only after no calls
/// have been made for the given Duration.
_Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) {
_DebounceTimer? debounceTimer;
return (T parameter) async {
if (debounceTimer != null && !debounceTimer!.isCompleted) {
debounceTimer!.cancel();
}
debounceTimer = _DebounceTimer();
try {
await debounceTimer!.future;
} catch (error) {
if (error is _CancelException) {
return null;
}
rethrow;
}
return function(parameter);
};
}
// A wrapper around Timer used for debouncing.
class _DebounceTimer {
_DebounceTimer() {
_timer = Timer(debounceDuration, _onComplete);
}
late final Timer _timer;
final Completer<void> _completer = Completer<void>();
void _onComplete() {
_completer.complete();
}
Future<void> get future => _completer.future;
bool get isCompleted => _completer.isCompleted;
void cancel() {
_timer.cancel();
_completer.completeError(const _CancelException());
}
}
// An exception indicating that the timer was canceled.
class _CancelException implements Exception {
const _CancelException();
}

View file

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/features/explore/providers/search_result_provider.dart';
import 'package:whispering_pages/features/explore/view/explore_page.dart';
enum SearchResultCategory {
books,
authors,
series,
tags,
narrators,
}
class SearchResultPage extends HookConsumerWidget {
const SearchResultPage({
super.key,
required this.query,
this.category,
Object? extra,
});
/// The search query.
final String query;
/// The category of the search result, if not provided, the search result will be displayed in all categories.
final SearchResultCategory? category;
@override
Widget build(BuildContext context, WidgetRef ref) {
final results = ref.watch(searchResultProvider(query));
return Scaffold(
appBar: AppBar(
title: Text(
category != null
? '${category.toString().split('.').last} in "$query"'
: 'Search result for $query',
),
),
body: results.when(
data: (options) {
if (options == null) {
return Container(
child: const Text('No data found'),
);
}
if (options is BookLibrarySearchResponse) {
if (category == null) {
return Container();
}
return switch (category!) {
SearchResultCategory.books => ListView.builder(
itemCount: options.book.length,
itemBuilder: (context, index) {
final book = BookExpanded.fromJson(
options.book[index].libraryItem.media.toJson(),
);
final metadata = BookMetadataExpanded.fromJson(
book.metadata.toJson(),
);
return BookSearchResultMini(
book: book,
metadata: metadata,
);
},
),
SearchResultCategory.authors => Container(),
SearchResultCategory.series => Container(),
SearchResultCategory.tags => Container(),
SearchResultCategory.narrators => Container(),
};
}
return null;
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Center(
child: Text('Error: $error'),
),
),
);
}
}

View file

@ -96,7 +96,11 @@ class AudiobookPlayer extends HookConsumerWidget {
controller: ref.watch(miniplayerControllerProvider),
elevation: 4,
onDismissed: () {
// add a delay before closing the player
// to allow the user to see the player closing
Future.delayed(const Duration(milliseconds: 300), () {
player.setSourceAudioBook(null);
});
},
curve: Curves.easeOut,
builder: (height, percentage) {

View file

@ -25,6 +25,15 @@ class Routes {
pathName: 'config',
name: 'settings',
);
static const search = _SimpleRoute(
pathName: 'search',
name: 'search',
// parentRoute: library,
);
static const explore = _SimpleRoute(
pathName: 'explore',
name: 'explore',
);
}
// a class to store path
@ -34,12 +43,17 @@ class _SimpleRoute {
required this.pathName,
this.pathParamName,
required this.name,
this.parentRoute,
});
final String pathName;
final String? pathParamName;
final String name;
final _SimpleRoute? parentRoute;
String get path =>
'${parentRoute?.path ?? ''}${parentRoute != null ? '/' : ''}$localPath';
String get localPath =>
'/$pathName${pathParamName != null ? '/:$pathParamName' : ''}';
}

View file

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:whispering_pages/features/explore/view/explore_page.dart';
import 'package:whispering_pages/features/explore/view/search_result_page.dart';
import 'package:whispering_pages/features/item_viewer/view/library_item_page.dart';
import 'package:whispering_pages/features/onboarding/view/onboarding_single_page.dart';
import 'package:whispering_pages/pages/app_settings.dart';
@ -75,6 +77,49 @@ class MyAppRouter {
),
],
),
// search/explore page
StatefulShellBranch(
routes: <RouteBase>[
GoRoute(
path: Routes.explore.path,
name: Routes.explore.name,
// builder: (context, state) => const ExplorePage(),
pageBuilder: defaultPageBuilder(const ExplorePage()),
),
// search page
GoRoute(
path: Routes.search.path,
name: Routes.search.name,
// builder: (context, state) {
// final libraryId = state
// .pathParameters[Routes.library.pathParamName]!;
// return LibrarySearchPage(
// libraryId: libraryId,
// extra: state.extra,
// );
// },
pageBuilder: (context, state) {
final queryParam = state.uri.queryParameters['q']!;
final category = state.uri.queryParameters['category'];
final child = SearchResultPage(
extra: state.extra,
query: queryParam,
category: category != null
? SearchResultCategory.values.firstWhere(
(e) => e.toString().split('.').last == category,
)
: null,
);
return buildPageWithDefaultTransition(
context: context,
state: state,
child: child,
);
},
),
],
),
// settings page
StatefulShellBranch(
routes: <RouteBase>[
GoRoute(

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:whispering_pages/features/explore/providers/search_controller.dart';
import 'package:whispering_pages/features/player/providers/player_form.dart';
import 'package:whispering_pages/features/player/view/audiobook_player.dart';
@ -29,6 +30,9 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
// Clamp the value between 0 and 1
percentExpanded = percentExpanded.clamp(0.0, 1.0);
final SearchController searchController =
ref.watch(globalSearchControllerProvider);
return Scaffold(
body: Stack(
children: [
@ -51,27 +55,27 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
unselectedFontSize:
Theme.of(context).textTheme.labelMedium!.fontSize!,
showUnselectedLabels: false,
fixedColor: Theme.of(context).colorScheme.onBackground,
// type: BottomNavigationBarType.fixed,
fixedColor: Theme.of(context).colorScheme.onSurface,
enableFeedback: true,
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),
items: _navigationItems
.map(
(item) => BottomNavigationBarItem(
icon: Icon(item.icon),
activeIcon: item.activeIcon != null
? Icon(item.activeIcon)
: Icon(item.icon),
label: item.name,
),
BottomNavigationBarItem(
label: 'Settings',
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
),
],
)
.toList(),
currentIndex: navigationShell.currentIndex,
onTap: (int index) => _onTap(context, index),
onTap: (int index) => _onTap(context, index, ref),
),
),
),
@ -80,7 +84,7 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
/// 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) {
void _onTap(BuildContext context, int index, WidgetRef ref) {
// 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.
@ -92,5 +96,53 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
// using the initialLocation parameter of goBranch.
initialLocation: index == navigationShell.currentIndex,
);
// Check if the current branch is the same as the branch that was tapped.
// If it is, debugPrint a message to the console.
if (index == navigationShell.currentIndex) {
// if current branch is explore, open the search view
if (index == 1) {
final searchController = ref.read(globalSearchControllerProvider);
// open the search view if not already open
if (!searchController.isOpen) {
searchController.openView();
} else {
searchController.closeView(null);
}
}
debugPrint('Tapped the current branch');
}
}
}
// list of constants with names and icons so that they can be used in the bottom navigation bar
// and reused for nav rail and other places
const _navigationItems = [
_NavigationItem(
name: 'Home',
icon: Icons.home_outlined,
activeIcon: Icons.home,
),
_NavigationItem(
name: 'Explore',
icon: Icons.search_outlined,
activeIcon: Icons.search,
),
_NavigationItem(
name: 'Settings',
icon: Icons.settings_outlined,
activeIcon: Icons.settings,
),
];
class _NavigationItem {
const _NavigationItem({
required this.name,
required this.icon,
this.activeIcon,
});
final String name;
final IconData icon;
final IconData? activeIcon;
}

View file

@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: "0763b45fa9294197a2885c8567927e2830ade852e5c896fd4ab7e0e348d0f373"
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev"
source: hosted
version: "3.5.0"
version: "3.6.1"
args:
dependency: transitive
description:
@ -101,10 +101,10 @@ packages:
dependency: "direct main"
description:
name: audio_video_progress_bar
sha256: ccc7d7b83d2a16c52d4a7fb332faabd1baa053fb0e4c16815aefd3945ab33b81
sha256: "552b1f73c56c4c88407999e0a8507176f60c56de3e6d63bc20a0eab48467d4c9"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
version: "2.0.3"
auto_scroll_text:
dependency: "direct main"
description:
@ -141,10 +141,10 @@ packages:
dependency: transitive
description:
name: build_daemon
sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9"
url: "https://pub.dev"
source: hosted
version: "4.0.1"
version: "4.0.2"
build_resolvers:
dependency: transitive
description:
@ -157,10 +157,10 @@ packages:
dependency: "direct dev"
description:
name: build_runner
sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa"
url: "https://pub.dev"
source: hosted
version: "2.4.9"
version: "2.4.10"
build_runner_core:
dependency: transitive
description:
@ -281,6 +281,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32"
url: "https://pub.dev"
source: hosted
version: "0.3.4+1"
crypto:
dependency: transitive
description:
@ -333,10 +341,10 @@ packages:
dependency: "direct main"
description:
name: easy_stepper
sha256: "8d1d9f048f6a079dcfa98cea56650d77976f21a50033bf99dd0d82046e12060b"
sha256: d09e974ec9148480072f8a7d3b0779dfdbc1a3ec1ff7daa7fbda95b0c1fe7453
url: "https://pub.dev"
source: hosted
version: "0.8.4"
version: "0.8.5"
fake_async:
dependency: transitive
description:
@ -365,10 +373,10 @@ packages:
dependency: transitive
description:
name: file_picker
sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030
sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a"
url: "https://pub.dev"
source: hosted
version: "5.5.0"
version: "8.0.3"
fixnum:
dependency: transitive
description:
@ -426,18 +434,18 @@ packages:
dependency: "direct main"
description:
name: flutter_material_pickers
sha256: "1100bfd9a296a6680578aba8c51a0db114fb8ef94708fe320fe6da92b1f8c0e1"
sha256: "1f0977df9d3977c6621fff602f6956107cf5ff0df58d3441459e5b2e37256131"
url: "https://pub.dev"
source: hosted
version: "3.6.0"
version: "3.7.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f"
sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e
url: "https://pub.dev"
source: hosted
version: "2.0.19"
version: "2.0.20"
flutter_riverpod:
dependency: transitive
description:
@ -516,10 +524,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: "81f94e16d7063b60f0da3a79f872e140d6518f306749303bf981abd7d6b46734"
sha256: abec47eb8c8c36ebf41d0a4c64dbbe7f956e39a012b3aafc530e951bdc12fe3f
url: "https://pub.dev"
source: hosted
version: "14.1.0"
version: "14.1.4"
graphs:
dependency: transitive
description:
@ -580,10 +588,10 @@ packages:
dependency: transitive
description:
name: image
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
url: "https://pub.dev"
source: hosted
version: "4.1.7"
version: "4.2.0"
infinite_listview:
dependency: transitive
description:
@ -596,10 +604,10 @@ packages:
dependency: transitive
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.18.1"
version: "0.19.0"
io:
dependency: transitive
description:
@ -652,18 +660,18 @@ packages:
dependency: "direct main"
description:
name: just_audio
sha256: b7cb6bbf3750caa924d03f432ba401ec300fd90936b3f73a9b33d58b1e96286b
sha256: "5abfab1d199e01ab5beffa61b3e782350df5dad036cb8c83b79fa45fc656614e"
url: "https://pub.dev"
source: hosted
version: "0.9.37"
version: "0.9.38"
just_audio_background:
dependency: "direct main"
description:
name: just_audio_background
sha256: "3454ffc97edfa1282b7f42759bfa8aa13d9114a24465f4101e0d3ae58a9327fb"
sha256: "0a8815284b22756d447c9d616d6c4c022dfcc390f23814b637ef891e51279c69"
url: "https://pub.dev"
source: hosted
version: "0.0.1-beta.11"
version: "0.0.1-beta.12"
just_audio_media_kit:
dependency: "direct main"
description:
@ -676,42 +684,42 @@ packages:
dependency: transitive
description:
name: just_audio_platform_interface
sha256: c3dee0014248c97c91fe6299edb73dc4d6c6930a2f4f713579cd692d9e47f4a1
sha256: "0243828cce503c8366cc2090cefb2b3c871aa8ed2f520670d76fd47aa1ab2790"
url: "https://pub.dev"
source: hosted
version: "4.2.2"
version: "4.3.0"
just_audio_web:
dependency: transitive
description:
name: just_audio_web
sha256: "134356b0fe3d898293102b33b5fd618831ffdc72bb7a1b726140abdf22772b70"
sha256: "0edb481ad4aa1ff38f8c40f1a3576013c3420bf6669b686fe661627d49bc606c"
url: "https://pub.dev"
source: hosted
version: "0.4.9"
version: "0.4.11"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev"
source: hosted
version: "10.0.0"
version: "10.0.4"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.3"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.1"
lints:
dependency: transitive
description:
@ -740,10 +748,10 @@ packages:
dependency: "direct main"
description:
name: lottie
sha256: ce2bb2605753915080e4ee47f036a64228c88dc7f56f7bc1dbe912d75b55b1e2
sha256: "6a24ade5d3d918c306bb1c21a6b9a04aab0489d51a2582522eea820b4093b62b"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.2"
matcher:
dependency: transitive
description:
@ -788,10 +796,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.12.0"
mime:
dependency: transitive
description:
@ -851,18 +859,18 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514"
url: "https://pub.dev"
source: hosted
version: "2.2.4"
version: "2.2.5"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.0"
path_provider_linux:
dependency: transitive
description:
@ -931,10 +939,10 @@ packages:
dependency: transitive
description:
name: pubspec_parse
sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
url: "https://pub.dev"
source: hosted
version: "1.2.3"
version: "1.3.0"
riverpod:
dependency: transitive
description:
@ -1011,10 +1019,10 @@ packages:
dependency: transitive
description:
name: shelf_web_socket
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "2.0.0"
shelfsdk:
dependency: "direct main"
description:
@ -1159,10 +1167,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev"
source: hosted
version: "0.6.1"
version: "0.7.0"
timing:
dependency: transitive
description:
@ -1183,10 +1191,10 @@ packages:
dependency: transitive
description:
name: universal_platform
sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
url: "https://pub.dev"
source: hosted
version: "1.0.0+1"
version: "1.1.0"
uri_parser:
dependency: transitive
description:
@ -1207,18 +1215,18 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775"
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
url: "https://pub.dev"
source: hosted
version: "6.3.1"
version: "6.3.3"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5"
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
url: "https://pub.dev"
source: hosted
version: "6.2.5"
version: "6.3.0"
url_launcher_linux:
dependency: transitive
description:
@ -1231,10 +1239,10 @@ packages:
dependency: transitive
description:
name: url_launcher_macos
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.2.0"
url_launcher_platform_interface:
dependency: transitive
description:
@ -1279,10 +1287,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev"
source: hosted
version: "13.0.0"
version: "14.2.1"
watcher:
dependency: transitive
description:
@ -1299,22 +1307,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078"
url: "https://pub.dev"
source: hosted
version: "0.1.5"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42"
sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276
url: "https://pub.dev"
source: hosted
version: "2.4.5"
version: "3.0.0"
win32:
dependency: transitive
description:
name: win32
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
url: "https://pub.dev"
source: hosted
version: "5.5.0"
version: "5.5.1"
xdg_directories:
dependency: transitive
description:
@ -1340,5 +1356,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.3.4 <4.0.0"
flutter: ">=3.19.0"
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0"