diff --git a/lib/screens/bookmarks/model/search_bookmarks.model.dart b/lib/screens/bookmarks/model/search_bookmarks.model.dart index 66252bd..f538657 100644 --- a/lib/screens/bookmarks/model/search_bookmarks.model.dart +++ b/lib/screens/bookmarks/model/search_bookmarks.model.dart @@ -1,11 +1,26 @@ import 'package:flutter/material.dart'; +import 'package:linkdy/constants/enums.dart'; +import 'package:linkdy/models/data/bookmarks.dart'; + class SearchBookmarksModel { final TextEditingController searchController; String? searchTerm; + int currentPage; + int limit; + List bookmarks; + LoadStatus inialLoadStatus; + bool loadingMore; + int maxNumber; SearchBookmarksModel({ required this.searchController, this.searchTerm = "", + this.currentPage = 0, + this.limit = 30, + required this.bookmarks, + this.inialLoadStatus = LoadStatus.loaded, + this.loadingMore = false, + this.maxNumber = 0, }); } diff --git a/lib/screens/bookmarks/provider/add_bookmark.provider.dart b/lib/screens/bookmarks/provider/add_bookmark.provider.dart index 2670fbd..5100c16 100644 --- a/lib/screens/bookmarks/provider/add_bookmark.provider.dart +++ b/lib/screens/bookmarks/provider/add_bookmark.provider.dart @@ -98,7 +98,7 @@ class AddBookmark extends _$AddBookmark { processModal.close(); if (result.successful == true) { - ref.invalidate(bookmarksRequestProvider); + ref.read(bookmarksProvider.notifier).refresh(); ref.watch(routerProvider).pop(); } else { showSnacbkar( diff --git a/lib/screens/bookmarks/provider/add_bookmark.provider.g.dart b/lib/screens/bookmarks/provider/add_bookmark.provider.g.dart index 95eba2a..78c92a4 100644 --- a/lib/screens/bookmarks/provider/add_bookmark.provider.g.dart +++ b/lib/screens/bookmarks/provider/add_bookmark.provider.g.dart @@ -175,7 +175,7 @@ final getTagsProvider = ); typedef GetTagsRef = AutoDisposeFutureProviderRef>; -String _$addBookmarkHash() => r'7a2a1c6c6597aa28fa478d2ef9dc3bc4d71c143d'; +String _$addBookmarkHash() => r'ace81358d08fd833b2aa06edddcb10f00728c30c'; /// See also [AddBookmark]. @ProviderFor(AddBookmark) diff --git a/lib/screens/bookmarks/provider/search_bookmarks.provider.dart b/lib/screens/bookmarks/provider/search_bookmarks.provider.dart index 63574dc..021f539 100644 --- a/lib/screens/bookmarks/provider/search_bookmarks.provider.dart +++ b/lib/screens/bookmarks/provider/search_bookmarks.provider.dart @@ -1,28 +1,67 @@ import 'package:flutter/material.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:linkdy/screens/bookmarks/model/search_bookmarks.model.dart'; -import 'package:linkdy/models/api_response.dart'; -import 'package:linkdy/models/data/bookmarks.dart'; +import 'package:linkdy/constants/enums.dart'; import 'package:linkdy/providers/api_client_provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'search_bookmarks.provider.g.dart'; @riverpod -FutureOr?> fetchSearchBookmarks(FetchSearchBookmarksRef ref) async { +FutureOr fetchSearchBookmarks(FetchSearchBookmarksRef ref, int limit) async { if (ref.watch(searchBookmarksProvider).searchTerm == "") return null; - final result = await ref.read(apiClientProvider)!.fetchBookmarks(q: ref.watch(searchBookmarksProvider).searchTerm); - return result; + + final result = await ref.read(apiClientProvider)!.fetchBookmarks( + q: ref.watch(searchBookmarksProvider).searchTerm, + limit: limit, + offset: 0, + ); + + if (result.successful == true) { + ref.read(searchBookmarksProvider).bookmarks = result.content!.results!; + ref.read(searchBookmarksProvider).maxNumber = result.content!.count!; + ref.read(searchBookmarksProvider).currentPage = 0; + ref.read(searchBookmarksProvider).loadingMore = false; + ref.read(searchBookmarksProvider).inialLoadStatus = LoadStatus.loaded; + } else { + ref.read(searchBookmarksProvider).inialLoadStatus = LoadStatus.error; + } + ref.read(searchBookmarksProvider.notifier).notifyListeners(); +} + +@riverpod +FutureOr fetchSearchBookmarksLoadMore(FetchSearchBookmarksLoadMoreRef ref) async { + final provider = ref.watch(searchBookmarksProvider); + + final newOffset = provider.limit * (provider.currentPage + 1); + + final result = await ref.watch(apiClientProvider)!.fetchBookmarks( + q: provider.searchTerm, + limit: provider.limit, + offset: newOffset, + ); + await Future.delayed(Duration(seconds: 3)); + + if (result.successful == true) { + provider.bookmarks = [...provider.bookmarks, ...result.content!.results!]; + provider.maxNumber = result.content!.count!; + provider.currentPage = provider.currentPage + 1; + } + + ref.read(searchBookmarksProvider.notifier).setLoadingMore(false); } @riverpod class SearchBookmarks extends _$SearchBookmarks { @override SearchBookmarksModel build() { - return SearchBookmarksModel( + final model = SearchBookmarksModel( searchController: TextEditingController(), + bookmarks: [], ); + ref.read(fetchSearchBookmarksProvider(model.limit)); + return model; } void clearSearch() { @@ -35,8 +74,22 @@ class SearchBookmarks extends _$SearchBookmarks { ref.notifyListeners(); } - void setSearchTerm(String value) { - state.searchTerm = value; + void setSearchTerm() { + state.searchTerm = state.searchController.text; + ref.notifyListeners(); + } + + void setLoadingMore(bool value) { + state.loadingMore = value; + ref.notifyListeners(); + } + + void setInitialLoadStatus(LoadStatus status) { + state.inialLoadStatus = status; + ref.notifyListeners(); + } + + void notifyListeners() { ref.notifyListeners(); } } diff --git a/lib/screens/bookmarks/provider/search_bookmarks.provider.g.dart b/lib/screens/bookmarks/provider/search_bookmarks.provider.g.dart index e22ab86..1009acc 100644 --- a/lib/screens/bookmarks/provider/search_bookmarks.provider.g.dart +++ b/lib/screens/bookmarks/provider/search_bookmarks.provider.g.dart @@ -7,24 +7,174 @@ part of 'search_bookmarks.provider.dart'; // ************************************************************************** String _$fetchSearchBookmarksHash() => - r'2b871e9d1f75fb7f7b969f819cfdd9442593c4cb'; + r'dea1740df4ad970bc1ffd90d85aa0d1d2cf94bf2'; + +/// 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)); + } +} /// See also [fetchSearchBookmarks]. @ProviderFor(fetchSearchBookmarks) -final fetchSearchBookmarksProvider = - AutoDisposeFutureProvider?>.internal( - fetchSearchBookmarks, - name: r'fetchSearchBookmarksProvider', +const fetchSearchBookmarksProvider = FetchSearchBookmarksFamily(); + +/// See also [fetchSearchBookmarks]. +class FetchSearchBookmarksFamily extends Family> { + /// See also [fetchSearchBookmarks]. + const FetchSearchBookmarksFamily(); + + /// See also [fetchSearchBookmarks]. + FetchSearchBookmarksProvider call( + int limit, + ) { + return FetchSearchBookmarksProvider( + limit, + ); + } + + @override + FetchSearchBookmarksProvider getProviderOverride( + covariant FetchSearchBookmarksProvider provider, + ) { + return call( + provider.limit, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'fetchSearchBookmarksProvider'; +} + +/// See also [fetchSearchBookmarks]. +class FetchSearchBookmarksProvider extends AutoDisposeFutureProvider { + /// See also [fetchSearchBookmarks]. + FetchSearchBookmarksProvider( + int limit, + ) : this._internal( + (ref) => fetchSearchBookmarks( + ref as FetchSearchBookmarksRef, + limit, + ), + from: fetchSearchBookmarksProvider, + name: r'fetchSearchBookmarksProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$fetchSearchBookmarksHash, + dependencies: FetchSearchBookmarksFamily._dependencies, + allTransitiveDependencies: + FetchSearchBookmarksFamily._allTransitiveDependencies, + limit: limit, + ); + + FetchSearchBookmarksProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.limit, + }) : super.internal(); + + final int limit; + + @override + Override overrideWith( + FutureOr Function(FetchSearchBookmarksRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: FetchSearchBookmarksProvider._internal( + (ref) => create(ref as FetchSearchBookmarksRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + limit: limit, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _FetchSearchBookmarksProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is FetchSearchBookmarksProvider && other.limit == limit; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, limit.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin FetchSearchBookmarksRef on AutoDisposeFutureProviderRef { + /// The parameter `limit` of this provider. + int get limit; +} + +class _FetchSearchBookmarksProviderElement + extends AutoDisposeFutureProviderElement + with FetchSearchBookmarksRef { + _FetchSearchBookmarksProviderElement(super.provider); + + @override + int get limit => (origin as FetchSearchBookmarksProvider).limit; +} + +String _$fetchSearchBookmarksLoadMoreHash() => + r'4c085e4dc487554a209fa73170294b5b8e279c24'; + +/// See also [fetchSearchBookmarksLoadMore]. +@ProviderFor(fetchSearchBookmarksLoadMore) +final fetchSearchBookmarksLoadMoreProvider = + AutoDisposeFutureProvider.internal( + fetchSearchBookmarksLoadMore, + name: r'fetchSearchBookmarksLoadMoreProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$fetchSearchBookmarksHash, + : _$fetchSearchBookmarksLoadMoreHash, dependencies: null, allTransitiveDependencies: null, ); -typedef FetchSearchBookmarksRef - = AutoDisposeFutureProviderRef?>; -String _$searchBookmarksHash() => r'56efd123e03f6a080f4a61cc91794aab2e563199'; +typedef FetchSearchBookmarksLoadMoreRef = AutoDisposeFutureProviderRef; +String _$searchBookmarksHash() => r'db9f8d8cc488e820791e22c594ad9046da73ec25'; /// See also [SearchBookmarks]. @ProviderFor(SearchBookmarks) diff --git a/lib/screens/bookmarks/ui/search_bookmarks.dart b/lib/screens/bookmarks/ui/search_bookmarks.dart index fe9c981..059c0d7 100644 --- a/lib/screens/bookmarks/ui/search_bookmarks.dart +++ b/lib/screens/bookmarks/ui/search_bookmarks.dart @@ -6,6 +6,7 @@ import 'package:linkdy/screens/bookmarks/ui/bookmark_item.dart'; import 'package:linkdy/widgets/enter_search_term_screen.dart'; import 'package:linkdy/widgets/error_screen.dart'; +import 'package:linkdy/constants/enums.dart'; import 'package:linkdy/providers/router_provider.dart'; import 'package:linkdy/i18n/strings.g.dart'; @@ -16,6 +17,18 @@ class SearchBookmarksScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final provider = ref.watch(searchBookmarksProvider); + + bool scrollListener(ScrollUpdateNotification scrollNotification) { + if (scrollNotification.metrics.extentAfter < 100 && + provider.loadingMore == false && + provider.bookmarks.length < provider.maxNumber) { + ref.read(searchBookmarksProvider.notifier).setLoadingMore(true); + ref.watch(fetchSearchBookmarksLoadMoreProvider); + } + return false; + } + return Scaffold( appBar: AppBar( toolbarHeight: 68, @@ -25,13 +38,19 @@ class SearchBookmarksScreen extends ConsumerWidget { onPressed: () => ref.watch(routerProvider).pop(), ), ), + titleSpacing: 0, title: Padding( - padding: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.only(bottom: 8, right: 16), child: ClipRRect( borderRadius: BorderRadius.circular(50), child: TextFormField( controller: ref.watch(searchBookmarksProvider).searchController, - onChanged: ref.read(searchBookmarksProvider.notifier).setSearchTerm, + onChanged: (_) => ref.read(searchBookmarksProvider.notifier).notifyListeners(), + onEditingComplete: () { + ref.read(searchBookmarksProvider.notifier).setSearchTerm(); + ref.read(searchBookmarksProvider.notifier).setInitialLoadStatus(LoadStatus.loading); + ref.read(fetchSearchBookmarksProvider(provider.limit)); + }, decoration: InputDecoration( hintText: t.bookmarks.search.searchBookmarks, prefixIcon: const Icon(Icons.search_rounded), @@ -56,36 +75,47 @@ class SearchBookmarksScreen extends ConsumerWidget { ), body: Builder( builder: (context) { - if (ref.watch(searchBookmarksProvider).searchTerm == "") { + if (provider.searchTerm == "") { return EnterSearchTermScreen(message: t.bookmarks.search.inputSearchTerm); } - return ref.watch(fetchSearchBookmarksProvider).when( - data: (data) { - if (data == null || data.successful == false) { - return ErrorScreen( - error: t.bookmarks.search.cannotSearchError, - ); - } else if (data.content!.results!.isEmpty) { - return NoDataScreen( - message: t.bookmarks.search.inputtedSearchTermNoResults, - ); - } else { - return ListView.builder( - itemCount: data.content!.results!.length, - itemBuilder: (context, index) => BookmarkItem( - bookmark: data.content!.results![index], - ), - ); - } - }, - error: (error, stackTrace) => ErrorScreen( - error: t.bookmarks.search.cannotSearchError, - ), - loading: () => const Center( - child: CircularProgressIndicator(), - ), - ); + if (provider.inialLoadStatus == LoadStatus.loading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (provider.inialLoadStatus == LoadStatus.error) { + return ErrorScreen( + error: t.bookmarks.search.cannotSearchError, + ); + } + + if (provider.bookmarks.isEmpty) { + return NoDataScreen( + message: t.bookmarks.search.inputtedSearchTermNoResults, + ); + } + + return NotificationListener( + onNotification: scrollListener, + child: ListView.builder( + itemCount: provider.loadingMore ? provider.bookmarks.length + 1 : provider.bookmarks.length, + itemBuilder: (context, index) { + if (provider.loadingMore == true && index == provider.bookmarks.length) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + return BookmarkItem( + bookmark: provider.bookmarks[index], + ); + }, + ), + ); }, ), );