diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index c116b11..1a66e95 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: 48 (24 per locale) +/// Strings: 54 (27 per locale) /// -/// Built on 2024-02-22 at 10:40 UTC +/// Built on 2024-02-22 at 17:30 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -150,6 +150,7 @@ class Translations implements BaseTranslations { // Translations late final _StringsOnboardingEn onboarding = _StringsOnboardingEn._(_root); late final _StringsLinksEn links = _StringsLinksEn._(_root); + late final _StringsSearchEn search = _StringsSearchEn._(_root); late final _StringsSettingsEn settings = _StringsSettingsEn._(_root); } @@ -191,6 +192,17 @@ class _StringsLinksEn { // Translations String get links => 'Links'; + late final _StringsLinksDatesEn dates = _StringsLinksDatesEn._(_root); +} + +// Path: search +class _StringsSearchEn { + _StringsSearchEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get search => 'Search'; } // Path: settings @@ -204,6 +216,17 @@ class _StringsSettingsEn { String get disconnectFromServer => 'Disconnect from server'; } +// Path: links.dates +class _StringsLinksDatesEn { + _StringsLinksDatesEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String todayAt({required Object time}) => 'Today, ${time}'; + String yesterdayAt({required Object time}) => 'Yesterday, ${time}'; +} + // Path: class _StringsEs implements Translations { /// You can call this constructor and build your own translation instance of this locale. @@ -230,6 +253,7 @@ class _StringsEs implements Translations { // Translations @override late final _StringsOnboardingEs onboarding = _StringsOnboardingEs._(_root); @override late final _StringsLinksEs links = _StringsLinksEs._(_root); + @override late final _StringsSearchEs search = _StringsSearchEs._(_root); @override late final _StringsSettingsEs settings = _StringsSettingsEs._(_root); } @@ -271,6 +295,17 @@ class _StringsLinksEs implements _StringsLinksEn { // Translations @override String get links => 'Enlaces'; + @override late final _StringsLinksDatesEs dates = _StringsLinksDatesEs._(_root); +} + +// Path: search +class _StringsSearchEs implements _StringsSearchEn { + _StringsSearchEs._(this._root); + + @override final _StringsEs _root; // ignore: unused_field + + // Translations + @override String get search => 'Buscar'; } // Path: settings @@ -284,6 +319,17 @@ class _StringsSettingsEs implements _StringsSettingsEn { @override String get disconnectFromServer => 'Desconectar del servidor'; } +// Path: links.dates +class _StringsLinksDatesEs implements _StringsLinksDatesEn { + _StringsLinksDatesEs._(this._root); + + @override final _StringsEs _root; // ignore: unused_field + + // Translations + @override String todayAt({required Object time}) => 'Hoy, ${time}'; + @override String yesterdayAt({required Object time}) => 'Ayer, ${time}'; +} + /// Flat map(s) containing all translations. /// Only for edge cases! For simple maps, use the map function of this library. @@ -312,6 +358,9 @@ extension on Translations { case 'onboarding.connecting': return 'Connecting...'; case 'onboarding.cannotConnectToServer': return 'Cannot connect to the server.'; case 'links.links': return 'Links'; + case 'links.dates.todayAt': return ({required Object time}) => 'Today, ${time}'; + case 'links.dates.yesterdayAt': return ({required Object time}) => 'Yesterday, ${time}'; + case 'search.search': return 'Search'; case 'settings.settings': return 'Settings'; case 'settings.disconnectFromServer': return 'Disconnect from server'; default: return null; @@ -344,6 +393,9 @@ extension on _StringsEs { case 'onboarding.connecting': return 'Conectando...'; case 'onboarding.cannotConnectToServer': return 'No se puede conectar con el servidor.'; case 'links.links': return 'Enlaces'; + case 'links.dates.todayAt': return ({required Object time}) => 'Hoy, ${time}'; + case 'links.dates.yesterdayAt': return ({required Object time}) => 'Ayer, ${time}'; + case 'search.search': return 'Buscar'; case 'settings.settings': return 'Ajustes'; case 'settings.disconnectFromServer': return 'Desconectar del servidor'; default: return null; diff --git a/lib/i18n/strings_en.i18n.json b/lib/i18n/strings_en.i18n.json index dbf2d0b..3ecc4bf 100644 --- a/lib/i18n/strings_en.i18n.json +++ b/lib/i18n/strings_en.i18n.json @@ -23,7 +23,14 @@ "cannotConnectToServer": "Cannot connect to the server." }, "links": { - "links": "Links" + "links": "Links", + "dates": { + "todayAt": "Today, $time", + "yesterdayAt": "Yesterday, $time" + } + }, + "search": { + "search": "Search" }, "settings": { "settings": "Settings", diff --git a/lib/i18n/strings_es.i18n.json b/lib/i18n/strings_es.i18n.json index ea9f22b..c44ac05 100644 --- a/lib/i18n/strings_es.i18n.json +++ b/lib/i18n/strings_es.i18n.json @@ -23,7 +23,14 @@ "cannotConnectToServer": "No se puede conectar con el servidor." }, "links": { - "links": "Enlaces" + "links": "Enlaces", + "dates": { + "todayAt": "Hoy, $time", + "yesterdayAt": "Ayer, $time" + } + }, + "search": { + "search": "Buscar" }, "settings": { "settings": "Ajustes", diff --git a/lib/models/api_response.dart b/lib/models/api_response.dart index 3424ae6..2df8b72 100644 --- a/lib/models/api_response.dart +++ b/lib/models/api_response.dart @@ -1,6 +1,6 @@ -class ApiResponse { +class ApiResponse { final bool successful; - final dynamic content; + final T? content; final int? statusCode; const ApiResponse({ diff --git a/lib/models/data/bookmarks.dart b/lib/models/data/bookmarks.dart new file mode 100644 index 0000000..14ccf42 --- /dev/null +++ b/lib/models/data/bookmarks.dart @@ -0,0 +1,91 @@ +class Bookmarks { + final int? count; + final int? next; + final int? previous; + final List? results; + + Bookmarks({ + this.count, + this.next, + this.previous, + this.results, + }); + + factory Bookmarks.fromJson(Map json) => Bookmarks( + count: json["count"], + next: json["next"], + previous: json["previous"], + results: json["results"] == null ? [] : List.from(json["results"]!.map((x) => Bookmark.fromJson(x))), + ); + + Map toJson() => { + "count": count, + "next": next, + "previous": previous, + "results": results == null ? [] : List.from(results!.map((x) => x.toJson())), + }; +} + +class Bookmark { + final int? id; + final String? url; + final String? title; + final String? description; + final String? notes; + final String? websiteTitle; + final String? websiteDescription; + final bool? isArchived; + final bool? unread; + final bool? shared; + final List? tagNames; + final DateTime? dateAdded; + final DateTime? dateModified; + + Bookmark({ + this.id, + this.url, + this.title, + this.description, + this.notes, + this.websiteTitle, + this.websiteDescription, + this.isArchived, + this.unread, + this.shared, + this.tagNames, + this.dateAdded, + this.dateModified, + }); + + factory Bookmark.fromJson(Map json) => Bookmark( + id: json["id"], + url: json["url"], + title: json["title"], + description: json["description"], + notes: json["notes"], + websiteTitle: json["website_title"], + websiteDescription: json["website_description"], + isArchived: json["is_archived"], + unread: json["unread"], + shared: json["shared"], + tagNames: json["tag_names"] == null ? [] : List.from(json["tag_names"]!.map((x) => x)), + dateAdded: json["date_added"] == null ? null : DateTime.parse(json["date_added"]), + dateModified: json["date_modified"] == null ? null : DateTime.parse(json["date_modified"]), + ); + + Map toJson() => { + "id": id, + "url": url, + "title": title, + "description": description, + "notes": notes, + "website_title": websiteTitle, + "website_description": websiteDescription, + "is_archived": isArchived, + "unread": unread, + "shared": shared, + "tag_names": tagNames == null ? [] : List.from(tagNames!.map((x) => x)), + "date_added": dateAdded?.toIso8601String(), + "date_modified": dateModified?.toIso8601String(), + }; +} diff --git a/lib/providers/api_client_provider.dart b/lib/providers/api_client_provider.dart index afca682..237e772 100644 --- a/lib/providers/api_client_provider.dart +++ b/lib/providers/api_client_provider.dart @@ -1,5 +1,7 @@ +import 'package:dio/dio.dart'; import 'package:my_linkding/providers/server_instances_provider.dart'; import 'package:my_linkding/services/api_client.dart'; +import 'package:my_linkding/utils/api_base_url.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'api_client_provider.g.dart'; @@ -10,7 +12,18 @@ class ApiClientProvider extends _$ApiClientProvider { ApiClient? build() { final savedInstances = ref.watch(serverInstancesProvider); if (savedInstances.isNotEmpty) { - return ApiClient(serverInstance: savedInstances[0]); + final instance = savedInstances[0]; + return ApiClient( + serverInstance: instance, + dioInstance: Dio( + BaseOptions( + baseUrl: apiBaseUrl(instance), + headers: { + "Authorization": "Token ${instance.token}", + }, + ), + ), + ); } return null; } diff --git a/lib/providers/api_client_provider.g.dart b/lib/providers/api_client_provider.g.dart index 1a90cff..de8ce47 100644 --- a/lib/providers/api_client_provider.g.dart +++ b/lib/providers/api_client_provider.g.dart @@ -6,7 +6,7 @@ part of 'api_client_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$apiClientProviderHash() => r'8c44873c4e0758acfcd64b181134b80a43327f46'; +String _$apiClientProviderHash() => r'b408d1acc96953690397094dd10f466660ec6290'; /// See also [ApiClientProvider]. @ProviderFor(ApiClientProvider) diff --git a/lib/screens/links/model/links.model.dart b/lib/screens/links/model/links.model.dart index 220b65d..ffd0438 100644 --- a/lib/screens/links/model/links.model.dart +++ b/lib/screens/links/model/links.model.dart @@ -1,9 +1,9 @@ -import 'package:my_linkding/constants/enums.dart'; - class LinksModel { - final LoadStatus loadStatus; + int currentPage; + int limit; LinksModel({ - required this.loadStatus, + this.currentPage = 0, + this.limit = 100, }); } diff --git a/lib/screens/links/provider/links.provider.dart b/lib/screens/links/provider/links.provider.dart index 8b13789..a610102 100644 --- a/lib/screens/links/provider/links.provider.dart +++ b/lib/screens/links/provider/links.provider.dart @@ -1 +1,31 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:my_linkding/screens/links/model/links.model.dart'; + +import 'package:my_linkding/models/api_response.dart'; +import 'package:my_linkding/models/data/bookmarks.dart'; +import 'package:my_linkding/providers/api_client_provider.dart'; + +part 'links.provider.g.dart'; + +@riverpod +FutureOr> linksRequest(LinksRequestRef ref) async { + final result = await ref.watch(apiClientProviderProvider)!.fetchBookmarks(); + return result; +} + +@riverpod +class Links extends _$Links { + @override + LinksModel build() { + return LinksModel(); + } + + void setCurrentPage(int page) { + state.currentPage = page; + } + + void setLimit(int limit) { + state.limit = limit; + } +} diff --git a/lib/screens/links/provider/links.provider.g.dart b/lib/screens/links/provider/links.provider.g.dart new file mode 100644 index 0000000..12617dd --- /dev/null +++ b/lib/screens/links/provider/links.provider.g.dart @@ -0,0 +1,39 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'links.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$linksRequestHash() => r'be41da02d669705dc7723ba9fbab0c5b59fd233b'; + +/// See also [linksRequest]. +@ProviderFor(linksRequest) +final linksRequestProvider = + AutoDisposeFutureProvider>.internal( + linksRequest, + name: r'linksRequestProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$linksRequestHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef LinksRequestRef = AutoDisposeFutureProviderRef>; +String _$linksHash() => r'a609009ca33885b1efd0d7c1122bee8299b98af3'; + +/// See also [Links]. +@ProviderFor(Links) +final linksProvider = AutoDisposeNotifierProvider.internal( + Links.new, + name: r'linksProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$linksHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$Links = 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/links/ui/links.dart b/lib/screens/links/ui/links.dart index 6b4cafb..b7ed9db 100644 --- a/lib/screens/links/ui/links.dart +++ b/lib/screens/links/ui/links.dart @@ -1,12 +1,40 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:my_linkding/screens/links/provider/links.provider.dart'; import 'package:my_linkding/i18n/strings.g.dart'; -class Links extends StatelessWidget { +class Links extends ConsumerWidget { const Links({Key? key}) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final bookmarks = ref.watch(linksRequestProvider); + + String validateStrings(String? string1, String? string2) { + if (string1 != null && string1.isNotEmpty) { + return string1; + } else if (string2 != null && string2.isNotEmpty) { + return string2; + } else { + return ""; + } + } + + String dateString(DateTime date) { + final today = DateTime.now(); + final yesterday = DateTime.now().subtract(const Duration(days: 1)); + + if (date.day == today.day && date.month == today.month && date.year == today.year) { + return t.links.dates.todayAt(time: "${date.hour}:${date.minute}"); + } else if (date.day == yesterday.day && date.month == yesterday.month && date.year == yesterday.year) { + return t.links.dates.yesterdayAt(time: "${date.hour}:${date.minute}"); + } else { + return "${date.day}/${date.month}/${date.year} - ${date.hour}:${date.minute}"; + } + } + return Material( color: Colors.transparent, child: NestedScrollView( @@ -26,65 +54,109 @@ class Links extends StatelessWidget { top: false, bottom: false, child: Builder( - builder: (context) => CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - SliverList.builder( - itemCount: 40, - itemBuilder: (context, index) => ListTile( - title: Text( - "This is the title", - style: TextStyle( - fontSize: 18, - color: Theme.of(context).colorScheme.onSurface, + builder: (context) => RefreshIndicator( + displacement: 95, + onRefresh: () => ref.refresh(linksRequestProvider.future), + child: CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (bookmarks.isLoading && !bookmarks.isRefreshing) + const SliverFillRemaining( + child: Center(child: CircularProgressIndicator()), + ), + if (bookmarks.value != null && bookmarks.value!.successful == false) + const SliverFillRemaining( + child: Center( + child: Icon(Icons.error), ), ), - subtitle: Column( - children: [ - Text( - "All the speed he took, all the turns he’d taken and the dripping chassis of a skyscraper canyon. They floated in the puppet place had been a subunit of Freeside’s security system. The knives seemed to move of their own accord, gliding with a ritual lack of urgency through the center of his closed left eyelid. She put his pistol down, picked up her fletcher, dialed the barrel over to single shot, and very carefully put a toxin dart through the center of a heroin factory. Still it was a square of faint light. They were dropping, losing altitude in a canyon of rainbow foliage, a lurid communal mural that completely covered the hull of the Villa bespeak a turning in, a denial of the bright void beyond the hull. A narrow wedge of light from a half-open service hatch at the clinic, Molly took him to the simple Chinese hollow points Shin had sold him. Still it was a steady pulse of pain midway down his spine. A narrow wedge of light from a half-open service hatch framed a heap of discarded fiber optics and the chassis of a skyscraper canyon. The Tessier-Ashpool ice shattered, peeling away from the banks of every computer in the center of his closed left eyelid.", - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 4), - Row( + if (bookmarks.value != null && + bookmarks.value!.successful == true && + bookmarks.value!.content!.results!.isEmpty) + const SliverFillRemaining( + child: Center( + child: Text("No bookmarks"), + ), + ), + if (bookmarks.value != null && + bookmarks.value!.successful == true && + bookmarks.value!.content!.results!.isNotEmpty) + SliverList.builder( + itemCount: bookmarks.value?.content?.results?.length, + itemBuilder: (context, index) { + final link = bookmarks.value?.content?.results?[index]; + return Column( children: [ - Expanded( - child: Text( - "#flutter #apps", + ListTile( + isThreeLine: true, + title: Text( + validateStrings(link?.title, link?.websiteTitle), + maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, + fontSize: 18, + color: Theme.of(context).colorScheme.onSurface, ), ), - ), - Container( - width: 1, - height: 12, - margin: const EdgeInsets.symmetric(horizontal: 8), - color: Theme.of(context).dividerColor, - ), - Text( - "1 week ago", - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant, + subtitle: Column( + children: [ + Text( + validateStrings(link?.description, link?.websiteDescription), + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: Text( + link?.tagNames?.map((tag) => "#$tag").join(" ") ?? '', + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + if (link?.dateModified != null) ...[ + Container( + width: 1, + height: 12, + margin: const EdgeInsets.symmetric(horizontal: 8), + color: Theme.of(context).colorScheme.tertiary.withOpacity(0.3), + ), + Text( + dateString(link!.dateModified!), + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), + ], ), ), + if (index < bookmarks.value!.content!.results!.length - 1) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Divider( + color: Theme.of(context).colorScheme.tertiary.withOpacity(0.3), + ), + ), ], - ), - ], + ); + }, ), - ), - ), - ], + ], + ), ), ), ), diff --git a/lib/screens/onboarding/provider/connect.provider.dart b/lib/screens/onboarding/provider/connect.provider.dart index 185fcec..0ae17a6 100644 --- a/lib/screens/onboarding/provider/connect.provider.dart +++ b/lib/screens/onboarding/provider/connect.provider.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:uuid/uuid.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -5,6 +6,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:my_linkding/screens/onboarding/model/connect.model.dart'; import 'package:my_linkding/providers/server_instances_provider.dart'; +import 'package:my_linkding/utils/api_base_url.dart'; import 'package:my_linkding/i18n/strings.g.dart'; import 'package:my_linkding/constants/enums.dart'; import 'package:my_linkding/utils/process_modal.dart'; @@ -88,7 +90,17 @@ FutureOr connectToServer(ConnectToServerRef ref) async { token: ref.watch(connectProvider).tokenController.text, ); - final apiClient = ApiClient(serverInstance: serverInstance); + final apiClient = ApiClient( + serverInstance: serverInstance, + dioInstance: Dio( + BaseOptions( + baseUrl: apiBaseUrl(serverInstance), + headers: { + "Authorization": "Token ${serverInstance.token}", + }, + ), + ), + ); final processModal = ProcessModal(); processModal.open(t.onboarding.connecting); diff --git a/lib/screens/onboarding/provider/connect.provider.g.dart b/lib/screens/onboarding/provider/connect.provider.g.dart index b8f0e43..8d6205e 100644 --- a/lib/screens/onboarding/provider/connect.provider.g.dart +++ b/lib/screens/onboarding/provider/connect.provider.g.dart @@ -6,7 +6,7 @@ part of 'connect.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$connectToServerHash() => r'85925cb547fccb708e657d263e39a46867ac2bca'; +String _$connectToServerHash() => r'a12cade5244ec6d3b19897053380bd6c5fcf1265'; /// See also [connectToServer]. @ProviderFor(connectToServer) diff --git a/lib/services/api_client.dart b/lib/services/api_client.dart index a3825e6..04cb161 100644 --- a/lib/services/api_client.dart +++ b/lib/services/api_client.dart @@ -1,19 +1,43 @@ import 'package:dio/dio.dart'; +import 'package:my_linkding/models/api_response.dart'; +import 'package:my_linkding/models/data/bookmarks.dart'; import 'package:my_linkding/models/server_instance.dart'; -import 'package:my_linkding/utils/http_request_client.dart'; - -final dio = Dio(); class ApiClient { final ServerInstance serverInstance; + final Dio dioInstance; const ApiClient({ required this.serverInstance, + required this.dioInstance, }); Future checkConnectionInstance() async { - final result = await HttpRequestClient.get(urlPath: "/bookmarks/?limit=1", server: serverInstance); - return result.successful; + try { + await dioInstance.get("/bookmarks/?limit=1"); + return true; + } catch (e) { + return false; + } + } + + Future> fetchBookmarks({String? q, int? limit, int? offset}) async { + try { + final response = await dioInstance.get( + "/bookmarks", + queryParameters: { + "q": q, + "limit": limit, + "offset": offset, + }, + ); + return ApiResponse( + successful: true, + content: Bookmarks.fromJson(response.data), + ); + } catch (e, stackTrace) { + return const ApiResponse(successful: false); + } } } diff --git a/lib/utils/api_base_url.dart b/lib/utils/api_base_url.dart new file mode 100644 index 0000000..895c483 --- /dev/null +++ b/lib/utils/api_base_url.dart @@ -0,0 +1,5 @@ +import 'package:my_linkding/models/server_instance.dart'; + +String apiBaseUrl(ServerInstance server) { + return "${server.method}://${server.ipDomain}${server.port != null ? ':${server.port}' : ""}${server.path ?? ""}/api"; +} diff --git a/lib/utils/http_request_client.dart b/lib/utils/http_request_client.dart deleted file mode 100644 index 53fea21..0000000 --- a/lib/utils/http_request_client.dart +++ /dev/null @@ -1,210 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:my_linkding/models/server_instance.dart'; - -enum ExceptionType { socket, timeout, handshake, http, unknown } - -class HttpResponse { - final bool successful; - final String? body; - final int? statusCode; - final ExceptionType? exception; - - const HttpResponse({ - required this.successful, - required this.body, - required this.statusCode, - this.exception, - }); -} - -String getConnectionString({ - required ServerInstance server, - required String urlPath, -}) { - return "${server.method}://${server.ipDomain}${server.port != null ? ':${server.port}' : ""}${server.path ?? ""}/api$urlPath"; -} - -class HttpRequestClient { - static Future get({ - required String urlPath, - required ServerInstance server, - int timeout = 10, - }) async { - final String connectionString = getConnectionString(server: server, urlPath: urlPath); - try { - HttpClient httpClient = HttpClient(); - HttpClientRequest request = await httpClient.getUrl(Uri.parse(connectionString)); - request.headers.set('Authorization', 'Token ${server.token}'); - HttpClientResponse response = await request.close().timeout( - Duration(seconds: timeout), - ); - String reply = await response.transform(utf8.decoder).join(); - httpClient.close(); - return HttpResponse( - successful: response.statusCode >= 400 ? false : true, - body: reply, - statusCode: response.statusCode, - ); - } on SocketException { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.socket, - ); - } on TimeoutException { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.timeout, - ); - } on HandshakeException { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.handshake, - ); - } on HttpException { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.http, - ); - } catch (e) { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.unknown, - ); - } - } - - static Future post({ - required String urlPath, - required ServerInstance server, - dynamic body, - int timeout = 10, - }) async { - final String connectionString = getConnectionString(server: server, urlPath: urlPath); - try { - HttpClient httpClient = HttpClient(); - HttpClientRequest request = await httpClient.postUrl(Uri.parse(connectionString)); - request.headers.set('Authorization', 'Token ${server.token}'); - request.headers.set('content-type', 'application/json'); - request.add(utf8.encode(json.encode(body))); - HttpClientResponse response = await request.close().timeout( - Duration(seconds: timeout), - ); - String reply = await response.transform(utf8.decoder).join(); - httpClient.close(); - return HttpResponse( - successful: response.statusCode >= 400 ? false : true, - body: reply, - statusCode: response.statusCode, - ); - } on SocketException { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.socket, - ); - } on TimeoutException { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.timeout, - ); - } on HttpException { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.http, - ); - } on HandshakeException { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.handshake, - ); - } catch (e) { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.unknown, - ); - } - } - - static Future put({ - required String urlPath, - required ServerInstance server, - dynamic body, - int timeout = 10, - }) async { - final String connectionString = getConnectionString(server: server, urlPath: urlPath); - try { - HttpClient httpClient = HttpClient(); - HttpClientRequest request = await httpClient.putUrl(Uri.parse(connectionString)); - request.headers.set('Authorization', 'Token ${server.token}'); - request.headers.set('content-type', 'application/json'); - request.add(utf8.encode(json.encode(body))); - HttpClientResponse response = await request.close().timeout( - Duration(seconds: timeout), - ); - String reply = await response.transform(utf8.decoder).join(); - httpClient.close(); - return HttpResponse( - successful: response.statusCode >= 400 ? false : true, - body: reply, - statusCode: response.statusCode, - ); - } on SocketException { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.socket, - ); - } on TimeoutException { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.timeout, - ); - } on HttpException { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.http, - ); - } on HandshakeException { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.handshake, - ); - } catch (e) { - return const HttpResponse( - successful: false, - body: null, - statusCode: null, - exception: ExceptionType.unknown, - ); - } - } -} diff --git a/lib/widgets/layout.dart b/lib/widgets/layout.dart index 96bfa62..095377f 100644 --- a/lib/widgets/layout.dart +++ b/lib/widgets/layout.dart @@ -1,6 +1,7 @@ import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:my_linkding/i18n/strings.g.dart'; import 'package:my_linkding/widgets/system_overlay_style.dart'; @@ -35,18 +36,18 @@ class Layout extends StatelessWidget { bottomNavigationBar: NavigationBar( onDestinationSelected: (s) => goBranch(s), selectedIndex: navigationShell.currentIndex, - destinations: const [ + destinations: [ NavigationDestination( - icon: Icon(Icons.link_rounded), - label: "Links", + icon: const Icon(Icons.link_rounded), + label: t.links.links, ), NavigationDestination( - icon: Icon(Icons.search_rounded), - label: "Search", + icon: const Icon(Icons.search_rounded), + label: t.search.search, ), NavigationDestination( - icon: Icon(Icons.settings_rounded), - label: "Settings", + icon: const Icon(Icons.settings_rounded), + label: t.settings.settings, ), ], ),