1
0
mirror of https://github.com/go-vikunja/app synced 2024-06-02 18:49:47 +00:00

added error handler to client for network requests

This commit is contained in:
Benimautner 2022-09-27 17:53:21 +02:00
parent 57b219836f
commit 397a06e9a0
9 changed files with 128 additions and 91 deletions

View File

@ -32,15 +32,18 @@ class Client {
return otherClient._token == _token;
}
Client(this.global, {String? token, String? base, bool authenticated = false}) {
Client(this.global,
{String? token, String? base, bool authenticated = false}) {
configure(token: token, base: base, authenticated: authenticated);
}
void reload_ignore_certs(bool? val) {
ignoreCertificates = val ?? false;
HttpOverrides.global = new IgnoreCertHttpOverrides(ignoreCertificates);
VikunjaGlobal.of(global.currentContext!).settingsManager.setIgnoreCertificates(ignoreCertificates);
VikunjaGlobal
.of(global.currentContext!)
.settingsManager
.setIgnoreCertificates(ignoreCertificates);
}
get _headers =>
@ -53,11 +56,11 @@ class Client {
int get hashCode => _token.hashCode;
void configure({String? token, String? base, bool? authenticated}) {
if(token != null)
if (token != null)
_token = token;
if(base != null)
if (base != null)
_base = base.endsWith('/api/v1') ? base : '$base/api/v1';
if(authenticated != null)
if (authenticated != null)
this.authenticated = authenticated;
}
@ -69,9 +72,11 @@ class Client {
Future<Response> get(String url,
[Map<String, List<String>>? queryParameters]) {
final uri = Uri.parse('${this.base}$url').replace(queryParameters: queryParameters);
final uri = Uri.parse('${this.base}$url').replace(
queryParameters: queryParameters);
return http.get(uri, headers: _headers)
.then(_handleResponse, onError: _handleError);
.then(_handleResponse).onError((error, stackTrace) =>
_handleError(error, stackTrace));
}
Future<Response> delete(String url) {
@ -80,7 +85,8 @@ class Client {
'${this.base}$url'.toUri()!,
headers: _headers,
)
.then(_handleResponse, onError: _handleError);
.then(_handleResponse).onError((error, stackTrace) =>
_handleError(error, stackTrace));
}
Future<Response> post(String url, {dynamic body}) {
@ -90,7 +96,8 @@ class Client {
headers: _headers,
body: _encoder.convert(body),
)
.then(_handleResponse, onError: _handleError);
.then(_handleResponse).onError((error, stackTrace) =>
_handleError(error, stackTrace));
}
Future<Response> put(String url, {dynamic body}) {
@ -100,14 +107,16 @@ class Client {
headers: _headers,
body: _encoder.convert(body),
)
.then(_handleResponse, onError: _handleError);
.then(_handleResponse).onError((error, stackTrace) =>
_handleError(error, stackTrace));
}
void _handleError(dynamic e) {
log(e.toString());
FutureOr<Response> _handleError(Object? e, StackTrace? st) {
SnackBar snackBar = SnackBar(
content: Text("Error on request: " + e.toString()));
content: Text("Error on request: " + e.toString()),
action: SnackBarAction(label: "Clear", onPressed: () => global.currentState?.clearSnackBars()),);
global.currentState?.showSnackBar(snackBar);
return Response("", 0, {}, error: true);
}
Map<String, String> headersToMap(HttpHeaders headers) {

View File

@ -43,7 +43,10 @@ class ListAPIService extends APIService implements ListService {
@override
Future<List<TaskList>> getAll() {
return client.get('/lists').then(
(list) => convertList(list.body, (result) => TaskList.fromJson(result)));
(list) {
if (list.body.toString().isEmpty)
return Future.value([]);
return convertList(list.body, (result) => TaskList.fromJson(result));});
}
@override

View File

@ -1,5 +1,5 @@
import 'dart:async';
import 'dart:developer';
import 'package:vikunja_app/api/client.dart';
import 'package:vikunja_app/api/service.dart';
import 'package:vikunja_app/models/namespace.dart';
@ -29,8 +29,10 @@ class NamespaceAPIService extends APIService implements NamespaceService {
@override
Future<List<Namespace>> getAll() {
return client.get('/namespaces').then((response) =>
convertList(response.body, (result) => Namespace.fromJson(result)));
return client.get('/namespaces').then((response) {
if(response.error)
return Future.value([]);
return convertList(response.body, (result) => Namespace.fromJson(result));});
}
@override

View File

@ -1,9 +1,10 @@
// This is a wrapper class to be able to return the headers up to the provider
// to properly handle things like pagination with it.
class Response {
Response(this.body, this.statusCode, this.headers);
Response(this.body, this.statusCode, this.headers, {this.error = false});
final dynamic body;
final int statusCode;
final Map<String, String> headers;
final bool error;
}

View File

@ -43,6 +43,8 @@ class TaskAPIService extends APIService implements TaskService {
.get('/tasks/all')
.then((response) {
int page_n = 0;
if(response.error)
return Future.value([]);
if (response.headers["x-pagination-total-pages"] != null) {
page_n = int.parse(response.headers["x-pagination-total-pages"]!);
} else {
@ -69,10 +71,14 @@ class TaskAPIService extends APIService implements TaskService {
[Map<String, List<String>>? queryParameters]) {
return client
.get('/lists/$listId/tasks', queryParameters).then(
(response) => new Response(
convertList(response.body, (result) => Task.fromJson(result)),
response.statusCode,
response.headers));
(response) {
if(response.error)
return Response("", 0, {}, error: true);
return new Response(
convertList(response.body, (result) => Task.fromJson(result)),
response.statusCode,
response.headers);
});
}
@override
@ -81,7 +87,9 @@ class TaskAPIService extends APIService implements TaskService {
return client
.get('/tasks/all?$optionString')
.then((value) {
return convertList(value.body, (result) => Task.fromJson(result));
if(value.error)
return Future.value([]);
return convertList(value.body, (result) => Task.fromJson(result));
});
}

View File

@ -10,45 +10,42 @@ import '../components/TaskTile.dart';
import '../models/task.dart';
class LandingPage extends StatefulWidget {
const LandingPage(
{Key? key})
: super(key: key);
const LandingPage({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => LandingPageState();
}
class LandingPageState extends State<LandingPage> with AfterLayoutMixin<LandingPage> {
enum LandingPageStatus { built, loading, success, error }
class LandingPageState extends State<LandingPage>
with AfterLayoutMixin<LandingPage> {
int? defaultList;
List<Task>? _list;
List<Task> _list = [];
LandingPageStatus landingPageStatus = LandingPageStatus.built;
static const platform = const MethodChannel('vikunja');
Future<void> _updateDefaultList() async {
return VikunjaGlobal.of(context)
.listService
.getDefaultList()
.then((value) => setState(() => defaultList = value == null ? null : int.tryParse(value)));
return VikunjaGlobal.of(context).listService.getDefaultList().then(
(value) => setState(
() => defaultList = value == null ? null : int.tryParse(value)));
}
@override
void initState() {
Future.delayed(Duration.zero, () =>
_updateDefaultList().then((value) {
try {
platform.invokeMethod("isQuickTile","").then((value) => {
if(value is bool && value)
_addItemDialog(context)
});
} catch (e) {
log(e.toString());
}}));
Future.delayed(
Duration.zero,
() => _updateDefaultList().then((value) {
try {
platform.invokeMethod("isQuickTile", "").then((value) =>
{if (value is bool && value) _addItemDialog(context)});
} catch (e) {
log(e.toString());
}
}));
super.initState();
}
@override
void afterFirstLayout(BuildContext context) {
try {
@ -66,46 +63,55 @@ class LandingPageState extends State<LandingPage> with AfterLayoutMixin<LandingP
}
}
@override
Widget build(BuildContext context) {
if(_list == null || _list!.isEmpty)
_loadList(context);
Widget body;
switch (landingPageStatus) {
case LandingPageStatus.built:
_loadList(context);
body = new Stack(children: [ListView(), Center(child: CircularProgressIndicator(),)]);
break;
case LandingPageStatus.loading:
body = new Stack(children: [ListView(), Center(child: CircularProgressIndicator(),)]);
break;
case LandingPageStatus.error:
body = new Stack(children: [ListView(), Center(child: Text("There was an error loading this view"))]);
break;
case LandingPageStatus.success:
body = ListView(
scrollDirection: Axis.vertical,
shrinkWrap: true,
padding: EdgeInsets.symmetric(vertical: 8.0),
children:
ListTile.divideTiles(context: context, tiles: _listTasks(context))
.toList(),
);
break;
}
return new Scaffold(
body: RefreshIndicator(
onRefresh: () => _loadList(context),
child: _list != null ? ListView(
scrollDirection: Axis.vertical,
shrinkWrap: true,
padding: EdgeInsets.symmetric(vertical: 8.0),
children: ListTile.divideTiles(
context: context, tiles: _listTasks(context)).toList(),
) : new Center(child: CircularProgressIndicator(),),
),
body:
RefreshIndicator(onRefresh: () => _loadList(context), child: body),
floatingActionButton: Builder(
builder: (context) =>
FloatingActionButton(
builder: (context) => FloatingActionButton(
onPressed: () {
_addItemDialog(context);
},
child: const Icon(Icons.add),
)
)
);
)));
}
_addItemDialog(BuildContext context) {
if(defaultList == null) {
if (defaultList == null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Please select a default list in the settings'),
));
} else {
showDialog(
context: context,
builder: (_) =>
AddDialog(
onAddTask: (title, dueDate) => _addTask(title, dueDate, context),
decoration: new InputDecoration(
labelText: 'Task Name', hintText: 'eg. Milk')));
builder: (_) => AddDialog(
onAddTask: (title, dueDate) => _addTask(title, dueDate, context),
decoration: new InputDecoration(
labelText: 'Task Name', hintText: 'eg. Milk')));
}
}
@ -132,9 +138,8 @@ class LandingPageState extends State<LandingPage> with AfterLayoutMixin<LandingP
_loadList(context).then((value) => setState(() {}));
}
List<Widget> _listTasks(BuildContext context) {
var tasks = (_list?.map((task) => _buildTile(task, context)) ?? []).toList();
var tasks = (_list.map((task) => _buildTile(task, context))).toList();
//tasks.addAll(_loadingTasks.map(_buildLoadingTile));
return tasks;
}
@ -142,28 +147,36 @@ class LandingPageState extends State<LandingPage> with AfterLayoutMixin<LandingP
TaskTile _buildTile(Task task, BuildContext context) {
// key: UniqueKey() seems like a weird workaround to fix the loading issue
// is there a better way?
return TaskTile(key: UniqueKey(), task: task,onEdit: () => _loadList(context), showInfo: true,);
return TaskTile(
key: UniqueKey(),
task: task,
onEdit: () => _loadList(context),
showInfo: true,
);
}
Future<void> _loadList(BuildContext context) {
log("reloading list");
_list = [];
landingPageStatus = LandingPageStatus.loading;
// FIXME: loads and reschedules tasks each time list is updated
VikunjaGlobal.of(context).scheduleDueNotifications();
return VikunjaGlobal.of(context)
.taskService
.getByOptions(VikunjaGlobal.of(context).taskServiceOptions)
.then((taskList) {
VikunjaGlobal.of(context)
.listService
.getAll()
.then((lists) {
//taskList.forEach((task) {task.list = lists.firstWhere((element) => element.id == task.list_id);});
setState(() {
_list = taskList;
});
});
.then<Future<void>?>((taskList) {
if (taskList.isEmpty) {
landingPageStatus = LandingPageStatus.error;
return null;
}
return VikunjaGlobal.of(context).listService.getAll().then((lists) {
//taskList.forEach((task) {task.list = lists.firstWhere((element) => element.id == task.list_id);});
setState(() {
_list = taskList;
landingPageStatus = LandingPageStatus.success;
});
return null;
});
});
}
}

View File

@ -119,7 +119,7 @@ class _ListPageState extends State<ListPage> {
}(),
),
)
: Center(child: Text('This list is empty.')),
: Stack(children: [ListView(), Center(child: Text('This list is empty.'))]),
onRefresh: _loadList,
)
: Center(child: CircularProgressIndicator()),

View File

@ -66,7 +66,7 @@ class _NamespacePageState extends State<NamespacePage>
},
))).toList(),
)
: Center(child: Text('This namespace is empty.')),
: Stack(children: [ListView(),Center(child: Text('This namespace is empty.'))]),
onRefresh: _loadLists,
)
: Center(child: CircularProgressIndicator()),

View File

@ -56,14 +56,15 @@ class ListProvider with ChangeNotifier {
});
}
return VikunjaGlobal.of(context).taskService.getAllByList(listId, queryParams).then((response) {
_isLoading = false;
if(response.error)
return;
if (response.headers["x-pagination-total-pages"] != null) {
_maxPages = int.parse(response.headers["x-pagination-total-pages"]!);
}
_tasks.addAll(response.body);
_isLoading = false;
notifyListeners();
});
}).onError((error, stackTrace) {_isLoading = false;});
}
Future<void> loadBuckets({required BuildContext context, required int listId, int page = 1}) {