refactor(ui): More compact view of console.dart

pull/203/head
Inex Code 2023-04-04 19:06:14 +03:00 committed by Gitea
parent 4fde816023
commit 466a221dd0
6 changed files with 396 additions and 51 deletions

View File

@ -47,7 +47,8 @@
},
"console_page": {
"title": "Console",
"waiting": "Waiting for initialization…"
"waiting": "Waiting for initialization…",
"copy": "Copy"
},
"about_us_page": {
"title": "About us"

View File

@ -20,7 +20,13 @@ class RequestLoggingLink extends Link {
final Request request, [
final NextLink? forward,
]) async* {
_logToAppConsole(request);
getIt.get<ConsoleModel>().addMessage(
GraphQlRequestMessage(
operation: request.operation,
variables: request.variables,
context: request.context,
),
);
yield* forward!(request);
}
}
@ -29,7 +35,13 @@ class ResponseLoggingParser extends ResponseParser {
@override
Response parseResponse(final Map<String, dynamic> body) {
final response = super.parseResponse(body);
_logToAppConsole(response);
getIt.get<ConsoleModel>().addMessage(
GraphQlResponseMessage(
data: response.data,
errors: response.errors,
context: response.context,
),
);
return response;
}

View File

@ -65,9 +65,11 @@ class ConsoleInterceptor extends InterceptorsWrapper {
final RequestInterceptorHandler handler,
) async {
addMessage(
Message(
text:
'request-uri: ${options.uri}\nheaders: ${options.headers}\ndata: ${options.data}',
RestApiRequestMessage(
method: options.method,
data: options.data.toString(),
headers: options.headers,
uri: options.uri,
),
);
return super.onRequest(options, handler);
@ -79,9 +81,11 @@ class ConsoleInterceptor extends InterceptorsWrapper {
final ResponseInterceptorHandler handler,
) async {
addMessage(
Message(
text:
'response-uri: ${response.realUri}\ncode: ${response.statusCode}\ndata: ${response.toString()}\n',
RestApiResponseMessage(
method: response.requestOptions.method,
statusCode: response.statusCode,
data: response.data.toString(),
uri: response.realUri,
),
);
return super.onResponse(

View File

@ -1,20 +1,74 @@
import 'package:graphql/client.dart';
import 'package:intl/intl.dart';
final DateFormat formatter = DateFormat('hh:mm');
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})
: type = MessageType.warning,
: severity = MessageSeverity.warning,
time = DateTime.now();
final String? text;
final DateTime time;
final MessageType type;
final MessageSeverity severity;
String get timeString => formatter.format(time);
}
enum MessageType {
enum MessageSeverity {
normal,
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;
}

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

View File

@ -5,7 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/get_it_config.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()
class ConsolePage extends StatefulWidget {
@ -29,21 +29,29 @@ class _ConsolePageState extends State<ConsolePage> {
super.dispose();
}
void update() => setState(() => {});
bool paused = false;
void update() {
if (!paused) {
setState(() => {});
}
}
@override
Widget build(final BuildContext context) => SafeArea(
child: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(53),
child: Column(
children: [
BrandHeader(
title: 'console_page.title'.tr(),
hasBackButton: true,
),
],
appBar: AppBar(
title: Text('console_page.title'.tr()),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
actions: [
IconButton(
icon: Icon(paused ? Icons.play_arrow_outlined : Icons.pause_outlined),
onPressed: () => setState(() => paused = !paused),
),
],
),
body: FutureBuilder(
future: getIt.allReady(),
@ -62,33 +70,7 @@ class _ConsolePageState extends State<ConsolePage> {
const SizedBox(height: 20),
...UnmodifiableListView(
messages
.map((final 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),
],
),
),
);
})
.map((final message) => LogListItem(message: message))
.toList()
.reversed,
),