mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-01-09 09:31:13 +00:00
refactor(ui): More compact view of console.dart
This commit is contained in:
parent
4fde816023
commit
466a221dd0
|
@ -47,7 +47,8 @@
|
||||||
},
|
},
|
||||||
"console_page": {
|
"console_page": {
|
||||||
"title": "Console",
|
"title": "Console",
|
||||||
"waiting": "Waiting for initialization…"
|
"waiting": "Waiting for initialization…",
|
||||||
|
"copy": "Copy"
|
||||||
},
|
},
|
||||||
"about_us_page": {
|
"about_us_page": {
|
||||||
"title": "About us"
|
"title": "About us"
|
||||||
|
|
|
@ -20,7 +20,13 @@ class RequestLoggingLink extends Link {
|
||||||
final Request request, [
|
final Request request, [
|
||||||
final NextLink? forward,
|
final NextLink? forward,
|
||||||
]) async* {
|
]) async* {
|
||||||
_logToAppConsole(request);
|
getIt.get<ConsoleModel>().addMessage(
|
||||||
|
GraphQlRequestMessage(
|
||||||
|
operation: request.operation,
|
||||||
|
variables: request.variables,
|
||||||
|
context: request.context,
|
||||||
|
),
|
||||||
|
);
|
||||||
yield* forward!(request);
|
yield* forward!(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +35,13 @@ class ResponseLoggingParser extends ResponseParser {
|
||||||
@override
|
@override
|
||||||
Response parseResponse(final Map<String, dynamic> body) {
|
Response parseResponse(final Map<String, dynamic> body) {
|
||||||
final response = super.parseResponse(body);
|
final response = super.parseResponse(body);
|
||||||
_logToAppConsole(response);
|
getIt.get<ConsoleModel>().addMessage(
|
||||||
|
GraphQlResponseMessage(
|
||||||
|
data: response.data,
|
||||||
|
errors: response.errors,
|
||||||
|
context: response.context,
|
||||||
|
),
|
||||||
|
);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,9 +65,11 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
final RequestInterceptorHandler handler,
|
final RequestInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
addMessage(
|
addMessage(
|
||||||
Message(
|
RestApiRequestMessage(
|
||||||
text:
|
method: options.method,
|
||||||
'request-uri: ${options.uri}\nheaders: ${options.headers}\ndata: ${options.data}',
|
data: options.data.toString(),
|
||||||
|
headers: options.headers,
|
||||||
|
uri: options.uri,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return super.onRequest(options, handler);
|
return super.onRequest(options, handler);
|
||||||
|
@ -79,9 +81,11 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
final ResponseInterceptorHandler handler,
|
final ResponseInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
addMessage(
|
addMessage(
|
||||||
Message(
|
RestApiResponseMessage(
|
||||||
text:
|
method: response.requestOptions.method,
|
||||||
'response-uri: ${response.realUri}\ncode: ${response.statusCode}\ndata: ${response.toString()}\n',
|
statusCode: response.statusCode,
|
||||||
|
data: response.data.toString(),
|
||||||
|
uri: response.realUri,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return super.onResponse(
|
return super.onResponse(
|
||||||
|
|
|
@ -1,20 +1,74 @@
|
||||||
|
import 'package:graphql/client.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
final DateFormat formatter = DateFormat('hh:mm');
|
final DateFormat formatter = DateFormat('hh:mm');
|
||||||
|
|
||||||
class Message {
|
class Message {
|
||||||
Message({this.text, this.type = MessageType.normal}) : time = DateTime.now();
|
Message({this.text, this.severity = MessageSeverity.normal})
|
||||||
|
: time = DateTime.now();
|
||||||
Message.warn({this.text})
|
Message.warn({this.text})
|
||||||
: type = MessageType.warning,
|
: severity = MessageSeverity.warning,
|
||||||
time = DateTime.now();
|
time = DateTime.now();
|
||||||
|
|
||||||
final String? text;
|
final String? text;
|
||||||
final DateTime time;
|
final DateTime time;
|
||||||
final MessageType type;
|
final MessageSeverity severity;
|
||||||
String get timeString => formatter.format(time);
|
String get timeString => formatter.format(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MessageType {
|
enum MessageSeverity {
|
||||||
normal,
|
normal,
|
||||||
warning,
|
warning,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RestApiRequestMessage extends Message {
|
||||||
|
RestApiRequestMessage({
|
||||||
|
this.method,
|
||||||
|
this.uri,
|
||||||
|
this.data,
|
||||||
|
this.headers,
|
||||||
|
}) : super(text: 'request-uri: $uri\nheaders: $headers\ndata: $data');
|
||||||
|
|
||||||
|
final String? method;
|
||||||
|
final Uri? uri;
|
||||||
|
final String? data;
|
||||||
|
final Map<String, dynamic>? headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RestApiResponseMessage extends Message {
|
||||||
|
RestApiResponseMessage({
|
||||||
|
this.method,
|
||||||
|
this.uri,
|
||||||
|
this.statusCode,
|
||||||
|
this.data,
|
||||||
|
}) : super(text: 'response-uri: $uri\ncode: $statusCode\ndata: $data');
|
||||||
|
|
||||||
|
final String? method;
|
||||||
|
final Uri? uri;
|
||||||
|
final int? statusCode;
|
||||||
|
final String? data;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GraphQlResponseMessage extends Message {
|
||||||
|
GraphQlResponseMessage({
|
||||||
|
this.data,
|
||||||
|
this.errors,
|
||||||
|
this.context,
|
||||||
|
}) : super(text: 'GraphQL Response\ndata: $data');
|
||||||
|
|
||||||
|
final Map<String, dynamic>? data;
|
||||||
|
final List<GraphQLError>? errors;
|
||||||
|
final Context? context;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GraphQlRequestMessage extends Message {
|
||||||
|
GraphQlRequestMessage({
|
||||||
|
this.operation,
|
||||||
|
this.variables,
|
||||||
|
this.context,
|
||||||
|
}) : super(text: 'GraphQL Request\noperation: $operation');
|
||||||
|
|
||||||
|
final Operation? operation;
|
||||||
|
final Map<String, dynamic>? variables;
|
||||||
|
final Context? context;
|
||||||
|
}
|
||||||
|
|
292
lib/ui/components/list_tiles/log_list_tile.dart
Normal file
292
lib/ui/components/list_tiles/log_list_tile.dart
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/message.dart';
|
||||||
|
|
||||||
|
class LogListItem extends StatelessWidget {
|
||||||
|
const LogListItem({
|
||||||
|
required this.message,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) {
|
||||||
|
final messageItem = message;
|
||||||
|
if (messageItem is RestApiRequestMessage) {
|
||||||
|
return _RestApiRequestMessageItem(message: messageItem);
|
||||||
|
} else if (messageItem is RestApiResponseMessage) {
|
||||||
|
return _RestApiResponseMessageItem(message: messageItem);
|
||||||
|
} else if (messageItem is GraphQlResponseMessage) {
|
||||||
|
return _GraphQlResponseMessageItem(message: messageItem);
|
||||||
|
} else if (messageItem is GraphQlRequestMessage) {
|
||||||
|
return _GraphQlRequestMessageItem(message: messageItem);
|
||||||
|
} else {
|
||||||
|
return _DefaultMessageItem(message: messageItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RestApiRequestMessageItem extends StatelessWidget {
|
||||||
|
const _RestApiRequestMessageItem({required this.message});
|
||||||
|
|
||||||
|
final RestApiRequestMessage message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => ListTile(
|
||||||
|
title: Text(
|
||||||
|
'${message.method}\n${message.uri}',
|
||||||
|
),
|
||||||
|
subtitle: Text(message.timeString),
|
||||||
|
leading: const Icon(Icons.upload_outlined),
|
||||||
|
iconColor: Theme.of(context).colorScheme.secondary,
|
||||||
|
onTap: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (final BuildContext context) => AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: Text(
|
||||||
|
'${message.method}\n${message.uri}',
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
Text(message.timeString),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Headers is a map of key-value pairs
|
||||||
|
if (message.headers != null) const Text('Headers'),
|
||||||
|
if (message.headers != null)
|
||||||
|
Text(
|
||||||
|
message.headers!.entries
|
||||||
|
.map((final entry) => '${entry.key}: ${entry.value}')
|
||||||
|
.join('\n'),
|
||||||
|
),
|
||||||
|
if (message.data != null && message.data != 'null')
|
||||||
|
const Text('Data'),
|
||||||
|
if (message.data != null && message.data != 'null')
|
||||||
|
Text(message.data!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
// A button to copy the request to the clipboard
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: message.text));
|
||||||
|
},
|
||||||
|
child: Text('console_page.copy'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('basis.close'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RestApiResponseMessageItem extends StatelessWidget {
|
||||||
|
const _RestApiResponseMessageItem({required this.message});
|
||||||
|
|
||||||
|
final RestApiResponseMessage message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => ListTile(
|
||||||
|
title: Text(
|
||||||
|
'${message.statusCode} ${message.method}\n${message.uri}',
|
||||||
|
),
|
||||||
|
subtitle: Text(message.timeString),
|
||||||
|
leading: const Icon(Icons.download_outlined),
|
||||||
|
iconColor: Theme.of(context).colorScheme.primary,
|
||||||
|
onTap: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (final BuildContext context) => AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: Text(
|
||||||
|
'${message.statusCode} ${message.method}\n${message.uri}',
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
Text(message.timeString),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Headers is a map of key-value pairs
|
||||||
|
if (message.data != null && message.data != 'null')
|
||||||
|
const Text('Data'),
|
||||||
|
if (message.data != null && message.data != 'null')
|
||||||
|
Text(message.data!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
// A button to copy the request to the clipboard
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: message.text));
|
||||||
|
},
|
||||||
|
child: Text('console_page.copy'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('basis.close'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GraphQlResponseMessageItem extends StatelessWidget {
|
||||||
|
const _GraphQlResponseMessageItem({required this.message});
|
||||||
|
|
||||||
|
final GraphQlResponseMessage message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => ListTile(
|
||||||
|
title: Text(
|
||||||
|
'GraphQL Response at ${message.timeString}',
|
||||||
|
),
|
||||||
|
subtitle: Text(message.data.toString(),
|
||||||
|
overflow: TextOverflow.ellipsis, maxLines: 1,),
|
||||||
|
leading: const Icon(Icons.arrow_circle_down_outlined),
|
||||||
|
iconColor: Theme.of(context).colorScheme.tertiary,
|
||||||
|
onTap: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (final BuildContext context) => AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: Text(
|
||||||
|
'GraphQL Response at ${message.timeString}',
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
Text(message.timeString),
|
||||||
|
const Divider(),
|
||||||
|
if (message.data != null) const Text('Data'),
|
||||||
|
// Data is a map of key-value pairs
|
||||||
|
if (message.data != null)
|
||||||
|
Text(
|
||||||
|
message.data!.entries
|
||||||
|
.map((final entry) => '${entry.key}: ${entry.value}')
|
||||||
|
.join('\n'),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
if (message.errors != null) const Text('Errors'),
|
||||||
|
if (message.errors != null)
|
||||||
|
Text(
|
||||||
|
message.errors!
|
||||||
|
.map((final entry) =>
|
||||||
|
'${entry.message} at ${entry.locations}',)
|
||||||
|
.join('\n'),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
if (message.context != null) const Text('Context'),
|
||||||
|
if (message.context != null)
|
||||||
|
Text(
|
||||||
|
message.context!.toString(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
// A button to copy the request to the clipboard
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: message.text));
|
||||||
|
},
|
||||||
|
child: Text('console_page.copy'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('basis.close'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GraphQlRequestMessageItem extends StatelessWidget {
|
||||||
|
const _GraphQlRequestMessageItem({required this.message});
|
||||||
|
|
||||||
|
final GraphQlRequestMessage message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => ListTile(
|
||||||
|
title: Text(
|
||||||
|
'GraphQL Request at ${message.timeString}',
|
||||||
|
),
|
||||||
|
subtitle: Text(message.operation.toString(),
|
||||||
|
overflow: TextOverflow.ellipsis, maxLines: 1,),
|
||||||
|
leading: const Icon(Icons.arrow_circle_up_outlined),
|
||||||
|
iconColor: Theme.of(context).colorScheme.secondary,
|
||||||
|
onTap: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (final BuildContext context) => AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: Text(
|
||||||
|
'GraphQL Response at ${message.timeString}',
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
Text(message.timeString),
|
||||||
|
const Divider(),
|
||||||
|
if (message.operation != null) const Text('Operation'),
|
||||||
|
// Data is a map of key-value pairs
|
||||||
|
if (message.operation != null)
|
||||||
|
Text(
|
||||||
|
message.operation!.toString(),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
if (message.variables != null) const Text('Variables'),
|
||||||
|
if (message.variables != null)
|
||||||
|
Text(
|
||||||
|
message.variables!.entries
|
||||||
|
.map((final entry) => '${entry.key}: ${entry.value}')
|
||||||
|
.join('\n'),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
if (message.context != null) const Text('Context'),
|
||||||
|
if (message.context != null)
|
||||||
|
Text(
|
||||||
|
message.context!.toString(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
// A button to copy the request to the clipboard
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: message.text));
|
||||||
|
},
|
||||||
|
child: Text('console_page.copy'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('basis.close'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DefaultMessageItem extends StatelessWidget {
|
||||||
|
const _DefaultMessageItem({required this.message});
|
||||||
|
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: DefaultTextStyle.of(context).style,
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: '${message.timeString}: \n',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(text: message.text),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/models/message.dart';
|
import 'package:selfprivacy/logic/models/message.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
|
import 'package:selfprivacy/ui/components/list_tiles/log_list_tile.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class ConsolePage extends StatefulWidget {
|
class ConsolePage extends StatefulWidget {
|
||||||
|
@ -29,22 +29,30 @@ class _ConsolePageState extends State<ConsolePage> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void update() => setState(() => {});
|
bool paused = false;
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
if (!paused) {
|
||||||
|
setState(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) => SafeArea(
|
Widget build(final BuildContext context) => SafeArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: PreferredSize(
|
appBar: AppBar(
|
||||||
preferredSize: const Size.fromHeight(53),
|
title: Text('console_page.title'.tr()),
|
||||||
child: Column(
|
leading: IconButton(
|
||||||
children: [
|
icon: const Icon(Icons.arrow_back),
|
||||||
BrandHeader(
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
title: 'console_page.title'.tr(),
|
),
|
||||||
hasBackButton: true,
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(paused ? Icons.play_arrow_outlined : Icons.pause_outlined),
|
||||||
|
onPressed: () => setState(() => paused = !paused),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
body: FutureBuilder(
|
body: FutureBuilder(
|
||||||
future: getIt.allReady(),
|
future: getIt.allReady(),
|
||||||
builder: (
|
builder: (
|
||||||
|
@ -62,33 +70,7 @@ class _ConsolePageState extends State<ConsolePage> {
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
...UnmodifiableListView(
|
...UnmodifiableListView(
|
||||||
messages
|
messages
|
||||||
.map((final message) {
|
.map((final message) => LogListItem(message: message))
|
||||||
final bool isError =
|
|
||||||
message.type == MessageType.warning;
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
style: DefaultTextStyle.of(context).style,
|
|
||||||
children: <TextSpan>[
|
|
||||||
TextSpan(
|
|
||||||
text:
|
|
||||||
'${message.timeString}${isError ? '(Error)' : ''}: \n',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: isError
|
|
||||||
? Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.error
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextSpan(text: message.text),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.toList()
|
.toList()
|
||||||
.reversed,
|
.reversed,
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue