mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-01-08 17:11:14 +00:00
feat: console log feature refactor.
listing scroll performance fix, uniform code and widget UI for different log item types, dialog data can now be selected & copy-pasted
This commit is contained in:
parent
22fbbb051e
commit
00545c34b4
|
@ -1,12 +1,12 @@
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:selfprivacy/logic/get_it/api_config.dart';
|
import 'package:selfprivacy/logic/get_it/api_config.dart';
|
||||||
import 'package:selfprivacy/logic/get_it/api_connection_repository.dart';
|
import 'package:selfprivacy/logic/get_it/api_connection_repository.dart';
|
||||||
import 'package:selfprivacy/logic/get_it/console.dart';
|
import 'package:selfprivacy/logic/get_it/console_model.dart';
|
||||||
import 'package:selfprivacy/logic/get_it/navigation.dart';
|
import 'package:selfprivacy/logic/get_it/navigation.dart';
|
||||||
|
|
||||||
export 'package:selfprivacy/logic/get_it/api_config.dart';
|
export 'package:selfprivacy/logic/get_it/api_config.dart';
|
||||||
export 'package:selfprivacy/logic/get_it/api_connection_repository.dart';
|
export 'package:selfprivacy/logic/get_it/api_connection_repository.dart';
|
||||||
export 'package:selfprivacy/logic/get_it/console.dart';
|
export 'package:selfprivacy/logic/get_it/console_model.dart';
|
||||||
export 'package:selfprivacy/logic/get_it/navigation.dart';
|
export 'package:selfprivacy/logic/get_it/navigation.dart';
|
||||||
|
|
||||||
final GetIt getIt = GetIt.instance;
|
final GetIt getIt = GetIt.instance;
|
||||||
|
|
|
@ -4,15 +4,10 @@ import 'package:graphql_flutter/graphql_flutter.dart';
|
||||||
import 'package:http/io_client.dart';
|
import 'package:http/io_client.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
|
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
|
||||||
import 'package:selfprivacy/logic/models/message.dart';
|
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||||
|
|
||||||
void _logToAppConsole<T>(final T objectToLog) {
|
void _addConsoleLog(final ConsoleLog message) =>
|
||||||
getIt.get<ConsoleModel>().addMessage(
|
getIt.get<ConsoleModel>().log(message);
|
||||||
Message(
|
|
||||||
text: objectToLog.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RequestLoggingLink extends Link {
|
class RequestLoggingLink extends Link {
|
||||||
@override
|
@override
|
||||||
|
@ -20,13 +15,13 @@ class RequestLoggingLink extends Link {
|
||||||
final Request request, [
|
final Request request, [
|
||||||
final NextLink? forward,
|
final NextLink? forward,
|
||||||
]) async* {
|
]) async* {
|
||||||
getIt.get<ConsoleModel>().addMessage(
|
_addConsoleLog(
|
||||||
GraphQlRequestMessage(
|
GraphQlRequestConsoleLog(
|
||||||
operation: request.operation,
|
operation: request.operation,
|
||||||
variables: request.variables,
|
variables: request.variables,
|
||||||
context: request.context,
|
context: request.context,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
yield* forward!(request);
|
yield* forward!(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,20 +30,25 @@ 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);
|
||||||
getIt.get<ConsoleModel>().addMessage(
|
_addConsoleLog(
|
||||||
GraphQlResponseMessage(
|
GraphQlResponseConsoleLog(
|
||||||
data: response.data,
|
data: response.data,
|
||||||
errors: response.errors,
|
errors: response.errors,
|
||||||
context: response.context,
|
context: response.context,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
GraphQLError parseError(final Map<String, dynamic> error) {
|
GraphQLError parseError(final Map<String, dynamic> error) {
|
||||||
final graphQlError = super.parseError(error);
|
final graphQlError = super.parseError(error);
|
||||||
_logToAppConsole(graphQlError);
|
_addConsoleLog(
|
||||||
|
ManualConsoleLog.warning(
|
||||||
|
customTitle: 'GraphQL Error',
|
||||||
|
content: graphQlError.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
return graphQlError;
|
return graphQlError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import 'package:dio/dio.dart';
|
||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
import 'package:pretty_dio_logger/pretty_dio_logger.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/console_log.dart';
|
||||||
|
|
||||||
abstract class RestApiMap {
|
abstract class RestApiMap {
|
||||||
Future<Dio> getClient({final BaseOptions? customOptions}) async {
|
Future<Dio> getClient({final BaseOptions? customOptions}) async {
|
||||||
|
@ -57,8 +57,8 @@ abstract class RestApiMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConsoleInterceptor extends InterceptorsWrapper {
|
class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
void addMessage(final Message message) {
|
void addConsoleLog(final ConsoleLog message) {
|
||||||
getIt.get<ConsoleModel>().addMessage(message);
|
getIt.get<ConsoleModel>().log(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -66,8 +66,8 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
final RequestOptions options,
|
final RequestOptions options,
|
||||||
final RequestInterceptorHandler handler,
|
final RequestInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
addMessage(
|
addConsoleLog(
|
||||||
RestApiRequestMessage(
|
RestApiRequestConsoleLog(
|
||||||
method: options.method,
|
method: options.method,
|
||||||
data: options.data.toString(),
|
data: options.data.toString(),
|
||||||
headers: options.headers,
|
headers: options.headers,
|
||||||
|
@ -82,8 +82,8 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
final Response response,
|
final Response response,
|
||||||
final ResponseInterceptorHandler handler,
|
final ResponseInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
addMessage(
|
addConsoleLog(
|
||||||
RestApiResponseMessage(
|
RestApiResponseConsoleLog(
|
||||||
method: response.requestOptions.method,
|
method: response.requestOptions.method,
|
||||||
statusCode: response.statusCode,
|
statusCode: response.statusCode,
|
||||||
data: response.data.toString(),
|
data: response.data.toString(),
|
||||||
|
@ -103,10 +103,12 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
) async {
|
) async {
|
||||||
final Response? response = err.response;
|
final Response? response = err.response;
|
||||||
log(err.toString());
|
log(err.toString());
|
||||||
addMessage(
|
addConsoleLog(
|
||||||
Message.warn(
|
ManualConsoleLog.warning(
|
||||||
text:
|
customTitle: 'RestAPI error',
|
||||||
'response-uri: ${response?.realUri}\ncode: ${response?.statusCode}\ndata: ${response?.toString()}\n',
|
content: 'response-uri: ${response?.realUri}\n'
|
||||||
|
'code: ${response?.statusCode}\n'
|
||||||
|
'data: ${response?.toString()}\n',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return super.onError(err, handler);
|
return super.onError(err, handler);
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/message.dart';
|
|
||||||
|
|
||||||
class ConsoleModel extends ChangeNotifier {
|
|
||||||
final List<Message> _messages = [];
|
|
||||||
|
|
||||||
List<Message> get messages => _messages;
|
|
||||||
|
|
||||||
void addMessage(final Message message) {
|
|
||||||
messages.add(message);
|
|
||||||
notifyListeners();
|
|
||||||
// Make sure we don't have too many messages
|
|
||||||
if (messages.length > 500) {
|
|
||||||
messages.removeAt(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
16
lib/logic/get_it/console_model.dart
Normal file
16
lib/logic/get_it/console_model.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||||
|
|
||||||
|
class ConsoleModel extends ChangeNotifier {
|
||||||
|
final List<ConsoleLog> _logs = [];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
141
lib/logic/models/console_log.dart
Normal file
141
lib/logic/models/console_log.dart
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import 'package:gql/language.dart';
|
||||||
|
import 'package:graphql/client.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
enum ConsoleLogSeverity {
|
||||||
|
normal,
|
||||||
|
warning,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Base entity for console logs.
|
||||||
|
///
|
||||||
|
/// TODO(misterfourtytwo): should we add?
|
||||||
|
///
|
||||||
|
/// * equality override
|
||||||
|
/// * translations of theese strings
|
||||||
|
sealed class ConsoleLog {
|
||||||
|
ConsoleLog({
|
||||||
|
final String? customTitle,
|
||||||
|
this.severity = ConsoleLogSeverity.normal,
|
||||||
|
}) : title = customTitle ??
|
||||||
|
(severity == ConsoleLogSeverity.warning ? 'Error' : 'Log'),
|
||||||
|
time = DateTime.now();
|
||||||
|
|
||||||
|
final DateTime time;
|
||||||
|
final ConsoleLogSeverity severity;
|
||||||
|
bool get isError => severity == ConsoleLogSeverity.warning;
|
||||||
|
|
||||||
|
/// title for both in listing and in dialog
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// formatted data to be shown in listing
|
||||||
|
String get content;
|
||||||
|
|
||||||
|
/// data available for copy in dialog
|
||||||
|
String? get shareableData => '$title\n'
|
||||||
|
'$content';
|
||||||
|
|
||||||
|
static final DateFormat _formatter = DateFormat('hh:mm:ss');
|
||||||
|
String get timeString => _formatter.format(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ManualConsoleLog extends ConsoleLog {
|
||||||
|
ManualConsoleLog({
|
||||||
|
required this.content,
|
||||||
|
super.customTitle,
|
||||||
|
super.severity,
|
||||||
|
});
|
||||||
|
|
||||||
|
ManualConsoleLog.warning({
|
||||||
|
required this.content,
|
||||||
|
super.customTitle,
|
||||||
|
}) : super(severity: ConsoleLogSeverity.warning);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String content;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RestApiRequestConsoleLog extends ConsoleLog {
|
||||||
|
RestApiRequestConsoleLog({
|
||||||
|
this.method,
|
||||||
|
this.uri,
|
||||||
|
this.headers,
|
||||||
|
this.data,
|
||||||
|
super.severity,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? method;
|
||||||
|
final Uri? uri;
|
||||||
|
final Map<String, dynamic>? headers;
|
||||||
|
final String? data;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => 'Rest API Request';
|
||||||
|
@override
|
||||||
|
String get content => 'method: $method\n'
|
||||||
|
'uri: $uri';
|
||||||
|
}
|
||||||
|
|
||||||
|
class RestApiResponseConsoleLog extends ConsoleLog {
|
||||||
|
RestApiResponseConsoleLog({
|
||||||
|
this.method,
|
||||||
|
this.uri,
|
||||||
|
this.statusCode,
|
||||||
|
this.data,
|
||||||
|
super.severity,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? method;
|
||||||
|
final Uri? uri;
|
||||||
|
final int? statusCode;
|
||||||
|
final String? data;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => 'Rest API Response';
|
||||||
|
@override
|
||||||
|
String get content => 'method: $method | status code: $statusCode\n'
|
||||||
|
'uri: $uri';
|
||||||
|
}
|
||||||
|
|
||||||
|
class GraphQlRequestConsoleLog extends ConsoleLog {
|
||||||
|
GraphQlRequestConsoleLog({
|
||||||
|
this.operation,
|
||||||
|
this.variables,
|
||||||
|
this.context,
|
||||||
|
super.severity,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Context? context;
|
||||||
|
final Operation? operation;
|
||||||
|
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'
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
class GraphQlResponseConsoleLog extends ConsoleLog {
|
||||||
|
GraphQlResponseConsoleLog({
|
||||||
|
this.data,
|
||||||
|
this.errors,
|
||||||
|
this.context,
|
||||||
|
super.severity,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Context? context;
|
||||||
|
final Map<String, dynamic>? data;
|
||||||
|
final List<GraphQLError>? errors;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => 'GraphQL Response';
|
||||||
|
@override
|
||||||
|
String get content => 'data: $data';
|
||||||
|
}
|
|
@ -1,75 +0,0 @@
|
||||||
import 'package:graphql/client.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
/// TODO(misterfourtytwo): add equality override
|
|
||||||
class Message {
|
|
||||||
Message({this.text, this.severity = MessageSeverity.normal})
|
|
||||||
: time = DateTime.now();
|
|
||||||
Message.warn({this.text})
|
|
||||||
: severity = MessageSeverity.warning,
|
|
||||||
time = DateTime.now();
|
|
||||||
|
|
||||||
final String? text;
|
|
||||||
final DateTime time;
|
|
||||||
final MessageSeverity severity;
|
|
||||||
|
|
||||||
static final DateFormat _formatter = DateFormat('hh:mm');
|
|
||||||
String get timeString => _formatter.format(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
|
@ -1,304 +0,0 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/message.dart';
|
|
||||||
import 'package:selfprivacy/utils/platform_adapter.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
|
|
||||||
if (message.text != null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
PlatformAdapter.setClipboard(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
|
|
||||||
if (message.text != null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
PlatformAdapter.setClipboard(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
|
|
||||||
if (message.text != null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
PlatformAdapter.setClipboard(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
|
|
||||||
if (message.text != null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
PlatformAdapter.setClipboard(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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/message.dart';
|
|
||||||
import 'package:selfprivacy/ui/components/list_tiles/log_list_tile.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
|
||||||
class ConsolePage extends StatefulWidget {
|
|
||||||
const ConsolePage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ConsolePage> createState() => _ConsolePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConsolePageState extends State<ConsolePage> {
|
|
||||||
bool paused = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
getIt<ConsoleModel>().addListener(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
getIt<ConsoleModel>().removeListener(update);
|
|
||||||
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void update() {
|
|
||||||
/// listener update could come at any time, like when widget is already
|
|
||||||
/// unmounted or during frame build, adding as postframe callback ensures
|
|
||||||
/// that element is marked for rebuild
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((final _) {
|
|
||||||
if (!paused && mounted) {
|
|
||||||
setState(() => {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void togglePause() {
|
|
||||||
paused ^= true;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) => SafeArea(
|
|
||||||
child: Scaffold(
|
|
||||||
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: togglePause,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: FutureBuilder(
|
|
||||||
future: getIt.allReady(),
|
|
||||||
builder: (
|
|
||||||
final BuildContext context,
|
|
||||||
final AsyncSnapshot<void> snapshot,
|
|
||||||
) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
final List<Message> messages =
|
|
||||||
getIt.get<ConsoleModel>().messages;
|
|
||||||
|
|
||||||
return ListView(
|
|
||||||
reverse: true,
|
|
||||||
shrinkWrap: true,
|
|
||||||
children: [
|
|
||||||
const Gap(20),
|
|
||||||
...messages.reversed.map(
|
|
||||||
(final message) => LogListItem(
|
|
||||||
key: ValueKey(message),
|
|
||||||
message: message,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Text('console_page.waiting'.tr()),
|
|
||||||
const Gap(16),
|
|
||||||
const CircularProgressIndicator.adaptive(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
186
lib/ui/pages/more/console/console_log_item_dialog.dart
Normal file
186
lib/ui/pages/more/console/console_log_item_dialog.dart
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||||
|
import 'package:selfprivacy/utils/platform_adapter.dart';
|
||||||
|
|
||||||
|
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()),
|
||||||
|
|
||||||
|
// headers bloc
|
||||||
|
if (log.headers?.isNotEmpty ?? false) const _SectionRow('Headers'),
|
||||||
|
...?log.headers?.entries
|
||||||
|
.map((final entry) => _KeyValueRow(entry.key, entry.value)),
|
||||||
|
|
||||||
|
// data bloc
|
||||||
|
const _SectionRow('Request data'),
|
||||||
|
_DataRow(log.data?.toString()),
|
||||||
|
],
|
||||||
|
(final RestApiResponseConsoleLog log) => [
|
||||||
|
if (log.method != null) _KeyValueRow('method', log.method),
|
||||||
|
if (log.uri != null) _KeyValueRow('uri', log.uri.toString()),
|
||||||
|
if (log.statusCode != null)
|
||||||
|
_KeyValueRow(
|
||||||
|
'statusCode',
|
||||||
|
log.statusCode.toString(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// data bloc
|
||||||
|
const _SectionRow('Response data'),
|
||||||
|
_DataRow(log.data?.toString()),
|
||||||
|
],
|
||||||
|
(final GraphQlRequestConsoleLog log) => [
|
||||||
|
// context
|
||||||
|
const _SectionRow('Context'),
|
||||||
|
_DataRow(log.context?.toString()),
|
||||||
|
// data
|
||||||
|
if (log.operation != null) const _SectionRow('Operation'),
|
||||||
|
_DataRow(log.stringifiedOperation), // errors
|
||||||
|
if (log.variables?.isNotEmpty ?? false)
|
||||||
|
const _SectionRow('Variables'),
|
||||||
|
...?log.variables?.entries.map(
|
||||||
|
(final entry) => _KeyValueRow(
|
||||||
|
entry.key,
|
||||||
|
'${entry.value}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
(final GraphQlResponseConsoleLog log) => [
|
||||||
|
// context
|
||||||
|
const _SectionRow('Context'),
|
||||||
|
_DataRow(log.context?.toString()),
|
||||||
|
// data
|
||||||
|
if (log.data != null) const _SectionRow('Data'),
|
||||||
|
...?log.data?.entries.map(
|
||||||
|
(final entry) => _KeyValueRow(
|
||||||
|
entry.key,
|
||||||
|
'${entry.value}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// errors
|
||||||
|
if (log.errors?.isNotEmpty ?? false) const _SectionRow('Errors'),
|
||||||
|
...?log.errors?.map(
|
||||||
|
(final entry) => _KeyValueRow(
|
||||||
|
entry.message,
|
||||||
|
'${entry.locations}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
(final ManualConsoleLog log) => [
|
||||||
|
_DataRow(log.content),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConsoleItemDialog extends StatelessWidget {
|
||||||
|
const ConsoleItemDialog({
|
||||||
|
required this.log,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
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: [
|
||||||
|
/// TODO(misterfourtytwo): maybe should add translations later
|
||||||
|
const Text('logged at: '),
|
||||||
|
SelectableText(
|
||||||
|
log.timeString,
|
||||||
|
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.content),
|
||||||
|
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 ?? ''),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SectionRow extends StatelessWidget {
|
||||||
|
const _SectionRow(this.title);
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 2.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SelectableText(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
71
lib/ui/pages/more/console/console_log_item_widget.dart
Normal file
71
lib/ui/pages/more/console/console_log_item_widget.dart
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/more/console/console_log_item_dialog.dart';
|
||||||
|
|
||||||
|
extension on ConsoleLog {
|
||||||
|
Color resolveColor(final BuildContext context) => isError
|
||||||
|
? Theme.of(context).colorScheme.error
|
||||||
|
: switch (this) {
|
||||||
|
(final RestApiRequestConsoleLog _) =>
|
||||||
|
Theme.of(context).colorScheme.secondary,
|
||||||
|
(final RestApiResponseConsoleLog _) =>
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
(final GraphQlRequestConsoleLog _) =>
|
||||||
|
Theme.of(context).colorScheme.secondary,
|
||||||
|
(final GraphQlResponseConsoleLog _) =>
|
||||||
|
Theme.of(context).colorScheme.tertiary,
|
||||||
|
(final ManualConsoleLog _) => Theme.of(context).colorScheme.tertiary,
|
||||||
|
};
|
||||||
|
|
||||||
|
IconData resolveIcon() => switch (this) {
|
||||||
|
(final RestApiRequestConsoleLog _) => Icons.upload_outlined,
|
||||||
|
(final RestApiResponseConsoleLog _) => Icons.download_outlined,
|
||||||
|
(final GraphQlRequestConsoleLog _) => Icons.arrow_circle_up_outlined,
|
||||||
|
(final GraphQlResponseConsoleLog _) => Icons.arrow_circle_down_outlined,
|
||||||
|
(final ManualConsoleLog _) => Icons.read_more_outlined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConsoleLogItemWidget extends StatelessWidget {
|
||||||
|
const ConsoleLogItemWidget({
|
||||||
|
required this.log,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
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()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
138
lib/ui/pages/more/console/console_page.dart
Normal file
138
lib/ui/pages/more/console/console_page.dart
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/more/console/console_log_item_widget.dart';
|
||||||
|
|
||||||
|
/// listing with 500 latest app operations.
|
||||||
|
@RoutePage()
|
||||||
|
class ConsolePage extends StatefulWidget {
|
||||||
|
const ConsolePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ConsolePage> createState() => _ConsolePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConsolePageState extends State<ConsolePage> {
|
||||||
|
/// should freeze logs state to properly read logs
|
||||||
|
bool paused = false;
|
||||||
|
late final Future<void> future;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
future = getIt.allReady();
|
||||||
|
getIt<ConsoleModel>().addListener(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
getIt<ConsoleModel>().removeListener(update);
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
/// listener update could come at any time, like when widget is already
|
||||||
|
/// unmounted or during frame build, adding as postframe callback ensures
|
||||||
|
/// that element is marked for rebuild
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((final _) {
|
||||||
|
if (!paused && mounted) {
|
||||||
|
setState(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void togglePause() {
|
||||||
|
paused ^= true;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => SafeArea(
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('console_page.title'.tr()),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.of(context).maybePop(),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
paused ? Icons.play_arrow_outlined : Icons.pause_outlined,
|
||||||
|
),
|
||||||
|
onPressed: togglePause,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SelectionArea(
|
||||||
|
child: Scrollbar(
|
||||||
|
child: FutureBuilder(
|
||||||
|
future: future,
|
||||||
|
builder: (
|
||||||
|
final BuildContext context,
|
||||||
|
final AsyncSnapshot<void> snapshot,
|
||||||
|
) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final List<ConsoleLog> messages =
|
||||||
|
getIt.get<ConsoleModel>().logs;
|
||||||
|
|
||||||
|
return _ConsoleViewLoaded(
|
||||||
|
logs: messages,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const _ConsoleViewLoading();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConsoleViewLoading extends StatelessWidget {
|
||||||
|
const _ConsoleViewLoading();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text('console_page.waiting'.tr()),
|
||||||
|
const Gap(16),
|
||||||
|
const Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConsoleViewLoaded extends StatelessWidget {
|
||||||
|
const _ConsoleViewLoaded({required this.logs});
|
||||||
|
|
||||||
|
final List<ConsoleLog> logs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => ListView.separated(
|
||||||
|
primary: true,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
itemCount: logs.length,
|
||||||
|
itemBuilder: (final BuildContext context, final int index) {
|
||||||
|
final log = logs[logs.length - 1 - index];
|
||||||
|
|
||||||
|
return ConsoleLogItemWidget(
|
||||||
|
key: ValueKey(log),
|
||||||
|
log: log,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (final context, final _) => const Divider(),
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import 'package:selfprivacy/ui/pages/dns_details/dns_details.dart';
|
||||||
import 'package:selfprivacy/ui/pages/more/about_application.dart';
|
import 'package:selfprivacy/ui/pages/more/about_application.dart';
|
||||||
import 'package:selfprivacy/ui/pages/more/app_settings/app_settings.dart';
|
import 'package:selfprivacy/ui/pages/more/app_settings/app_settings.dart';
|
||||||
import 'package:selfprivacy/ui/pages/more/app_settings/developer_settings.dart';
|
import 'package:selfprivacy/ui/pages/more/app_settings/developer_settings.dart';
|
||||||
import 'package:selfprivacy/ui/pages/more/console.dart';
|
import 'package:selfprivacy/ui/pages/more/console/console_page.dart';
|
||||||
import 'package:selfprivacy/ui/pages/more/more.dart';
|
import 'package:selfprivacy/ui/pages/more/more.dart';
|
||||||
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
|
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
|
||||||
import 'package:selfprivacy/ui/pages/providers/providers.dart';
|
import 'package:selfprivacy/ui/pages/providers/providers.dart';
|
||||||
|
@ -53,6 +53,7 @@ class RootRouter extends _$RootRouter {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RouteType get defaultRouteType => const RouteType.material();
|
RouteType get defaultRouteType => const RouteType.material();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final List<AutoRoute> routes = [
|
final List<AutoRoute> routes = [
|
||||||
AutoRoute(page: OnboardingRoute.page),
|
AutoRoute(page: OnboardingRoute.page),
|
||||||
|
|
Loading…
Reference in a new issue