diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 2f4df14..03ab0d8 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: 208 (104 per locale) +/// Strings: 218 (109 per locale) /// -/// Built on 2024-02-24 at 12:07 UTC +/// Built on 2024-02-24 at 12:45 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -227,6 +227,8 @@ class _StringsTagsEn { // Translations String get tags => 'Tags'; + String created({required Object created}) => 'Created: ${created}'; + late final _StringsTagsCreateTagEn createTag = _StringsTagsCreateTagEn._(_root); } // Path: settings @@ -341,6 +343,19 @@ class _StringsBookmarksSearchEn { String get inputtedSearchTermNoResults => 'The inputted search term doesn\'t have any results'; } +// Path: tags.createTag +class _StringsTagsCreateTagEn { + _StringsTagsCreateTagEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get createTag => 'Create tag'; + String get name => 'Name'; + String get creatingTag => 'Creating tag...'; + String get errorCreatingTag => 'An error occured when creating the tag.'; +} + // Path: settings.customization class _StringsSettingsCustomizationEn { _StringsSettingsCustomizationEn._(this._root); @@ -487,6 +502,8 @@ class _StringsTagsEs implements _StringsTagsEn { // Translations @override String get tags => 'Etiquetas'; + @override String created({required Object created}) => 'Creado: ${created}'; + @override late final _StringsTagsCreateTagEs createTag = _StringsTagsCreateTagEs._(_root); } // Path: settings @@ -601,6 +618,19 @@ class _StringsBookmarksSearchEs implements _StringsBookmarksSearchEn { @override String get inputtedSearchTermNoResults => 'El término de búsqueda introducido no tiene resultados'; } +// Path: tags.createTag +class _StringsTagsCreateTagEs implements _StringsTagsCreateTagEn { + _StringsTagsCreateTagEs._(this._root); + + @override final _StringsEs _root; // ignore: unused_field + + // Translations + @override String get createTag => 'Crear etiqueta'; + @override String get name => 'Nombre'; + @override String get creatingTag => 'Creando etiqueta...'; + @override String get errorCreatingTag => 'Ocurrió un error al crear la etiqueta.'; +} + // Path: settings.customization class _StringsSettingsCustomizationEs implements _StringsSettingsCustomizationEn { _StringsSettingsCustomizationEs._(this._root); @@ -714,6 +744,11 @@ extension on Translations { 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 'tags.tags': return 'Tags'; + case 'tags.created': return ({required Object created}) => 'Created: ${created}'; + case 'tags.createTag.createTag': return 'Create tag'; + case 'tags.createTag.name': return 'Name'; + case 'tags.createTag.creatingTag': return 'Creating tag...'; + case 'tags.createTag.errorCreatingTag': return 'An error occured when creating the tag.'; case 'settings.settings': return 'Settings'; case 'settings.appSettings': return 'App settings'; case 'settings.aboutApp': return 'About the app'; @@ -826,6 +861,11 @@ extension on _StringsEs { 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 'tags.tags': return 'Etiquetas'; + case 'tags.created': return ({required Object created}) => 'Creado: ${created}'; + case 'tags.createTag.createTag': return 'Crear etiqueta'; + case 'tags.createTag.name': return 'Nombre'; + case 'tags.createTag.creatingTag': return 'Creando etiqueta...'; + case 'tags.createTag.errorCreatingTag': return 'Ocurrió un error al crear la etiqueta.'; 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 efe2e02..ca75105 100644 --- a/lib/i18n/strings_en.i18n.json +++ b/lib/i18n/strings_en.i18n.json @@ -75,7 +75,14 @@ } }, "tags": { - "tags": "Tags" + "tags": "Tags", + "created": "Created: $created", + "createTag": { + "createTag": "Create tag", + "name": "Name", + "creatingTag": "Creating tag...", + "errorCreatingTag": "An error occured when creating the tag." + } }, "settings": { "settings": "Settings", diff --git a/lib/i18n/strings_es.i18n.json b/lib/i18n/strings_es.i18n.json index 285d912..e598b15 100644 --- a/lib/i18n/strings_es.i18n.json +++ b/lib/i18n/strings_es.i18n.json @@ -75,7 +75,14 @@ } }, "tags": { - "tags": "Etiquetas" + "tags": "Etiquetas", + "created": "Creado: $created", + "createTag": { + "createTag": "Crear etiqueta", + "name": "Nombre", + "creatingTag": "Creando etiqueta...", + "errorCreatingTag": "Ocurrió un error al crear la etiqueta." + } }, "settings": { "settings": "Ajustes", diff --git a/lib/models/data/tags.dart b/lib/models/data/tags.dart index 45d38c1..d350d3e 100644 --- a/lib/models/data/tags.dart +++ b/lib/models/data/tags.dart @@ -2,7 +2,7 @@ class TagsResponse { final int? count; final int? next; final int? previous; - final List? results; + final List? results; TagsResponse({ this.count, @@ -15,7 +15,7 @@ class TagsResponse { count: json["count"], next: json["next"], previous: json["previous"], - results: json["results"] == null ? [] : List.from(json["results"]!.map((x) => Result.fromJson(x))), + results: json["results"] == null ? [] : List.from(json["results"]!.map((x) => Tag.fromJson(x))), ); Map toJson() => { @@ -26,18 +26,18 @@ class TagsResponse { }; } -class Result { +class Tag { final int? id; final String? name; final DateTime? dateAdded; - Result({ + Tag({ this.id, this.name, this.dateAdded, }); - factory Result.fromJson(Map json) => Result( + factory Tag.fromJson(Map json) => Tag( id: json["id"], name: json["name"], dateAdded: json["date_added"] == null ? null : DateTime.parse(json["date_added"]), diff --git a/lib/screens/bookmarks/provider/add_bookmark.provider.dart b/lib/screens/bookmarks/provider/add_bookmark.provider.dart index 8cfdd0d..2670fbd 100644 --- a/lib/screens/bookmarks/provider/add_bookmark.provider.dart +++ b/lib/screens/bookmarks/provider/add_bookmark.provider.dart @@ -93,7 +93,7 @@ class AddBookmark extends _$AddBookmark { final processModal = ProcessModal(); processModal.open(t.bookmarks.addBookmark.savingBookmark); - final result = await ref.watch(apiClientProvider)!.fetchPostBookmark(newBookmark); + final result = await ref.watch(apiClientProvider)!.postBookmark(newBookmark); processModal.close(); diff --git a/lib/screens/bookmarks/provider/add_bookmark.provider.g.dart b/lib/screens/bookmarks/provider/add_bookmark.provider.g.dart index 2c11ff4..95eba2a 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'2953a4223fe70809fbcf37af23b4f61a62f0de9f'; +String _$addBookmarkHash() => r'7a2a1c6c6597aa28fa478d2ef9dc3bc4d71c143d'; /// See also [AddBookmark]. @ProviderFor(AddBookmark) diff --git a/lib/screens/tags/models/add_tag.modal.dart b/lib/screens/tags/models/add_tag.modal.dart new file mode 100644 index 0000000..b07290d --- /dev/null +++ b/lib/screens/tags/models/add_tag.modal.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +class AddTagModel { + final TextEditingController nameController; + + const AddTagModel({ + required this.nameController, + }); +} diff --git a/lib/screens/tags/provider/add_tag.provider.dart b/lib/screens/tags/provider/add_tag.provider.dart new file mode 100644 index 0000000..d94e0f2 --- /dev/null +++ b/lib/screens/tags/provider/add_tag.provider.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'package:linkdy/screens/tags/ui/add_tag_error_modal.dart'; +import 'package:linkdy/screens/tags/provider/tags.provider.dart'; +import 'package:linkdy/screens/tags/models/add_tag.modal.dart'; + +import 'package:linkdy/router/routes.dart'; +import 'package:linkdy/providers/api_client_provider.dart'; +import 'package:linkdy/providers/router_provider.dart'; +import 'package:linkdy/utils/process_modal.dart'; +import 'package:linkdy/i18n/strings.g.dart'; + +part 'add_tag.provider.g.dart'; + +@riverpod +class AddTag extends _$AddTag { + @override + AddTagModel build() { + return AddTagModel( + nameController: TextEditingController(), + ); + } + + void notifyOnWrite() { + ref.notifyListeners(); + } + + void onAddTag() async { + final processModal = ProcessModal(); + processModal.open(t.tags.createTag.creatingTag); + + final result = await ref.watch(apiClientProvider)!.postTag(state.nameController.text); + + processModal.close(); + if (result.successful == true) { + ref.invalidate(tagsRequestProvider); + ref.watch(routerProvider).pop(); + } else { + if (rootNavigatorKey.currentContext == null) return; + showDialog( + context: rootNavigatorKey.currentContext!, + builder: (context) => const AddTagErrorModal(), + ); + } + } +} diff --git a/lib/screens/tags/provider/add_tag.provider.g.dart b/lib/screens/tags/provider/add_tag.provider.g.dart new file mode 100644 index 0000000..1614e1e --- /dev/null +++ b/lib/screens/tags/provider/add_tag.provider.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'add_tag.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$addTagHash() => r'8157bc7911ae64627adabbe1528f519a4b1cb913'; + +/// See also [AddTag]. +@ProviderFor(AddTag) +final addTagProvider = + AutoDisposeNotifierProvider.internal( + AddTag.new, + name: r'addTagProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$addTagHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$AddTag = 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/add_tag_error_modal.dart b/lib/screens/tags/ui/add_tag_error_modal.dart new file mode 100644 index 0000000..68fd721 --- /dev/null +++ b/lib/screens/tags/ui/add_tag_error_modal.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:linkdy/i18n/strings.g.dart'; + +class AddTagErrorModal extends StatelessWidget { + const AddTagErrorModal({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(t.generic.error), + content: Text(t.tags.createTag.errorCreatingTag), + actions: [ + Row( + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(t.generic.close), + ), + ], + ), + ], + ); + } +} diff --git a/lib/screens/tags/ui/add_tag_modal.dart b/lib/screens/tags/ui/add_tag_modal.dart new file mode 100644 index 0000000..9b6c9b8 --- /dev/null +++ b/lib/screens/tags/ui/add_tag_modal.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:linkdy/screens/tags/provider/add_tag.provider.dart'; + +import 'package:linkdy/i18n/strings.g.dart'; + +class AddTagModal extends ConsumerWidget { + const AddTagModal({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = ref.watch(addTagProvider); + + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28), + ), + color: Theme.of(context).dialogBackgroundColor, + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.add_rounded, + color: Theme.of(context).colorScheme.secondary, + size: 24, + ), + const SizedBox(height: 16), + Text( + t.tags.createTag.createTag, + style: const TextStyle( + fontSize: 24, + ), + ), + const SizedBox(height: 24), + TextFormField( + controller: provider.nameController, + onChanged: (_) => ref.read(addTagProvider.notifier).notifyOnWrite(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.label_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + labelText: t.tags.createTag.name, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(top: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(t.generic.cancel), + ), + const SizedBox(width: 8), + TextButton( + onPressed: provider.nameController.text != "" + ? () => ref.read(addTagProvider.notifier).onAddTag() + : null, + child: Text(t.generic.confirm), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/tags/ui/tags.dart b/lib/screens/tags/ui/tags.dart index c0d379f..9a550d5 100644 --- a/lib/screens/tags/ui/tags.dart +++ b/lib/screens/tags/ui/tags.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:linkdy/screens/tags/provider/tags.provider.dart'; +import 'package:linkdy/screens/tags/ui/add_tag_modal.dart'; import 'package:linkdy/utils/date_to_string.dart'; import 'package:linkdy/widgets/error_screen.dart'; import 'package:linkdy/widgets/no_data_screen.dart'; @@ -17,22 +18,13 @@ class TagsScreen extends ConsumerWidget { 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), - // ); + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) => const AddTagModal(), + ); } return Scaffold( @@ -101,7 +93,7 @@ class TagsScreen extends ConsumerWidget { ), ), subtitle: Text( - dateToString(tag.dateAdded!), + t.tags.created(created: dateToString(tag.dateAdded!)), style: TextStyle( fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant, diff --git a/lib/services/api_client.dart b/lib/services/api_client.dart index c1e14ec..ef3d10b 100644 --- a/lib/services/api_client.dart +++ b/lib/services/api_client.dart @@ -70,7 +70,7 @@ class ApiClientService { } } - Future> fetchPostBookmark(PostBookmark bookmark) async { + Future> postBookmark(PostBookmark bookmark) async { try { final response = await dioInstance.post( "/bookmarks/", @@ -105,4 +105,20 @@ class ApiClientService { return const ApiResponse(successful: false); } } + + Future> postTag(String name) async { + try { + final response = await dioInstance.post( + "/tags/", + data: FormData.fromMap({"name": name}), + ); + return ApiResponse( + successful: true, + content: Tag.fromJson(response.data), + ); + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); + return const ApiResponse(successful: false); + } + } }