diff --git a/docs/install.md b/docs/install.md index 6320c217..f177b3d3 100644 --- a/docs/install.md +++ b/docs/install.md @@ -4,7 +4,7 @@ ```yaml dependencies: - alice: ^1.0.0-dev6 + alice: ^1.0.0-dev7 ``` 2. Choose adapter based on your HTTP client. **pubspec.yaml** file: diff --git a/examples/alice_http/lib/main.dart b/examples/alice_http/lib/main.dart index 0b53deda..3059cc06 100644 --- a/examples/alice_http/lib/main.dart +++ b/examples/alice_http/lib/main.dart @@ -39,7 +39,7 @@ class _MyAppState extends State { const Text( style: TextStyle(fontSize: 14), 'Welcome to example of Alice Http Inspector. ' - 'Click buttons below to generate sample data.', + 'Click buttons below to generate sample data.', ), ElevatedButton( onPressed: _runHttpHttpRequests, diff --git a/packages/alice/CHANGELOG.md b/packages/alice/CHANGELOG.md index 51db3a64..29529b2d 100644 --- a/packages/alice/CHANGELOG.md +++ b/packages/alice/CHANGELOG.md @@ -1,3 +1,6 @@ +# 1.0.0-dev-7 +* Refactored UI code. + # 1.0.0-dev.6 * [BREAKING_CHANGE] Bumped minimal Flutter version to 3.10.0. diff --git a/packages/alice/lib/alice.dart b/packages/alice/lib/alice.dart index d32ccd6d..cb9881ce 100644 --- a/packages/alice/lib/alice.dart +++ b/packages/alice/lib/alice.dart @@ -94,6 +94,7 @@ class Alice { return _aliceCore.isInspectorOpened(); } + /// Adds new adapter to Alice. void addAdapter(AliceAdapter adapter) { adapter.injectCore(_aliceCore); } diff --git a/packages/alice/lib/core/alice_adapter.dart b/packages/alice/lib/core/alice_adapter.dart index c0b59452..d0f5604c 100644 --- a/packages/alice/lib/core/alice_adapter.dart +++ b/packages/alice/lib/core/alice_adapter.dart @@ -1,5 +1,6 @@ import 'package:alice/core/alice_core.dart'; +/// Adapter mixin which is used in http client adapters. mixin AliceAdapter { late final AliceCore aliceCore; diff --git a/packages/alice/lib/core/alice_core.dart b/packages/alice/lib/core/alice_core.dart index cf44bd89..12c88eee 100644 --- a/packages/alice/lib/core/alice_core.dart +++ b/packages/alice/lib/core/alice_core.dart @@ -8,7 +8,7 @@ import 'package:alice/model/alice_http_call.dart'; import 'package:alice/model/alice_http_error.dart'; import 'package:alice/model/alice_http_response.dart'; import 'package:alice/model/alice_log.dart'; -import 'package:alice/ui/page/alice_calls_list_screen.dart'; +import 'package:alice/ui/common/alice_navigation.dart'; import 'package:alice/utils/num_comparison.dart'; import 'package:alice/utils/shake_detector.dart'; import 'package:collection/collection.dart' show IterableExtension; @@ -17,7 +17,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:rxdart/rxdart.dart'; class AliceCore { - /// Should user be notified with notification if there's new request catched + /// Should user be notified with notification if there's new request caught /// by Alice final bool showNotification; @@ -136,12 +136,8 @@ class AliceCore { } if (!_isInspectorOpened) { _isInspectorOpened = true; - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => AliceCallsListScreen(this, _aliceLogger), - ), - ).then((_) => _isInspectorOpened = false); + AliceNavigation.navigateToCallsList(core: this, logger: _aliceLogger) + .then((_) => _isInspectorOpened = false); } } diff --git a/packages/alice/lib/core/alice_logger.dart b/packages/alice/lib/core/alice_logger.dart index 286b5647..fbcd7185 100644 --- a/packages/alice/lib/core/alice_logger.dart +++ b/packages/alice/lib/core/alice_logger.dart @@ -4,6 +4,7 @@ import 'package:alice/model/alice_log.dart'; import 'package:alice/utils/num_comparison.dart'; import 'package:flutter/foundation.dart'; +/// Logger used to handle logs from application. class AliceLogger { AliceLogger({int? maximumSize = 1000}) : _maximumSize = maximumSize; @@ -63,8 +64,10 @@ class AliceLogger { ]; } - void clear() => _logs.value.clear(); + /// Clears all logs. + void clearLogs() => _logs.value.clear(); + /// Returns raw logs from Android via ADB. Future getAndroidRawLogs() async { if (Platform.isAndroid) { final ProcessResult process = @@ -74,11 +77,10 @@ class AliceLogger { return ''; } + /// Clears all raw logs. Future clearAndroidRawLogs() async { if (Platform.isAndroid) { await Process.run('logcat', ['-c']); } } - - void clearLogs() => logs.clear(); } diff --git a/packages/alice/lib/helper/alice_conversion_helper.dart b/packages/alice/lib/helper/alice_conversion_helper.dart index 4cb043c5..0b80608c 100644 --- a/packages/alice/lib/helper/alice_conversion_helper.dart +++ b/packages/alice/lib/helper/alice_conversion_helper.dart @@ -1,3 +1,4 @@ +/// Helper used in unit conversion. class AliceConversionHelper { static const int _kilobyteAsByte = 1000; static const int _megabyteAsByte = 1000000; @@ -13,6 +14,7 @@ class AliceConversionHelper { _ => '${_formatDouble(bytes / _megabyteAsByte)} MB', }; + /// Formats double with two numbers after dot. static String _formatDouble(double value) => value.toStringAsFixed(2); /// Format time in milliseconds diff --git a/packages/alice/lib/helper/alice_save_helper.dart b/packages/alice/lib/helper/alice_save_helper.dart index aef2cc9f..23026f72 100644 --- a/packages/alice/lib/helper/alice_save_helper.dart +++ b/packages/alice/lib/helper/alice_save_helper.dart @@ -2,10 +2,10 @@ import 'dart:convert' show JsonEncoder; import 'dart:io' show Directory, File, FileMode, IOSink, Platform; import 'package:alice/core/alice_utils.dart'; -import 'package:alice/helper/alice_alert_helper.dart'; import 'package:alice/helper/alice_conversion_helper.dart'; import 'package:alice/helper/operating_system.dart'; import 'package:alice/model/alice_http_call.dart'; +import 'package:alice/ui/common/alice_dialog.dart'; import 'package:alice/utils/alice_parser.dart'; import 'package:flutter/material.dart'; import 'package:open_filex/open_filex.dart'; @@ -58,10 +58,10 @@ class AliceSaveHelper { if (status) { await _saveToFile(context, calls); } else { - AliceAlertHelper.showAlert( - context, - 'Permission error', - "Permission not granted. Couldn't save logs.", + AliceGeneralDialog.show( + context: context, + title: 'Permission error', + description: "Permission not granted. Couldn't save logs.", ); } } @@ -73,10 +73,10 @@ class AliceSaveHelper { ) async { try { if (calls.isEmpty) { - AliceAlertHelper.showAlert( - context, - 'Error', - 'There are no logs to save', + AliceGeneralDialog.show( + context: context, + title: 'Error', + description: 'There are no logs to save', ); return ''; } @@ -100,10 +100,10 @@ class AliceSaveHelper { await sink.close(); if (context.mounted) { - AliceAlertHelper.showAlert( - context, - 'Success', - 'Successfully saved logs in ${file.path}', + AliceGeneralDialog.show( + context: context, + title: 'Success', + description: 'Successfully saved logs in ${file.path}', secondButtonTitle: Platform.isAndroid ? 'View file' : null, secondButtonAction: () => Platform.isAndroid ? OpenFilex.open(file.path) : null, @@ -113,19 +113,19 @@ class AliceSaveHelper { return file.path; } else { if (context.mounted) { - AliceAlertHelper.showAlert( - context, - 'Error', - 'Failed to save http calls to file', + AliceGeneralDialog.show( + context: context, + title: 'Error', + description: 'Failed to save http calls to file', ); } } } catch (exception) { if (context.mounted) { - AliceAlertHelper.showAlert( - context, - 'Error', - 'Failed to save http calls to file', + AliceGeneralDialog.show( + context: context, + title: 'Error', + description: 'Failed to save http calls to file', ); AliceUtils.log(exception.toString()); } @@ -179,7 +179,7 @@ class AliceSaveHelper { stringBuffer.writeAll([ 'Request size: ${AliceConversionHelper.formatBytes(call.request?.size ?? 0)}\n', - 'Request body: ${AliceParser.formatBody(call.request?.body, call.request?.contentType)}\n', + 'Request body: ${AliceBodyParser.formatBody(call.request?.body, call.request?.contentType)}\n', '--------------------------------------------\n', 'Response\n', '--------------------------------------------\n', @@ -187,7 +187,7 @@ class AliceSaveHelper { 'Response status: ${call.response?.status}\n', 'Response size: ${AliceConversionHelper.formatBytes(call.response?.size ?? 0)}\n', 'Response headers: ${_encoder.convert(call.response?.headers)}\n', - 'Response body: ${AliceParser.formatBody(call.response?.body, AliceParser.getContentType(call.response?.headers))}\n', + 'Response body: ${AliceBodyParser.formatBody(call.response?.body, AliceBodyParser.getContentType(call.response?.headers))}\n', ]); if (call.error != null) { diff --git a/packages/alice/lib/helper/operating_system.dart b/packages/alice/lib/helper/operating_system.dart index efe7461c..3adb8d70 100644 --- a/packages/alice/lib/helper/operating_system.dart +++ b/packages/alice/lib/helper/operating_system.dart @@ -1,3 +1,4 @@ +/// Definition of OS. abstract class OperatingSystem { static const String android = 'android'; static const String fuchsia = 'fuchsia'; diff --git a/packages/alice/lib/model/alice_form_data_file.dart b/packages/alice/lib/model/alice_form_data_file.dart index 7ff29d58..bea0160e 100644 --- a/packages/alice/lib/model/alice_form_data_file.dart +++ b/packages/alice/lib/model/alice_form_data_file.dart @@ -1,3 +1,4 @@ +/// Definition of data holder of form data file. class AliceFormDataFile { const AliceFormDataFile( this.fileName, diff --git a/packages/alice/lib/model/alice_from_data_field.dart b/packages/alice/lib/model/alice_from_data_field.dart index bcb4f7f7..d5770c40 100644 --- a/packages/alice/lib/model/alice_from_data_field.dart +++ b/packages/alice/lib/model/alice_from_data_field.dart @@ -1,3 +1,4 @@ +/// Definition of form data field. class AliceFormDataField { const AliceFormDataField( this.name, diff --git a/packages/alice/lib/model/alice_http_call.dart b/packages/alice/lib/model/alice_http_call.dart index 9341850c..2aa34eea 100644 --- a/packages/alice/lib/model/alice_http_call.dart +++ b/packages/alice/lib/model/alice_http_call.dart @@ -4,6 +4,7 @@ import 'package:alice/model/alice_http_error.dart'; import 'package:alice/model/alice_http_request.dart'; import 'package:alice/model/alice_http_response.dart'; +/// Definition of http calls data holder. class AliceHttpCall { AliceHttpCall(this.id) { loading = true; diff --git a/packages/alice/lib/model/alice_http_error.dart b/packages/alice/lib/model/alice_http_error.dart index de6972f3..7e0b5a76 100644 --- a/packages/alice/lib/model/alice_http_error.dart +++ b/packages/alice/lib/model/alice_http_error.dart @@ -1,3 +1,4 @@ +/// Definition of http error data holder. class AliceHttpError { dynamic error; StackTrace? stackTrace; diff --git a/packages/alice/lib/model/alice_http_request.dart b/packages/alice/lib/model/alice_http_request.dart index 42c2b7dc..4addaaa9 100644 --- a/packages/alice/lib/model/alice_http_request.dart +++ b/packages/alice/lib/model/alice_http_request.dart @@ -3,6 +3,7 @@ import 'dart:io' show Cookie; import 'package:alice/model/alice_form_data_file.dart'; import 'package:alice/model/alice_from_data_field.dart'; +/// Definition of http request data holder. class AliceHttpRequest { int size = 0; DateTime time = DateTime.now(); diff --git a/packages/alice/lib/model/alice_http_response.dart b/packages/alice/lib/model/alice_http_response.dart index 62da96ef..e237a9f3 100644 --- a/packages/alice/lib/model/alice_http_response.dart +++ b/packages/alice/lib/model/alice_http_response.dart @@ -1,3 +1,4 @@ +/// Definition of http response data holder. class AliceHttpResponse { int? status = 0; int size = 0; diff --git a/packages/alice/lib/model/alice_log.dart b/packages/alice/lib/model/alice_log.dart index 4af87c5b..d03cb402 100644 --- a/packages/alice/lib/model/alice_log.dart +++ b/packages/alice/lib/model/alice_log.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; +/// Definition of log data holder. @immutable class AliceLog with EquatableMixin { AliceLog({ diff --git a/packages/alice/lib/model/alice_menu_item.dart b/packages/alice/lib/model/alice_menu_item.dart deleted file mode 100644 index f6dd4fff..00000000 --- a/packages/alice/lib/model/alice_menu_item.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter/material.dart'; - -class AliceMenuItem { - const AliceMenuItem( - this.title, - this.iconData, - ); - - final String title; - final IconData iconData; -} diff --git a/packages/alice/lib/model/alice_sort_option.dart b/packages/alice/lib/model/alice_sort_option.dart deleted file mode 100644 index f3c56812..00000000 --- a/packages/alice/lib/model/alice_sort_option.dart +++ /dev/null @@ -1,18 +0,0 @@ -///Available sort options in inspector UI. -enum AliceSortOption { - time, - responseTime, - responseCode, - responseSize, - endpoint, -} - -extension AliceSortOptionsExtension on AliceSortOption { - String get name => switch (this) { - AliceSortOption.time => 'Create time (default)', - AliceSortOption.responseTime => 'Response time', - AliceSortOption.responseCode => 'Response code', - AliceSortOption.responseSize => 'Response size', - AliceSortOption.endpoint => 'Endpoint', - }; -} diff --git a/packages/alice/lib/model/alice_tab_item.dart b/packages/alice/lib/model/alice_tab_item.dart deleted file mode 100644 index c9a2fdf4..00000000 --- a/packages/alice/lib/model/alice_tab_item.dart +++ /dev/null @@ -1,11 +0,0 @@ -enum AliceTabItem { - inspector, - logger, -} - -extension AliceTabItemExtension on AliceTabItem { - String get title => switch (this) { - AliceTabItem.inspector => 'Inspector', - AliceTabItem.logger => 'Logger', - }; -} diff --git a/packages/alice/lib/ui/call_details/model/alice_call_details_tab.dart b/packages/alice/lib/ui/call_details/model/alice_call_details_tab.dart new file mode 100644 index 00000000..a8366ba1 --- /dev/null +++ b/packages/alice/lib/ui/call_details/model/alice_call_details_tab.dart @@ -0,0 +1,7 @@ +/// Definition of tabs used in call details page. +enum AliceCallDetailsTabItem { + overview, + request, + response, + error, +} diff --git a/packages/alice/lib/ui/call_details/model/alice_menu_item.dart b/packages/alice/lib/ui/call_details/model/alice_menu_item.dart new file mode 100644 index 00000000..a4308466 --- /dev/null +++ b/packages/alice/lib/ui/call_details/model/alice_menu_item.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +/// Definition of menu item used in call details. +class AliceCallDetailsMenuItem { + const AliceCallDetailsMenuItem( + this.title, + this.iconData, + ); + + final String title; + final IconData iconData; +} + +/// Definition of all call details menu item types. +enum AliceCallDetailsMenuItemType { + sort, + delete, + stats, + save, +} diff --git a/packages/alice/lib/ui/call_details/page/alice_call_details_page.dart b/packages/alice/lib/ui/call_details/page/alice_call_details_page.dart new file mode 100644 index 00000000..d664f924 --- /dev/null +++ b/packages/alice/lib/ui/call_details/page/alice_call_details_page.dart @@ -0,0 +1,125 @@ +import 'package:alice/core/alice_core.dart'; +import 'package:alice/helper/alice_save_helper.dart'; +import 'package:alice/model/alice_http_call.dart'; +import 'package:alice/ui/call_details/model/alice_call_details_tab.dart'; +import 'package:alice/ui/call_details/widget/alice_call_error_screen.dart'; +import 'package:alice/ui/call_details/widget/alice_call_overview_screen.dart'; +import 'package:alice/ui/call_details/widget/alice_call_request_screen.dart'; +import 'package:alice/ui/call_details/widget/alice_call_response_screen.dart'; +import 'package:alice/ui/common/alice_page.dart'; +import 'package:alice/utils/alice_theme.dart'; +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/material.dart'; +import 'package:share_plus/share_plus.dart'; + +/// Call details page which displays 4 tabs: overview, request, response, error. +class AliceCallDetailsPage extends StatefulWidget { + final AliceHttpCall call; + final AliceCore core; + + const AliceCallDetailsPage({ + required this.call, + required this.core, + super.key, + }); + + @override + State createState() => _AliceCallDetailsPageState(); +} + +/// State of call details page. +class _AliceCallDetailsPageState extends State + with SingleTickerProviderStateMixin { + AliceHttpCall get call => widget.call; + + @override + Widget build(BuildContext context) { + return AlicePage( + core: widget.core, + child: StreamBuilder>( + stream: widget.core.callsSubject, + initialData: [widget.call], + builder: (context, AsyncSnapshot> callsSnapshot) { + if (callsSnapshot.hasData && !callsSnapshot.hasError) { + final AliceHttpCall? call = callsSnapshot.data?.firstWhereOrNull( + (AliceHttpCall snapshotCall) => snapshotCall.id == widget.call.id, + ); + if (call != null) { + return DefaultTabController( + length: 4, + child: Scaffold( + appBar: AppBar( + bottom: TabBar( + indicatorColor: AliceTheme.lightRed, + tabs: AliceCallDetailsTabItem.values.map((item) { + return Tab( + icon: _getTabIcon(item: item), + text: _getTabName( + item: item, + ), + ); + }).toList(), + ), + title: const Text('Alice - HTTP Call Details'), + ), + body: TabBarView( + children: [ + AliceCallOverviewScreen(call: widget.call), + AliceCallRequestScreen(call: widget.call), + AliceCallResponseScreen(call: widget.call), + AliceCallErrorScreen(call: widget.call), + ], + ), + floatingActionButton: widget.core.showShareButton ?? false + ? FloatingActionButton( + backgroundColor: AliceTheme.lightRed, + key: const Key('share_key'), + onPressed: () async => await Share.share( + await AliceSaveHelper.buildCallLog(widget.call), + subject: 'Request Details', + ), + child: const Icon( + Icons.share, + color: AliceTheme.white, + ), + ) + : null, + ), + ); + } + } + + return const Center(child: Text('Failed to load data')); + }, + ), + ); + } + + /// Get tab name based on [item] type. + String _getTabName({required AliceCallDetailsTabItem item}) { + switch (item) { + case AliceCallDetailsTabItem.overview: + return "Overview"; + case AliceCallDetailsTabItem.request: + return "Request"; + case AliceCallDetailsTabItem.response: + return "Response"; + case AliceCallDetailsTabItem.error: + return "Error"; + } + } + + /// Get tab icon based on [item] type. + Icon _getTabIcon({required AliceCallDetailsTabItem item}) { + switch (item) { + case AliceCallDetailsTabItem.overview: + return const Icon(Icons.info_outline); + case AliceCallDetailsTabItem.request: + return const Icon(Icons.arrow_upward); + case AliceCallDetailsTabItem.response: + return const Icon(Icons.arrow_downward); + case AliceCallDetailsTabItem.error: + return const Icon(Icons.warning); + } + } +} diff --git a/packages/alice/lib/ui/widget/alice_call_error_widget.dart b/packages/alice/lib/ui/call_details/widget/alice_call_error_screen.dart similarity index 50% rename from packages/alice/lib/ui/widget/alice_call_error_widget.dart rename to packages/alice/lib/ui/call_details/widget/alice_call_error_screen.dart index 3db3aa0d..be744bb2 100644 --- a/packages/alice/lib/ui/widget/alice_call_error_widget.dart +++ b/packages/alice/lib/ui/call_details/widget/alice_call_error_screen.dart @@ -1,27 +1,20 @@ import 'package:alice/model/alice_http_call.dart'; -import 'package:alice/ui/widget/alice_base_call_details_widget.dart'; +import 'package:alice/ui/call_details/widget/alice_call_expandable_list_row.dart'; +import 'package:alice/ui/call_details/widget/alice_call_list_row.dart'; import 'package:alice/utils/alice_scroll_behavior.dart'; import 'package:flutter/material.dart'; -class AliceCallErrorWidget extends StatefulWidget { - const AliceCallErrorWidget( - this.call, { - super.key, - }); +/// Call error screen which displays info on HTTP call error. +class AliceCallErrorScreen extends StatelessWidget { + const AliceCallErrorScreen({super.key, required this.call}); final AliceHttpCall call; - @override - State createState() => _AliceCallErrorWidgetState(); -} - -class _AliceCallErrorWidgetState - extends AliceBaseCallDetailsWidgetState { @override Widget build(BuildContext context) { - if (widget.call.error != null) { - final dynamic error = widget.call.error?.error; - final StackTrace? stackTrace = widget.call.error?.stackTrace; + if (call.error != null) { + final dynamic error = call.error?.error; + final StackTrace? stackTrace = call.error?.stackTrace; final String errorText = error != null ? error.toString() : 'Error is empty'; @@ -31,9 +24,12 @@ class _AliceCallErrorWidgetState behavior: AliceScrollBehavior(), child: ListView( children: [ - getListRow('Error:', errorText), + AliceCallListRow(name: 'Error:', value: errorText), if (stackTrace != null) - getExpandableListRow('Stack trace:', stackTrace.toString()), + AliceCallExpandableListRow( + name: 'Stack trace:', + value: stackTrace.toString(), + ), ], ), ), diff --git a/packages/alice/lib/ui/call_details/widget/alice_call_expandable_list_row.dart b/packages/alice/lib/ui/call_details/widget/alice_call_expandable_list_row.dart new file mode 100644 index 00000000..8d9fbee0 --- /dev/null +++ b/packages/alice/lib/ui/call_details/widget/alice_call_expandable_list_row.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +/// Widget which displays expandable formatted row of text. +class AliceCallExpandableListRow extends StatelessWidget { + final String name; + final String value; + + const AliceCallExpandableListRow({ + required this.name, + required this.value, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ExpansionTile( + title: Text( + name, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + ), + subtitle: Text( + value, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + controlAffinity: ListTileControlAffinity.trailing, + tilePadding: const EdgeInsets.all(0), + children: [ + SelectableText( + value, + ), + ], + ); + } +} diff --git a/packages/alice/lib/ui/call_details/widget/alice_call_list_row.dart b/packages/alice/lib/ui/call_details/widget/alice_call_list_row.dart new file mode 100644 index 00000000..51c8079b --- /dev/null +++ b/packages/alice/lib/ui/call_details/widget/alice_call_list_row.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +/// Widget which displays formatted text row. It allows to select the text. +class AliceCallListRow extends StatelessWidget { + final String name; + final String? value; + + const AliceCallListRow({required this.name, this.value, super.key}); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const Padding( + padding: EdgeInsets.only(left: 5), + ), + Flexible( + child: value != null + ? SelectableText( + value!, + ) + : const SizedBox(), + ), + const Padding( + padding: EdgeInsets.only(bottom: 18), + ), + ], + ); + } +} diff --git a/packages/alice/lib/ui/call_details/widget/alice_call_overview_screen.dart b/packages/alice/lib/ui/call_details/widget/alice_call_overview_screen.dart new file mode 100644 index 00000000..507320de --- /dev/null +++ b/packages/alice/lib/ui/call_details/widget/alice_call_overview_screen.dart @@ -0,0 +1,66 @@ +import 'package:alice/helper/alice_conversion_helper.dart'; +import 'package:alice/model/alice_http_call.dart'; +import 'package:alice/ui/call_details/widget/alice_call_list_row.dart'; +import 'package:alice/utils/alice_scroll_behavior.dart'; +import 'package:flutter/material.dart'; + +/// Screen which displays call overview data, for example method, server. +class AliceCallOverviewScreen extends StatelessWidget { + final AliceHttpCall call; + const AliceCallOverviewScreen({super.key, required this.call}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(6), + child: ScrollConfiguration( + behavior: AliceScrollBehavior(), + child: ListView( + children: [ + AliceCallListRow( + name: 'Method: ', + value: call.method, + ), + AliceCallListRow( + name: 'Server: ', + value: call.server, + ), + AliceCallListRow( + name: 'Endpoint: ', + value: call.endpoint, + ), + AliceCallListRow( + name: 'Started:', + value: call.request?.time.toString(), + ), + AliceCallListRow( + name: 'Finished:', + value: call.response?.time.toString(), + ), + AliceCallListRow( + name: 'Duration:', + value: AliceConversionHelper.formatTime(call.duration), + ), + AliceCallListRow( + name: 'Bytes sent:', + value: AliceConversionHelper.formatBytes( + call.request?.size ?? 0, + ), + ), + AliceCallListRow( + name: 'Bytes received:', + value: AliceConversionHelper.formatBytes( + call.response?.size ?? 0, + ), + ), + AliceCallListRow(name: 'Client:', value: call.client), + AliceCallListRow( + name: 'Secure:', + value: call.secure.toString(), + ), + ], + ), + ), + ); + } +} diff --git a/packages/alice/lib/ui/call_details/widget/alice_call_request_screen.dart b/packages/alice/lib/ui/call_details/widget/alice_call_request_screen.dart new file mode 100644 index 00000000..017adbc6 --- /dev/null +++ b/packages/alice/lib/ui/call_details/widget/alice_call_request_screen.dart @@ -0,0 +1,91 @@ +import 'package:alice/helper/alice_conversion_helper.dart'; +import 'package:alice/model/alice_form_data_file.dart'; +import 'package:alice/model/alice_from_data_field.dart'; +import 'package:alice/model/alice_http_call.dart'; +import 'package:alice/ui/call_details/widget/alice_call_list_row.dart'; +import 'package:alice/utils/alice_parser.dart'; +import 'package:alice/utils/alice_scroll_behavior.dart'; +import 'package:flutter/material.dart'; + +/// Screen which displays information about call request: content, transfer, +/// headers. +class AliceCallRequestScreen extends StatelessWidget { + final AliceHttpCall call; + + const AliceCallRequestScreen({super.key, required this.call}); + + @override + Widget build(BuildContext context) { + final List rows = [ + AliceCallListRow(name: 'Started:', value: call.request?.time.toString()), + AliceCallListRow( + name: 'Bytes sent:', + value: AliceConversionHelper.formatBytes(call.request?.size ?? 0)), + AliceCallListRow( + name: 'Content type:', + value: AliceBodyParser.getContentType(call.request?.headers)), + ]; + + rows.add(AliceCallListRow(name: 'Body:', value: _getBodyContent())); + + final List? formDataFields = + call.request?.formDataFields; + if (formDataFields?.isNotEmpty ?? false) { + rows.add(const AliceCallListRow(name: 'Form data fields: ', value: '')); + rows.addAll([ + for (final AliceFormDataField field in formDataFields!) + AliceCallListRow(name: ' • ${field.name}:', value: field.value) + ]); + } + + final List? formDataFiles = call.request!.formDataFiles; + if (formDataFiles?.isNotEmpty ?? false) { + rows.add(const AliceCallListRow(name: 'Form data files: ', value: '')); + rows.addAll([ + for (final AliceFormDataFile file in formDataFiles!) + AliceCallListRow( + name: ' • ${file.fileName}:', + value: '${file.contentType} / ${file.length} B', + ) + ]); + } + + final Map? headers = call.request?.headers; + final String headersContent = + headers?.isEmpty ?? true ? 'Headers are empty' : ''; + rows.add(AliceCallListRow(name: 'Headers: ', value: headersContent)); + rows.addAll([ + for (final MapEntry header in headers?.entries ?? []) + AliceCallListRow( + name: ' • ${header.key}:', value: header.value.toString()) + ]); + + final Map? queryParameters = call.request?.queryParameters; + final String queryParametersContent = + queryParameters?.isEmpty ?? true ? 'Query parameters are empty' : ''; + rows.add(AliceCallListRow( + name: 'Query Parameters: ', value: queryParametersContent)); + rows.addAll([ + for (final MapEntry queryParam + in queryParameters?.entries ?? []) + AliceCallListRow( + name: ' • ${queryParam.key}:', value: queryParam.value.toString()) + ]); + + return Container( + padding: const EdgeInsets.all(6), + child: ScrollConfiguration( + behavior: AliceScrollBehavior(), + child: ListView(children: rows), + ), + ); + } + + String _getBodyContent() { + final dynamic body = call.request?.body; + return body != null + ? AliceBodyParser.formatBody( + body, AliceBodyParser.getContentType(call.request?.headers)) + : 'Body is empty'; + } +} diff --git a/packages/alice/lib/ui/call_details/widget/alice_call_response_screen.dart b/packages/alice/lib/ui/call_details/widget/alice_call_response_screen.dart new file mode 100644 index 00000000..d9d9906b --- /dev/null +++ b/packages/alice/lib/ui/call_details/widget/alice_call_response_screen.dart @@ -0,0 +1,360 @@ +import 'package:alice/helper/alice_conversion_helper.dart'; +import 'package:alice/model/alice_http_call.dart'; +import 'package:alice/ui/call_details/widget/alice_call_list_row.dart'; +import 'package:alice/utils/alice_parser.dart'; +import 'package:alice/utils/alice_scroll_behavior.dart'; +import 'package:alice/utils/num_comparison.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +/// Screen which displays information about HTTP call response. +class AliceCallResponseScreen extends StatelessWidget { + const AliceCallResponseScreen({super.key, required this.call}); + + final AliceHttpCall call; + + @override + Widget build(BuildContext context) { + if (!call.loading) { + return Container( + padding: const EdgeInsets.all(6), + child: ScrollConfiguration( + behavior: AliceScrollBehavior(), + child: ListView(children: [ + _GeneralDataColumn(call: call), + _HeaderDataColumn(call: call), + _BodyDataColumn(call: call) + ]), + ), + ); + } else { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [CircularProgressIndicator(), Text('Awaiting response...')], + ), + ); + } + } +} + +/// Column which displays general information like received time and bytes +/// count. +class _GeneralDataColumn extends StatelessWidget { + final AliceHttpCall call; + + const _GeneralDataColumn({required this.call}); + + @override + Widget build(BuildContext context) { + final int? status = call.response?.status; + final String statusText = status == -1 ? 'Error' : '$status'; + + return Column( + children: [ + AliceCallListRow( + name: 'Received:', value: call.response?.time.toString()), + AliceCallListRow( + name: 'Bytes received:', + value: AliceConversionHelper.formatBytes(call.response?.size ?? 0), + ), + AliceCallListRow( + name: 'Status:', + value: statusText, + ), + ], + ); + } +} + +class _HeaderDataColumn extends StatelessWidget { + final AliceHttpCall call; + + const _HeaderDataColumn({required this.call}); + + @override + Widget build(BuildContext context) { + final Map? headers = call.response?.headers; + final String headersContent = + headers?.isEmpty ?? true ? 'Headers are empty' : ''; + + return Column( + children: [ + AliceCallListRow(name: 'Headers: ', value: headersContent), + for (final MapEntry header in headers?.entries ?? []) + AliceCallListRow( + name: ' • ${header.key}:', + value: header.value.toString(), + ) + ], + ); + } +} + +class _BodyDataColumn extends StatefulWidget { + const _BodyDataColumn({required this.call}); + + final AliceHttpCall call; + + @override + State<_BodyDataColumn> createState() => _BodyDataColumnState(); +} + +class _BodyDataColumnState extends State<_BodyDataColumn> { + static const String _imageContentType = 'image'; + static const String _videoContentType = 'video'; + static const String _jsonContentType = 'json'; + static const String _xmlContentType = 'xml'; + static const String _textContentType = 'text'; + + static const int _largeOutputSize = 100000; + bool _showLargeBody = false; + bool _showUnsupportedBody = false; + + AliceHttpCall get call => widget.call; + + @override + Widget build(BuildContext context) { + if (_isImageResponse()) { + return _ImageBody( + call: call, + ); + } else if (_isVideoResponse()) { + return _VideoBody(call: call); + } else if (_isTextResponse()) { + if (_isLargeResponseBody()) { + return _LargeTextBody( + showLargeBody: _showLargeBody, + call: call, + onShowLargeBodyPressed: onShowLargeBodyPressed, + ); + } else { + return _TextBody(call: call); + } + } else { + return _UnknownBody( + call: call, + showUnsupportedBody: _showUnsupportedBody, + onShowUnsupportedBodyPressed: onShowUnsupportedBodyPressed, + ); + } + } + + bool _isImageResponse() { + return _getContentTypeOfResponse()! + .toLowerCase() + .contains(_imageContentType); + } + + bool _isVideoResponse() { + return _getContentTypeOfResponse()! + .toLowerCase() + .contains(_videoContentType); + } + + bool _isTextResponse() { + final responseContentTypeLowerCase = + _getContentTypeOfResponse()!.toLowerCase(); + + return responseContentTypeLowerCase.contains(_jsonContentType) || + responseContentTypeLowerCase.contains(_xmlContentType) || + responseContentTypeLowerCase.contains(_textContentType); + } + + String? _getContentTypeOfResponse() { + return AliceBodyParser.getContentType(call.response?.headers); + } + + bool _isLargeResponseBody() => + call.response?.body.toString().length.gt(_largeOutputSize) ?? false; + + void onShowLargeBodyPressed() { + setState(() { + _showLargeBody = true; + }); + } + + void onShowUnsupportedBodyPressed() { + setState(() { + _showUnsupportedBody = true; + }); + } +} + +class _ImageBody extends StatelessWidget { + const _ImageBody({ + required this.call, + }); + + final AliceHttpCall call; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Row( + children: [ + Text( + 'Body: Image', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 8), + Image.network( + call.uri, + fit: BoxFit.fill, + headers: _buildRequestHeaders(), + loadingBuilder: ( + BuildContext context, + Widget child, + ImageChunkEvent? loadingProgress, + ) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ), + const SizedBox(height: 8), + ], + ); + } + + Map _buildRequestHeaders() { + final requestHeaders = {}; + if (call.request?.headers != null) { + requestHeaders.addAll( + call.request!.headers.map( + (String key, dynamic value) => MapEntry( + key, + value.toString(), + ), + ), + ); + } + return requestHeaders; + } +} + +class _LargeTextBody extends StatelessWidget { + const _LargeTextBody({ + required this.showLargeBody, + required this.call, + required this.onShowLargeBodyPressed, + }); + + final bool showLargeBody; + final AliceHttpCall call; + final void Function() onShowLargeBodyPressed; + + @override + Widget build(BuildContext context) { + if (showLargeBody) { + return _TextBody(call: call); + } else { + return Column(children: [ + AliceCallListRow( + name: 'Body:', + value: 'Too large to show ' + '(${call.response?.body.toString().length ?? 0} Bytes)', + ), + const SizedBox(height: 8), + TextButton( + onPressed: onShowLargeBodyPressed, + child: const Text('Show body'), + ), + const Text('Warning! It will take some time to render output.') + ]); + } + } +} + +class _TextBody extends StatelessWidget { + const _TextBody({required this.call}); + + final AliceHttpCall call; + + @override + Widget build(BuildContext context) { + final Map? headers = call.response?.headers; + final String bodyContent = AliceBodyParser.formatBody( + call.response?.body, AliceBodyParser.getContentType(headers)); + return AliceCallListRow(name: 'Body:', value: bodyContent); + } +} + +class _VideoBody extends StatelessWidget { + const _VideoBody({required this.call}); + + final AliceHttpCall call; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Row( + children: [ + Text( + 'Body: Video', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 8), + TextButton( + child: const Text('Open video in web browser'), + onPressed: () async { + await launchUrl(Uri.parse(call.uri)); + }, + ), + const SizedBox(height: 8), + ], + ); + } +} + +class _UnknownBody extends StatelessWidget { + const _UnknownBody( + {required this.call, + required this.showUnsupportedBody, + required this.onShowUnsupportedBodyPressed}); + + final AliceHttpCall call; + final bool showUnsupportedBody; + final void Function() onShowUnsupportedBodyPressed; + + @override + Widget build(BuildContext context) { + final Map? headers = call.response?.headers; + final String contentType = + AliceBodyParser.getContentType(headers) ?? ''; + + if (showUnsupportedBody) { + final bodyContent = AliceBodyParser.formatBody( + call.response?.body, AliceBodyParser.getContentType(headers)); + return AliceCallListRow(name: 'Body:', value: bodyContent); + } else { + return Column( + children: [ + AliceCallListRow( + name: 'Body:', + value: + 'Unsupported body. Alice can render video/image/text body. ' + "Response has Content-Type: $contentType which can't be " + "handled. If you're feeling lucky you can try button below " + 'to try render body as text, but it may fail.'), + TextButton( + onPressed: onShowUnsupportedBodyPressed, + child: const Text('Show unsupported body'), + ), + ], + ); + } + } +} diff --git a/packages/alice/lib/ui/calls_list/model/alice_calls_list_sort_option.dart b/packages/alice/lib/ui/calls_list/model/alice_calls_list_sort_option.dart new file mode 100644 index 00000000..8d4827b0 --- /dev/null +++ b/packages/alice/lib/ui/calls_list/model/alice_calls_list_sort_option.dart @@ -0,0 +1,8 @@ +///Available sort options in inspector UI. +enum AliceCallsListSortOption { + time, + responseTime, + responseCode, + responseSize, + endpoint, +} diff --git a/packages/alice/lib/ui/calls_list/model/alice_calls_list_tab_item.dart b/packages/alice/lib/ui/calls_list/model/alice_calls_list_tab_item.dart new file mode 100644 index 00000000..8c058d82 --- /dev/null +++ b/packages/alice/lib/ui/calls_list/model/alice_calls_list_tab_item.dart @@ -0,0 +1,5 @@ +/// Tab items available in inspector view. +enum AliceCallsListTabItem { + inspector, + logger, +} diff --git a/packages/alice/lib/ui/calls_list/page/alice_calls_list_page.dart b/packages/alice/lib/ui/calls_list/page/alice_calls_list_page.dart new file mode 100644 index 00000000..79ae2bb0 --- /dev/null +++ b/packages/alice/lib/ui/calls_list/page/alice_calls_list_page.dart @@ -0,0 +1,432 @@ +import 'package:alice/core/alice_core.dart'; +import 'package:alice/core/alice_logger.dart'; +import 'package:alice/model/alice_http_call.dart'; +import 'package:alice/ui/call_details/model/alice_menu_item.dart'; +import 'package:alice/ui/calls_list/model/alice_calls_list_sort_option.dart'; +import 'package:alice/ui/calls_list/model/alice_calls_list_tab_item.dart'; +import 'package:alice/ui/calls_list/widget/alice_sort_dialog.dart'; +import 'package:alice/ui/common/alice_dialog.dart'; +import 'package:alice/ui/common/alice_navigation.dart'; +import 'package:alice/ui/common/alice_page.dart'; +import 'package:alice/ui/calls_list/widget/alice_calls_list_screen.dart'; +import 'package:alice/ui/calls_list/widget/alice_empty_logs_widget.dart'; +import 'package:alice/ui/calls_list/widget/alice_logs_screen.dart'; +import 'package:alice/utils/alice_theme.dart'; +import 'package:flutter/material.dart'; + +/// Page which displays list of calls caught by Alice. It displays tab view +/// where calls and logs can be inspected. It allows to sort calls, delete calls +/// and search calls. +class AliceCallsListPage extends StatefulWidget { + final AliceCore core; + final AliceLogger? logger; + + const AliceCallsListPage({ + required this.core, + this.logger, + super.key, + }); + + @override + State createState() => _AliceCallsListPageState(); +} + +class _AliceCallsListPageState extends State + with SingleTickerProviderStateMixin { + final TextEditingController _queryTextEditingController = + TextEditingController(); + final List _tabItems = AliceCallsListTabItem.values; + final ScrollController _scrollController = ScrollController(); + late final TabController? _tabController; + + AliceCallsListSortOption _sortOption = AliceCallsListSortOption.time; + bool _sortAscending = false; + bool _searchEnabled = false; + bool isAndroidRawLogsEnabled = false; + int _selectedIndex = 0; + + AliceCore get aliceCore => widget.core; + + @override + void initState() { + super.initState(); + + _tabController = TabController( + vsync: this, + length: _tabItems.length, + initialIndex: _tabItems.first.index, + ); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _tabController?.addListener(() { + _onTabChanged(_tabController!.index); + }); + }); + } + + @override + void dispose() { + _queryTextEditingController.dispose(); + _tabController?.dispose(); + _scrollController.dispose(); + + super.dispose(); + } + + /// Returns [true] when logger tab is opened. + bool get isLoggerTab => _selectedIndex == 1; + + @override + Widget build(BuildContext context) { + return AlicePage( + core: aliceCore, + child: Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: _onBackPressed, + ), + title: _searchEnabled + ? _SearchTextField( + textEditingController: _queryTextEditingController, + onChanged: _updateSearchQuery, + ) + : const Text('Alice'), + actions: isLoggerTab + ? [ + IconButton( + icon: const Icon(Icons.terminal), + onPressed: _onLogsChangePressed, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: _onClearLogsPressed, + ), + ] + : [ + IconButton( + icon: const Icon(Icons.search), + onPressed: _onSearchPressed, + ), + _ContextMenuButton( + onMenuItemSelected: _onMenuItemSelected, + ), + ], + bottom: TabBar( + controller: _tabController, + indicatorColor: AliceTheme.lightRed, + tabs: AliceCallsListTabItem.values.map((item) { + return Tab(text: _getTabName(item: item)); + }).toList(), + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + StreamBuilder>( + stream: aliceCore.callsSubject, + builder: (context, AsyncSnapshot> snapshot) { + final List calls = snapshot.data ?? []; + final String query = _queryTextEditingController.text.trim(); + if (query.isNotEmpty) { + calls.removeWhere((AliceHttpCall call) => !call.endpoint + .toLowerCase() + .contains(query.toLowerCase())); + } + if (calls.isNotEmpty) { + return AliceCallsListScreen( + calls: calls, + sortOption: _sortOption, + sortAscending: _sortAscending, + onListItemClicked: _onListItemPressed, + ); + } else { + return const AliceEmptyLogsWidget(); + } + }, + ), + AliceLogsScreen( + scrollController: _scrollController, + aliceLogger: widget.logger, + isAndroidRawLogsEnabled: isAndroidRawLogsEnabled, + ), + ], + ), + floatingActionButton: isLoggerTab + ? _LoggerFloatingActionButtons( + scrollLogsList: _scrollLogsList, + ) + : const SizedBox(), + ), + ); + } + + /// Get tab name based on [item] type. + String _getTabName({required AliceCallsListTabItem item}) { + switch (item) { + case AliceCallsListTabItem.inspector: + return "Inspector"; + case AliceCallsListTabItem.logger: + return "Logger"; + } + } + + /// Called when back button has been pressed. It navigates back to original + /// application. + void _onBackPressed() { + Navigator.of(context, rootNavigator: true).pop(); + } + + /// Called when clear logs has been pressed. It displays dialog and awaits for + /// user confirmation. + void _onClearLogsPressed() => AliceGeneralDialog.show( + context: context, + title: 'Delete logs', + description: 'Do you want to clear logs?', + firstButtonTitle: 'No', + secondButtonTitle: 'Yes', + secondButtonAction: _onLogsClearPressed, + ); + + /// Called when logs type mode pressed. + void _onLogsChangePressed() => setState(() { + isAndroidRawLogsEnabled = !isAndroidRawLogsEnabled; + }); + + /// Called when logs clear button has been pressed. + void _onLogsClearPressed() => setState(() { + if (isAndroidRawLogsEnabled) { + widget.logger?.clearAndroidRawLogs(); + } else { + widget.logger?.clearLogs(); + } + }); + + /// Called when search button. It displays search textfield. + void _onSearchPressed() => setState(() { + _searchEnabled = !_searchEnabled; + if (!_searchEnabled) { + _queryTextEditingController.text = ''; + } + }); + + /// Called on tab has been changed. + void _onTabChanged(int index) => setState( + () { + _selectedIndex = index; + if (_selectedIndex == 1) { + _searchEnabled = false; + _queryTextEditingController.text = ''; + } + }, + ); + + /// Called when menu item from overflow menu has been pressed. + void _onMenuItemSelected(AliceCallDetailsMenuItemType menuItem) { + switch (menuItem) { + case AliceCallDetailsMenuItemType.sort: + _onSortPressed(); + case AliceCallDetailsMenuItemType.delete: + _onRemovePressed(); + case AliceCallDetailsMenuItemType.stats: + _onStatsPressed(); + case AliceCallDetailsMenuItemType.save: + _saveToFile(); + } + } + + /// Called when item from the list has been pressed. It opens details page. + void _onListItemPressed(AliceHttpCall call) => + AliceNavigation.navigateToCallDetails(call: call, core: aliceCore); + + /// Called when remove all calls button has been pressed. + void _onRemovePressed() => AliceGeneralDialog.show( + context: context, + title: 'Delete calls', + description: 'Do you want to delete http calls?', + firstButtonTitle: 'No', + firstButtonAction: () => {}, + secondButtonTitle: 'Yes', + secondButtonAction: _removeCalls, + ); + + /// Removes all calls from Alice. + void _removeCalls() => aliceCore.removeCalls(); + + /// Called when stats button has been pressed. Navigates to stats page. + void _onStatsPressed() { + AliceNavigation.navigateToStats(core: aliceCore); + } + + /// Called when save to file has been pressed. It saves data to file. + void _saveToFile() => aliceCore.saveHttpRequests(context); + + /// Filters calls based on query. + void _updateSearchQuery(String query) => setState(() {}); + + /// Called when sort button has been pressed. It opens dialog where filters + /// can be picked. + Future _onSortPressed() async { + AliceSortDialogResult? result = await showDialog( + context: context, + builder: (BuildContext buildContext) => AliceSortDialog( + sortOption: _sortOption, + sortAscending: _sortAscending, + ), + ); + if (result != null) { + setState(() { + _sortOption = result.sortOption; + _sortAscending = result.sortAscending; + }); + } + } + + /// Scrolls logs list based on [top] parameter. + void _scrollLogsList(bool top) => top ? _scrollToTop() : _scrollToBottom(); + + /// Scrolls logs list to the top. + void _scrollToTop() { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.minScrollExtent, + duration: const Duration(microseconds: 500), + curve: Curves.ease, + ); + } + } + + /// Scrolls logs list to the bottom. + void _scrollToBottom() { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(microseconds: 500), + curve: Curves.ease, + ); + } + } +} + +/// Text field displayed in app bar. Used to search call logs. +class _SearchTextField extends StatelessWidget { + const _SearchTextField({ + required this.textEditingController, + required this.onChanged, + }); + + final TextEditingController textEditingController; + final void Function(String) onChanged; + + @override + Widget build(BuildContext context) { + return TextField( + controller: textEditingController, + autofocus: true, + decoration: const InputDecoration( + hintText: 'Search http request...', + hintStyle: TextStyle(fontSize: 16, color: AliceTheme.grey), + border: InputBorder.none, + ), + style: const TextStyle(fontSize: 16), + onChanged: onChanged, + ); + } +} + +/// Menu button displayed in app bar. It displays overflow menu with addtional +/// actions. +class _ContextMenuButton extends StatelessWidget { + const _ContextMenuButton({required this.onMenuItemSelected}); + + final void Function(AliceCallDetailsMenuItemType) onMenuItemSelected; + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + onSelected: onMenuItemSelected, + itemBuilder: (BuildContext context) => [ + for (final AliceCallDetailsMenuItemType item + in AliceCallDetailsMenuItemType.values) + PopupMenuItem( + value: item, + child: Row( + children: [ + Icon( + _getIcon(itemType: item), + color: AliceTheme.lightRed, + ), + const Padding( + padding: EdgeInsets.only(left: 10), + ), + Text(_getTitle(itemType: item)), + ], + ), + ), + ], + ); + } + + /// Get title of the menu item based on [itemType]. + String _getTitle({required AliceCallDetailsMenuItemType itemType}) { + switch (itemType) { + case AliceCallDetailsMenuItemType.sort: + return "Sort"; + case AliceCallDetailsMenuItemType.delete: + return "Delete"; + case AliceCallDetailsMenuItemType.stats: + return "Stats"; + case AliceCallDetailsMenuItemType.save: + return "Save"; + } + } + + /// Get icon of the menu item based [itemType]. + IconData _getIcon({required AliceCallDetailsMenuItemType itemType}) { + switch (itemType) { + case AliceCallDetailsMenuItemType.sort: + return Icons.sort; + case AliceCallDetailsMenuItemType.delete: + return Icons.delete; + case AliceCallDetailsMenuItemType.stats: + return Icons.insert_chart; + case AliceCallDetailsMenuItemType.save: + return Icons.save; + } + } +} + +/// FAB buttons used to scroll logs. Displayed only in logs tab. +class _LoggerFloatingActionButtons extends StatelessWidget { + const _LoggerFloatingActionButtons({required this.scrollLogsList}); + + final void Function(bool) scrollLogsList; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + heroTag: 'h1', + backgroundColor: AliceTheme.lightRed, + onPressed: () => scrollLogsList(true), + child: const Icon( + Icons.arrow_upward, + color: AliceTheme.white, + ), + ), + const SizedBox(height: 8), + FloatingActionButton( + heroTag: 'h2', + backgroundColor: AliceTheme.lightRed, + onPressed: () => scrollLogsList(false), + child: const Icon( + Icons.arrow_downward, + color: AliceTheme.white, + ), + ), + ], + ); + } +} diff --git a/packages/alice/lib/ui/calls_list/widget/alice_call_list_item_widget.dart b/packages/alice/lib/ui/calls_list/widget/alice_call_list_item_widget.dart new file mode 100644 index 00000000..c36ad100 --- /dev/null +++ b/packages/alice/lib/ui/calls_list/widget/alice_call_list_item_widget.dart @@ -0,0 +1,235 @@ +import 'package:alice/helper/alice_conversion_helper.dart'; +import 'package:alice/model/alice_http_call.dart'; +import 'package:alice/model/alice_http_response.dart'; +import 'package:alice/utils/alice_theme.dart'; +import 'package:flutter/material.dart'; + +const int _endpointMaxLines = 10; +const int _serverMaxLines = 5; + +/// Widget which renders one row in calls list view. It displays general +/// information about call. +class AliceCallListItemWidget extends StatelessWidget { + const AliceCallListItemWidget( + this.call, + this.itemClickAction, { + super.key, + }); + + final AliceHttpCall call; + final void Function(AliceHttpCall) itemClickAction; + + @override + Widget build(BuildContext context) { + final Color requestColor = _getEndpointTextColor(context); + final Color statusColor = _getStatusTextColor(context); + + return InkWell( + onTap: () => itemClickAction.call(call), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _EndpointAndMethod(call: call, color: requestColor), + const SizedBox(height: 4), + _ServerAddress(call: call), + const SizedBox(height: 4), + _ConnectionStats(call: call), + ], + ), + ), + _ResponseStatus( + call: call, + color: statusColor, + ), + ], + ), + ), + const Divider(height: 1, color: AliceTheme.grey), + ], + ), + ); + } + + /// Get response status text color based on response status. + Color _getStatusTextColor(BuildContext context) => + switch (call.response?.status) { + -1 => AliceTheme.red, + int status when status < 200 => + Theme.of(context).textTheme.bodyLarge?.color ?? AliceTheme.grey, + int status when status >= 200 && status < 300 => AliceTheme.green, + int status when status >= 300 && status < 400 => AliceTheme.orange, + int status when status >= 400 && status < 600 => AliceTheme.red, + _ => Theme.of(context).textTheme.bodyLarge!.color ?? AliceTheme.grey, + }; + + /// Returns endpoint text color based on call state. + Color _getEndpointTextColor(BuildContext context) => + call.loading ? AliceTheme.grey : _getStatusTextColor(context); +} + +/// Widget which renders server address line. +class _ServerAddress extends StatelessWidget { + final AliceHttpCall call; + + const _ServerAddress({required this.call}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 3), + child: Icon( + call.secure ? Icons.lock_outline : Icons.lock_open, + color: call.secure ? AliceTheme.green : AliceTheme.red, + size: 12, + ), + ), + Expanded( + child: Text( + call.server, + overflow: TextOverflow.ellipsis, + maxLines: _serverMaxLines, + style: const TextStyle( + fontSize: 14, + ), + ), + ), + ], + ); + } +} + +/// Widget which renders endpoint and the HTTP method line. +class _EndpointAndMethod extends StatelessWidget { + final AliceHttpCall call; + final Color color; + + const _EndpointAndMethod({required this.call, required this.color}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text( + call.method, + style: TextStyle(fontSize: 16, color: color), + ), + const Padding( + padding: EdgeInsets.only(left: 10), + ), + Flexible( + // ignore: avoid_unnecessary_containers + child: Container( + child: Text( + call.endpoint, + maxLines: _endpointMaxLines, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 16, color: color), + ), + ), + ), + ], + ); + } +} + +/// Widget which renders response status line. +class _ResponseStatus extends StatelessWidget { + final AliceHttpCall call; + final Color color; + + const _ResponseStatus({required this.call, required this.color}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 50, + child: Column( + children: [ + if (call.loading) ...[ + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + AliceTheme.lightRed, + ), + ), + ), + const SizedBox(height: 4), + ], + if (call.response != null) + Text( + _getStatus(call.response!), + style: TextStyle( + fontSize: 16, + color: color, + ), + ) + ], + ), + ); + } + + /// Get status based on [response]. + String _getStatus(AliceHttpResponse response) => switch (response.status) { + -1 => 'ERR', + 0 => '???', + _ => '${response.status}', + }; +} + +/// Widget which renders connection stats based on [call]. +class _ConnectionStats extends StatelessWidget { + final AliceHttpCall call; + + const _ConnectionStats({required this.call}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + call.request?.time != null + ? _formatTime(call.request!.time) + : 'n/a', + style: const TextStyle(fontSize: 12), + ), + ), + Flexible( + child: Text( + AliceConversionHelper.formatTime(call.duration), + style: const TextStyle(fontSize: 12), + ), + ), + Flexible( + child: Text( + '${AliceConversionHelper.formatBytes(call.request?.size ?? 0)} / ' + '${AliceConversionHelper.formatBytes(call.response?.size ?? 0)}', + style: const TextStyle(fontSize: 12), + ), + ), + ], + ); + } + + /// Formats call time. + String _formatTime(DateTime time) => '${formatTimeUnit(time.hour)}:' + '${formatTimeUnit(time.minute)}:' + '${formatTimeUnit(time.second)}:' + '${formatTimeUnit(time.millisecond)}'; + + /// Format one of time units. + String formatTimeUnit(int timeUnit) => timeUnit.toString().padLeft(2, '0'); +} diff --git a/packages/alice/lib/ui/widget/alice_calls_list_widget.dart b/packages/alice/lib/ui/calls_list/widget/alice_calls_list_screen.dart similarity index 79% rename from packages/alice/lib/ui/widget/alice_calls_list_widget.dart rename to packages/alice/lib/ui/calls_list/widget/alice_calls_list_screen.dart index f17f78a1..3dfd2151 100644 --- a/packages/alice/lib/ui/widget/alice_calls_list_widget.dart +++ b/packages/alice/lib/ui/calls_list/widget/alice_calls_list_screen.dart @@ -1,11 +1,12 @@ import 'package:alice/model/alice_http_call.dart'; -import 'package:alice/model/alice_sort_option.dart'; -import 'package:alice/ui/widget/alice_call_list_item_widget.dart'; +import 'package:alice/ui/calls_list/model/alice_calls_list_sort_option.dart'; +import 'package:alice/ui/calls_list/widget/alice_call_list_item_widget.dart'; import 'package:alice/utils/alice_scroll_behavior.dart'; import 'package:flutter/material.dart'; -class AliceCallsListWidget extends StatelessWidget { - const AliceCallsListWidget({ +/// Widget which displays calls list. It's hosted in tab in calls list page. +class AliceCallsListScreen extends StatelessWidget { + const AliceCallsListScreen({ super.key, required this.calls, this.sortOption, @@ -14,19 +15,19 @@ class AliceCallsListWidget extends StatelessWidget { }); final List calls; - final AliceSortOption? sortOption; + final AliceCallsListSortOption? sortOption; final bool sortAscending; final void Function(AliceHttpCall) onListItemClicked; List get _sortedCalls => switch (sortOption) { - AliceSortOption.time => sortAscending + AliceCallsListSortOption.time => sortAscending ? (calls ..sort((AliceHttpCall call1, AliceHttpCall call2) => call1.createdTime.compareTo(call2.createdTime))) : (calls ..sort((AliceHttpCall call1, AliceHttpCall call2) => call2.createdTime.compareTo(call1.createdTime))), - AliceSortOption.responseTime => sortAscending + AliceCallsListSortOption.responseTime => sortAscending ? (calls ..sort() ..sort((AliceHttpCall call1, AliceHttpCall call2) => @@ -34,7 +35,7 @@ class AliceCallsListWidget extends StatelessWidget { : (calls ..sort((AliceHttpCall call1, AliceHttpCall call2) => call2.response?.time.compareTo(call1.response!.time) ?? -1)), - AliceSortOption.responseCode => sortAscending + AliceCallsListSortOption.responseCode => sortAscending ? (calls ..sort((AliceHttpCall call1, AliceHttpCall call2) => call1.response?.status?.compareTo(call2.response!.status!) ?? @@ -43,14 +44,14 @@ class AliceCallsListWidget extends StatelessWidget { ..sort((AliceHttpCall call1, AliceHttpCall call2) => call2.response?.status?.compareTo(call1.response!.status!) ?? -1)), - AliceSortOption.responseSize => sortAscending + AliceCallsListSortOption.responseSize => sortAscending ? (calls ..sort((AliceHttpCall call1, AliceHttpCall call2) => call1.response?.size.compareTo(call2.response!.size) ?? -1)) : (calls ..sort((AliceHttpCall call1, AliceHttpCall call2) => call2.response?.size.compareTo(call1.response!.size) ?? -1)), - AliceSortOption.endpoint => sortAscending + AliceCallsListSortOption.endpoint => sortAscending ? (calls ..sort((AliceHttpCall call1, AliceHttpCall call2) => call1.endpoint.compareTo(call2.endpoint))) diff --git a/packages/alice/lib/ui/widget/alice_empty_logs_widget.dart b/packages/alice/lib/ui/calls_list/widget/alice_empty_logs_widget.dart similarity index 82% rename from packages/alice/lib/ui/widget/alice_empty_logs_widget.dart rename to packages/alice/lib/ui/calls_list/widget/alice_empty_logs_widget.dart index d17348a9..282a5561 100644 --- a/packages/alice/lib/ui/widget/alice_empty_logs_widget.dart +++ b/packages/alice/lib/ui/calls_list/widget/alice_empty_logs_widget.dart @@ -1,6 +1,7 @@ -import 'package:alice/utils/alice_constants.dart'; +import 'package:alice/utils/alice_theme.dart'; import 'package:flutter/material.dart'; +/// Widget which renders empty text for calls list. class AliceEmptyLogsWidget extends StatelessWidget { const AliceEmptyLogsWidget({ super.key, @@ -16,7 +17,7 @@ class AliceEmptyLogsWidget extends StatelessWidget { children: [ Icon( Icons.error_outline, - color: AliceConstants.orange, + color: AliceTheme.orange, ), SizedBox(height: 6), Text( diff --git a/packages/alice/lib/ui/widget/alice_log_list_widget.dart b/packages/alice/lib/ui/calls_list/widget/alice_log_list_widget.dart similarity index 90% rename from packages/alice/lib/ui/widget/alice_log_list_widget.dart rename to packages/alice/lib/ui/calls_list/widget/alice_log_list_widget.dart index a8820b65..4058768d 100644 --- a/packages/alice/lib/ui/widget/alice_log_list_widget.dart +++ b/packages/alice/lib/ui/calls_list/widget/alice_log_list_widget.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +/// Widget which renders log list for calls list page. class AliceLogListWidget extends StatefulWidget { const AliceLogListWidget({ required this.logsListenable, @@ -23,6 +24,7 @@ class AliceLogListWidget extends StatefulWidget { State createState() => _AliceLogListWidgetState(); } +/// State for logs list widget. class _AliceLogListWidgetState extends State { final DiagnosticLevel _minLevel = DiagnosticLevel.debug; @@ -47,7 +49,7 @@ class _AliceLogListWidgetState extends State { shrinkWrap: true, padding: const EdgeInsets.symmetric(vertical: 8), itemCount: filteredLogs.length, - itemBuilder: (context, i) => AliceLogEntryWidget(filteredLogs[i]), + itemBuilder: (context, i) => _AliceLogEntryWidget(filteredLogs[i]), ), ); }, @@ -55,8 +57,9 @@ class _AliceLogListWidgetState extends State { } } -class AliceLogEntryWidget extends StatelessWidget { - AliceLogEntryWidget(this.log) : super(key: ValueKey(log)); +/// Widget which renders one log entry in logs list. +class _AliceLogEntryWidget extends StatelessWidget { + _AliceLogEntryWidget(this.log) : super(key: ValueKey(log)); final AliceLog log; @@ -112,6 +115,7 @@ class AliceLogEntryWidget extends StatelessWidget { ); } + /// Formats log entry. List _toText( BuildContext context, String title, @@ -130,10 +134,12 @@ class AliceLogEntryWidget extends StatelessWidget { ]; } + /// Returns text color based on log level. Color _getTextColor(BuildContext context) { - return AliceTheme.getTextColor(context, log.level); + return AliceTheme.getLogTextColor(context, log.level); } + /// Returns icon based on log level. IconData _getLogIcon(DiagnosticLevel level) => switch (level) { DiagnosticLevel.hidden => Icons.all_inclusive_outlined, DiagnosticLevel.fine => Icons.bubble_chart_outlined, @@ -146,6 +152,7 @@ class AliceLogEntryWidget extends StatelessWidget { DiagnosticLevel.off => Icons.not_interested_outlined, }; + /// Copies to clipboard given error. Future _copyToClipboard(BuildContext context) async { final String? error = _stringify(log.error); final String? stackTrace = _stringify(log.stackTrace); @@ -165,6 +172,7 @@ class AliceLogEntryWidget extends StatelessWidget { } } + /// Formats text with json decode/encode. String? _stringify(dynamic object) { if (object == null) return null; if (object is String) return object.trim(); diff --git a/packages/alice/lib/ui/widget/alice_logs_widget.dart b/packages/alice/lib/ui/calls_list/widget/alice_logs_screen.dart similarity index 71% rename from packages/alice/lib/ui/widget/alice_logs_widget.dart rename to packages/alice/lib/ui/calls_list/widget/alice_logs_screen.dart index ad8668c4..ed49e728 100644 --- a/packages/alice/lib/ui/widget/alice_logs_widget.dart +++ b/packages/alice/lib/ui/calls_list/widget/alice_logs_screen.dart @@ -1,11 +1,12 @@ import 'package:alice/core/alice_logger.dart'; -import 'package:alice/ui/widget/alice_empty_logs_widget.dart'; -import 'package:alice/ui/widget/alice_log_list_widget.dart'; -import 'package:alice/ui/widget/alice_raw_log_list_widger.dart'; +import 'package:alice/ui/calls_list/widget/alice_empty_logs_widget.dart'; +import 'package:alice/ui/calls_list/widget/alice_log_list_widget.dart'; +import 'package:alice/ui/calls_list/widget/alice_raw_log_list_widger.dart'; import 'package:flutter/material.dart'; -class AliceLogsWidget extends StatelessWidget { - const AliceLogsWidget({ +/// Screen hosted in calls list which displays logs list. +class AliceLogsScreen extends StatelessWidget { + const AliceLogsScreen({ super.key, required this.scrollController, this.aliceLogger, diff --git a/packages/alice/lib/ui/widget/alice_raw_log_list_widger.dart b/packages/alice/lib/ui/calls_list/widget/alice_raw_log_list_widger.dart similarity index 96% rename from packages/alice/lib/ui/widget/alice_raw_log_list_widger.dart rename to packages/alice/lib/ui/calls_list/widget/alice_raw_log_list_widger.dart index 8d144047..2ab1a20a 100644 --- a/packages/alice/lib/ui/widget/alice_raw_log_list_widger.dart +++ b/packages/alice/lib/ui/calls_list/widget/alice_raw_log_list_widger.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +/// Widget which displays raw logs list (logs collected with ADB). class AliceRawLogListWidget extends StatelessWidget { const AliceRawLogListWidget({ required this.scrollController, diff --git a/packages/alice/lib/ui/calls_list/widget/alice_sort_dialog.dart b/packages/alice/lib/ui/calls_list/widget/alice_sort_dialog.dart new file mode 100644 index 00000000..6cba9274 --- /dev/null +++ b/packages/alice/lib/ui/calls_list/widget/alice_sort_dialog.dart @@ -0,0 +1,108 @@ +import 'package:alice/ui/calls_list/model/alice_calls_list_sort_option.dart'; +import 'package:flutter/material.dart'; + +/// Dialog which can be used to sort alice calls. +class AliceSortDialog extends StatelessWidget { + final AliceCallsListSortOption sortOption; + final bool sortAscending; + + const AliceSortDialog({ + super.key, + required this.sortOption, + required this.sortAscending, + }); + + @override + Widget build(BuildContext context) { + AliceCallsListSortOption currentSortOption = sortOption; + bool currentSortAscending = sortAscending; + return Theme( + data: ThemeData( + brightness: Brightness.light, + ), + child: StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: const Text('Select filter'), + content: Wrap( + children: [ + for (final AliceCallsListSortOption sortOption + in AliceCallsListSortOption.values) + RadioListTile( + title: Text(_getName( + option: sortOption, + )), + value: sortOption, + groupValue: currentSortOption, + onChanged: (AliceCallsListSortOption? value) { + if (value != null) { + setState(() { + currentSortOption = value; + }); + } + }, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Descending'), + Switch( + value: currentSortAscending, + onChanged: (value) { + setState(() { + currentSortAscending = value; + }); + }, + activeTrackColor: Colors.grey, + activeColor: Colors.white, + ), + const Text('Ascending'), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop( + AliceSortDialogResult( + sortOption: currentSortOption, + sortAscending: currentSortAscending, + ), + ); + }, + child: const Text('Use filter'), + ), + ], + ); + }, + ), + ); + } + + /// Get sort option name based on [option]. + String _getName({required AliceCallsListSortOption option}) { + return switch (option) { + AliceCallsListSortOption.time => 'Create time (default)', + AliceCallsListSortOption.responseTime => 'Response time', + AliceCallsListSortOption.responseCode => 'Response code', + AliceCallsListSortOption.responseSize => 'Response size', + AliceCallsListSortOption.endpoint => 'Endpoint', + }; + } +} + +/// Result of alice sort dialog. +class AliceSortDialogResult { + final AliceCallsListSortOption sortOption; + final bool sortAscending; + + AliceSortDialogResult({ + required this.sortOption, + required this.sortAscending, + }); +} diff --git a/packages/alice/lib/helper/alice_alert_helper.dart b/packages/alice/lib/ui/common/alice_dialog.dart similarity index 84% rename from packages/alice/lib/helper/alice_alert_helper.dart rename to packages/alice/lib/ui/common/alice_dialog.dart index 445327de..02ae6473 100644 --- a/packages/alice/lib/helper/alice_alert_helper.dart +++ b/packages/alice/lib/ui/common/alice_dialog.dart @@ -1,12 +1,13 @@ import 'package:alice/utils/alice_theme.dart'; import 'package:flutter/material.dart'; -class AliceAlertHelper { +/// General dialogs used in Alice. +class AliceGeneralDialog { /// Helper method used to open alarm with given title and description. - static Future showAlert( - BuildContext context, - String title, - String description, { + static Future show({ + required BuildContext context, + required String title, + required String description, String firstButtonTitle = 'Accept', String? secondButtonTitle, Function? firstButtonAction, @@ -16,9 +17,7 @@ class AliceAlertHelper { context: context, builder: (BuildContext buildContext) { return Theme( - data: ThemeData( - colorScheme: AliceTheme.getColorScheme(), - ), + data: AliceTheme.getTheme(), child: AlertDialog( title: Text(title), content: Text(description), diff --git a/packages/alice/lib/ui/common/alice_navigation.dart b/packages/alice/lib/ui/common/alice_navigation.dart new file mode 100644 index 00000000..6c2f5152 --- /dev/null +++ b/packages/alice/lib/ui/common/alice_navigation.dart @@ -0,0 +1,56 @@ +import 'package:alice/core/alice_core.dart'; +import 'package:alice/core/alice_logger.dart'; +import 'package:alice/model/alice_http_call.dart'; +import 'package:alice/ui/call_details/page/alice_call_details_page.dart'; +import 'package:alice/ui/calls_list/page/alice_calls_list_page.dart'; +import 'package:alice/ui/stats/alice_stats_page.dart'; +import 'package:flutter/material.dart'; + +/// Simple navigation helper class for Alice. +class AliceNavigation { + /// Navigates to calls list page. + static Future navigateToCallsList({ + required AliceCore core, + required AliceLogger logger, + }) { + return _navigateToPage( + core: core, + child: AliceCallsListPage(core: core, logger: logger), + ); + } + + /// Navigates to call details page. + static Future navigateToCallDetails({ + required AliceHttpCall call, + required AliceCore core, + }) { + return _navigateToPage( + core: core, child: AliceCallDetailsPage(call: call, core: core)); + } + + /// Navigates to stats page. + static Future navigateToStats({required AliceCore core}) { + return _navigateToPage( + core: core, + child: AliceStatsPage(core), + ); + } + + /// Common helper method which checks whether context is available for + /// navigation and navigates to a specific page. + static Future _navigateToPage({ + required AliceCore core, + required Widget child, + }) { + var context = core.getContext(); + if (context == null) { + throw StateError("Context is null in AliceCore."); + } + return Navigator.push( + context, + MaterialPageRoute( + builder: (context) => child, + ), + ); + } +} diff --git a/packages/alice/lib/ui/common/alice_page.dart b/packages/alice/lib/ui/common/alice_page.dart new file mode 100644 index 00000000..1f4167e1 --- /dev/null +++ b/packages/alice/lib/ui/common/alice_page.dart @@ -0,0 +1,22 @@ +import 'package:alice/core/alice_core.dart'; +import 'package:alice/utils/alice_theme.dart'; +import 'package:flutter/material.dart'; + +/// Common page widget which is used across Alice pages. +class AlicePage extends StatelessWidget { + const AlicePage({super.key, required this.core, required this.child}); + + final AliceCore core; + final Widget child; + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: core.directionality ?? Directionality.of(context), + child: Theme( + data: AliceTheme.getTheme(), + child: child, + ), + ); + } +} diff --git a/packages/alice/lib/ui/page/alice_call_details_screen.dart b/packages/alice/lib/ui/page/alice_call_details_screen.dart deleted file mode 100644 index bc4ac41f..00000000 --- a/packages/alice/lib/ui/page/alice_call_details_screen.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:alice/core/alice_core.dart'; -import 'package:alice/helper/alice_save_helper.dart'; -import 'package:alice/model/alice_http_call.dart'; -import 'package:alice/ui/widget/alice_call_error_widget.dart'; -import 'package:alice/ui/widget/alice_call_overview_widget.dart'; -import 'package:alice/ui/widget/alice_call_request_widget.dart'; -import 'package:alice/ui/widget/alice_call_response_widget.dart'; -import 'package:alice/utils/alice_constants.dart'; -import 'package:alice/utils/alice_theme.dart'; -import 'package:collection/collection.dart' show IterableExtension; -import 'package:flutter/material.dart'; -import 'package:share_plus/share_plus.dart'; - -class AliceCallDetailsScreen extends StatefulWidget { - final AliceHttpCall call; - final AliceCore core; - - const AliceCallDetailsScreen(this.call, this.core, {super.key}); - - @override - State createState() => _AliceCallDetailsScreenState(); -} - -class _AliceCallDetailsScreenState extends State - with SingleTickerProviderStateMixin { - AliceHttpCall get call => widget.call; - - @override - Widget build(BuildContext context) { - return Directionality( - textDirection: widget.core.directionality ?? Directionality.of(context), - child: Theme( - data: ThemeData( - colorScheme: AliceTheme.getColorScheme(), - ), - child: StreamBuilder>( - stream: widget.core.callsSubject, - initialData: [widget.call], - builder: (context, AsyncSnapshot> callsSnapshot) { - if (callsSnapshot.hasData && !callsSnapshot.hasError) { - final AliceHttpCall? call = callsSnapshot.data?.firstWhereOrNull( - (AliceHttpCall snapshotCall) => - snapshotCall.id == widget.call.id, - ); - if (call != null) { - return DefaultTabController( - length: 4, - child: Scaffold( - appBar: AppBar( - bottom: const TabBar( - indicatorColor: AliceConstants.lightRed, - tabs: [ - Tab(icon: Icon(Icons.info_outline), text: 'Overview'), - Tab(icon: Icon(Icons.arrow_upward), text: 'Request'), - Tab( - icon: Icon(Icons.arrow_downward), - text: 'Response'), - Tab( - icon: Icon(Icons.warning), - text: 'Error', - ), - ], - ), - title: const Text('Alice - HTTP Call Details'), - ), - body: TabBarView( - children: [ - AliceCallOverviewWidget(widget.call), - AliceCallRequestWidget(widget.call), - AliceCallResponseWidget(widget.call), - AliceCallErrorWidget(widget.call), - ], - ), - floatingActionButton: widget.core.showShareButton ?? false - ? FloatingActionButton( - backgroundColor: AliceConstants.lightRed, - key: const Key('share_key'), - onPressed: () async => await Share.share( - await AliceSaveHelper.buildCallLog(widget.call), - subject: 'Request Details', - ), - child: const Icon( - Icons.share, - color: AliceConstants.white, - ), - ) - : null, - ), - ); - } - } - - return const Center(child: Text('Failed to load data')); - }, - ), - ), - ); - } -} diff --git a/packages/alice/lib/ui/page/alice_calls_list_screen.dart b/packages/alice/lib/ui/page/alice_calls_list_screen.dart deleted file mode 100644 index 4df7b0f8..00000000 --- a/packages/alice/lib/ui/page/alice_calls_list_screen.dart +++ /dev/null @@ -1,384 +0,0 @@ -import 'package:alice/core/alice_core.dart'; -import 'package:alice/core/alice_logger.dart'; -import 'package:alice/helper/alice_alert_helper.dart'; -import 'package:alice/model/alice_http_call.dart'; -import 'package:alice/model/alice_menu_item.dart'; -import 'package:alice/model/alice_sort_option.dart'; -import 'package:alice/model/alice_tab_item.dart'; -import 'package:alice/ui/page/alice_call_details_screen.dart'; -import 'package:alice/ui/page/alice_stats_screen.dart'; -import 'package:alice/ui/widget/alice_calls_list_widget.dart'; -import 'package:alice/ui/widget/alice_empty_logs_widget.dart'; -import 'package:alice/ui/widget/alice_logs_widget.dart'; -import 'package:alice/utils/alice_constants.dart'; -import 'package:alice/utils/alice_theme.dart'; -import 'package:flutter/material.dart'; - -class AliceCallsListScreen extends StatefulWidget { - final AliceCore _aliceCore; - final AliceLogger? _aliceLogger; - - const AliceCallsListScreen( - this._aliceCore, - this._aliceLogger, { - super.key, - }); - - @override - State createState() => _AliceCallsListScreenState(); -} - -class _AliceCallsListScreenState extends State - with SingleTickerProviderStateMixin { - final TextEditingController _queryTextEditingController = - TextEditingController(); - static const List _menuItems = [ - AliceMenuItem('Sort', Icons.sort), - AliceMenuItem('Delete', Icons.delete), - AliceMenuItem('Stats', Icons.insert_chart), - AliceMenuItem('Save', Icons.save), - ]; - final List _tabItems = AliceTabItem.values; - late final ScrollController _scrollController = ScrollController(); - - AliceSortOption? _sortOption = AliceSortOption.time; - bool _sortAscending = false; - bool _searchEnabled = false; - bool isAndroidRawLogsEnabled = false; - int _selectedIndex = 0; - - late final TabController? _tabController; - - AliceCore get aliceCore => widget._aliceCore; - - @override - void initState() { - super.initState(); - - _tabController = TabController( - vsync: this, - length: _tabItems.length, - initialIndex: _tabItems.first.index, - ); - - WidgetsBinding.instance.addPostFrameCallback((_) { - _tabController?.addListener(() { - _onTabChanged(_tabController!.index); - }); - }); - } - - @override - void dispose() { - _queryTextEditingController.dispose(); - _tabController?.dispose(); - _scrollController.dispose(); - - super.dispose(); - } - - bool get isLoggerTab => _selectedIndex == 1; - - @override - Widget build(BuildContext context) { - return Directionality( - textDirection: - widget._aliceCore.directionality ?? Directionality.of(context), - child: Theme( - data: ThemeData( - colorScheme: AliceTheme.getColorScheme(), - ), - child: Scaffold( - appBar: AppBar( - title: _searchEnabled - ? TextField( - controller: _queryTextEditingController, - autofocus: true, - decoration: const InputDecoration( - hintText: 'Search http request...', - hintStyle: - TextStyle(fontSize: 16, color: AliceConstants.grey), - border: InputBorder.none, - ), - style: const TextStyle(fontSize: 16), - onChanged: _updateSearchQuery, - ) - : const Text('Alice'), - actions: isLoggerTab - ? [ - IconButton( - icon: const Icon(Icons.terminal), - onPressed: _onLogsChangeClicked, - ), - IconButton( - icon: const Icon(Icons.delete), - onPressed: _showClearLogsDialog, - ), - ] - : [ - IconButton( - icon: const Icon(Icons.search), - onPressed: _onSearchClicked, - ), - PopupMenuButton( - onSelected: _onMenuItemSelected, - itemBuilder: (BuildContext context) => [ - for (final AliceMenuItem item in _menuItems) - PopupMenuItem( - value: item, - child: Row( - children: [ - Icon( - item.iconData, - color: AliceConstants.lightRed, - ), - const Padding( - padding: EdgeInsets.only(left: 10), - ), - Text(item.title), - ], - ), - ), - ], - ), - ], - bottom: TabBar( - controller: _tabController, - indicatorColor: AliceConstants.orange, - tabs: [ - for (final AliceTabItem item in _tabItems) - Tab(text: item.title.toUpperCase()), - ], - ), - ), - body: TabBarView( - controller: _tabController, - children: [ - StreamBuilder>( - stream: aliceCore.callsSubject, - builder: - (context, AsyncSnapshot> snapshot) { - final List calls = snapshot.data ?? []; - final String query = _queryTextEditingController.text.trim(); - if (query.isNotEmpty) { - calls.removeWhere((AliceHttpCall call) => !call.endpoint - .toLowerCase() - .contains(query.toLowerCase())); - } - if (calls.isNotEmpty) { - return AliceCallsListWidget( - calls: calls, - sortOption: _sortOption, - sortAscending: _sortAscending, - onListItemClicked: _onListItemClicked, - ); - } else { - return const AliceEmptyLogsWidget(); - } - }, - ), - AliceLogsWidget( - scrollController: _scrollController, - aliceLogger: widget._aliceLogger, - isAndroidRawLogsEnabled: isAndroidRawLogsEnabled, - ), - ], - ), - floatingActionButton: isLoggerTab - ? Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FloatingActionButton( - heroTag: 'h1', - backgroundColor: AliceConstants.orange, - onPressed: () => _scrollLogsList(true), - child: const Icon( - Icons.arrow_upward, - color: AliceConstants.white, - ), - ), - const SizedBox(height: 8), - FloatingActionButton( - heroTag: 'h2', - backgroundColor: AliceConstants.orange, - onPressed: () => _scrollLogsList(false), - child: const Icon( - Icons.arrow_downward, - color: AliceConstants.white, - ), - ), - ], - ) - : const SizedBox(), - ), - ), - ); - } - - void _showClearLogsDialog() => AliceAlertHelper.showAlert( - context, - 'Delete logs', - 'Do you want to clear logs?', - firstButtonTitle: 'No', - secondButtonTitle: 'Yes', - secondButtonAction: _onLogsClearClicked, - ); - - void _onLogsChangeClicked() => setState(() { - isAndroidRawLogsEnabled = !isAndroidRawLogsEnabled; - }); - - void _onLogsClearClicked() => setState(() { - if (isAndroidRawLogsEnabled) { - widget._aliceLogger?.clearAndroidRawLogs(); - } else { - widget._aliceLogger?.clearLogs(); - } - }); - - void _onSearchClicked() => setState(() { - _searchEnabled = !_searchEnabled; - if (!_searchEnabled) { - _queryTextEditingController.text = ''; - } - }); - - void _onTabChanged(int index) => setState(() { - _selectedIndex = index; - if (_selectedIndex == 1) { - _searchEnabled = false; - _queryTextEditingController.text = ''; - } - }); - - void _onMenuItemSelected(AliceMenuItem menuItem) { - if (menuItem.title == 'Sort') { - _showSortDialog(); - } - if (menuItem.title == 'Delete') { - _showRemoveDialog(); - } - if (menuItem.title == 'Stats') { - _showStatsScreen(); - } - if (menuItem.title == 'Save') { - _saveToFile(); - } - } - - void _onListItemClicked(AliceHttpCall call) => Navigator.push( - widget._aliceCore.getContext()!, - MaterialPageRoute( - builder: (_) => AliceCallDetailsScreen(call, widget._aliceCore), - ), - ); - - void _showRemoveDialog() => AliceAlertHelper.showAlert( - context, - 'Delete calls', - 'Do you want to delete http calls?', - firstButtonTitle: 'No', - firstButtonAction: () => {}, - secondButtonTitle: 'Yes', - secondButtonAction: _removeCalls, - ); - - void _removeCalls() => aliceCore.removeCalls(); - - void _showStatsScreen() { - Navigator.push( - aliceCore.getContext()!, - MaterialPageRoute( - builder: (context) => AliceStatsScreen(widget._aliceCore), - ), - ); - } - - void _saveToFile() => aliceCore.saveHttpRequests(context); - - void _updateSearchQuery(String query) => setState(() {}); - - Future _showSortDialog() => showDialog( - context: context, - builder: (BuildContext buildContext) => Theme( - data: ThemeData( - brightness: Brightness.light, - ), - child: AlertDialog( - title: const Text('Select filter'), - content: StatefulBuilder( - builder: (context, setState) => Wrap( - children: [ - for (final AliceSortOption sortOption - in AliceSortOption.values) - RadioListTile( - title: Text(sortOption.name), - value: sortOption, - groupValue: _sortOption, - onChanged: (AliceSortOption? value) { - setState(() { - _sortOption = value; - }); - }, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Descending'), - Switch( - value: _sortAscending, - onChanged: (value) { - setState(() { - _sortAscending = value; - }); - }, - activeTrackColor: Colors.grey, - activeColor: Colors.white, - ), - const Text('Ascending'), - ], - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - sortCalls(); - }, - child: const Text('Use filter'), - ), - ], - ), - ), - ); - - void sortCalls() => setState(() {}); - - void _scrollLogsList(bool top) => top ? _scrollToTop() : _scrollToBottom(); - - void _scrollToTop() { - if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.minScrollExtent, - duration: const Duration(microseconds: 500), - curve: Curves.ease, - ); - } - } - - void _scrollToBottom() { - if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: const Duration(microseconds: 500), - curve: Curves.ease, - ); - } - } -} diff --git a/packages/alice/lib/ui/page/alice_stats_screen.dart b/packages/alice/lib/ui/page/alice_stats_screen.dart deleted file mode 100644 index e565d892..00000000 --- a/packages/alice/lib/ui/page/alice_stats_screen.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'package:alice/core/alice_core.dart'; -import 'package:alice/helper/alice_conversion_helper.dart'; -import 'package:alice/model/alice_http_call.dart'; -import 'package:alice/ui/widget/alice_stats_row.dart'; -import 'package:alice/utils/alice_theme.dart'; -import 'package:alice/utils/num_comparison.dart'; -import 'package:flutter/material.dart'; - -class AliceStatsScreen extends StatelessWidget { - final AliceCore aliceCore; - - const AliceStatsScreen( - this.aliceCore, { - super.key, - }); - - int _getTotalRequests() => calls.length; - - int _getSuccessRequests() => calls - .where( - (AliceHttpCall call) => - (call.response?.status.gte(200) ?? false) && - (call.response?.status.lt(300) ?? false), - ) - .toList() - .length; - - int _getRedirectionRequests() => calls - .where( - (AliceHttpCall call) => - (call.response?.status.gte(300) ?? false) && - (call.response?.status.lt(400) ?? false), - ) - .toList() - .length; - - int _getErrorRequests() => calls - .where( - (AliceHttpCall call) => - (call.response?.status.gte(400) ?? false) && - (call.response?.status.lt(600) ?? false), - ) - .toList() - .length; - - int _getPendingRequests() => - calls.where((AliceHttpCall call) => call.loading).toList().length; - - int _getBytesSent() => calls.fold( - 0, - (int sum, AliceHttpCall call) => sum + (call.request?.size ?? 0), - ); - - int _getBytesReceived() => calls.fold( - 0, - (int sum, AliceHttpCall call) => sum + (call.response?.size ?? 0), - ); - - int _getAverageRequestTime() { - int requestTimeSum = 0; - int requestsWithDurationCount = 0; - for (final AliceHttpCall call in calls) { - if (call.duration != 0) { - requestTimeSum = call.duration; - requestsWithDurationCount++; - } - } - if (requestTimeSum == 0) { - return 0; - } - return requestTimeSum ~/ requestsWithDurationCount; - } - - int _getMaxRequestTime() { - int maxRequestTime = 0; - for (final AliceHttpCall call in calls) { - if (call.duration > maxRequestTime) { - maxRequestTime = call.duration; - } - } - return maxRequestTime; - } - - int _getMinRequestTime() { - int minRequestTime = 10000000; - if (calls.isEmpty) { - minRequestTime = 0; - } else { - for (final AliceHttpCall call in calls) { - if (call.duration != 0 && call.duration < minRequestTime) { - minRequestTime = call.duration; - } - } - } - return minRequestTime; - } - - int _getRequests(String requestType) => - calls.where((call) => call.method == requestType).toList().length; - - int _getSecuredRequests() => - calls.where((call) => call.secure).toList().length; - - int _getUnsecuredRequests() => - calls.where((call) => !call.secure).toList().length; - - List get calls => aliceCore.callsSubject.value; - - @override - Widget build(BuildContext context) { - return Directionality( - textDirection: aliceCore.directionality ?? Directionality.of(context), - child: Theme( - data: ThemeData(colorScheme: AliceTheme.getColorScheme()), - child: Scaffold( - appBar: AppBar( - title: const Text('Alice - HTTP Inspector - Stats'), - ), - body: Container( - padding: const EdgeInsets.all(8), - child: ListView( - children: [ - AliceStatsRow('Total requests:', '${_getTotalRequests()}'), - AliceStatsRow('Pending requests:', '${_getPendingRequests()}'), - AliceStatsRow('Success requests:', '${_getSuccessRequests()}'), - AliceStatsRow( - 'Redirection requests:', - '${_getRedirectionRequests()}', - ), - AliceStatsRow('Error requests:', '${_getErrorRequests()}'), - AliceStatsRow( - 'Bytes send:', - AliceConversionHelper.formatBytes(_getBytesSent()), - ), - AliceStatsRow( - 'Bytes received:', - AliceConversionHelper.formatBytes(_getBytesReceived()), - ), - AliceStatsRow( - 'Average request time:', - AliceConversionHelper.formatTime(_getAverageRequestTime()), - ), - AliceStatsRow( - 'Max request time:', - AliceConversionHelper.formatTime(_getMaxRequestTime()), - ), - AliceStatsRow( - 'Min request time:', - AliceConversionHelper.formatTime(_getMinRequestTime()), - ), - AliceStatsRow('Get requests:', '${_getRequests('GET')} '), - AliceStatsRow('Post requests:', '${_getRequests('POST')} '), - AliceStatsRow('Delete requests:', '${_getRequests('DELETE')} '), - AliceStatsRow('Put requests:', '${_getRequests('PUT')} '), - AliceStatsRow('Patch requests:', '${_getRequests('PATCH')} '), - AliceStatsRow('Secured requests:', '${_getSecuredRequests()}'), - AliceStatsRow( - 'Unsecured requests:', '${_getUnsecuredRequests()}'), - ], - ), - ), - ), - ), - ); - } -} diff --git a/packages/alice/lib/ui/stats/alice_stats_page.dart b/packages/alice/lib/ui/stats/alice_stats_page.dart new file mode 100644 index 00000000..d139046e --- /dev/null +++ b/packages/alice/lib/ui/stats/alice_stats_page.dart @@ -0,0 +1,178 @@ +import 'package:alice/core/alice_core.dart'; +import 'package:alice/helper/alice_conversion_helper.dart'; +import 'package:alice/model/alice_http_call.dart'; +import 'package:alice/ui/common/alice_page.dart'; +import 'package:alice/ui/widget/alice_stats_row.dart'; +import 'package:alice/utils/num_comparison.dart'; +import 'package:flutter/material.dart'; + +/// General stats page for currently caught HTTP calls. +class AliceStatsPage extends StatelessWidget { + final AliceCore aliceCore; + + const AliceStatsPage( + this.aliceCore, { + super.key, + }); + + @override + Widget build(BuildContext context) { + return AlicePage( + core: aliceCore, + child: Scaffold( + appBar: AppBar( + title: const Text('Alice - HTTP Inspector - Stats'), + ), + body: Container( + padding: const EdgeInsets.all(8), + child: ListView( + children: [ + AliceStatsRow('Total requests:', '${_getTotalRequests()}'), + AliceStatsRow('Pending requests:', '${_getPendingRequests()}'), + AliceStatsRow('Success requests:', '${_getSuccessRequests()}'), + AliceStatsRow( + 'Redirection requests:', + '${_getRedirectionRequests()}', + ), + AliceStatsRow('Error requests:', '${_getErrorRequests()}'), + AliceStatsRow( + 'Bytes send:', + AliceConversionHelper.formatBytes(_getBytesSent()), + ), + AliceStatsRow( + 'Bytes received:', + AliceConversionHelper.formatBytes(_getBytesReceived()), + ), + AliceStatsRow( + 'Average request time:', + AliceConversionHelper.formatTime(_getAverageRequestTime()), + ), + AliceStatsRow( + 'Max request time:', + AliceConversionHelper.formatTime(_getMaxRequestTime()), + ), + AliceStatsRow( + 'Min request time:', + AliceConversionHelper.formatTime(_getMinRequestTime()), + ), + AliceStatsRow('Get requests:', '${_getRequests('GET')} '), + AliceStatsRow('Post requests:', '${_getRequests('POST')} '), + AliceStatsRow('Delete requests:', '${_getRequests('DELETE')} '), + AliceStatsRow('Put requests:', '${_getRequests('PUT')} '), + AliceStatsRow('Patch requests:', '${_getRequests('PATCH')} '), + AliceStatsRow('Secured requests:', '${_getSecuredRequests()}'), + AliceStatsRow( + 'Unsecured requests:', '${_getUnsecuredRequests()}'), + ], + ), + ), + ), + ); + } + + /// Returns count of requests. + int _getTotalRequests() => _calls.length; + + /// Returns count of success requests. + int _getSuccessRequests() => _calls + .where( + (AliceHttpCall call) => + (call.response?.status.gte(200) ?? false) && + (call.response?.status.lt(300) ?? false), + ) + .toList() + .length; + + /// Returns count of redirection requests. + int _getRedirectionRequests() => _calls + .where( + (AliceHttpCall call) => + (call.response?.status.gte(300) ?? false) && + (call.response?.status.lt(400) ?? false), + ) + .toList() + .length; + + /// Returns count of error requests. + int _getErrorRequests() => _calls + .where( + (AliceHttpCall call) => + (call.response?.status.gte(400) ?? false) && + (call.response?.status.lt(600) ?? false), + ) + .toList() + .length; + + /// Returns count of pending requests. + int _getPendingRequests() => + _calls.where((AliceHttpCall call) => call.loading).toList().length; + + /// Returns total bytes sent count. + int _getBytesSent() => _calls.fold( + 0, + (int sum, AliceHttpCall call) => sum + (call.request?.size ?? 0), + ); + + /// Returns total bytes received count. + int _getBytesReceived() => _calls.fold( + 0, + (int sum, AliceHttpCall call) => sum + (call.response?.size ?? 0), + ); + + /// Returns average request time of all calls. + int _getAverageRequestTime() { + int requestTimeSum = 0; + int requestsWithDurationCount = 0; + for (final AliceHttpCall call in _calls) { + if (call.duration != 0) { + requestTimeSum = call.duration; + requestsWithDurationCount++; + } + } + if (requestTimeSum == 0) { + return 0; + } + return requestTimeSum ~/ requestsWithDurationCount; + } + + /// Returns max request time of all calls. + int _getMaxRequestTime() { + int maxRequestTime = 0; + for (final AliceHttpCall call in _calls) { + if (call.duration > maxRequestTime) { + maxRequestTime = call.duration; + } + } + return maxRequestTime; + } + + /// Returns min request time of all calls. + int _getMinRequestTime() { + int minRequestTime = 10000000; + if (_calls.isEmpty) { + minRequestTime = 0; + } else { + for (final AliceHttpCall call in _calls) { + if (call.duration != 0 && call.duration < minRequestTime) { + minRequestTime = call.duration; + } + } + } + return minRequestTime; + } + + /// Get all requests with [requestType]. + int _getRequests(String requestType) => + _calls.where((call) => call.method == requestType).toList().length; + + /// Get all secured requests count. + int _getSecuredRequests() => + _calls.where((call) => call.secure).toList().length; + + /// Get unsecured requests count. + int _getUnsecuredRequests() => + _calls.where((call) => !call.secure).toList().length; + + /// Get all calls from Alice. + List get _calls => aliceCore.callsSubject.value; +} diff --git a/packages/alice/lib/ui/widget/alice_base_call_details_widget.dart b/packages/alice/lib/ui/widget/alice_base_call_details_widget.dart deleted file mode 100644 index fe8bcf19..00000000 --- a/packages/alice/lib/ui/widget/alice_base_call_details_widget.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'dart:convert' show JsonEncoder; - -import 'package:alice/helper/alice_conversion_helper.dart'; -import 'package:alice/utils/alice_parser.dart'; -import 'package:flutter/material.dart'; - -abstract class AliceBaseCallDetailsWidgetState - extends State { - final JsonEncoder encoder = const JsonEncoder.withIndent(' '); - - Widget getListRow(String name, String? value) => Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - name, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const Padding( - padding: EdgeInsets.only(left: 5), - ), - Flexible( - child: value != null - ? SelectableText( - value, - ) - : const SizedBox(), - ), - const Padding( - padding: EdgeInsets.only(bottom: 18), - ), - ], - ); - - Widget getExpandableListRow(String name, String value) => Theme( - data: Theme.of(context).copyWith(dividerColor: Colors.transparent), - child: ExpansionTile( - title: Text( - name, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), - ), - subtitle: Text( - value, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - controlAffinity: ListTileControlAffinity.trailing, - tilePadding: const EdgeInsets.all(0), - children: [ - SelectableText( - value, - ), - ], - ), - ); - - String formatBytes(int bytes) => AliceConversionHelper.formatBytes(bytes); - - String formatDuration(int duration) => - AliceConversionHelper.formatTime(duration); - - String formatBody(dynamic body, String? contentType) => - AliceParser.formatBody(body, contentType); - - String? getContentType(Map? headers) => - AliceParser.getContentType(headers); -} diff --git a/packages/alice/lib/ui/widget/alice_call_list_item_widget.dart b/packages/alice/lib/ui/widget/alice_call_list_item_widget.dart deleted file mode 100644 index e1cc28c0..00000000 --- a/packages/alice/lib/ui/widget/alice_call_list_item_widget.dart +++ /dev/null @@ -1,178 +0,0 @@ -import 'package:alice/helper/alice_conversion_helper.dart'; -import 'package:alice/model/alice_http_call.dart'; -import 'package:alice/model/alice_http_response.dart'; -import 'package:alice/utils/alice_constants.dart'; -import 'package:flutter/material.dart'; - -const int _endpointMaxLines = 10; -const int _serverMaxLines = 5; - -class AliceCallListItemWidget extends StatelessWidget { - const AliceCallListItemWidget( - this.call, - this.itemClickAction, { - super.key, - }); - - final AliceHttpCall call; - final void Function(AliceHttpCall) itemClickAction; - - String _formatTime(DateTime time) => '${formatTimeUnit(time.hour)}:' - '${formatTimeUnit(time.minute)}:' - '${formatTimeUnit(time.second)}:' - '${formatTimeUnit(time.millisecond)}'; - - String formatTimeUnit(int timeUnit) => timeUnit.toString().padLeft(2, '0'); - - Color? _getStatusTextColor(BuildContext context) => - switch (call.response?.status) { - -1 => AliceConstants.red, - int status when status < 200 => - Theme.of(context).textTheme.bodyLarge?.color, - int status when status >= 200 && status < 300 => AliceConstants.green, - int status when status >= 300 && status < 400 => AliceConstants.orange, - int status when status >= 400 && status < 600 => AliceConstants.red, - _ => Theme.of(context).textTheme.bodyLarge?.color, - }; - - Color? _getEndpointTextColor(BuildContext context) => - call.loading ? AliceConstants.grey : _getStatusTextColor(context); - - String _getStatus(AliceHttpResponse response) => switch (response.status) { - -1 => 'ERR', - 0 => '???', - _ => '${response.status}', - }; - - @override - Widget build(BuildContext context) { - final Color? color = _getEndpointTextColor(context); - - return InkWell( - onTap: () => itemClickAction.call(call), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - call.method, - style: TextStyle(fontSize: 16, color: color), - ), - const Padding( - padding: EdgeInsets.only(left: 10), - ), - Flexible( - // ignore: avoid_unnecessary_containers - child: Container( - child: Text( - call.endpoint, - maxLines: _endpointMaxLines, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 16, color: color), - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 3), - child: Icon( - call.secure - ? Icons.lock_outline - : Icons.lock_open, - color: call.secure - ? AliceConstants.green - : AliceConstants.red, - size: 12, - ), - ), - Expanded( - child: Text( - call.server, - overflow: TextOverflow.ellipsis, - maxLines: _serverMaxLines, - style: const TextStyle( - fontSize: 14, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - call.request?.time != null - ? _formatTime(call.request!.time) - : 'n/a', - style: const TextStyle(fontSize: 12), - ), - ), - Flexible( - child: Text( - AliceConversionHelper.formatTime(call.duration), - style: const TextStyle(fontSize: 12), - ), - ), - Flexible( - child: Text( - '${AliceConversionHelper.formatBytes(call.request?.size ?? 0)} / ' - '${AliceConversionHelper.formatBytes(call.response?.size ?? 0)}', - style: const TextStyle(fontSize: 12), - ), - ), - ], - ), - ], - ), - ), - SizedBox( - width: 50, - child: Column( - children: [ - if (call.loading) ...[ - const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - AliceConstants.lightRed, - ), - ), - ), - const SizedBox(height: 4), - ], - if (call.response != null) - Text( - _getStatus(call.response!), - style: TextStyle( - fontSize: 16, - color: _getStatusTextColor(context), - ), - ) - ], - ), - ), - ], - ), - ), - const Divider(height: 1, color: AliceConstants.grey), - ], - ), - ); - } -} diff --git a/packages/alice/lib/ui/widget/alice_call_overview_widget.dart b/packages/alice/lib/ui/widget/alice_call_overview_widget.dart deleted file mode 100644 index a5453261..00000000 --- a/packages/alice/lib/ui/widget/alice_call_overview_widget.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:alice/model/alice_http_call.dart'; -import 'package:alice/ui/widget/alice_base_call_details_widget.dart'; -import 'package:alice/utils/alice_scroll_behavior.dart'; -import 'package:flutter/material.dart'; - -class AliceCallOverviewWidget extends StatefulWidget { - final AliceHttpCall call; - - const AliceCallOverviewWidget(this.call, {super.key}); - - @override - State createState() { - return _AliceCallOverviewWidget(); - } -} - -class _AliceCallOverviewWidget - extends AliceBaseCallDetailsWidgetState { - AliceHttpCall get _call => widget.call; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(6), - child: ScrollConfiguration( - behavior: AliceScrollBehavior(), - child: ListView( - children: [ - getListRow('Method: ', _call.method), - getListRow('Server: ', _call.server), - getListRow('Endpoint: ', _call.endpoint), - getListRow('Started:', _call.request?.time.toString()), - getListRow('Finished:', _call.response?.time.toString()), - getListRow('Duration:', formatDuration(_call.duration)), - getListRow('Bytes sent:', formatBytes(_call.request?.size ?? 0)), - getListRow( - 'Bytes received:', - formatBytes(_call.response?.size ?? 0), - ), - getListRow('Client:', _call.client), - getListRow('Secure:', _call.secure.toString()), - ], - ), - ), - ); - } -} diff --git a/packages/alice/lib/ui/widget/alice_call_request_widget.dart b/packages/alice/lib/ui/widget/alice_call_request_widget.dart deleted file mode 100644 index 4b9719f8..00000000 --- a/packages/alice/lib/ui/widget/alice_call_request_widget.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:alice/model/alice_form_data_file.dart'; -import 'package:alice/model/alice_from_data_field.dart'; -import 'package:alice/model/alice_http_call.dart'; -import 'package:alice/ui/widget/alice_base_call_details_widget.dart'; -import 'package:alice/utils/alice_scroll_behavior.dart'; -import 'package:flutter/material.dart'; - -class AliceCallRequestWidget extends StatefulWidget { - final AliceHttpCall call; - - const AliceCallRequestWidget(this.call, {super.key}); - - @override - State createState() { - return _AliceCallRequestWidget(); - } -} - -class _AliceCallRequestWidget - extends AliceBaseCallDetailsWidgetState { - AliceHttpCall get _call => widget.call; - - @override - Widget build(BuildContext context) { - final List rows = [ - getListRow('Started:', _call.request?.time.toString()), - getListRow('Bytes sent:', formatBytes(_call.request?.size ?? 0)), - getListRow('Content type:', getContentType(_call.request?.headers)), - ]; - - final dynamic body = _call.request?.body; - final String bodyContent = body != null - ? formatBody(body, getContentType(_call.request?.headers)) - : 'Body is empty'; - rows.add(getListRow('Body:', bodyContent)); - - final List? formDataFields = - _call.request?.formDataFields; - if (formDataFields?.isNotEmpty ?? false) { - rows.add(getListRow('Form data fields: ', '')); - rows.addAll([ - for (final AliceFormDataField field in formDataFields!) - getListRow(' • ${field.name}:', field.value) - ]); - } - - final List? formDataFiles = _call.request!.formDataFiles; - if (formDataFiles?.isNotEmpty ?? false) { - rows.add(getListRow('Form data files: ', '')); - rows.addAll([ - for (final AliceFormDataFile file in formDataFiles!) - getListRow( - ' • ${file.fileName}:', - '${file.contentType} / ${file.length} B', - ) - ]); - } - - final Map? headers = _call.request?.headers; - final String headersContent = - headers?.isEmpty ?? true ? 'Headers are empty' : ''; - rows.add(getListRow('Headers: ', headersContent)); - rows.addAll([ - for (final MapEntry header - in _call.request?.headers.entries ?? []) - getListRow(' • ${header.key}:', header.value.toString()) - ]); - - final Map? queryParameters = - _call.request?.queryParameters; - final String queryParametersContent = - queryParameters?.isEmpty ?? true ? 'Query parameters are empty' : ''; - rows.add(getListRow('Query Parameters: ', queryParametersContent)); - rows.addAll([ - for (final MapEntry qParam - in _call.request?.queryParameters.entries ?? []) - getListRow(' • ${qParam.key}:', qParam.value.toString()) - ]); - - return Container( - padding: const EdgeInsets.all(6), - child: ScrollConfiguration( - behavior: AliceScrollBehavior(), - child: ListView(children: rows), - ), - ); - } -} diff --git a/packages/alice/lib/ui/widget/alice_call_response_widget.dart b/packages/alice/lib/ui/widget/alice_call_response_widget.dart deleted file mode 100644 index 98a01450..00000000 --- a/packages/alice/lib/ui/widget/alice_call_response_widget.dart +++ /dev/null @@ -1,284 +0,0 @@ -import 'package:alice/model/alice_http_call.dart'; -import 'package:alice/ui/widget/alice_base_call_details_widget.dart'; -import 'package:alice/utils/alice_constants.dart'; -import 'package:alice/utils/alice_scroll_behavior.dart'; -import 'package:alice/utils/num_comparison.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class AliceCallResponseWidget extends StatefulWidget { - final AliceHttpCall call; - - const AliceCallResponseWidget(this.call, {super.key}); - - @override - State createState() { - return _AliceCallResponseWidgetState(); - } -} - -class _AliceCallResponseWidgetState - extends AliceBaseCallDetailsWidgetState { - static const String _imageContentType = 'image'; - static const String _videoContentType = 'video'; - static const String _jsonContentType = 'json'; - static const String _xmlContentType = 'xml'; - static const String _textContentType = 'text'; - - static const int _kLargeOutputSize = 100000; - bool _showLargeBody = false; - bool _showUnsupportedBody = false; - - AliceHttpCall get _call => widget.call; - - @override - Widget build(BuildContext context) { - if (!_call.loading) { - return Container( - padding: const EdgeInsets.all(6), - child: ScrollConfiguration( - behavior: AliceScrollBehavior(), - child: ListView(children: [ - ..._buildGeneralDataRows(), - ..._buildHeadersRows(), - ..._buildBodyRows(), - ]), - ), - ); - } else { - return const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [CircularProgressIndicator(), Text('Awaiting response...')], - ), - ); - } - } - - List _buildGeneralDataRows() { - final rows = [ - getListRow('Received:', _call.response?.time.toString()), - getListRow('Bytes received:', formatBytes(_call.response?.size ?? 0)), - ]; - - final int? status = _call.response?.status; - final String statusText = status == -1 ? 'Error' : '$status'; - - rows.add(getListRow('Status:', statusText)); - return rows; - } - - List _buildHeadersRows() { - final List rows = []; - final Map? headers = _call.response?.headers; - final String headersContent = - headers?.isEmpty ?? true ? 'Headers are empty' : ''; - rows.add(getListRow('Headers: ', headersContent)); - rows.addAll([ - for (final MapEntry header - in _call.response?.headers?.entries ?? []) - getListRow(' • ${header.key}:', header.value.toString()) - ]); - - return rows; - } - - List _buildBodyRows() => [ - if (_isImageResponse()) - ..._buildImageBodyRows() - else if (_isVideoResponse()) - ..._buildVideoBodyRows() - else if (_isTextResponse()) ...[ - if (_isLargeResponseBody()) - ..._buildLargeBodyTextRows() - else - ..._buildTextBodyRows(), - ] else - ..._buildUnknownBodyRows() - ]; - - List _buildImageBodyRows() { - return [ - Column( - children: [ - const Row( - children: [ - Text( - 'Body: Image', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - const SizedBox(height: 8), - Image.network( - _call.uri, - fit: BoxFit.fill, - headers: _buildRequestHeaders(), - loadingBuilder: ( - BuildContext context, - Widget child, - ImageChunkEvent? loadingProgress, - ) { - if (loadingProgress == null) return child; - return Center( - child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), - ); - }, - ), - const SizedBox(height: 8), - ], - ), - ]; - } - - List _buildLargeBodyTextRows() { - final rows = []; - if (_showLargeBody) { - return _buildTextBodyRows(); - } else { - rows - ..add( - getListRow( - 'Body:', - 'Too large to show ' - '(${_call.response?.body.toString().length ?? 0} Bytes)', - ), - ) - ..add(const SizedBox(height: 8)) - ..add( - ElevatedButton( - style: ButtonStyle( - backgroundColor: - WidgetStateProperty.all(AliceConstants.lightRed), - ), - onPressed: () { - setState(() { - _showLargeBody = true; - }); - }, - child: const Text('Show body'), - ), - ) - ..add(const SizedBox(height: 8)) - ..add(const Text('Warning! It will take some time to render output.')); - } - return rows; - } - - List _buildTextBodyRows() { - final List rows = []; - final Map? headers = _call.response?.headers; - final String bodyContent = - formatBody(_call.response?.body, getContentType(headers)); - rows.add(getListRow('Body:', bodyContent)); - return rows; - } - - List _buildVideoBodyRows() { - final rows = [ - const Row( - children: [ - Text( - 'Body: Video', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - const SizedBox(height: 8), - TextButton( - child: const Text('Open video in web browser'), - onPressed: () async { - await launchUrl(Uri.parse(_call.uri)); - }, - ), - const SizedBox(height: 8), - ]; - - return rows; - } - - List _buildUnknownBodyRows() { - final List rows = []; - final Map? headers = _call.response?.headers; - final String contentType = getContentType(headers) ?? ''; - - if (_showUnsupportedBody) { - final bodyContent = - formatBody(_call.response?.body, getContentType(headers)); - rows.add(getListRow('Body:', bodyContent)); - } else { - rows - ..add( - getListRow( - 'Body:', - 'Unsupported body. Alice can render video/image/text body. ' - "Response has Content-Type: $contentType which can't be " - "handled. If you're feeling lucky you can try button below " - 'to try render body as text, but it may fail.'), - ) - ..add( - ElevatedButton( - style: ButtonStyle( - backgroundColor: - WidgetStateProperty.all(AliceConstants.lightRed), - ), - onPressed: () { - setState(() { - _showUnsupportedBody = true; - }); - }, - child: const Text('Show unsupported body'), - ), - ); - } - return rows; - } - - Map _buildRequestHeaders() { - final requestHeaders = {}; - if (_call.request?.headers != null) { - requestHeaders.addAll( - _call.request!.headers.map( - (String key, dynamic value) => MapEntry( - key, - value.toString(), - ), - ), - ); - } - return requestHeaders; - } - - bool _isImageResponse() { - return _getContentTypeOfResponse()! - .toLowerCase() - .contains(_imageContentType); - } - - bool _isVideoResponse() { - return _getContentTypeOfResponse()! - .toLowerCase() - .contains(_videoContentType); - } - - bool _isTextResponse() { - final responseContentTypeLowerCase = - _getContentTypeOfResponse()!.toLowerCase(); - - return responseContentTypeLowerCase.contains(_jsonContentType) || - responseContentTypeLowerCase.contains(_xmlContentType) || - responseContentTypeLowerCase.contains(_textContentType); - } - - String? _getContentTypeOfResponse() { - return getContentType(_call.response?.headers); - } - - bool _isLargeResponseBody() => - _call.response?.body.toString().length.gt(_kLargeOutputSize) ?? false; -} diff --git a/packages/alice/lib/ui/widget/alice_stats_row.dart b/packages/alice/lib/ui/widget/alice_stats_row.dart index 5bafcecb..e9333cc0 100644 --- a/packages/alice/lib/ui/widget/alice_stats_row.dart +++ b/packages/alice/lib/ui/widget/alice_stats_row.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +/// Line of texts used in stats. class AliceStatsRow extends StatelessWidget { const AliceStatsRow( this.label, diff --git a/packages/alice/lib/utils/alice_constants.dart b/packages/alice/lib/utils/alice_constants.dart deleted file mode 100644 index 6233aa5f..00000000 --- a/packages/alice/lib/utils/alice_constants.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class AliceConstants { - static const Color red = Color(0xffff3f34); - static const Color lightRed = Color(0xffff5e57); - static const Color green = Color(0xff05c46b); - static const Color grey = Color(0xff808e9b); - static const Color orange = Color(0xffffa801); - static const Color white = Color(0xffffffff); -} diff --git a/packages/alice/lib/utils/alice_parser.dart b/packages/alice/lib/utils/alice_parser.dart index 1b25e606..08e2ab15 100644 --- a/packages/alice/lib/utils/alice_parser.dart +++ b/packages/alice/lib/utils/alice_parser.dart @@ -1,6 +1,7 @@ import 'dart:convert'; -class AliceParser { +/// Body parser helper used to parsing body data. +class AliceBodyParser { static const String _emptyBody = 'Body is empty'; static const String _unknownContentType = 'Unknown'; static const String _jsonContentTypeSmall = 'content-type'; @@ -10,6 +11,7 @@ class AliceParser { static const String _parseFailedText = 'Failed to parse '; static const JsonEncoder encoder = JsonEncoder.withIndent(' '); + /// Tries to parse json. If it fails, it will return the json itself. static String _parseJson(dynamic json) { try { return encoder.convert(json); @@ -18,6 +20,7 @@ class AliceParser { } } + /// Tries to parse json. If it fails, it will return the json itself. static dynamic _decodeJson(dynamic body) { try { return json.decode(body as String); @@ -26,6 +29,9 @@ class AliceParser { } } + /// Formats body based on [contentType]. If body is null it will return + /// [_emptyBody]. Otherwise if body type is json - it will try to format it. + /// static String formatBody(dynamic body, String? contentType) { try { if (body == null) { @@ -65,6 +71,8 @@ class AliceParser { } } + /// Get content type from [headers]. It looks for json and if it can't find + /// it, it will return unknown content type. static String? getContentType(Map? headers) { if (headers != null) { if (headers.containsKey(_jsonContentTypeSmall)) { diff --git a/packages/alice/lib/utils/alice_scroll_behavior.dart b/packages/alice/lib/utils/alice_scroll_behavior.dart index e834dd61..4f41e4c0 100644 --- a/packages/alice/lib/utils/alice_scroll_behavior.dart +++ b/packages/alice/lib/utils/alice_scroll_behavior.dart @@ -2,11 +2,11 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +/// Scroll behavior for Alice. class AliceScrollBehavior extends MaterialScrollBehavior { @override Set get dragDevices => { PointerDeviceKind.touch, PointerDeviceKind.mouse, - // etc. }; } diff --git a/packages/alice/lib/utils/alice_theme.dart b/packages/alice/lib/utils/alice_theme.dart index e23c08a4..f41e4d59 100644 --- a/packages/alice/lib/utils/alice_theme.dart +++ b/packages/alice/lib/utils/alice_theme.dart @@ -1,17 +1,40 @@ -import 'package:alice/utils/alice_constants.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +/// All definitions for theming Alice. class AliceTheme { + static const Color red = Color(0xffff3f34); + static const Color lightRed = Color(0xffff5e57); + static const Color green = Color(0xff05c46b); + static const Color grey = Color(0xff808e9b); + static const Color orange = Color(0xffffa801); + static const Color white = Color(0xffffffff); + + /// Returns general theme data. + static ThemeData getTheme() { + return ThemeData( + useMaterial3: true, + colorScheme: AliceTheme._getColorScheme(), + dividerColor: Colors.transparent, + buttonTheme: const ButtonThemeData( + buttonColor: lightRed, + textTheme: ButtonTextTheme.primary, + ), + ); + } + + /// Checks whether is dark mode enabled. static bool get _isDarkMode => SchedulerBinding.instance.platformDispatcher.platformBrightness == Brightness.dark; - static ColorScheme getColorScheme() => _isDarkMode - ? const ColorScheme.dark(primary: AliceConstants.lightRed) - : const ColorScheme.light(primary: AliceConstants.lightRed); + /// Returns color scheme based on dark mode. + static ColorScheme _getColorScheme() => _isDarkMode + ? const ColorScheme.dark(primary: AliceTheme.lightRed) + : const ColorScheme.light(primary: AliceTheme.lightRed); - static Color getTextColor(BuildContext context, DiagnosticLevel level) => + /// Return log text color based on diagnostic [level]. + static Color getLogTextColor(BuildContext context, DiagnosticLevel level) => switch (level) { DiagnosticLevel.hidden => Colors.grey, DiagnosticLevel.fine => Colors.grey, diff --git a/packages/alice/lib/utils/num_comparison.dart b/packages/alice/lib/utils/num_comparison.dart index 67d3aab7..da05928b 100644 --- a/packages/alice/lib/utils/num_comparison.dart +++ b/packages/alice/lib/utils/num_comparison.dart @@ -1,3 +1,4 @@ +/// Extension used to compare numbers. extension NumComparison on num? { bool get isZero => this != null && this! == 0; diff --git a/packages/alice/lib/utils/shake_detector.dart b/packages/alice/lib/utils/shake_detector.dart index c0140ad9..5ab13387 100644 --- a/packages/alice/lib/utils/shake_detector.dart +++ b/packages/alice/lib/utils/shake_detector.dart @@ -24,8 +24,9 @@ class ShakeDetector { /// Time before shake count resets final int shakeCountResetTime; - int mShakeTimestamp = DateTime.now().millisecondsSinceEpoch; - int mShakeCount = 0; + int _shakeTimestamp = DateTime.now().millisecondsSinceEpoch; + // ignore: unused_field + int _shakeCount = 0; /// StreamSubscription for Accelerometer events StreamSubscription? streamSubscription; @@ -66,17 +67,17 @@ class ShakeDetector { if (gForce > shakeThresholdGravity) { final int now = DateTime.now().millisecondsSinceEpoch; // ignore shake events too close to each other (500ms) - if (mShakeTimestamp + shakeSlopTimeMS > now) { + if (_shakeTimestamp + shakeSlopTimeMS > now) { return; } // reset the shake count after 3 seconds of no shakes - if (mShakeTimestamp + shakeCountResetTime < now) { - mShakeCount = 0; + if (_shakeTimestamp + shakeCountResetTime < now) { + _shakeCount = 0; } - mShakeTimestamp = now; - mShakeCount++; + _shakeTimestamp = now; + _shakeCount++; onPhoneShake?.call(); } diff --git a/packages/alice/pubspec.yaml b/packages/alice/pubspec.yaml index 7a315095..377d8122 100644 --- a/packages/alice/pubspec.yaml +++ b/packages/alice/pubspec.yaml @@ -1,6 +1,6 @@ name: alice description: Alice is an HTTP Inspector tool which helps debugging http requests. It catches and stores http requests and responses, which can be viewed via simple UI. -version: 1.0.0-dev.6 +version: 1.0.0-dev.7 homepage: https://github.com/jhomlala/alice repository: https://github.com/jhomlala/alice diff --git a/packages/alice_dio/.flutter-plugins b/packages/alice_dio/.flutter-plugins index 3f3fc33b..ca8458e8 100644 --- a/packages/alice_dio/.flutter-plugins +++ b/packages/alice_dio/.flutter-plugins @@ -1,7 +1,7 @@ # This is a generated file; do not edit or check into version control. flutter_local_notifications=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\flutter_local_notifications-17.1.2\\ open_filex=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\open_filex-4.4.0\\ -package_info_plus=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\package_info_plus-6.0.0\\ +package_info_plus=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\package_info_plus-8.0.0\\ path_provider=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider-2.1.3\\ path_provider_android=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_android-2.2.5\\ path_provider_foundation=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_foundation-2.4.0\\ @@ -9,12 +9,12 @@ path_provider_linux=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.de path_provider_windows=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_windows-2.2.1\\ permission_handler=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\permission_handler-11.3.1\\ permission_handler_android=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\permission_handler_android-12.0.6\\ -permission_handler_apple=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\permission_handler_apple-9.4.4\\ +permission_handler_apple=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\permission_handler_apple-9.4.5\\ permission_handler_html=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\permission_handler_html-0.1.1\\ permission_handler_windows=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\permission_handler_windows-0.2.1\\ sensors_plus=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\sensors_plus-5.0.1\\ share_plus=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\share_plus-9.0.0\\ -url_launcher=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\url_launcher-6.2.6\\ +url_launcher=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\url_launcher-6.3.0\\ url_launcher_android=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\url_launcher_android-6.3.3\\ url_launcher_ios=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\url_launcher_ios-6.3.0\\ url_launcher_linux=C:\\Users\\jhoml\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\url_launcher_linux-3.1.1\\ diff --git a/packages/alice_dio/.flutter-plugins-dependencies b/packages/alice_dio/.flutter-plugins-dependencies index a255a090..2893d1df 100644 --- a/packages/alice_dio/.flutter-plugins-dependencies +++ b/packages/alice_dio/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_local_notifications","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_local_notifications-17.1.2\\\\","native_build":true,"dependencies":[]},{"name":"open_filex","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\open_filex-4.4.0\\\\","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-6.0.0\\\\","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.0\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"permission_handler_apple","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_apple-9.4.4\\\\","native_build":true,"dependencies":[]},{"name":"sensors_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sensors_plus-5.0.1\\\\","native_build":true,"dependencies":[]},{"name":"share_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\share_plus-9.0.0\\\\","native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_ios-6.3.0\\\\","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_local_notifications","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_local_notifications-17.1.2\\\\","native_build":true,"dependencies":[]},{"name":"open_filex","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\open_filex-4.4.0\\\\","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-6.0.0\\\\","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.5\\\\","native_build":true,"dependencies":[]},{"name":"permission_handler_android","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_android-12.0.6\\\\","native_build":true,"dependencies":[]},{"name":"sensors_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sensors_plus-5.0.1\\\\","native_build":true,"dependencies":[]},{"name":"share_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\share_plus-9.0.0\\\\","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_android-6.3.3\\\\","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_local_notifications","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_local_notifications-17.1.2\\\\","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-6.0.0\\\\","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.0\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"share_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\share_plus-9.0.0\\\\","native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_macos-3.2.0\\\\","native_build":true,"dependencies":[]}],"linux":[{"name":"package_info_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-6.0.0\\\\","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[]},{"name":"share_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\share_plus-9.0.0\\\\","native_build":false,"dependencies":["url_launcher_linux"]},{"name":"url_launcher_linux","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_linux-3.1.1\\\\","native_build":true,"dependencies":[]}],"windows":[{"name":"package_info_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-6.0.0\\\\","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.2.1\\\\","native_build":false,"dependencies":[]},{"name":"permission_handler_windows","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_windows-0.2.1\\\\","native_build":true,"dependencies":[]},{"name":"share_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\share_plus-9.0.0\\\\","native_build":true,"dependencies":["url_launcher_windows"]},{"name":"url_launcher_windows","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_windows-3.1.1\\\\","native_build":true,"dependencies":[]}],"web":[{"name":"package_info_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-6.0.0\\\\","dependencies":[]},{"name":"permission_handler_html","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_html-0.1.1\\\\","dependencies":[]},{"name":"sensors_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sensors_plus-5.0.1\\\\","dependencies":[]},{"name":"share_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\share_plus-9.0.0\\\\","dependencies":["url_launcher_web"]},{"name":"url_launcher_web","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_web-2.3.1\\\\","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_local_notifications","dependencies":[]},{"name":"open_filex","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"sensors_plus","dependencies":[]},{"name":"share_plus","dependencies":["url_launcher_web","url_launcher_windows","url_launcher_linux"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2024-06-04 15:18:48.565607","version":"3.22.1"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_local_notifications","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_local_notifications-17.1.2\\\\","native_build":true,"dependencies":[]},{"name":"open_filex","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\open_filex-4.4.0\\\\","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-8.0.0\\\\","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.0\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"permission_handler_apple","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_apple-9.4.5\\\\","native_build":true,"dependencies":[]},{"name":"sensors_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sensors_plus-5.0.1\\\\","native_build":true,"dependencies":[]},{"name":"share_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\share_plus-9.0.0\\\\","native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_ios-6.3.0\\\\","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_local_notifications","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_local_notifications-17.1.2\\\\","native_build":true,"dependencies":[]},{"name":"open_filex","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\open_filex-4.4.0\\\\","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-8.0.0\\\\","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.5\\\\","native_build":true,"dependencies":[]},{"name":"permission_handler_android","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_android-12.0.6\\\\","native_build":true,"dependencies":[]},{"name":"sensors_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sensors_plus-5.0.1\\\\","native_build":true,"dependencies":[]},{"name":"share_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\share_plus-9.0.0\\\\","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_android-6.3.3\\\\","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_local_notifications","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_local_notifications-17.1.2\\\\","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-8.0.0\\\\","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.0\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"share_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\share_plus-9.0.0\\\\","native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_macos-3.2.0\\\\","native_build":true,"dependencies":[]}],"linux":[{"name":"package_info_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-8.0.0\\\\","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[]},{"name":"share_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\share_plus-9.0.0\\\\","native_build":false,"dependencies":["url_launcher_linux"]},{"name":"url_launcher_linux","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_linux-3.1.1\\\\","native_build":true,"dependencies":[]}],"windows":[{"name":"package_info_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-8.0.0\\\\","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.2.1\\\\","native_build":false,"dependencies":[]},{"name":"permission_handler_windows","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_windows-0.2.1\\\\","native_build":true,"dependencies":[]},{"name":"share_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\share_plus-9.0.0\\\\","native_build":true,"dependencies":["url_launcher_windows"]},{"name":"url_launcher_windows","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_windows-3.1.1\\\\","native_build":true,"dependencies":[]}],"web":[{"name":"package_info_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-8.0.0\\\\","dependencies":[]},{"name":"permission_handler_html","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_html-0.1.1\\\\","dependencies":[]},{"name":"sensors_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sensors_plus-5.0.1\\\\","dependencies":[]},{"name":"share_plus","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\share_plus-9.0.0\\\\","dependencies":["url_launcher_web"]},{"name":"url_launcher_web","path":"C:\\\\Users\\\\jhoml\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_web-2.3.1\\\\","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_local_notifications","dependencies":[]},{"name":"open_filex","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"sensors_plus","dependencies":[]},{"name":"share_plus","dependencies":["url_launcher_web","url_launcher_windows","url_launcher_linux"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2024-06-22 20:45:38.341312","version":"3.22.1"} \ No newline at end of file