Load bookmarks
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class ApiResponse {
|
||||
class ApiResponse<T> {
|
||||
final bool successful;
|
||||
final dynamic content;
|
||||
final T? content;
|
||||
final int? statusCode;
|
||||
|
||||
const ApiResponse({
|
||||
|
||||
91
lib/models/data/bookmarks.dart
Normal file
91
lib/models/data/bookmarks.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'api_client_provider.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$apiClientProviderHash() => r'8c44873c4e0758acfcd64b181134b80a43327f46';
|
||||
String _$apiClientProviderHash() => r'b408d1acc96953690397094dd10f466660ec6290';
|
||||
|
||||
/// See also [ApiClientProvider].
|
||||
@ProviderFor(ApiClientProvider)
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
39
lib/screens/links/provider/links.provider.g.dart
Normal file
39
lib/screens/links/provider/links.provider.g.dart
Normal 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
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'connect.provider.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$connectToServerHash() => r'85925cb547fccb708e657d263e39a46867ac2bca';
|
||||
String _$connectToServerHash() => r'a12cade5244ec6d3b19897053380bd6c5fcf1265';
|
||||
|
||||
/// See also [connectToServer].
|
||||
@ProviderFor(connectToServer)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
lib/utils/api_base_url.dart
Normal file
5
lib/utils/api_base_url.dart
Normal 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";
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user