Load bookmarks

This commit is contained in:
Juan Gilsanz Polo
2024-02-22 18:32:20 +01:00
parent 926147edbd
commit 1521fd0f7e
17 changed files with 429 additions and 286 deletions

View File

@@ -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<AppLocale, Translations> {
// 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: <root>
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;

View File

@@ -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",

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
class ApiResponse {
class ApiResponse<T> {
final bool successful;
final dynamic content;
final T? content;
final int? statusCode;
const ApiResponse({

View File

@@ -0,0 +1,91 @@
class Bookmarks {
final int? count;
final int? next;
final int? previous;
final List<Bookmark>? results;
Bookmarks({
this.count,
this.next,
this.previous,
this.results,
});
factory Bookmarks.fromJson(Map<String, dynamic> json) => Bookmarks(
count: json["count"],
next: json["next"],
previous: json["previous"],
results: json["results"] == null ? [] : List<Bookmark>.from(json["results"]!.map((x) => Bookmark.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"count": count,
"next": next,
"previous": previous,
"results": results == null ? [] : List<dynamic>.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<String>? 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<String, dynamic> 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<String>.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<String, dynamic> 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<dynamic>.from(tagNames!.map((x) => x)),
"date_added": dateAdded?.toIso8601String(),
"date_modified": dateModified?.toIso8601String(),
};
}

View File

@@ -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;
}

View File

@@ -6,7 +6,7 @@ part of 'api_client_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$apiClientProviderHash() => r'8c44873c4e0758acfcd64b181134b80a43327f46';
String _$apiClientProviderHash() => r'b408d1acc96953690397094dd10f466660ec6290';
/// See also [ApiClientProvider].
@ProviderFor(ApiClientProvider)

View File

@@ -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,
});
}

View File

@@ -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<ApiResponse<Bookmarks>> 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;
}
}

View File

@@ -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<ApiResponse<Bookmarks>>.internal(
linksRequest,
name: r'linksRequestProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$linksRequestHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef LinksRequestRef = AutoDisposeFutureProviderRef<ApiResponse<Bookmarks>>;
String _$linksHash() => r'a609009ca33885b1efd0d7c1122bee8299b98af3';
/// See also [Links].
@ProviderFor(Links)
final linksProvider = AutoDisposeNotifierProvider<Links, LinksModel>.internal(
Links.new,
name: r'linksProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$linksHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$Links = AutoDisposeNotifier<LinksModel>;
// 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

@@ -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,16 +54,47 @@ class Links extends StatelessWidget {
top: false,
bottom: false,
child: Builder(
builder: (context) => CustomScrollView(
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),
),
),
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: 40,
itemBuilder: (context, index) => ListTile(
itemCount: bookmarks.value?.content?.results?.length,
itemBuilder: (context, index) {
final link = bookmarks.value?.content?.results?[index];
return Column(
children: [
ListTile(
isThreeLine: true,
title: Text(
"This is the title",
validateStrings(link?.title, link?.websiteTitle),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18,
color: Theme.of(context).colorScheme.onSurface,
@@ -44,8 +103,8 @@ class Links extends StatelessWidget {
subtitle: Column(
children: [
Text(
"All the speed he took, all the turns hed taken and the dripping chassis of a skyscraper canyon. They floated in the puppet place had been a subunit of Freesides 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,
validateStrings(link?.description, link?.websiteDescription),
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
@@ -57,34 +116,47 @@ class Links extends StatelessWidget {
children: [
Expanded(
child: Text(
"#flutter #apps",
link?.tagNames?.map((tag) => "#$tag").join(" ") ?? '',
overflow: TextOverflow.ellipsis,
style: TextStyle(
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).dividerColor,
color: Theme.of(context).colorScheme.tertiary.withOpacity(0.3),
),
Text(
"1 week ago",
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),
),
),
],
);
},
),
],
),
),
),
),

View File

@@ -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<bool> 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);

View File

@@ -6,7 +6,7 @@ part of 'connect.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$connectToServerHash() => r'85925cb547fccb708e657d263e39a46867ac2bca';
String _$connectToServerHash() => r'a12cade5244ec6d3b19897053380bd6c5fcf1265';
/// See also [connectToServer].
@ProviderFor(connectToServer)

View File

@@ -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<bool> 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<ApiResponse<Bookmarks>> 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);
}
}
}

View File

@@ -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";
}

View File

@@ -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<HttpResponse> 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<HttpResponse> 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<HttpResponse> 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,
);
}
}
}

View File

@@ -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,
),
],
),