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",
|
"title": "Console",
|
||||||
"waiting": "Waiting for initialization…",
|
"waiting": "Waiting for initialization…",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
|
"copy_raw": "Raw response",
|
||||||
"historyEmpty": "No data yet",
|
"historyEmpty": "No data yet",
|
||||||
"error":"Error",
|
"error":"Error",
|
||||||
"log":"Log",
|
"log":"Log",
|
||||||
|
@ -55,14 +56,19 @@
|
||||||
"rest_api_response":"Rest API Response",
|
"rest_api_response":"Rest API Response",
|
||||||
"graphql_request":"GraphQL Request",
|
"graphql_request":"GraphQL Request",
|
||||||
"graphql_response":"GraphQL Response",
|
"graphql_response":"GraphQL Response",
|
||||||
"logged_at": "logged at: ",
|
"logged_at": "Logged at",
|
||||||
"data": "Data",
|
"data": "Data",
|
||||||
"erros":"Errors",
|
"errors":"Errors",
|
||||||
|
"error_path": "Path",
|
||||||
|
"error_locations": "Locations",
|
||||||
|
"error_extensions": "Extensions",
|
||||||
"request_data": "Request data",
|
"request_data": "Request data",
|
||||||
"headers": "Headers",
|
"headers": "Headers",
|
||||||
"response_data": "Response data",
|
"response_data": "Response data",
|
||||||
"context": "Context",
|
"context": "Context",
|
||||||
"operation": "Operation",
|
"operation": "Operation",
|
||||||
|
"operation_type": "Operation type",
|
||||||
|
"operation_name": "Operation name",
|
||||||
"variables": "Variables"
|
"variables": "Variables"
|
||||||
},
|
},
|
||||||
"about_application_page": {
|
"about_application_page": {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||||
|
@ -17,9 +18,10 @@ class RequestLoggingLink extends Link {
|
||||||
]) async* {
|
]) async* {
|
||||||
_addConsoleLog(
|
_addConsoleLog(
|
||||||
GraphQlRequestConsoleLog(
|
GraphQlRequestConsoleLog(
|
||||||
|
// context: request.context,
|
||||||
|
operationType: request.type.name,
|
||||||
operation: request.operation,
|
operation: request.operation,
|
||||||
variables: request.variables,
|
variables: request.variables,
|
||||||
context: request.context,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
yield* forward!(request);
|
yield* forward!(request);
|
||||||
|
@ -32,9 +34,10 @@ class ResponseLoggingParser extends ResponseParser {
|
||||||
final response = super.parseResponse(body);
|
final response = super.parseResponse(body);
|
||||||
_addConsoleLog(
|
_addConsoleLog(
|
||||||
GraphQlResponseConsoleLog(
|
GraphQlResponseConsoleLog(
|
||||||
|
// context: response.context,
|
||||||
data: response.data,
|
data: response.data,
|
||||||
errors: response.errors,
|
errors: response.errors,
|
||||||
context: response.context,
|
rawResponse: jsonEncode(response.response),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
@ -68,10 +69,10 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
) async {
|
) async {
|
||||||
addConsoleLog(
|
addConsoleLog(
|
||||||
RestApiRequestConsoleLog(
|
RestApiRequestConsoleLog(
|
||||||
method: options.method,
|
|
||||||
data: options.data.toString(),
|
|
||||||
headers: options.headers,
|
|
||||||
uri: options.uri,
|
uri: options.uri,
|
||||||
|
method: options.method,
|
||||||
|
headers: options.headers,
|
||||||
|
data: jsonEncode(options.data),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return super.onRequest(options, handler);
|
return super.onRequest(options, handler);
|
||||||
|
@ -84,10 +85,10 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
) async {
|
) async {
|
||||||
addConsoleLog(
|
addConsoleLog(
|
||||||
RestApiResponseConsoleLog(
|
RestApiResponseConsoleLog(
|
||||||
|
uri: response.realUri,
|
||||||
method: response.requestOptions.method,
|
method: response.requestOptions.method,
|
||||||
statusCode: response.statusCode,
|
statusCode: response.statusCode,
|
||||||
data: response.data.toString(),
|
data: jsonEncode(response.data),
|
||||||
uri: response.realUri,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return super.onResponse(
|
return super.onResponse(
|
||||||
|
@ -103,12 +104,13 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
) async {
|
) async {
|
||||||
final Response? response = err.response;
|
final Response? response = err.response;
|
||||||
log(err.toString());
|
log(err.toString());
|
||||||
|
|
||||||
addConsoleLog(
|
addConsoleLog(
|
||||||
ManualConsoleLog.warning(
|
ManualConsoleLog.warning(
|
||||||
customTitle: 'RestAPI error',
|
customTitle: 'RestAPI error',
|
||||||
content: 'response-uri: ${response?.realUri}\n'
|
content: '"uri": "${response?.realUri}",\n'
|
||||||
'code: ${response?.statusCode}\n'
|
'"status_code": ${response?.statusCode},\n'
|
||||||
'data: ${response?.toString()}\n',
|
'"response": ${jsonEncode(response)}',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return super.onError(err, handler);
|
return super.onError(err, handler);
|
||||||
|
|
|
@ -530,7 +530,7 @@ class ServerInstallationRepository {
|
||||||
|
|
||||||
Future<void> deleteDomain() async {
|
Future<void> deleteDomain() async {
|
||||||
await box.delete(BNames.serverDomain);
|
await box.delete(BNames.serverDomain);
|
||||||
getIt<ApiConfigModel>().init();
|
await getIt<ApiConfigModel>().init();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveIsServerStarted(final bool value) async {
|
Future<void> saveIsServerStarted(final bool value) async {
|
||||||
|
@ -604,6 +604,6 @@ class ServerInstallationRepository {
|
||||||
BNames.hasFinalChecked,
|
BNames.hasFinalChecked,
|
||||||
BNames.isLoading,
|
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';
|
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||||
|
|
||||||
class ConsoleModel extends ChangeNotifier {
|
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> _logs = [];
|
||||||
|
final List<ConsoleLog> _incomingQueue = [];
|
||||||
|
|
||||||
|
bool _paused = false;
|
||||||
|
bool get paused => _paused;
|
||||||
List<ConsoleLog> get logs => _logs;
|
List<ConsoleLog> get logs => _logs;
|
||||||
|
|
||||||
void log(final ConsoleLog newLog) {
|
void log(final ConsoleLog newLog) {
|
||||||
logs.add(newLog);
|
if (paused) {
|
||||||
notifyListeners();
|
_incomingQueue.add(newLog);
|
||||||
// Make sure we don't have too many
|
if (_incomingQueue.length > incomingBufferBreakpoint) {
|
||||||
if (logs.length > 500) {
|
logs.removeRange(0, _incomingQueue.length - logBufferLimit);
|
||||||
logs.removeAt(0);
|
}
|
||||||
|
} 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 'dart:convert';
|
||||||
import 'package:graphql/client.dart';
|
|
||||||
|
import 'package:gql/language.dart' as gql;
|
||||||
|
import 'package:graphql/client.dart' as gql_client;
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
enum ConsoleLogSeverity {
|
enum ConsoleLogSeverity {
|
||||||
|
@ -12,7 +14,6 @@ enum ConsoleLogSeverity {
|
||||||
/// TODO(misterfourtytwo): should we add?
|
/// TODO(misterfourtytwo): should we add?
|
||||||
///
|
///
|
||||||
/// * equality override
|
/// * equality override
|
||||||
/// * translations of theese strings
|
|
||||||
sealed class ConsoleLog {
|
sealed class ConsoleLog {
|
||||||
ConsoleLog({
|
ConsoleLog({
|
||||||
final String? customTitle,
|
final String? customTitle,
|
||||||
|
@ -32,13 +33,23 @@ sealed class ConsoleLog {
|
||||||
String get content;
|
String get content;
|
||||||
|
|
||||||
/// data available for copy in dialog
|
/// data available for copy in dialog
|
||||||
String? get shareableData => '$title\n'
|
String? get shareableData => '{"title":"$title",\n'
|
||||||
'{\n$content\n}';
|
'"timestamp": "$fullUTCString",\n'
|
||||||
|
'"data":{\n$content\n}'
|
||||||
|
'\n}';
|
||||||
|
|
||||||
static final DateFormat _formatter = DateFormat('hh:mm:ss');
|
static final DateFormat _formatter = DateFormat('hh:mm:ss');
|
||||||
String get timeString => _formatter.format(time);
|
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 {
|
class ManualConsoleLog extends ConsoleLog {
|
||||||
ManualConsoleLog({
|
ManualConsoleLog({
|
||||||
required this.content,
|
required this.content,
|
||||||
|
@ -72,8 +83,10 @@ class RestApiRequestConsoleLog extends ConsoleLog {
|
||||||
@override
|
@override
|
||||||
String get title => 'Rest API Request';
|
String get title => 'Rest API Request';
|
||||||
@override
|
@override
|
||||||
String get content => 'method: $method\n'
|
String get content => '"method": "$method",\n'
|
||||||
'uri: $uri';
|
'"uri": "$uri",\n'
|
||||||
|
'"headers": ${jsonEncode(headers)},\n'
|
||||||
|
'"data": $data';
|
||||||
}
|
}
|
||||||
|
|
||||||
class RestApiResponseConsoleLog extends ConsoleLog {
|
class RestApiResponseConsoleLog extends ConsoleLog {
|
||||||
|
@ -93,49 +106,70 @@ class RestApiResponseConsoleLog extends ConsoleLog {
|
||||||
@override
|
@override
|
||||||
String get title => 'Rest API Response';
|
String get title => 'Rest API Response';
|
||||||
@override
|
@override
|
||||||
String get content => 'method: $method | status code: $statusCode\n'
|
String get content => '"method": "$method",\n'
|
||||||
'uri: $uri';
|
'"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 {
|
class GraphQlRequestConsoleLog extends ConsoleLog {
|
||||||
GraphQlRequestConsoleLog({
|
GraphQlRequestConsoleLog({
|
||||||
this.operation,
|
required this.operationType,
|
||||||
this.variables,
|
required this.operation,
|
||||||
this.context,
|
required this.variables,
|
||||||
|
// this.context,
|
||||||
super.severity,
|
super.severity,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Context? context;
|
// final gql_client.Context? context;
|
||||||
final Operation? operation;
|
final String operationType;
|
||||||
|
final gql_client.Operation? operation;
|
||||||
|
String get operationDocument =>
|
||||||
|
operation != null ? gql.printNode(operation!.document) : 'null';
|
||||||
final Map<String, dynamic>? variables;
|
final Map<String, dynamic>? variables;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get title => 'GraphQL Request';
|
String get title => 'GraphQL Request';
|
||||||
@override
|
@override
|
||||||
String get content => 'name: ${operation?.operationName}\n'
|
String get content =>
|
||||||
'document: ${operation?.document != null ? printNode(operation!.document) : null}';
|
// '"context": ${context?.encode},\n'
|
||||||
String get stringifiedOperation => operation == null
|
'"variables": ${jsonEncode(variables)},\n'
|
||||||
? 'null'
|
'"type": "$operationType",\n'
|
||||||
: 'Operation{\n'
|
'"name": "${operation?.operationName}",\n'
|
||||||
'\tname: ${operation?.operationName},\n'
|
'"document": ${jsonEncode(operationDocument)}';
|
||||||
'\tdocument: ${operation?.document != null ? printNode(operation!.document) : null}\n'
|
|
||||||
'}';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class GraphQlResponseConsoleLog extends ConsoleLog {
|
class GraphQlResponseConsoleLog extends ConsoleLog
|
||||||
|
implements LogWithRawResponse {
|
||||||
GraphQlResponseConsoleLog({
|
GraphQlResponseConsoleLog({
|
||||||
|
required this.rawResponse,
|
||||||
|
// this.context,
|
||||||
this.data,
|
this.data,
|
||||||
this.errors,
|
this.errors,
|
||||||
this.context,
|
|
||||||
super.severity,
|
super.severity,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Context? context;
|
@override
|
||||||
|
final String rawResponse;
|
||||||
|
// final gql_client.Context? context;
|
||||||
final Map<String, dynamic>? data;
|
final Map<String, dynamic>? data;
|
||||||
final List<GraphQLError>? errors;
|
final List<gql_client.GraphQLError>? errors;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get title => 'GraphQL Response';
|
String get title => 'GraphQL Response';
|
||||||
@override
|
@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) {
|
List<Widget> unwrapContent(final BuildContext context) => switch (this) {
|
||||||
(final RestApiRequestConsoleLog log) => [
|
(final RestApiRequestConsoleLog log) => [
|
||||||
if (log.method != null) _KeyValueRow('method', log.method),
|
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
|
// headers bloc
|
||||||
if (log.headers?.isNotEmpty ?? false)
|
if (log.headers?.isNotEmpty ?? false) ...[
|
||||||
const _SectionRow('console_page.headers'),
|
const _SectionRow('console_page.headers'),
|
||||||
...?log.headers?.entries
|
for (final entry in log.headers!.entries)
|
||||||
.map((final entry) => _KeyValueRow(entry.key, entry.value)),
|
_KeyValueRow(entry.key, '${entry.value}'),
|
||||||
|
],
|
||||||
|
|
||||||
// data bloc
|
// data
|
||||||
const _SectionRow('console_page.data'),
|
const _SectionRow('console_page.data'),
|
||||||
_DataRow(log.data?.toString()),
|
_DataRow('${log.data}'),
|
||||||
],
|
],
|
||||||
(final RestApiResponseConsoleLog log) => [
|
(final RestApiResponseConsoleLog log) => [
|
||||||
if (log.method != null) _KeyValueRow('method', log.method),
|
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}'),
|
||||||
if (log.statusCode != null)
|
if (log.statusCode != null)
|
||||||
_KeyValueRow('statusCode', log.statusCode.toString()),
|
_KeyValueRow('statusCode', '${log.statusCode}'),
|
||||||
|
|
||||||
// data bloc
|
// data
|
||||||
const _SectionRow('console_page.response_data'),
|
const _SectionRow('console_page.response_data'),
|
||||||
_DataRow(log.data?.toString()),
|
_DataRow('${log.data}'),
|
||||||
],
|
],
|
||||||
(final GraphQlRequestConsoleLog log) => [
|
(final GraphQlRequestConsoleLog log) => [
|
||||||
// context
|
// // context
|
||||||
const _SectionRow('console_page.context'),
|
// if (log.context != null) ...[
|
||||||
_DataRow(log.context?.toString()),
|
// const _SectionRow('console_page.context'),
|
||||||
// data
|
// _DataRow('${log.context}'),
|
||||||
if (log.operation != null)
|
// ],
|
||||||
const _SectionRow('console_page.operation'),
|
|
||||||
_DataRow(log.stringifiedOperation), // errors
|
const _SectionRow('console_page.operation'),
|
||||||
if (log.variables?.isNotEmpty ?? false)
|
if (log.operation != null) ...[
|
||||||
const _SectionRow('console_page.variables'),
|
_KeyValueRow(
|
||||||
...?log.variables?.entries.map(
|
'console_page.operation_type'.tr(),
|
||||||
(final entry) => _KeyValueRow(
|
log.operationType,
|
||||||
entry.key,
|
|
||||||
'${entry.value}',
|
|
||||||
),
|
),
|
||||||
),
|
_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) => [
|
(final GraphQlResponseConsoleLog log) => [
|
||||||
// context
|
// // context
|
||||||
const _SectionRow('console_page.context'),
|
// const _SectionRow('console_page.context'),
|
||||||
_DataRow(log.context?.toString()),
|
// _DataRow('${log.context}'),
|
||||||
// data
|
// data
|
||||||
if (log.data != null) const _SectionRow('console_page.data'),
|
if (log.data != null) ...[
|
||||||
...?log.data?.entries.map(
|
const _SectionRow('console_page.data'),
|
||||||
(final entry) => _KeyValueRow(
|
for (final entry in log.data!.entries)
|
||||||
entry.key,
|
_KeyValueRow(entry.key, '${entry.value}'),
|
||||||
'${entry.value}',
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
// errors
|
// errors
|
||||||
if (log.errors?.isNotEmpty ?? false)
|
if (log.errors?.isNotEmpty ?? false) ...[
|
||||||
const _SectionRow('console_page.errors'),
|
const _SectionRow('console_page.errors'),
|
||||||
...?log.errors?.map(
|
for (final entry in log.errors!) ...[
|
||||||
(final entry) => _KeyValueRow(
|
_KeyValueRow(
|
||||||
entry.message,
|
'${'console_page.error_message'.tr()}: ',
|
||||||
'${entry.locations}',
|
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) => [
|
(final ManualConsoleLog log) => [
|
||||||
_DataRow(log.content),
|
_DataRow(log.content),
|
||||||
|
@ -74,6 +100,7 @@ extension on ConsoleLog {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// dialog with detailed log content
|
||||||
class ConsoleItemDialog extends StatelessWidget {
|
class ConsoleItemDialog extends StatelessWidget {
|
||||||
const ConsoleItemDialog({
|
const ConsoleItemDialog({
|
||||||
required this.log,
|
required this.log,
|
||||||
|
@ -83,80 +110,66 @@ class ConsoleItemDialog extends StatelessWidget {
|
||||||
final ConsoleLog log;
|
final ConsoleLog log;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) {
|
Widget build(final BuildContext context) => AlertDialog(
|
||||||
final content = log.unwrapContent(context);
|
scrollable: true,
|
||||||
|
title: Text(log.title),
|
||||||
return AlertDialog(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
scrollable: true,
|
vertical: 16,
|
||||||
title: Text(log.title),
|
horizontal: 12,
|
||||||
content: Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
content: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
Row(
|
children: [
|
||||||
children: [
|
const Divider(),
|
||||||
Text('logged_at'.tr()),
|
Padding(
|
||||||
SelectableText(
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
log.timeString,
|
child: SelectableText.rich(
|
||||||
style: const TextStyle(
|
TextSpan(
|
||||||
fontWeight: FontWeight.w700,
|
style: DefaultTextStyle.of(context).style,
|
||||||
fontFeatures: [FontFeature.tabularFigures()],
|
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 {
|
/// different sections delimiter with `title`
|
||||||
const _DataRow(this.data);
|
|
||||||
final String? data;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) => SelectableText(
|
|
||||||
data ?? 'null',
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.w400),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SectionRow extends StatelessWidget {
|
class _SectionRow extends StatelessWidget {
|
||||||
const _SectionRow(this.title);
|
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;
|
final ConsoleLog log;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) => ListTile(
|
Widget build(final BuildContext context) => Padding(
|
||||||
dense: true,
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
title: SelectableText.rich(
|
child: ListTile(
|
||||||
TextSpan(
|
dense: true,
|
||||||
style: DefaultTextStyle.of(context).style,
|
title: Text.rich(
|
||||||
children: <TextSpan>[
|
TextSpan(
|
||||||
TextSpan(
|
style: DefaultTextStyle.of(context).style,
|
||||||
text: '${log.timeString}: ',
|
children: <TextSpan>[
|
||||||
style: const TextStyle(
|
TextSpan(
|
||||||
fontFeatures: [FontFeature.tabularFigures()],
|
text: '${log.timeString}: ',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFeatures: [FontFeature.tabularFigures()],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
TextSpan(
|
||||||
TextSpan(
|
text: log.title,
|
||||||
text: log.title,
|
style: const TextStyle(
|
||||||
style: const TextStyle(
|
fontWeight: FontWeight.bold,
|
||||||
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> {
|
class _ConsolePageState extends State<ConsolePage> {
|
||||||
|
ConsoleModel get console => getIt<ConsoleModel>();
|
||||||
|
|
||||||
/// should freeze logs state to properly read logs
|
/// should freeze logs state to properly read logs
|
||||||
bool paused = false;
|
|
||||||
late final Future<void> future;
|
late final Future<void> future;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -25,12 +26,12 @@ class _ConsolePageState extends State<ConsolePage> {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
future = getIt.allReady();
|
future = getIt.allReady();
|
||||||
getIt<ConsoleModel>().addListener(update);
|
console.addListener(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
getIt<ConsoleModel>().removeListener(update);
|
console.removeListener(update);
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
@ -40,17 +41,12 @@ class _ConsolePageState extends State<ConsolePage> {
|
||||||
/// unmounted or during frame build, adding as postframe callback ensures
|
/// unmounted or during frame build, adding as postframe callback ensures
|
||||||
/// that element is marked for rebuild
|
/// that element is marked for rebuild
|
||||||
WidgetsBinding.instance.addPostFrameCallback((final _) {
|
WidgetsBinding.instance.addPostFrameCallback((final _) {
|
||||||
if (!paused && mounted) {
|
if (mounted) {
|
||||||
setState(() => {});
|
setState(() => {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void togglePause() {
|
|
||||||
paused ^= true;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) => SafeArea(
|
Widget build(final BuildContext context) => SafeArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
@ -63,34 +59,31 @@ class _ConsolePageState extends State<ConsolePage> {
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
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(
|
body: Scrollbar(
|
||||||
child: Scrollbar(
|
child: FutureBuilder(
|
||||||
child: FutureBuilder(
|
future: future,
|
||||||
future: future,
|
builder: (
|
||||||
builder: (
|
final BuildContext context,
|
||||||
final BuildContext context,
|
final AsyncSnapshot<void> snapshot,
|
||||||
final AsyncSnapshot<void> snapshot,
|
) {
|
||||||
) {
|
if (snapshot.hasData) {
|
||||||
if (snapshot.hasData) {
|
final List<ConsoleLog> logs = console.logs;
|
||||||
final List<ConsoleLog> logs =
|
|
||||||
getIt.get<ConsoleModel>().logs;
|
|
||||||
|
|
||||||
return logs.isEmpty
|
return logs.isEmpty
|
||||||
? const _ConsoleViewEmpty()
|
? const _ConsoleViewEmpty()
|
||||||
: _ConsoleViewLoaded(
|
: _ConsoleViewLoaded(logs: logs);
|
||||||
logs: logs,
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const _ConsoleViewLoading();
|
return const _ConsoleViewLoading();
|
||||||
},
|
},
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -135,7 +128,7 @@ class _ConsoleViewLoaded extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) => ListView.separated(
|
Widget build(final BuildContext context) => ListView.separated(
|
||||||
primary: true,
|
primary: true,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
itemCount: logs.length,
|
itemCount: logs.length,
|
||||||
itemBuilder: (final BuildContext context, final int index) {
|
itemBuilder: (final BuildContext context, final int index) {
|
||||||
final log = logs[logs.length - 1 - index];
|
final log = logs[logs.length - 1 - index];
|
||||||
|
|
Loading…
Reference in a new issue