diff --git a/lib/config/app_screens.dart b/lib/config/app_screens.dart index 6040ef4..921abf4 100644 --- a/lib/config/app_screens.dart +++ b/lib/config/app_screens.dart @@ -10,6 +10,11 @@ final appScreens = [ route: RoutesPaths.bookmarks, name: t.bookmarks.bookmarks, ), + AppRoute( + icon: Icons.label_rounded, + route: RoutesPaths.tags, + name: t.tags.tags, + ), AppRoute( icon: Icons.settings_rounded, route: RoutesPaths.settings, diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index d3f59e8..2f4df14 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 222 (111 per locale) +/// Strings: 208 (104 per locale) /// -/// Built on 2024-02-24 at 11:55 UTC +/// Built on 2024-02-24 at 12:07 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -151,7 +151,7 @@ class Translations implements BaseTranslations { late final _StringsGenericEn generic = _StringsGenericEn._(_root); late final _StringsOnboardingEn onboarding = _StringsOnboardingEn._(_root); late final _StringsBookmarksEn bookmarks = _StringsBookmarksEn._(_root); - late final _StringsSearchEn search = _StringsSearchEn._(_root); + late final _StringsTagsEn tags = _StringsTagsEn._(_root); late final _StringsSettingsEn settings = _StringsSettingsEn._(_root); late final _StringsWebviewEn webview = _StringsWebviewEn._(_root); late final _StringsColorsEn colors = _StringsColorsEn._(_root); @@ -219,21 +219,14 @@ class _StringsBookmarksEn { late final _StringsBookmarksSearchEn search = _StringsBookmarksSearchEn._(_root); } -// Path: search -class _StringsSearchEn { - _StringsSearchEn._(this._root); +// Path: tags +class _StringsTagsEn { + _StringsTagsEn._(this._root); final Translations _root; // ignore: unused_field // Translations - String get search => 'Search'; - String get bookmarks => 'Bookmarks'; String get tags => 'Tags'; - String get clearSearch => 'Clear search'; - String get inputSearchTerm => 'Input a search term'; - String get cannotSearchError => 'Cannot search content due to an error'; - String get inputtedSearchTermNoResults => 'The inputted search term doesn\'t have any results'; - String created({required Object created}) => 'Created: ${created}'; } // Path: settings @@ -418,7 +411,7 @@ class _StringsEs implements Translations { @override late final _StringsGenericEs generic = _StringsGenericEs._(_root); @override late final _StringsOnboardingEs onboarding = _StringsOnboardingEs._(_root); @override late final _StringsBookmarksEs bookmarks = _StringsBookmarksEs._(_root); - @override late final _StringsSearchEs search = _StringsSearchEs._(_root); + @override late final _StringsTagsEs tags = _StringsTagsEs._(_root); @override late final _StringsSettingsEs settings = _StringsSettingsEs._(_root); @override late final _StringsWebviewEs webview = _StringsWebviewEs._(_root); @override late final _StringsColorsEs colors = _StringsColorsEs._(_root); @@ -486,21 +479,14 @@ class _StringsBookmarksEs implements _StringsBookmarksEn { @override late final _StringsBookmarksSearchEs search = _StringsBookmarksSearchEs._(_root); } -// Path: search -class _StringsSearchEs implements _StringsSearchEn { - _StringsSearchEs._(this._root); +// Path: tags +class _StringsTagsEs implements _StringsTagsEn { + _StringsTagsEs._(this._root); @override final _StringsEs _root; // ignore: unused_field // Translations - @override String get search => 'Buscar'; - @override String get bookmarks => 'Marcadores'; @override String get tags => 'Etiquetas'; - @override String get clearSearch => 'Limpiar búsqueda'; - @override String get inputSearchTerm => 'Introduce un término de búsqueda'; - @override String get cannotSearchError => 'No se puede buscar el contenido por un error'; - @override String get inputtedSearchTermNoResults => 'El término de búsqueda introducido no tiene resultados'; - @override String created({required Object created}) => 'Creado: ${created}'; } // Path: settings @@ -727,14 +713,7 @@ extension on Translations { case 'bookmarks.search.inputSearchTerm': return 'Input a search term'; case 'bookmarks.search.cannotSearchError': return 'Cannot search content due to an error'; case 'bookmarks.search.inputtedSearchTermNoResults': return 'The inputted search term doesn\'t have any results'; - case 'search.search': return 'Search'; - case 'search.bookmarks': return 'Bookmarks'; - case 'search.tags': return 'Tags'; - case 'search.clearSearch': return 'Clear search'; - case 'search.inputSearchTerm': return 'Input a search term'; - case 'search.cannotSearchError': return 'Cannot search content due to an error'; - case 'search.inputtedSearchTermNoResults': return 'The inputted search term doesn\'t have any results'; - case 'search.created': return ({required Object created}) => 'Created: ${created}'; + case 'tags.tags': return 'Tags'; case 'settings.settings': return 'Settings'; case 'settings.appSettings': return 'App settings'; case 'settings.aboutApp': return 'About the app'; @@ -846,14 +825,7 @@ extension on _StringsEs { case 'bookmarks.search.inputSearchTerm': return 'Introduce un término de búsqueda'; case 'bookmarks.search.cannotSearchError': return 'No se puede buscar el contenido por un error'; case 'bookmarks.search.inputtedSearchTermNoResults': return 'El término de búsqueda introducido no tiene resultados'; - case 'search.search': return 'Buscar'; - case 'search.bookmarks': return 'Marcadores'; - case 'search.tags': return 'Etiquetas'; - case 'search.clearSearch': return 'Limpiar búsqueda'; - case 'search.inputSearchTerm': return 'Introduce un término de búsqueda'; - case 'search.cannotSearchError': return 'No se puede buscar el contenido por un error'; - case 'search.inputtedSearchTermNoResults': return 'El término de búsqueda introducido no tiene resultados'; - case 'search.created': return ({required Object created}) => 'Creado: ${created}'; + case 'tags.tags': return 'Etiquetas'; case 'settings.settings': return 'Ajustes'; case 'settings.appSettings': return 'Ajustes de la aplicación'; case 'settings.aboutApp': return 'Sobre la aplicación'; diff --git a/lib/i18n/strings_en.i18n.json b/lib/i18n/strings_en.i18n.json index 61422fe..efe2e02 100644 --- a/lib/i18n/strings_en.i18n.json +++ b/lib/i18n/strings_en.i18n.json @@ -74,15 +74,8 @@ "inputtedSearchTermNoResults": "The inputted search term doesn't have any results" } }, - "search": { - "search": "Search", - "bookmarks": "Bookmarks", - "tags": "Tags", - "clearSearch": "Clear search", - "inputSearchTerm": "Input a search term", - "cannotSearchError": "Cannot search content due to an error", - "inputtedSearchTermNoResults": "The inputted search term doesn't have any results", - "created": "Created: $created" + "tags": { + "tags": "Tags" }, "settings": { "settings": "Settings", diff --git a/lib/i18n/strings_es.i18n.json b/lib/i18n/strings_es.i18n.json index b722cd0..285d912 100644 --- a/lib/i18n/strings_es.i18n.json +++ b/lib/i18n/strings_es.i18n.json @@ -74,15 +74,8 @@ "inputtedSearchTermNoResults": "El término de búsqueda introducido no tiene resultados" } }, - "search": { - "search": "Buscar", - "bookmarks": "Marcadores", - "tags": "Etiquetas", - "clearSearch": "Limpiar búsqueda", - "inputSearchTerm": "Introduce un término de búsqueda", - "cannotSearchError": "No se puede buscar el contenido por un error", - "inputtedSearchTermNoResults": "El término de búsqueda introducido no tiene resultados", - "created": "Creado: $created" + "tags": { + "tags": "Etiquetas" }, "settings": { "settings": "Ajustes", diff --git a/lib/router/paths.dart b/lib/router/paths.dart index 8f1b158..554685a 100644 --- a/lib/router/paths.dart +++ b/lib/router/paths.dart @@ -3,7 +3,7 @@ class RoutesPaths { static const bookmarks = "/bookmarks"; static const bookmarksSearch = "/bookmarks/search"; static const webview = "/webview"; - static const search = "/search"; + static const tags = "/tags"; static const settings = "/settings"; static const customization = "/settings/customization"; static const generalSettings = "/settings/general"; diff --git a/lib/router/routes.dart b/lib/router/routes.dart index 1cb68c0..917106e 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -7,6 +7,7 @@ import 'package:linkdy/screens/onboarding/ui/onboarding.dart'; import 'package:linkdy/screens/settings/ui/customization/customization.dart'; import 'package:linkdy/screens/settings/ui/general_settings/general_settings.dart'; import 'package:linkdy/screens/settings/ui/settings.dart'; +import 'package:linkdy/screens/tags/ui/tags.dart'; import 'package:linkdy/screens/webview/ui/webview.dart'; import 'package:linkdy/widgets/layout.dart'; @@ -37,6 +38,10 @@ final List appRoutes = [ path: RoutesPaths.bookmarks, builder: (context, state) => const BookmarksScreen(), ), + GoRoute( + path: RoutesPaths.tags, + builder: (context, state) => const TagsScreen(), + ), GoRoute( path: RoutesPaths.settings, builder: (context, state) => const SettingsScreen(), diff --git a/lib/screens/bookmarks/ui/search_bookmarks.dart b/lib/screens/bookmarks/ui/search_bookmarks.dart index 5e2668d..9057f00 100644 --- a/lib/screens/bookmarks/ui/search_bookmarks.dart +++ b/lib/screens/bookmarks/ui/search_bookmarks.dart @@ -32,7 +32,7 @@ class SearchBookmarksScreen extends ConsumerWidget { controller: ref.watch(searchBookmarksProvider).searchController, onChanged: ref.read(searchBookmarksProvider.notifier).setSearchTerm, decoration: InputDecoration( - hintText: t.search.search, + hintText: t.bookmarks.search.searchBookmarks, prefixIcon: const Icon(Icons.search_rounded), border: InputBorder.none, filled: true, @@ -44,7 +44,7 @@ class SearchBookmarksScreen extends ConsumerWidget { Icons.close_rounded, size: 20, ), - tooltip: t.search.clearSearch, + tooltip: t.bookmarks.search.clearSearch, ) : null, ), diff --git a/lib/screens/tags/models/tags.model.dart b/lib/screens/tags/models/tags.model.dart new file mode 100644 index 0000000..9fd8bfe --- /dev/null +++ b/lib/screens/tags/models/tags.model.dart @@ -0,0 +1,9 @@ +class TagsModel { + int currentPage; + int limit; + + TagsModel({ + this.currentPage = 0, + this.limit = 100, + }); +} diff --git a/lib/screens/tags/provider/tags.provider.dart b/lib/screens/tags/provider/tags.provider.dart new file mode 100644 index 0000000..667ca59 --- /dev/null +++ b/lib/screens/tags/provider/tags.provider.dart @@ -0,0 +1,32 @@ +import 'package:linkdy/screens/tags/models/tags.model.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'package:linkdy/screens/bookmarks/model/links.model.dart'; + +import 'package:linkdy/models/data/tags.dart'; +import 'package:linkdy/models/api_response.dart'; +import 'package:linkdy/providers/api_client_provider.dart'; + +part 'tags.provider.g.dart'; + +@riverpod +FutureOr> tagsRequest(TagsRequestRef ref) async { + final result = await ref.watch(apiClientProvider)!.fetchTags(); + return result; +} + +@riverpod +class Tags extends _$Tags { + @override + TagsModel build() { + return TagsModel(); + } + + void setCurrentPage(int page) { + state.currentPage = page; + } + + void setLimit(int limit) { + state.limit = limit; + } +} diff --git a/lib/screens/tags/provider/tags.provider.g.dart b/lib/screens/tags/provider/tags.provider.g.dart new file mode 100644 index 0000000..2f7eed9 --- /dev/null +++ b/lib/screens/tags/provider/tags.provider.g.dart @@ -0,0 +1,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tags.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$tagsRequestHash() => r'52b3c212ed070b39718aef1010c44fbb7120ef51'; + +/// See also [tagsRequest]. +@ProviderFor(tagsRequest) +final tagsRequestProvider = + AutoDisposeFutureProvider>.internal( + tagsRequest, + name: r'tagsRequestProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$tagsRequestHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef TagsRequestRef + = AutoDisposeFutureProviderRef>; +String _$tagsHash() => r'6e26b566459377a3a6bc0f1884b9a05855151d3f'; + +/// See also [Tags]. +@ProviderFor(Tags) +final tagsProvider = AutoDisposeNotifierProvider.internal( + Tags.new, + name: r'tagsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$tagsHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$Tags = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/screens/tags/ui/tags.dart b/lib/screens/tags/ui/tags.dart new file mode 100644 index 0000000..c0d379f --- /dev/null +++ b/lib/screens/tags/ui/tags.dart @@ -0,0 +1,134 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:linkdy/screens/tags/provider/tags.provider.dart'; +import 'package:linkdy/utils/date_to_string.dart'; +import 'package:linkdy/widgets/error_screen.dart'; +import 'package:linkdy/widgets/no_data_screen.dart'; + +import 'package:linkdy/i18n/strings.g.dart'; + +class TagsScreen extends ConsumerWidget { + const TagsScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tags = ref.watch(tagsRequestProvider); + + final width = MediaQuery.of(context).size.width; + + void openAddModal() { + // showGeneralDialog( + // context: context, + // barrierColor: !(width > 700 || !(Platform.isAndroid || Platform.isIOS)) ? Colors.transparent : Colors.black54, + // transitionBuilder: (context, anim1, anim2, child) { + // return SlideTransition( + // position: Tween(begin: const Offset(0, 1), end: const Offset(0, 0)).animate( + // CurvedAnimation(parent: anim1, curve: Curves.easeInOutCubicEmphasized), + // ), + // child: child, + // ); + // }, + // pageBuilder: (context, animation, secondaryAnimation) => AddBookmarkModal(fullscreen: width <= 700), + // ); + } + + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + title: Text(t.tags.tags), + ), + ), + ], + body: SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => RefreshIndicator( + displacement: 120, + onRefresh: () => ref.refresh(tagsRequestProvider.future), + child: CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (tags.isLoading && !tags.isRefreshing) + const SliverFillRemaining( + child: Center(child: CircularProgressIndicator()), + ), + if (tags.value != null && tags.value!.successful == false) + SliverFillRemaining( + child: ErrorScreen( + error: t.bookmarks.cannotLoadBookmarks, + ), + ), + if (tags.value != null && tags.value!.successful == true && tags.value!.content!.results!.isEmpty) + SliverFillRemaining( + child: NoDataScreen( + message: t.bookmarks.noBookmarksAdded, + ), + ), + if (tags.value != null && tags.value!.successful == true && tags.value!.content!.results!.isNotEmpty) + SliverList.builder( + itemCount: tags.value!.content!.results!.length + 1, + itemBuilder: (context, index) { + // index == bookmarks.value!.content!.results!.length -> itemCount + 1 + if (index == tags.value!.content!.results!.length) { + // Bottom gap for FAB + return const SizedBox(height: 80); + } + final tag = tags.value?.content?.results?[index]; + return Column( + children: [ + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), + dense: true, + title: Text( + tag!.name!, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + subtitle: Text( + dateToString(tag.dateAdded!), + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + if (index < tags.value!.content!.results!.length - 1) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Divider( + color: Theme.of(context).colorScheme.tertiary.withOpacity(0.3), + ), + ), + ], + ); + }, + ), + ], + ), + ), + ), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: openAddModal, + child: const Icon(Icons.add_rounded), + ), + ); + } +}