mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-11-19 07:09:14 +00:00
feat: some more work on console_page
* console_log's copy data is now a valid json object for all log types * graphQLResponse now provides raw response object for copy * console_model now handles pause in itself, so UI pipeline doesn't disturb pause (like when revisiting page / hot reloading) * some minor console_page UI tweaks
This commit is contained in:
parent
0ee46e1c1e
commit
4e0779f5e7
|
@ -48,6 +48,7 @@
|
|||
"title": "Console",
|
||||
"waiting": "Waiting for initialization…",
|
||||
"copy": "Copy",
|
||||
"copy_raw": "Raw response",
|
||||
"historyEmpty": "No data yet",
|
||||
"error":"Error",
|
||||
"log":"Log",
|
||||
|
@ -55,14 +56,19 @@
|
|||
"rest_api_response":"Rest API Response",
|
||||
"graphql_request":"GraphQL Request",
|
||||
"graphql_response":"GraphQL Response",
|
||||
"logged_at": "logged at: ",
|
||||
"logged_at": "Logged at",
|
||||
"data": "Data",
|
||||
"erros":"Errors",
|
||||
"errors":"Errors",
|
||||
"error_path": "Path",
|
||||
"error_locations": "Locations",
|
||||
"error_extensions": "Extensions",
|
||||
"request_data": "Request data",
|
||||
"headers": "Headers",
|
||||
"response_data": "Response data",
|
||||
"context": "Context",
|
||||
"operation": "Operation",
|
||||
"operation_type": "Operation type",
|
||||
"operation_name": "Operation name",
|
||||
"variables": "Variables"
|
||||
},
|
||||
"about_application_page": {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||
|
@ -17,9 +18,10 @@ class RequestLoggingLink extends Link {
|
|||
]) async* {
|
||||
_addConsoleLog(
|
||||
GraphQlRequestConsoleLog(
|
||||
// context: request.context,
|
||||
operationType: request.type.name,
|
||||
operation: request.operation,
|
||||
variables: request.variables,
|
||||
context: request.context,
|
||||
),
|
||||
);
|
||||
yield* forward!(request);
|
||||
|
@ -32,9 +34,10 @@ class ResponseLoggingParser extends ResponseParser {
|
|||
final response = super.parseResponse(body);
|
||||
_addConsoleLog(
|
||||
GraphQlResponseConsoleLog(
|
||||
// context: response.context,
|
||||
data: response.data,
|
||||
errors: response.errors,
|
||||
context: response.context,
|
||||
rawResponse: jsonEncode(response.response),
|
||||
),
|
||||
);
|
||||
return response;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
|
@ -68,10 +69,10 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
|||
) async {
|
||||
addConsoleLog(
|
||||
RestApiRequestConsoleLog(
|
||||
method: options.method,
|
||||
data: options.data.toString(),
|
||||
headers: options.headers,
|
||||
uri: options.uri,
|
||||
method: options.method,
|
||||
headers: options.headers,
|
||||
data: jsonEncode(options.data),
|
||||
),
|
||||
);
|
||||
return super.onRequest(options, handler);
|
||||
|
@ -84,10 +85,10 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
|||
) async {
|
||||
addConsoleLog(
|
||||
RestApiResponseConsoleLog(
|
||||
uri: response.realUri,
|
||||
method: response.requestOptions.method,
|
||||
statusCode: response.statusCode,
|
||||
data: response.data.toString(),
|
||||
uri: response.realUri,
|
||||
data: jsonEncode(response.data),
|
||||
),
|
||||
);
|
||||
return super.onResponse(
|
||||
|
@ -103,12 +104,13 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
|||
) async {
|
||||
final Response? response = err.response;
|
||||
log(err.toString());
|
||||
|
||||
addConsoleLog(
|
||||
ManualConsoleLog.warning(
|
||||
customTitle: 'RestAPI error',
|
||||
content: 'response-uri: ${response?.realUri}\n'
|
||||
'code: ${response?.statusCode}\n'
|
||||
'data: ${response?.toString()}\n',
|
||||
content: '"uri": "${response?.realUri}",\n'
|
||||
'"status_code": ${response?.statusCode},\n'
|
||||
'"response": ${jsonEncode(response)}',
|
||||
),
|
||||
);
|
||||
return super.onError(err, handler);
|
||||
|
|
|
@ -530,7 +530,7 @@ class ServerInstallationRepository {
|
|||
|
||||
Future<void> deleteDomain() async {
|
||||
await box.delete(BNames.serverDomain);
|
||||
getIt<ApiConfigModel>().init();
|
||||
await getIt<ApiConfigModel>().init();
|
||||
}
|
||||
|
||||
Future<void> saveIsServerStarted(final bool value) async {
|
||||
|
@ -604,6 +604,6 @@ class ServerInstallationRepository {
|
|||
BNames.hasFinalChecked,
|
||||
BNames.isLoading,
|
||||
]);
|
||||
getIt<ApiConfigModel>().init();
|
||||
await getIt<ApiConfigModel>().init();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,50 @@ import 'package:flutter/material.dart';
|
|||
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||
|
||||
class ConsoleModel extends ChangeNotifier {
|
||||
/// limit for history, so logs won't affect memory and overflow
|
||||
static const logBufferLimit = 500;
|
||||
|
||||
/// differs from log buffer limit so as to not rearrange memory each time
|
||||
/// we add incoming log
|
||||
static const incomingBufferBreakpoint = 750;
|
||||
|
||||
final List<ConsoleLog> _logs = [];
|
||||
final List<ConsoleLog> _incomingQueue = [];
|
||||
|
||||
bool _paused = false;
|
||||
bool get paused => _paused;
|
||||
List<ConsoleLog> get logs => _logs;
|
||||
|
||||
void log(final ConsoleLog newLog) {
|
||||
logs.add(newLog);
|
||||
notifyListeners();
|
||||
// Make sure we don't have too many
|
||||
if (logs.length > 500) {
|
||||
logs.removeAt(0);
|
||||
if (paused) {
|
||||
_incomingQueue.add(newLog);
|
||||
if (_incomingQueue.length > incomingBufferBreakpoint) {
|
||||
logs.removeRange(0, _incomingQueue.length - logBufferLimit);
|
||||
}
|
||||
} else {
|
||||
logs.add(newLog);
|
||||
_updateQueue();
|
||||
}
|
||||
}
|
||||
|
||||
void play() {
|
||||
_logs.addAll(_incomingQueue);
|
||||
_paused = false;
|
||||
_updateQueue();
|
||||
_incomingQueue.clear();
|
||||
}
|
||||
|
||||
void pause() {
|
||||
_paused = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// drop logs over the limit and
|
||||
void _updateQueue() {
|
||||
// Make sure we don't have too many
|
||||
if (logs.length > logBufferLimit) {
|
||||
logs.removeRange(0, logs.length - logBufferLimit);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:gql/language.dart';
|
||||
import 'package:graphql/client.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:gql/language.dart' as gql;
|
||||
import 'package:graphql/client.dart' as gql_client;
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
enum ConsoleLogSeverity {
|
||||
|
@ -12,7 +14,6 @@ enum ConsoleLogSeverity {
|
|||
/// TODO(misterfourtytwo): should we add?
|
||||
///
|
||||
/// * equality override
|
||||
/// * translations of theese strings
|
||||
sealed class ConsoleLog {
|
||||
ConsoleLog({
|
||||
final String? customTitle,
|
||||
|
@ -32,13 +33,23 @@ sealed class ConsoleLog {
|
|||
String get content;
|
||||
|
||||
/// data available for copy in dialog
|
||||
String? get shareableData => '$title\n'
|
||||
'{\n$content\n}';
|
||||
String? get shareableData => '{"title":"$title",\n'
|
||||
'"timestamp": "$fullUTCString",\n'
|
||||
'"data":{\n$content\n}'
|
||||
'\n}';
|
||||
|
||||
static final DateFormat _formatter = DateFormat('hh:mm:ss');
|
||||
String get timeString => _formatter.format(time);
|
||||
|
||||
String get fullUTCString => time.toUtc().toIso8601String();
|
||||
}
|
||||
|
||||
abstract class LogWithRawResponse {
|
||||
String get rawResponse;
|
||||
}
|
||||
|
||||
/// entity for manually created logs, as opposed to automated ones coming
|
||||
/// from requests / responses
|
||||
class ManualConsoleLog extends ConsoleLog {
|
||||
ManualConsoleLog({
|
||||
required this.content,
|
||||
|
@ -72,8 +83,10 @@ class RestApiRequestConsoleLog extends ConsoleLog {
|
|||
@override
|
||||
String get title => 'Rest API Request';
|
||||
@override
|
||||
String get content => 'method: $method\n'
|
||||
'uri: $uri';
|
||||
String get content => '"method": "$method",\n'
|
||||
'"uri": "$uri",\n'
|
||||
'"headers": ${jsonEncode(headers)},\n'
|
||||
'"data": $data';
|
||||
}
|
||||
|
||||
class RestApiResponseConsoleLog extends ConsoleLog {
|
||||
|
@ -93,49 +106,70 @@ class RestApiResponseConsoleLog extends ConsoleLog {
|
|||
@override
|
||||
String get title => 'Rest API Response';
|
||||
@override
|
||||
String get content => 'method: $method | status code: $statusCode\n'
|
||||
'uri: $uri';
|
||||
String get content => '"method": "$method",\n'
|
||||
'"status_code": $statusCode,\n'
|
||||
'"uri": "$uri",\n'
|
||||
'"data": $data';
|
||||
}
|
||||
|
||||
/// there is no actual getter for context fields outside of its class
|
||||
/// one can extract unique entries by their type, which implements
|
||||
/// `ContextEntry` class, I'll leave the code here if in the future
|
||||
/// some entries will actually be needed.
|
||||
// extension ContextEncoder on gql_client.Context {
|
||||
// String get encode {
|
||||
// return '""';
|
||||
// }
|
||||
// }
|
||||
|
||||
class GraphQlRequestConsoleLog extends ConsoleLog {
|
||||
GraphQlRequestConsoleLog({
|
||||
this.operation,
|
||||
this.variables,
|
||||
this.context,
|
||||
required this.operationType,
|
||||
required this.operation,
|
||||
required this.variables,
|
||||
// this.context,
|
||||
super.severity,
|
||||
});
|
||||
|
||||
final Context? context;
|
||||
final Operation? operation;
|
||||
// final gql_client.Context? context;
|
||||
final String operationType;
|
||||
final gql_client.Operation? operation;
|
||||
String get operationDocument =>
|
||||
operation != null ? gql.printNode(operation!.document) : 'null';
|
||||
final Map<String, dynamic>? variables;
|
||||
|
||||
@override
|
||||
String get title => 'GraphQL Request';
|
||||
@override
|
||||
String get content => 'name: ${operation?.operationName}\n'
|
||||
'document: ${operation?.document != null ? printNode(operation!.document) : null}';
|
||||
String get stringifiedOperation => operation == null
|
||||
? 'null'
|
||||
: 'Operation{\n'
|
||||
'\tname: ${operation?.operationName},\n'
|
||||
'\tdocument: ${operation?.document != null ? printNode(operation!.document) : null}\n'
|
||||
'}';
|
||||
String get content =>
|
||||
// '"context": ${context?.encode},\n'
|
||||
'"variables": ${jsonEncode(variables)},\n'
|
||||
'"type": "$operationType",\n'
|
||||
'"name": "${operation?.operationName}",\n'
|
||||
'"document": ${jsonEncode(operationDocument)}';
|
||||
}
|
||||
|
||||
class GraphQlResponseConsoleLog extends ConsoleLog {
|
||||
class GraphQlResponseConsoleLog extends ConsoleLog
|
||||
implements LogWithRawResponse {
|
||||
GraphQlResponseConsoleLog({
|
||||
required this.rawResponse,
|
||||
// this.context,
|
||||
this.data,
|
||||
this.errors,
|
||||
this.context,
|
||||
super.severity,
|
||||
});
|
||||
|
||||
final Context? context;
|
||||
@override
|
||||
final String rawResponse;
|
||||
// final gql_client.Context? context;
|
||||
final Map<String, dynamic>? data;
|
||||
final List<GraphQLError>? errors;
|
||||
final List<gql_client.GraphQLError>? errors;
|
||||
|
||||
@override
|
||||
String get title => 'GraphQL Response';
|
||||
@override
|
||||
String get content => 'data: $data';
|
||||
String get content =>
|
||||
// '"context": ${context?.encode},\n'
|
||||
'"data": ${jsonEncode(data)},\n'
|
||||
'"errors": $errors';
|
||||
}
|
||||
|
|
|
@ -7,66 +7,92 @@ extension on ConsoleLog {
|
|||
List<Widget> unwrapContent(final BuildContext context) => switch (this) {
|
||||
(final RestApiRequestConsoleLog log) => [
|
||||
if (log.method != null) _KeyValueRow('method', log.method),
|
||||
if (log.uri != null) _KeyValueRow('uri', log.uri.toString()),
|
||||
if (log.uri != null) _KeyValueRow('uri', '${log.uri}'),
|
||||
|
||||
// headers bloc
|
||||
if (log.headers?.isNotEmpty ?? false)
|
||||
if (log.headers?.isNotEmpty ?? false) ...[
|
||||
const _SectionRow('console_page.headers'),
|
||||
...?log.headers?.entries
|
||||
.map((final entry) => _KeyValueRow(entry.key, entry.value)),
|
||||
for (final entry in log.headers!.entries)
|
||||
_KeyValueRow(entry.key, '${entry.value}'),
|
||||
],
|
||||
|
||||
// data bloc
|
||||
// data
|
||||
const _SectionRow('console_page.data'),
|
||||
_DataRow(log.data?.toString()),
|
||||
_DataRow('${log.data}'),
|
||||
],
|
||||
(final RestApiResponseConsoleLog log) => [
|
||||
if (log.method != null) _KeyValueRow('method', log.method),
|
||||
if (log.uri != null) _KeyValueRow('uri', log.uri.toString()),
|
||||
if (log.method != null) _KeyValueRow('method', '${log.method}'),
|
||||
if (log.uri != null) _KeyValueRow('uri', '${log.uri}'),
|
||||
if (log.statusCode != null)
|
||||
_KeyValueRow('statusCode', log.statusCode.toString()),
|
||||
_KeyValueRow('statusCode', '${log.statusCode}'),
|
||||
|
||||
// data bloc
|
||||
// data
|
||||
const _SectionRow('console_page.response_data'),
|
||||
_DataRow(log.data?.toString()),
|
||||
_DataRow('${log.data}'),
|
||||
],
|
||||
(final GraphQlRequestConsoleLog log) => [
|
||||
// context
|
||||
const _SectionRow('console_page.context'),
|
||||
_DataRow(log.context?.toString()),
|
||||
// data
|
||||
if (log.operation != null)
|
||||
const _SectionRow('console_page.operation'),
|
||||
_DataRow(log.stringifiedOperation), // errors
|
||||
if (log.variables?.isNotEmpty ?? false)
|
||||
const _SectionRow('console_page.variables'),
|
||||
...?log.variables?.entries.map(
|
||||
(final entry) => _KeyValueRow(
|
||||
entry.key,
|
||||
'${entry.value}',
|
||||
// // context
|
||||
// if (log.context != null) ...[
|
||||
// const _SectionRow('console_page.context'),
|
||||
// _DataRow('${log.context}'),
|
||||
// ],
|
||||
|
||||
const _SectionRow('console_page.operation'),
|
||||
if (log.operation != null) ...[
|
||||
_KeyValueRow(
|
||||
'console_page.operation_type'.tr(),
|
||||
log.operationType,
|
||||
),
|
||||
),
|
||||
_KeyValueRow(
|
||||
'console_page.operation_name'.tr(),
|
||||
log.operation?.operationName,
|
||||
),
|
||||
const Divider(),
|
||||
// data
|
||||
_DataRow(log.operationDocument),
|
||||
],
|
||||
// preset variables
|
||||
if (log.variables?.isNotEmpty ?? false) ...[
|
||||
const _SectionRow('console_page.variables'),
|
||||
for (final entry in log.variables!.entries)
|
||||
_KeyValueRow(entry.key, '${entry.value}'),
|
||||
],
|
||||
],
|
||||
(final GraphQlResponseConsoleLog log) => [
|
||||
// context
|
||||
const _SectionRow('console_page.context'),
|
||||
_DataRow(log.context?.toString()),
|
||||
// // context
|
||||
// const _SectionRow('console_page.context'),
|
||||
// _DataRow('${log.context}'),
|
||||
// data
|
||||
if (log.data != null) const _SectionRow('console_page.data'),
|
||||
...?log.data?.entries.map(
|
||||
(final entry) => _KeyValueRow(
|
||||
entry.key,
|
||||
'${entry.value}',
|
||||
),
|
||||
),
|
||||
if (log.data != null) ...[
|
||||
const _SectionRow('console_page.data'),
|
||||
for (final entry in log.data!.entries)
|
||||
_KeyValueRow(entry.key, '${entry.value}'),
|
||||
],
|
||||
// errors
|
||||
if (log.errors?.isNotEmpty ?? false)
|
||||
if (log.errors?.isNotEmpty ?? false) ...[
|
||||
const _SectionRow('console_page.errors'),
|
||||
...?log.errors?.map(
|
||||
(final entry) => _KeyValueRow(
|
||||
entry.message,
|
||||
'${entry.locations}',
|
||||
),
|
||||
),
|
||||
for (final entry in log.errors!) ...[
|
||||
_KeyValueRow(
|
||||
'${'console_page.error_message'.tr()}: ',
|
||||
entry.message,
|
||||
),
|
||||
_KeyValueRow(
|
||||
'${'console_page.error_path'.tr()}: ',
|
||||
'${entry.path}',
|
||||
),
|
||||
if (entry.locations?.isNotEmpty ?? false)
|
||||
_KeyValueRow(
|
||||
'${'console_page.error_locations'.tr()}: ',
|
||||
'${entry.locations}',
|
||||
),
|
||||
if (entry.extensions?.isNotEmpty ?? false)
|
||||
_KeyValueRow(
|
||||
'${'console_page.error_extensions'.tr()}: ',
|
||||
'${entry.extensions}',
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
],
|
||||
],
|
||||
(final ManualConsoleLog log) => [
|
||||
_DataRow(log.content),
|
||||
|
@ -74,6 +100,7 @@ extension on ConsoleLog {
|
|||
};
|
||||
}
|
||||
|
||||
/// dialog with detailed log content
|
||||
class ConsoleItemDialog extends StatelessWidget {
|
||||
const ConsoleItemDialog({
|
||||
required this.log,
|
||||
|
@ -83,80 +110,66 @@ class ConsoleItemDialog extends StatelessWidget {
|
|||
final ConsoleLog log;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final content = log.unwrapContent(context);
|
||||
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(log.title),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text('logged_at'.tr()),
|
||||
SelectableText(
|
||||
log.timeString,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
Widget build(final BuildContext context) => AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(log.title),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 12,
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: SelectableText.rich(
|
||||
TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '${'console_page.logged_at'.tr()}: ',
|
||||
style: const TextStyle(),
|
||||
),
|
||||
TextSpan(
|
||||
text: '${log.timeString} (${log.fullUTCString})',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
...content,
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
// A button to copy the request to the clipboard
|
||||
if (log.shareableData?.isNotEmpty ?? false)
|
||||
TextButton(
|
||||
onPressed: () => PlatformAdapter.setClipboard(log.shareableData!),
|
||||
child: Text('console_page.copy'.tr()),
|
||||
),
|
||||
// close dialog
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('basis.close'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KeyValueRow extends StatelessWidget {
|
||||
const _KeyValueRow(this.title, this.value);
|
||||
|
||||
final String title;
|
||||
final String? value;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => SelectableText.rich(
|
||||
TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '$title: ',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: value ?? ''),
|
||||
const Divider(),
|
||||
...log.unwrapContent(context),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (log is LogWithRawResponse)
|
||||
TextButton(
|
||||
onPressed: () => PlatformAdapter.setClipboard(
|
||||
(log as LogWithRawResponse).rawResponse,
|
||||
),
|
||||
child: Text('console_page.copy_raw'.tr()),
|
||||
),
|
||||
// A button to copy the request to the clipboard
|
||||
if (log.shareableData?.isNotEmpty ?? false)
|
||||
TextButton(
|
||||
onPressed: () => PlatformAdapter.setClipboard(log.shareableData!),
|
||||
child: Text('console_page.copy'.tr()),
|
||||
),
|
||||
// close dialog
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('basis.close'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _DataRow extends StatelessWidget {
|
||||
const _DataRow(this.data);
|
||||
final String? data;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => SelectableText(
|
||||
data ?? 'null',
|
||||
style: const TextStyle(fontWeight: FontWeight.w400),
|
||||
);
|
||||
}
|
||||
|
||||
/// different sections delimiter with `title`
|
||||
class _SectionRow extends StatelessWidget {
|
||||
const _SectionRow(this.title);
|
||||
|
||||
|
@ -184,3 +197,44 @@ class _SectionRow extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// data row with a {key: value} pair
|
||||
class _KeyValueRow extends StatelessWidget {
|
||||
const _KeyValueRow(this.title, this.value);
|
||||
|
||||
final String title;
|
||||
final String? value;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: SelectableText.rich(
|
||||
TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '$title: ',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: value ?? ''),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// data row with only text
|
||||
class _DataRow extends StatelessWidget {
|
||||
const _DataRow(this.data);
|
||||
|
||||
final String? data;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: SelectableText(
|
||||
data ?? 'null',
|
||||
style: const TextStyle(fontWeight: FontWeight.w400),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,37 +35,41 @@ class ConsoleLogItemWidget extends StatelessWidget {
|
|||
final ConsoleLog log;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => ListTile(
|
||||
dense: true,
|
||||
title: SelectableText.rich(
|
||||
TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '${log.timeString}: ',
|
||||
style: const TextStyle(
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
Widget build(final BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '${log.timeString}: ',
|
||||
style: const TextStyle(
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: log.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
TextSpan(
|
||||
text: log.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
log.content,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
),
|
||||
leading: Icon(log.resolveIcon()),
|
||||
iconColor: log.resolveColor(context),
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (final BuildContext context) =>
|
||||
ConsoleItemDialog(log: log),
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
log.content,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
),
|
||||
leading: Icon(log.resolveIcon()),
|
||||
iconColor: log.resolveColor(context),
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (final BuildContext context) => ConsoleItemDialog(log: log),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,8 +16,9 @@ class ConsolePage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ConsolePageState extends State<ConsolePage> {
|
||||
ConsoleModel get console => getIt<ConsoleModel>();
|
||||
|
||||
/// should freeze logs state to properly read logs
|
||||
bool paused = false;
|
||||
late final Future<void> future;
|
||||
|
||||
@override
|
||||
|
@ -25,12 +26,12 @@ class _ConsolePageState extends State<ConsolePage> {
|
|||
super.initState();
|
||||
|
||||
future = getIt.allReady();
|
||||
getIt<ConsoleModel>().addListener(update);
|
||||
console.addListener(update);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
getIt<ConsoleModel>().removeListener(update);
|
||||
console.removeListener(update);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
@ -40,17 +41,12 @@ class _ConsolePageState extends State<ConsolePage> {
|
|||
/// unmounted or during frame build, adding as postframe callback ensures
|
||||
/// that element is marked for rebuild
|
||||
WidgetsBinding.instance.addPostFrameCallback((final _) {
|
||||
if (!paused && mounted) {
|
||||
if (mounted) {
|
||||
setState(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void togglePause() {
|
||||
paused ^= true;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => SafeArea(
|
||||
child: Scaffold(
|
||||
|
@ -63,34 +59,31 @@ class _ConsolePageState extends State<ConsolePage> {
|
|||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
paused ? Icons.play_arrow_outlined : Icons.pause_outlined,
|
||||
console.paused
|
||||
? Icons.play_arrow_outlined
|
||||
: Icons.pause_outlined,
|
||||
),
|
||||
onPressed: togglePause,
|
||||
onPressed: console.paused ? console.play : console.pause,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SelectionArea(
|
||||
child: Scrollbar(
|
||||
child: FutureBuilder(
|
||||
future: future,
|
||||
builder: (
|
||||
final BuildContext context,
|
||||
final AsyncSnapshot<void> snapshot,
|
||||
) {
|
||||
if (snapshot.hasData) {
|
||||
final List<ConsoleLog> logs =
|
||||
getIt.get<ConsoleModel>().logs;
|
||||
body: Scrollbar(
|
||||
child: FutureBuilder(
|
||||
future: future,
|
||||
builder: (
|
||||
final BuildContext context,
|
||||
final AsyncSnapshot<void> snapshot,
|
||||
) {
|
||||
if (snapshot.hasData) {
|
||||
final List<ConsoleLog> logs = console.logs;
|
||||
|
||||
return logs.isEmpty
|
||||
? const _ConsoleViewEmpty()
|
||||
: _ConsoleViewLoaded(
|
||||
logs: logs,
|
||||
);
|
||||
}
|
||||
return logs.isEmpty
|
||||
? const _ConsoleViewEmpty()
|
||||
: _ConsoleViewLoaded(logs: logs);
|
||||
}
|
||||
|
||||
return const _ConsoleViewLoading();
|
||||
},
|
||||
),
|
||||
return const _ConsoleViewLoading();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -135,7 +128,7 @@ class _ConsoleViewLoaded extends StatelessWidget {
|
|||
@override
|
||||
Widget build(final BuildContext context) => ListView.separated(
|
||||
primary: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: logs.length,
|
||||
itemBuilder: (final BuildContext context, final int index) {
|
||||
final log = logs[logs.length - 1 - index];
|
||||
|
|
Loading…
Reference in a new issue