Commit đầu

This commit is contained in:
minhhieu2312 2026-02-26 10:39:42 +07:00
commit 8fea656229
194 changed files with 12449 additions and 0 deletions

140
.gitignore vendored Normal file
View File

@ -0,0 +1,140 @@
# Do not remove or rename entries in this file, only add new ones
# See https://github.com/flutter/flutter/issues/128635 for more context.
# Miscellaneous
*.class
*.lock
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# Visual Studio Code related
.classpath
.project
.settings/
.vscode/*
# Flutter repo-specific
/bin/cache/
/bin/internal/bootstrap.bat
/bin/internal/bootstrap.sh
/bin/mingit/
/dev/benchmarks/mega_gallery/
/dev/bots/.recipe_deps
/dev/bots/android_tools/
/dev/devicelab/ABresults*.json
/dev/docs/doc/
/dev/docs/api_docs.zip
/dev/docs/flutter.docs.zip
/dev/docs/lib/
/dev/docs/pubspec.yaml
/dev/integration_tests/**/xcuserdata
/dev/integration_tests/**/Pods
/packages/flutter/coverage/
version
analysis_benchmark.json
# packages file containing multi-root paths
.packages.generated
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
**/generated_plugin_registrant.dart
.packages
.pub-preload-cache/
.pub-cache/
.pub/
build/
flutter_*.png
linked_*.ds
unlinked.ds
unlinked_spec.ds
# Android related
**/android/**/gradle-wrapper.jar
.gradle/
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/key.properties
*.jks
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/.last_build_id
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/ephemeral
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# macOS
**/Flutter/ephemeral/
**/Pods/
**/macos/Flutter/GeneratedPluginRegistrant.swift
**/macos/Flutter/ephemeral
**/xcuserdata/
# Windows
**/windows/flutter/ephemeral/
**/windows/flutter/generated_plugin_registrant.cc
**/windows/flutter/generated_plugin_registrant.h
**/windows/flutter/generated_plugins.cmake
# Linux
**/linux/flutter/ephemeral/
**/linux/flutter/generated_plugin_registrant.cc
**/linux/flutter/generated_plugin_registrant.h
**/linux/flutter/generated_plugins.cmake
# Coverage
coverage/
# Symbols
app.*.symbols
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock
!.vscode/settings.json

86
README.md Normal file
View File

@ -0,0 +1,86 @@
# baseproject
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
## Cấu hình Environment NDK (Windows)
- Khi build báo lỗi không tìm thấy **Android NDK Clang**, ngoài `local.properties` có thể thêm biến môi trường:
- `ANDROID_NDK_HOME=C:\Users\hieudm\AppData\Local\Android\Sdk\ndk\26.1.10909125`
- (Tuỳ chọn) `ANDROID_NDK_ROOT=C:\Users\hieudm\AppData\Local\Android\Sdk\ndk\26.1.10909125`
- Đảm bảo thư mục trên tồn tại và trùng với version NDK đã cài trong Android Studio.
- Sau khi thêm biến môi trường, **đóng và mở lại** terminal/IDE rồi chạy:
```bash
flutter doctor -v
flutter run
```
## Cấu hình môi trường Android
- **Java / JDK**
- Cài Android Studio mới nhất (kèm JDK 21/17).
- Dùng cùng JDK mà `flutter doctor -v` đang báo (không cần cấu hình thêm nếu đã OK).
- **Android SDK & NDK**
- Mở **Android Studio**`Settings``Android SDK` → tab **SDK Tools`.
- Bật **Android SDK Command-line Tools**, **CMake**, **NDK (Side by side)**.
- Bật **Show Package Details**, chọn NDK **26.1.10909125**.
- **`android/local.properties`**
- File cần có dạng:
```properties
sdk.dir=C:\\Users\\hieudm\\AppData\\Local\\Android\\Sdk
ndk.dir=C:\\Users\\hieudm\\AppData\\Local\\Android\\Sdk\\ndk\\26.1.10909125
flutter.sdk=C:\\flutter
flutter.buildMode=debug
flutter.versionName=1.0.0
flutter.versionCode=1
```
- Thay `C:\\Users\\hieudm` nếu user Windows khác.
- **`android/app/build.gradle` (Android)**
- Bên trong block `android { ... }` cần có:
```groovy
android {
namespace "com.example.baseproject"
compileSdkVersion 36
ndkVersion "26.1.10909125"
...
}
```
- **Lệnh tiện ích**
- Tạo splash:
```bash
flutter pub run flutter_native_splash:create
```
- Chạy build runner:
```bash
flutter pub run build_runner build --delete-conflicting-outputs
```
- Tạo app icon:
```bash
flutter pub run flutter_launcher_icons:main
```
## Ghi chú iOS
- Khi gặp lỗi CocoaPods có thể thử:
```bash
pod deintegrate
pod cache clean --all
pod install
```

189
alice/CHANGELOG.md Normal file
View File

@ -0,0 +1,189 @@
## 0.2.4
* Updated dependencies
## 0.2.3
* Updated dependencies
## 0.2.2
* Updated dependencies
* Changed default sort filter of create time from ascending to descending. This will show latest HTTP calls on top of the list.
## 0.2.1
* Added directionality support (by Abdol Hussain Mozaffari https://github.com/mozaffari)
* Updated dependencies (by https://github.com/Nyan274)
## 0.2.0
* Migrate to null safety (by https://github.com/ARIFCSE10)
* Updated Dio interceptor
* Updated dependencies
## 0.1.12
* Fixed query parameter issue not handled properly (by https://github.com/shreyas18jan).
* Removed shake dependency and added sensors dependency. Shake will be detected with sensors.
* Updated other dependencies.
* Added maxCallsCount which handles max number of calls stored in memory.
* Refactored notification text.
* Added sorting in inspector UI.
* Added additional chopper request error handling.
## 0.1.11
* Updated dependencies
* Lint fixes
## 0.1.10
* Lint update
* General refactor
* Dart format
## 0.1.9
* Lint update
## 0.1.8
* Lint update
## 0.1.7
* Updated dependencies
## 0.1.6
* Updated dependencies
* Removed unused android/ios native code
* Migrated example to v2 android
## 0.1.5
* Changed video_player and Chewie to Better Player. Better Player will be used to display videos.
## 0.1.4
* Updated texts in call details to be selectable
* Fixed general bugs
* Fixed video not disposed properly
## 0.1.3
* Updated documentation
## 0.1.2
* Updated dependencies
* Added documentation
* General refactor
## 0.1.1
* Removed sound in ios notification
* Upgraded local notification library
## 0.1.0
* Promoted to 0.1.0
* Added Android/iOS dummy classes for pubdev score fix
## 0.0.33
* Fixed share issue
## 0.0.32
* Code style refactor
## 0.0.31
* Fixed file save path of iOS
* Fixed Stream request body
## 0.0.30
* Added better duration and bytes formatting
## 0.0.29
* Added possibility to add generic http call
* Refactored rendering of invalid body in application/json response
## 0.0.28
* Fixed rendering body responses of unknown content-type
## 0.0.27
* UI polishing
* File & email content polishing
## 0.0.26
* Added search support in calls screen
* Disabled notifications sound (by https://github.com/itsJoKr Josip Krnjic)
## 0.0.25
* Added notificationIcon parameter
* Added better notification handling
* Refactored codebase
* Added setNavigatorKey method
* Added FormData support for Dio requests
## 0.0.24
* Updated dependencies
* Prepare for 1.0.0 version of sensors and package_info. ([dart_lsc](https://github.com/amirh/dart_lsc))
## 0.0.23
* Updated to dart 2.6.0
* Added AliceHttpExtensions, AliceHttpClientExtensions
## 0.0.22
* Updated dependencies
* Refactored response page. If response is image or video, Alice will show it in response page. Large
body outputs will be not shown by default. There is a "Show body" button to show large output.
## 0.0.21
* Added Chopper support
* Added AndroidX support
## 0.0.20
* Updated dependencies
## 0.0.19
* Updated dependencies
## 0.0.18
* Added share option in call details. Share allows user to share curl of the request. (by: Praveenkumar Ramasamy https://github.com/pravinarr)
## 0.0.17
* Added shake option to open inspector from everywhere (by https://github.com/MattisBrizard MattisBrizard)
* Fixed double-encoding of request body if request body is a minified json (by https://github.com/knaeckeKami knaeckeKami)
* Added dark theme (idea by: https://github.com/Agondev Agondev)
## 0.0.16
* Fixed server text overflow
## 0.0.15
* Updated dependencies
## 0.0.14
* Fixed Dio API breaking change
## 0.0.13
* Updated dependencies
* Notification won't init when showNotification is off
## 0.0.12
* Updated flutter local notification dependency version
* Refactor
## 0.0.11
* Fixed iOS version issues (fixed by https://github.com/britannio Britannio Jarrett)
## 0.0.10
* Added stats feature
* Added save feature
* Added secured/not secured connection indicator in call list item
* Query parameters feature (Dio only)
* Fix for Uint8List SDK breaking change
* Updated dependencies
* Refactored code
## 0.0.6
* Fixed http/http package requests
## 0.0.5
* Updated dependencies
* Navigator key can be provided now from application (instead of using Alice's navigator key)
## 0.0.4
* Updated Kotlin version
## 0.0.3
* Removed gif from package
## 0.0.2
* Bug fixes
## 0.0.1
* Initial release

201
alice/LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

252
alice/README.md Normal file
View File

@ -0,0 +1,252 @@
<p align="center">
<img src="https://raw.githubusercontent.com/jhomlala/alice/master/media/logo.png" width="250px">
</p>
# Alice
[![pub package](https://img.shields.io/pub/v/alice.svg)](https://pub.dartlang.org/packages/alice)
[![pub package](https://img.shields.io/github/license/jhomlala/alice.svg?style=flat)](https://github.com/jhomlala/alice)
[![pub package](https://img.shields.io/badge/platform-flutter-blue.svg)](https://github.com/jhomlala/alice)
Alice is an HTTP Inspector tool for Flutter which helps debugging http requests. It catches and stores http requests and responses, which can be viewed via simple UI. It is inspired from [Chuck](https://github.com/jgilfelt/chuck) and [Chucker](https://github.com/ChuckerTeam/chucker).
<table>
<tr>
<td>
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/1.png">
</td>
<td>
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/2.png">
</td>
<td>
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/3.png">
</td>
<td>
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/4.png">
</td>
<td>
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/5.png">
</td>
<td>
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/6.png">
</td>
</tr>
<tr>
<td>
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/7.png">
</td>
<td>
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/8.png">
</td>
<td>
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/9.png">
</td>
<td>
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/10.png">
</td>
<td>
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/11.png">
</td>
<td>
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/12.png">
</td>
</tr>
</table>
**Supported Dart http client plugins:**
- Dio
- HttpClient from dart:io package
- Http from http/http package
- Chopper
- Generic HTTP client
**Features:**
✔️ Detailed logs for each HTTP calls (HTTP Request, HTTP Response)
✔️ Inspector UI for viewing HTTP calls
✔️ Save HTTP calls to file
✔️ Statistics
✔️ Notification on HTTP call
✔️ Support for top used HTTP clients in Dart
✔️ Error handling
✔️ Shake to open inspector
✔️ HTTP calls search
## Install
1. Add this to your **pubspec.yaml** file:
```yaml
dependencies:
alice: ^0.2.4
```
2. Install it
```bash
$ flutter packages get
```
3. Import it
```dart
import 'package:alice/alice.dart';
```
## Usage
### Alice configuration
1. Create Alice instance:
```dart
Alice alice = Alice();
```
2. Add navigator key to your application:
```dart
MaterialApp( navigatorKey: alice.getNavigatorKey(), home: ...)
```
You need to add this navigator key in order to show inspector UI.
You can use also your navigator key in Alice:
```dart
Alice alice = Alice(showNotification: true, navigatorKey: yourNavigatorKeyHere);
```
If you need to pass navigatorKey lazily, you can use:
```dart
alice.setNavigatorKey(yourNavigatorKeyHere);
```
This is minimal configuration required to run Alice. Can set optional settings in Alice constructor, which are presented below. If you don't want to change anything, you can move to Http clients configuration.
### Additional settings
You can set `showNotification` in Alice constructor to show notification. Clicking on this notification will open inspector.
```dart
Alice alice = Alice(..., showNotification: true);
```
You can set `showInspectorOnShake` in Alice constructor to open inspector by shaking your device (default disabled):
```dart
Alice alice = Alice(..., showInspectorOnShake: true);
```
If you want to use dark mode just add `darkTheme` flag:
```dart
Alice alice = Alice(..., darkTheme: true);
```
If you want to pass another notification icon, you can use `notificationIcon` parameter. Default value is @mipmap/ic_launcher.
```dart
Alice alice = Alice(..., notificationIcon: "myNotificationIconResourceName");
```
If you want to limit max numbers of HTTP calls saved in memory, you may use `maxCallsCount` parameter.
```dart
Alice alice = Alice(..., maxCallsCount: 1000));
```
If you want to change the Directionality of Alice, you can use the `directionality` parameter. If the parameter is set to null, the Directionality of the app will be used.
```dart
Alice alice = Alice(..., directionality: TextDirection.ltr);
```
### HTTP Client configuration
If you're using Dio, you just need to add interceptor.
```dart
Dio dio = Dio();
dio.interceptors.add(alice.getDioInterceptor());
```
If you're using HttpClient from dart:io package:
```dart
httpClient
.getUrl(Uri.parse("https://jsonplaceholder.typicode.com/posts"))
.then((request) async {
alice.onHttpClientRequest(request);
var httpResponse = await request.close();
var responseBody = await httpResponse.transform(utf8.decoder).join();
alice.onHttpClientResponse(httpResponse, request, body: responseBody);
});
```
If you're using http from http/http package:
```dart
http.get('https://jsonplaceholder.typicode.com/posts').then((response) {
alice.onHttpResponse(response);
});
```
If you're using Chopper. you need to add interceptor:
```dart
chopper = ChopperClient(
interceptors: alice.getChopperInterceptor(),
);
```
If you have other HTTP client you can use generic http call interface:
```dart
AliceHttpCall aliceHttpCall = AliceHttpCall(id);
alice.addHttpCall(aliceHttpCall);
```
## Show inspector manually
You may need that if you won't use shake or notification:
```dart
alice.showInspector();
```
## Saving calls
Alice supports saving logs to your mobile device storage. In order to make save feature works, you need to add in your Android application manifest:
```xml
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
```
## Extensions
You can use extensions to shorten your http and http client code. This is optional, but may improve your codebase.
Example:
1. Import:
```dart
import 'package:alice/core/alice_http_client_extensions.dart';
import 'package:alice/core/alice_http_extensions.dart';
```
2. Use extensions:
```dart
http
.post('https://jsonplaceholder.typicode.com/posts', body: body)
.interceptWithAlice(alice, body: body);
```
```dart
httpClient
.postUrl(Uri.parse("https://jsonplaceholder.typicode.com/posts"))
.interceptWithAlice(alice, body: body, headers: Map());
```
## Example
See complete example here: https://github.com/jhomlala/alice/blob/master/example/lib/main.dart
To run project, you need to call this command in your terminal:
```bash
flutter pub run build_runner build --delete-conflicting-outputs
```
You need to run this command to build Chopper generated classes. You should run this command only once,
you don't need to run this command each time before running project (unless you modify something in Chopper endpoints).
<p align="center">
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/13.png">
<p align="center">

View File

@ -0,0 +1,21 @@
#include: package:lint/analysis_options.yaml
analyzer:
strong-mode:
implicit-dynamic: false
exclude:
- "**/*.chopper.dart"
linter:
rules:
close_sinks: true
sort_constructors_first: false
avoid_classes_with_only_static_members: false
avoid_void_async: false
avoid_positional_boolean_parameters: false
avoid_function_literals_in_foreach_calls: false
prefer_constructors_over_static_methods: false
sort_unnamed_constructors_first: false
sized_box_for_whitespace: false
invalid_dependency: false
sort_pub_dependencies: false

113
alice/lib/alice.dart Normal file
View File

@ -0,0 +1,113 @@
import 'dart:io';
import 'package:alice/core/alice_chopper_response_interceptor.dart';
import 'package:alice/core/alice_http_adapter.dart';
import 'package:alice/model/alice_http_call.dart';
import 'package:chopper/chopper.dart';
import 'package:http/http.dart' as http;
import 'package:alice/core/alice_core.dart';
import 'package:alice/core/alice_dio_interceptor.dart';
import 'package:alice/core/alice_http_client_adapter.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class Alice {
/// Should user be notified with notification if there's new request catched
/// by Alice
final bool showNotification;
/// Should inspector be opened on device shake (works only with physical
/// with sensors)
final bool showInspectorOnShake;
/// Should inspector use dark theme
final bool darkTheme;
/// Icon url for notification
final String notificationIcon;
///Max number of calls that are stored in memory. When count is reached, FIFO
///method queue will be used to remove elements.
final int maxCallsCount;
///Directionality of app. Directionality of the app will be used if set to null.
final TextDirection? directionality;
GlobalKey<NavigatorState>? _navigatorKey;
late AliceCore _aliceCore;
late AliceHttpClientAdapter _httpClientAdapter;
late AliceHttpAdapter _httpAdapter;
/// Creates alice instance.
Alice({
GlobalKey<NavigatorState>? navigatorKey,
this.showNotification = true,
this.showInspectorOnShake = false,
this.darkTheme = false,
this.notificationIcon = "@mipmap/ic_launcher",
this.maxCallsCount = 1000,
this.directionality,
}) {
_navigatorKey = navigatorKey ?? GlobalKey<NavigatorState>();
_aliceCore = AliceCore(
_navigatorKey,
showNotification: showNotification,
showInspectorOnShake: showInspectorOnShake,
darkTheme: darkTheme,
notificationIcon: notificationIcon,
maxCallsCount: maxCallsCount,
directionality: directionality,
);
_httpClientAdapter = AliceHttpClientAdapter(_aliceCore);
_httpAdapter = AliceHttpAdapter(_aliceCore);
}
/// Set custom navigation key. This will help if there's route library.
void setNavigatorKey(GlobalKey<NavigatorState> navigatorKey) {
_navigatorKey = navigatorKey;
_aliceCore.navigatorKey = navigatorKey;
}
/// Get currently used navigation key
GlobalKey<NavigatorState>? getNavigatorKey() {
return _navigatorKey;
}
/// Get Dio interceptor which should be applied to Dio instance.
AliceDioInterceptor getDioInterceptor() {
return AliceDioInterceptor(_aliceCore);
}
/// Handle request from HttpClient
void onHttpClientRequest(HttpClientRequest request, {dynamic body}) {
_httpClientAdapter.onRequest(request, body: body);
}
/// Handle response from HttpClient
void onHttpClientResponse(HttpClientResponse response, HttpClientRequest request, {dynamic body}) {
_httpClientAdapter.onResponse(response, request, body: body);
}
/// Handle both request and response from http package
void onHttpResponse(http.Response response, {dynamic body}) {
_httpAdapter.onResponse(response, body: body);
}
/// Opens Http calls inspector. This will navigate user to the new fullscreen
/// page where all listened http calls can be viewed.
void showInspector() {
_aliceCore.navigateToCallListScreen();
}
/// Get chopper interceptor. This should be added to Chopper instance.
// List<ResponseInterceptor> getChopperInterceptor() {
// return [AliceChopperInterceptor(_aliceCore)];
// }
/// Handle generic http call. Can be used to any http client.
void addHttpCall(AliceHttpCall aliceHttpCall) {
assert(aliceHttpCall.request != null, "Http call request can't be null");
assert(aliceHttpCall.response != null, "Http call response can't be null");
_aliceCore.addCall(aliceHttpCall);
}
}

View File

@ -0,0 +1,123 @@
// import 'dart:async';
// import 'dart:convert';
// import 'package:alice/core/alice_utils.dart';
// import 'package:alice/model/alice_http_call.dart';
// import 'package:alice/model/alice_http_request.dart';
// import 'package:alice/model/alice_http_response.dart';
// import 'package:chopper/chopper.dart' as chopper;
// import 'package:http/http.dart';
// import 'alice_core.dart';
// class AliceChopperInterceptor extends chopper.ResponseInterceptor with chopper.RequestInterceptor {
// /// AliceCore instance
// final AliceCore aliceCore;
// /// Creates instance of chopper interceptor
// AliceChopperInterceptor(this.aliceCore);
// /// Creates hashcode based on request
// int getRequestHashCode(BaseRequest baseRequest) {
// int hashCodeSum = 0;
// hashCodeSum += baseRequest.url.hashCode;
// hashCodeSum += baseRequest.method.hashCode;
// if (baseRequest.headers.isNotEmpty) {
// baseRequest.headers.forEach((key, value) {
// hashCodeSum += key.hashCode;
// hashCodeSum += value.hashCode;
// });
// }
// if (baseRequest.contentLength != null) {
// hashCodeSum += baseRequest.contentLength.hashCode;
// }
// return hashCodeSum.hashCode;
// }
// /// Handles chopper request and creates alice http call
// @override
// FutureOr<chopper.Request> onRequest(chopper.Request request) async {
// try {
// final baseRequest = await request.toBaseRequest();
// final AliceHttpCall call = AliceHttpCall(getRequestHashCode(baseRequest));
// String endpoint = "";
// String server = "";
// if (request.baseUrl.isEmpty) {
// final List<String> split = request.url.split("/");
// if (split.length > 2) {
// server = split[1] + split[2];
// }
// if (split.length > 4) {
// endpoint = "/";
// for (int splitIndex = 3; splitIndex < split.length; splitIndex++) {
// // ignore: use_string_buffers
// endpoint += "${split[splitIndex]}/";
// }
// endpoint = endpoint.substring(0, endpoint.length - 1);
// }
// } else {
// endpoint = request.url;
// server = request.baseUrl;
// }
// call.method = request.method;
// call.endpoint = endpoint;
// call.server = server;
// call.client = "Chopper";
// if (request.baseUrl.contains("https") || request.url.contains("https")) {
// call.secure = true;
// }
// final AliceHttpRequest aliceHttpRequest = AliceHttpRequest();
// if (request.body == null) {
// aliceHttpRequest.size = 0;
// aliceHttpRequest.body = "";
// } else {
// aliceHttpRequest.size = utf8.encode(request.body as String).length;
// aliceHttpRequest.body = request.body;
// }
// aliceHttpRequest.time = DateTime.now();
// aliceHttpRequest.headers = request.headers;
// String? contentType = "unknown";
// if (request.headers.containsKey("Content-Type")) {
// contentType = request.headers["Content-Type"];
// }
// aliceHttpRequest.contentType = contentType;
// aliceHttpRequest.queryParameters = request.parameters;
// call.request = aliceHttpRequest;
// call.response = AliceHttpResponse();
// aliceCore.addCall(call);
// } catch (exception) {
// AliceUtils.log(exception.toString());
// }
// return request;
// }
// /// Handles chopper response and adds data to existing alice http call
// @override
// FutureOr<chopper.Response> onResponse(chopper.Response response) {
// final httpResponse = AliceHttpResponse();
// httpResponse.status = response.statusCode;
// if (response.body == null) {
// httpResponse.body = "";
// httpResponse.size = 0;
// } else {
// httpResponse.body = response.body;
// httpResponse.size = utf8.encode(response.body.toString()).length;
// }
// httpResponse.time = DateTime.now();
// final Map<String, String> headers = {};
// response.headers.forEach((header, values) {
// headers[header] = values.toString();
// });
// httpResponse.headers = headers;
// aliceCore.addResponse(httpResponse, getRequestHashCode(response.base.request!));
// return response;
// }
// }

View File

@ -0,0 +1,264 @@
import 'dart:async';
import 'package:alice/core/alice_utils.dart';
import 'package:alice/helper/alice_save_helper.dart';
import 'package:alice/model/alice_http_error.dart';
import 'package:alice/model/alice_http_call.dart';
import 'package:alice/model/alice_http_response.dart';
import 'package:alice/ui/page/alice_calls_list_screen.dart';
import 'package:alice/utils/shake_detector.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart';
// 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
/// by Alice
final bool showNotification;
/// Should inspector be opened on device shake (works only with physical
/// with sensors)
final bool showInspectorOnShake;
/// Should inspector use dark theme
final bool darkTheme;
/// Rx subject which contains all intercepted http calls
final BehaviorSubject<List<AliceHttpCall>> callsSubject = BehaviorSubject.seeded([]);
/// Icon url for notification
final String notificationIcon;
///Max number of calls that are stored in memory. When count is reached, FIFO
///method queue will be used to remove elements.
final int maxCallsCount;
///Directionality of app. If null then directionality of context will be used.
final TextDirection? directionality;
// late FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin;
GlobalKey<NavigatorState>? navigatorKey;
Brightness _brightness = Brightness.light;
bool _isInspectorOpened = false;
ShakeDetector? _shakeDetector;
StreamSubscription? _callsSubscription;
String? _notificationMessage;
String? _notificationMessageShown;
bool _notificationProcessing = false;
/// Creates alice core instance
AliceCore(
this.navigatorKey, {
required this.showNotification,
required this.showInspectorOnShake,
required this.darkTheme,
required this.notificationIcon,
required this.maxCallsCount,
this.directionality,
}) {
if (showNotification) {
_initializeNotificationsPlugin();
_callsSubscription = callsSubject.listen((_) => _onCallsChanged());
}
if (showInspectorOnShake) {
_shakeDetector = ShakeDetector.autoStart(
onPhoneShake: () {
navigateToCallListScreen();
},
shakeThresholdGravity: 5,
);
}
_brightness = darkTheme ? Brightness.dark : Brightness.light;
}
/// Dispose subjects and subscriptions
void dispose() {
callsSubject.close();
_shakeDetector?.stopListening();
_callsSubscription?.cancel();
}
/// Get currently used brightness
Brightness get brightness => _brightness;
void _initializeNotificationsPlugin() {
// _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
// final initializationSettingsAndroid = AndroidInitializationSettings(notificationIcon);
// const initializationSettingsIOS = IOSInitializationSettings();
// final initializationSettings =
// InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
// _flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: _onSelectedNotification);
}
void _onCallsChanged() async {
if (callsSubject.value.isNotEmpty) {
_notificationMessage = _getNotificationMessage();
if (_notificationMessage != _notificationMessageShown && !_notificationProcessing) {
// await _showLocalNotification();
_onCallsChanged();
}
}
}
Future<void> _onSelectedNotification(String? payload) async {
assert(payload != null, "payload can't be null");
navigateToCallListScreen();
return;
}
/// Opens Http calls inspector. This will navigate user to the new fullscreen
/// page where all listened http calls can be viewed.
void navigateToCallListScreen() {
final context = getContext();
if (context == null) {
AliceUtils.log("Cant start Alice HTTP Inspector. Please add NavigatorKey to your application");
return;
}
if (!_isInspectorOpened) {
_isInspectorOpened = true;
Navigator.push<void>(
context,
MaterialPageRoute(
builder: (context) => AliceCallsListScreen(this),
),
).then((onValue) => _isInspectorOpened = false);
}
}
/// Get context from navigator key. Used to open inspector route.
BuildContext? getContext() => navigatorKey?.currentState?.overlay?.context;
String _getNotificationMessage() {
final List<AliceHttpCall> calls = callsSubject.value;
final int successCalls = calls
.where((call) => call.response != null && call.response!.status! >= 200 && call.response!.status! < 300)
.toList()
.length;
final int redirectCalls = calls
.where((call) => call.response != null && call.response!.status! >= 300 && call.response!.status! < 400)
.toList()
.length;
final int errorCalls = calls
.where((call) => call.response != null && call.response!.status! >= 400 && call.response!.status! < 600)
.toList()
.length;
final int loadingCalls = calls.where((call) => call.loading).toList().length;
final StringBuffer notificationsMessage = StringBuffer();
if (loadingCalls > 0) {
notificationsMessage.write("Loading: $loadingCalls");
notificationsMessage.write(" | ");
}
if (successCalls > 0) {
notificationsMessage.write("Success: $successCalls");
notificationsMessage.write(" | ");
}
if (redirectCalls > 0) {
notificationsMessage.write("Redirect: $redirectCalls");
notificationsMessage.write(" | ");
}
if (errorCalls > 0) {
notificationsMessage.write("Error: $errorCalls");
}
String notificationMessageString = notificationsMessage.toString();
if (notificationMessageString.endsWith(" | ")) {
notificationMessageString = notificationMessageString.substring(0, notificationMessageString.length - 3);
}
return notificationMessageString;
}
// Future _showLocalNotification() async {
// _notificationProcessing = true;
// const channelId = "Alice";
// const channelName = "Alice";
// const channelDescription = "Alice";
// final androidPlatformChannelSpecifics = AndroidNotificationDetails(
// channelId, channelName, channelDescription,
// enableVibration: false,
// playSound: false,
// largeIcon: DrawableResourceAndroidBitmap(notificationIcon));
// const iOSPlatformChannelSpecifics =
// IOSNotificationDetails(presentSound: false);
// final platformChannelSpecifics = NotificationDetails(
// android: androidPlatformChannelSpecifics,
// iOS: iOSPlatformChannelSpecifics);
// final String? message = _notificationMessage;
// await _flutterLocalNotificationsPlugin.show(
// 0,
// "Alice (total: ${callsSubject.value.length} requests)",
// message,
// platformChannelSpecifics,
// payload: "");
// _notificationMessageShown = message;
// _notificationProcessing = false;
// return;
// }
/// Add alice http call to calls subject
void addCall(AliceHttpCall call) {
final callsCount = callsSubject.value.length;
if (callsCount >= maxCallsCount) {
final originalCalls = callsSubject.value;
final calls = List<AliceHttpCall>.from(originalCalls);
calls.sort((call1, call2) => call1.createdTime.compareTo(call2.createdTime));
final indexToReplace = originalCalls.indexOf(calls.first);
originalCalls[indexToReplace] = call;
callsSubject.add(originalCalls);
} else {
callsSubject.add([...callsSubject.value, call]);
}
}
/// Add error to existing alice http call
void addError(AliceHttpError error, int requestId) {
final AliceHttpCall? selectedCall = _selectCall(requestId);
if (selectedCall == null) {
AliceUtils.log("Selected call is null");
return;
}
selectedCall.error = error;
callsSubject.add([...callsSubject.value]);
}
/// Add response to existing alice http call
void addResponse(AliceHttpResponse response, int requestId) {
final AliceHttpCall? selectedCall = _selectCall(requestId);
if (selectedCall == null) {
AliceUtils.log("Selected call is null");
return;
}
selectedCall.loading = false;
selectedCall.response = response;
selectedCall.duration = response.time.millisecondsSinceEpoch - selectedCall.request!.time.millisecondsSinceEpoch;
callsSubject.add([...callsSubject.value]);
}
/// Add alice http call to calls subject
void addHttpCall(AliceHttpCall aliceHttpCall) {
assert(aliceHttpCall.request != null, "Http call request can't be null");
assert(aliceHttpCall.response != null, "Http call response can't be null");
callsSubject.add([...callsSubject.value, aliceHttpCall]);
}
/// Remove all calls from calls subject
void removeCalls() {
callsSubject.add([]);
}
AliceHttpCall? _selectCall(int requestId) => callsSubject.value.firstWhereOrNull((call) => call.id == requestId);
/// Save all calls to file
void saveHttpRequests(BuildContext context) {
AliceSaveHelper.saveCalls(context, callsSubject.value, _brightness);
}
}

View File

@ -0,0 +1,144 @@
import 'dart:convert';
import 'package:alice/core/alice_core.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/model/alice_http_error.dart';
import 'package:alice/model/alice_http_request.dart';
import 'package:alice/model/alice_http_response.dart';
import 'package:dio/dio.dart';
class AliceDioInterceptor extends InterceptorsWrapper {
/// AliceCore instance
final AliceCore aliceCore;
/// Creates dio interceptor
AliceDioInterceptor(this.aliceCore);
/// Handles dio request and creates alice http call based on it
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final AliceHttpCall call = AliceHttpCall(options.hashCode);
final Uri uri = options.uri;
call.method = options.method;
var path = options.uri.path;
if (path.isEmpty) {
path = "/";
}
call.endpoint = path;
call.server = uri.host;
call.client = "Dio";
call.uri = options.uri.toString();
if (uri.scheme == "https") {
call.secure = true;
}
final AliceHttpRequest request = AliceHttpRequest();
final dynamic data = options.data;
if (data == null) {
request.size = 0;
request.body = "";
} else {
if (data is FormData) {
request.body += "Form data";
if (data.fields.isNotEmpty == true) {
final List<AliceFormDataField> fields = [];
data.fields.forEach((entry) {
fields.add(AliceFormDataField(entry.key, entry.value));
});
request.formDataFields = fields;
}
if (data.files.isNotEmpty == true) {
final List<AliceFormDataFile> files = [];
data.files.forEach((entry) {
files.add(AliceFormDataFile(entry.value.filename,
entry.value.contentType.toString(), entry.value.length));
});
request.formDataFiles = files;
}
} else {
request.size = utf8.encode(data.toString()).length;
request.body = data;
}
}
request.time = DateTime.now();
request.headers = options.headers;
request.contentType = options.contentType.toString();
request.queryParameters = options.queryParameters;
call.request = request;
call.response = AliceHttpResponse();
aliceCore.addCall(call);
handler.next(options);
}
/// Handles dio response and adds data to alice http call
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final httpResponse = AliceHttpResponse();
httpResponse.status = response.statusCode;
if (response.data == null) {
httpResponse.body = "";
httpResponse.size = 0;
} else {
httpResponse.body = response.data;
httpResponse.size = utf8.encode(response.data.toString()).length;
}
httpResponse.time = DateTime.now();
final Map<String, String> headers = {};
response.headers.forEach((header, values) {
headers[header] = values.toString();
});
httpResponse.headers = headers;
aliceCore.addResponse(httpResponse, response.requestOptions.hashCode);
handler.next(response);
}
/// Handles error and adds data to alice http call
@override
void onError(DioError error, ErrorInterceptorHandler handler) {
final httpError = AliceHttpError();
httpError.error = error.toString();
if (error is Error) {
final basicError = error as Error;
httpError.stackTrace = basicError.stackTrace;
}
aliceCore.addError(httpError, error.requestOptions.hashCode);
final httpResponse = AliceHttpResponse();
httpResponse.time = DateTime.now();
if (error.response == null) {
httpResponse.status = -1;
aliceCore.addResponse(httpResponse, error.requestOptions.hashCode);
} else {
httpResponse.status = error.response!.statusCode;
if (error.response!.data == null) {
httpResponse.body = "";
httpResponse.size = 0;
} else {
httpResponse.body = error.response!.data;
httpResponse.size = utf8.encode(error.response!.data.toString()).length;
}
final Map<String, String> headers = {};
error.response!.headers.forEach((header, values) {
headers[header] = values.toString();
});
httpResponse.headers = headers;
aliceCore.addResponse(
httpResponse, error.response!.requestOptions.hashCode);
}
handler.next(error);
}
}

View File

@ -0,0 +1,89 @@
import 'dart:convert';
import 'package:alice/core/alice_core.dart';
import 'package:alice/model/alice_http_call.dart';
import 'package:alice/model/alice_http_request.dart';
import 'package:alice/model/alice_http_response.dart';
import 'package:http/http.dart' as http;
class AliceHttpAdapter {
/// AliceCore instance
final AliceCore aliceCore;
/// Creates alice http adapter
AliceHttpAdapter(this.aliceCore);
/// Handles http response. It creates both request and response from http call
void onResponse(http.Response response, {dynamic body}) {
if (response.request == null) {
return;
}
final request = response.request!;
final AliceHttpCall call = AliceHttpCall(response.request.hashCode);
call.loading = true;
call.client = "HttpClient (http package)";
call.uri = request.url.toString();
call.method = request.method;
var path = request.url.path;
if (path.isEmpty) {
path = "/";
}
call.endpoint = path;
call.server = request.url.host;
if (request.url.scheme == "https") {
call.secure = true;
}
final AliceHttpRequest httpRequest = AliceHttpRequest();
if (response.request is http.Request) {
// we are guaranteed` the existence of body and headers
if (body != null) {
httpRequest.body = body;
}
// ignore: cast_nullable_to_non_nullable
httpRequest.body = body ?? (response.request as http.Request).body ?? "";
httpRequest.size = utf8.encode(httpRequest.body.toString()).length;
httpRequest.headers =
Map<String, dynamic>.from(response.request!.headers);
} else if (body == null) {
httpRequest.size = 0;
httpRequest.body = "";
} else {
httpRequest.size = utf8.encode(body.toString()).length;
httpRequest.body = body;
}
httpRequest.time = DateTime.now();
String? contentType = "unknown";
if (httpRequest.headers.containsKey("Content-Type")) {
contentType = httpRequest.headers["Content-Type"] as String?;
}
httpRequest.contentType = contentType;
httpRequest.queryParameters = response.request!.url.queryParameters;
final AliceHttpResponse httpResponse = AliceHttpResponse();
httpResponse.status = response.statusCode;
httpResponse.body = response.body;
httpResponse.size = utf8.encode(response.body.toString()).length;
httpResponse.time = DateTime.now();
final Map<String, String> responseHeaders = {};
response.headers.forEach((header, values) {
responseHeaders[header] = values.toString();
});
httpResponse.headers = responseHeaders;
call.request = httpRequest;
call.response = httpResponse;
call.loading = false;
call.duration = 0;
aliceCore.addCall(call);
}
}

View File

@ -0,0 +1,84 @@
import 'dart:convert';
import 'dart:io';
import 'package:alice/core/alice_core.dart';
import 'package:alice/model/alice_http_call.dart';
import 'package:alice/model/alice_http_request.dart';
import 'package:alice/model/alice_http_response.dart';
class AliceHttpClientAdapter {
/// AliceCore instance
final AliceCore aliceCore;
/// Creates alice http client adapter
AliceHttpClientAdapter(this.aliceCore);
/// Handles httpClientRequest and creates http alice call from it
void onRequest(HttpClientRequest request, {dynamic body}) {
final AliceHttpCall call = AliceHttpCall(request.hashCode);
call.loading = true;
call.client = "HttpClient (io package)";
call.method = request.method;
call.uri = request.uri.toString();
var path = request.uri.path;
if (path.isEmpty) {
path = "/";
}
call.endpoint = path;
call.server = request.uri.host;
if (request.uri.scheme == "https") {
call.secure = true;
}
final AliceHttpRequest httpRequest = AliceHttpRequest();
if (body == null) {
httpRequest.size = 0;
httpRequest.body = "";
} else {
httpRequest.size = utf8.encode(body.toString()).length;
httpRequest.body = body;
}
httpRequest.time = DateTime.now();
final Map<String, dynamic> headers = <String, dynamic>{};
httpRequest.headers.forEach((header, dynamic value) {
headers[header] = value;
});
httpRequest.headers = headers;
String? contentType = "unknown";
if (headers.containsKey("Content-Type")) {
contentType = headers["Content-Type"] as String?;
}
httpRequest.contentType = contentType;
httpRequest.cookies = request.cookies;
call.request = httpRequest;
call.response = AliceHttpResponse();
aliceCore.addCall(call);
}
/// Handles httpClientRequest and adds response to http alice call
void onResponse(HttpClientResponse response, HttpClientRequest request,
{dynamic body}) async {
final AliceHttpResponse httpResponse = AliceHttpResponse();
httpResponse.status = response.statusCode;
if (body != null) {
httpResponse.body = body;
httpResponse.size = utf8.encode(body.toString()).length;
} else {
httpResponse.body = "";
httpResponse.size = 0;
}
httpResponse.time = DateTime.now();
final Map<String, String> headers = {};
response.headers.forEach((header, values) {
headers[header] = values.toString();
});
httpResponse.headers = headers;
aliceCore.addResponse(httpResponse, request.hashCode);
}
}

View File

@ -0,0 +1,29 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:alice/alice.dart';
extension AliceHttpClientExtensions on Future<HttpClientRequest> {
/// Intercept http client with alice. This extension method provides additional
/// helpful method to intercept httpClientResponse.
Future<HttpClientResponse> interceptWithAlice(Alice alice,
{dynamic body, Map<String, dynamic>? headers}) async {
final HttpClientRequest request = await this;
if (body != null) {
request.write(body);
}
if (headers != null) {
headers.forEach(
(String key, dynamic value) {
request.headers.add(key, value as Object);
},
);
}
alice.onHttpClientRequest(request, body: body);
final httpResponse = await request.close();
final responseBody = await utf8.decoder.bind(httpResponse).join();
alice.onHttpClientResponse(httpResponse, request, body: responseBody);
return httpResponse;
}
}

View File

@ -0,0 +1,12 @@
import 'package:alice/alice.dart';
import 'package:http/http.dart';
extension AliceHttpExtensions on Future<Response> {
/// Intercept http request with alice. This extension method provides additional
/// helpful method to intercept https' response.
Future<Response> interceptWithAlice(Alice alice, {dynamic body}) async {
final Response response = await this;
alice.onHttpResponse(response, body: body);
return response;
}
}

View File

@ -0,0 +1,11 @@
import 'package:flutter/foundation.dart';
///Utils used across multiple classes in app.
class AliceUtils {
static void log(String logMessage) {
if (!kReleaseMode) {
// ignore: avoid_print
print(logMessage);
}
}
}

View File

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
class AliceAlertHelper {
///Helper method used to open alarm with given title and description.
static void showAlert(
BuildContext context,
String title,
String description, {
String firstButtonTitle = "Accept",
String? secondButtonTitle,
Function? firstButtonAction,
Function? secondButtonAction,
Brightness? brightness,
}) {
final List<Widget> actions = [];
actions.add(
TextButton(
onPressed: () {
if (firstButtonAction != null) {
firstButtonAction();
}
Navigator.of(context).pop();
},
child: Text(firstButtonTitle),
),
);
if (secondButtonTitle != null) {
actions.add(
TextButton(
onPressed: () {
if (secondButtonAction != null) {
secondButtonAction();
}
Navigator.of(context).pop();
},
child: Text(secondButtonTitle),
),
);
}
showDialog<void>(
context: context,
builder: (BuildContext buildContext) {
return Theme(
data: ThemeData(
brightness: brightness ?? Brightness.light,
),
child: AlertDialog(
title: Text(title),
content: Text(description),
actions: actions,
),
);
},
);
}
}

View File

@ -0,0 +1,41 @@
class AliceConversionHelper {
static const int _kilobyteAsByte = 1000;
static const int _megabyteAsByte = 1000000;
static const int _secondAsMillisecond = 1000;
static const int _minuteAsMillisecond = 60000;
/// Format bytes text
static String formatBytes(int bytes) {
if (bytes < 0) {
return "-1 B";
}
if (bytes <= _kilobyteAsByte) {
return "$bytes B";
}
if (bytes <= _megabyteAsByte) {
return "${_formatDouble(bytes / _kilobyteAsByte)} kB";
}
return "${_formatDouble(bytes / _megabyteAsByte)} MB";
}
static String _formatDouble(double value) => value.toStringAsFixed(2);
/// Format time in milliseconds
static String formatTime(int timeInMillis) {
if (timeInMillis < 0) {
return "-1 ms";
}
if (timeInMillis <= _secondAsMillisecond) {
return "$timeInMillis ms";
}
if (timeInMillis <= _minuteAsMillisecond) {
return "${_formatDouble(timeInMillis / _secondAsMillisecond)} s";
}
final Duration duration = Duration(milliseconds: timeInMillis);
return "${duration.inMinutes} min ${duration.inSeconds.remainder(60)} s "
"${duration.inMilliseconds.remainder(1000)} ms";
}
}

View File

@ -0,0 +1,149 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
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/model/alice_http_call.dart';
import 'package:alice/utils/alice_parser.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
class AliceSaveHelper {
static const JsonEncoder _encoder = JsonEncoder.withIndent(' ');
/// Top level method used to save calls to file
static void saveCalls(BuildContext context, List<AliceHttpCall> calls, Brightness brightness) {
_checkPermissions(context, calls, brightness);
}
static void _checkPermissions(BuildContext context, List<AliceHttpCall> calls, Brightness brightness) async {
final status = await Permission.storage.status;
if (status.isGranted) {
_saveToFile(context, calls, brightness);
} else {
final status = await Permission.storage.request();
if (status.isGranted) {
_saveToFile(context, calls, brightness);
} else {
AliceAlertHelper.showAlert(context, "Permission error", "Permission not granted. Couldn't save logs.",
brightness: brightness);
}
}
}
static Future<String> _saveToFile(BuildContext context, List<AliceHttpCall> calls, Brightness brightness) async {
try {
if (calls.isEmpty) {
AliceAlertHelper.showAlert(context, "Error", "There are no logs to save", brightness: brightness);
return "";
}
final bool isAndroid = Platform.isAndroid;
final Directory externalDir =
await (isAndroid ? getExternalStorageDirectory() as FutureOr<Directory> : getApplicationDocumentsDirectory());
final String fileName = "alice_log_${DateTime.now().millisecondsSinceEpoch}.txt";
final File file = File("${externalDir.path}/$fileName");
file.createSync();
final IOSink sink = file.openWrite(mode: FileMode.append);
sink.write(await _buildAliceLog());
calls.forEach((AliceHttpCall call) {
sink.write(_buildCallLog(call));
});
await sink.flush();
await sink.close();
AliceAlertHelper.showAlert(context, "Success", "Successfully saved logs in ${file.path}",
secondButtonTitle: isAndroid ? "View file" : null,
//secondButtonAction: () => isAndroid ? OpenFile.open(file.path) : null,
brightness: brightness);
return file.path;
} catch (exception) {
AliceAlertHelper.showAlert(context, "Error", "Failed to save http calls to file", brightness: brightness);
AliceUtils.log(exception.toString());
}
return "";
}
static Future<String> _buildAliceLog() async {
final StringBuffer stringBuffer = StringBuffer();
final packageInfo = await PackageInfo.fromPlatform();
stringBuffer.write("Alice - HTTP Inspector\n");
stringBuffer.write("App name: ${packageInfo.appName}\n");
stringBuffer.write("Package: ${packageInfo.packageName}\n");
stringBuffer.write("Version: ${packageInfo.version}\n");
stringBuffer.write("Build number: ${packageInfo.buildNumber}\n");
stringBuffer.write("Generated: ${DateTime.now().toIso8601String()}\n");
stringBuffer.write("\n");
return stringBuffer.toString();
}
static String _buildCallLog(AliceHttpCall call) {
final StringBuffer stringBuffer = StringBuffer();
stringBuffer.write("===========================================\n");
stringBuffer.write("Id: ${call.id}\n");
stringBuffer.write("============================================\n");
stringBuffer.write("--------------------------------------------\n");
stringBuffer.write("General data\n");
stringBuffer.write("--------------------------------------------\n");
stringBuffer.write("Server: ${call.server} \n");
stringBuffer.write("Method: ${call.method} \n");
stringBuffer.write("Endpoint: ${call.endpoint} \n");
stringBuffer.write("Client: ${call.client} \n");
stringBuffer.write("Duration ${AliceConversionHelper.formatTime(call.duration)}\n");
stringBuffer.write("Secured connection: ${call.secure}\n");
stringBuffer.write("Completed: ${!call.loading} \n");
stringBuffer.write("--------------------------------------------\n");
stringBuffer.write("Request\n");
stringBuffer.write("--------------------------------------------\n");
stringBuffer.write("Request time: ${call.request!.time}\n");
stringBuffer.write("Request content type: ${call.request!.contentType}\n");
stringBuffer.write("Request cookies: ${_encoder.convert(call.request!.cookies)}\n");
stringBuffer.write("Request headers: ${_encoder.convert(call.request!.headers)}\n");
if (call.request!.queryParameters.isNotEmpty) {
stringBuffer.write("Request query params: ${_encoder.convert(call.request!.queryParameters)}\n");
}
stringBuffer.write("Request size: ${AliceConversionHelper.formatBytes(call.request!.size)}\n");
stringBuffer.write(
"Request body: ${AliceParser.formatBody(call.request!.body, AliceParser.getContentType(call.request!.headers))}\n");
stringBuffer.write("--------------------------------------------\n");
stringBuffer.write("Response\n");
stringBuffer.write("--------------------------------------------\n");
stringBuffer.write("Response time: ${call.response!.time}\n");
stringBuffer.write("Response status: ${call.response!.status}\n");
stringBuffer.write("Response size: ${AliceConversionHelper.formatBytes(call.response!.size)}\n");
stringBuffer.write("Response headers: ${_encoder.convert(call.response!.headers)}\n");
stringBuffer.write(
"Response body: ${AliceParser.formatBody(call.response!.body, AliceParser.getContentType(call.response!.headers))}\n");
if (call.error != null) {
stringBuffer.write("--------------------------------------------\n");
stringBuffer.write("Error\n");
stringBuffer.write("--------------------------------------------\n");
stringBuffer.write("Error: ${call.error!.error}\n");
if (call.error!.stackTrace != null) {
stringBuffer.write("Error stacktrace: ${call.error!.stackTrace}\n");
}
}
stringBuffer.write("--------------------------------------------\n");
stringBuffer.write("Curl\n");
stringBuffer.write("--------------------------------------------\n");
stringBuffer.write(call.getCurlCommand());
stringBuffer.write("\n");
stringBuffer.write("==============================================\n");
stringBuffer.write("\n");
return stringBuffer.toString();
}
static Future<String> buildCallLog(AliceHttpCall call) async {
try {
return await _buildAliceLog() + _buildCallLog(call);
} catch (exception) {
return "Failed to generate call log";
}
}
}

View File

@ -0,0 +1,7 @@
class AliceFormDataFile {
final String? fileName;
final String contentType;
final int length;
AliceFormDataFile(this.fileName, this.contentType, this.length);
}

View File

@ -0,0 +1,6 @@
class AliceFormDataField {
final String name;
final String value;
AliceFormDataField(this.name, this.value);
}

View File

@ -0,0 +1,83 @@
import 'package:alice/model/alice_http_error.dart';
import 'package:alice/model/alice_http_request.dart';
import 'package:alice/model/alice_http_response.dart';
import 'dart:convert';
class AliceHttpCall {
final int id;
late DateTime createdTime;
String client = "";
bool loading = true;
bool secure = false;
String method = "";
String endpoint = "";
String server = "";
String uri = "";
int duration = 0;
AliceHttpRequest? request;
AliceHttpResponse? response;
AliceHttpError? error;
AliceHttpCall(this.id) {
loading = true;
createdTime = DateTime.now();
}
void setResponse(AliceHttpResponse response) {
this.response = response;
loading = false;
}
String getCurlCommand() {
var compressed = false;
var curlCmd = "curl";
curlCmd += " -X $method";
final headers = request!.headers;
headers.forEach((key, dynamic value) {
if ("Accept-Encoding" == key && "gzip" == value) {
compressed = true;
}
curlCmd += " -H '$key: $value'";
});
String requestBody = request!.body.toString();
if (requestBody != '' && requestBody.trim() != "{}") {
try {
requestBody = json.encode(request!.body);
} catch (e) {
requestBody = request!.body.toString();
}
curlCmd += " --data \'${requestBody.replaceAll("\n", "\\n")}'";
}
final queryParamMap = request!.queryParameters;
int paramCount = queryParamMap.keys.length;
var queryParams = "";
if (paramCount > 0) {
queryParams += "?";
queryParamMap.forEach((key, dynamic value) {
queryParams += '$key=$value';
paramCount -= 1;
if (paramCount > 0) {
queryParams += "&";
}
});
}
// If server already has http(s) don't add it again
if (server.contains("http://") || server.contains("https://")) {
// ignore: join_return_with_assignment
curlCmd += "${compressed ? " --compressed " : " "}${"'$server$endpoint$queryParams'"}";
} else {
// ignore: join_return_with_assignment
print(server);
print(endpoint);
curlCmd +=
"${compressed ? " --compressed " : " "}'$uri'"; //{"'${secure ? 'https://' : 'http://'}$server$endpoint$queryParams'"}
}
return curlCmd;
}
}

View File

@ -0,0 +1,4 @@
class AliceHttpError {
dynamic error;
StackTrace? stackTrace;
}

View File

@ -0,0 +1,16 @@
import 'dart:io';
import 'package:alice/model/alice_form_data_file.dart';
import 'package:alice/model/alice_from_data_field.dart';
class AliceHttpRequest {
int size = 0;
DateTime time = DateTime.now();
Map<String, dynamic> headers = <String, dynamic>{};
dynamic body = "";
String? contentType = "";
List<Cookie> cookies = [];
Map<String, dynamic> queryParameters = <String, dynamic>{};
List<AliceFormDataFile>? formDataFiles;
List<AliceFormDataField>? formDataFields;
}

View File

@ -0,0 +1,7 @@
class AliceHttpResponse {
int? status = 0;
int size = 0;
DateTime time = DateTime.now();
dynamic body;
Map<String, String>? headers;
}

View File

@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
class AliceMenuItem {
final String title;
final IconData iconData;
AliceMenuItem(this.title, this.iconData);
}

View File

@ -0,0 +1,25 @@
///Available sort options in inspector UI.
enum AliceSortOption {
time,
responseTime,
responseCode,
responseSize,
endpoint,
}
extension AliceSortOptionsExtension on AliceSortOption {
String get name {
switch (this) {
case AliceSortOption.time:
return "Create time (default)";
case AliceSortOption.responseTime:
return "Response time";
case AliceSortOption.responseCode:
return "Response code";
case AliceSortOption.responseSize:
return "Response size";
case AliceSortOption.endpoint:
return "Endpoint";
}
}
}

View File

@ -0,0 +1,135 @@
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/utils/alice_constants.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: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);
@override
_AliceCallDetailsScreenState createState() => _AliceCallDetailsScreenState();
}
class _AliceCallDetailsScreenState extends State<AliceCallDetailsScreen> with SingleTickerProviderStateMixin {
AliceHttpCall get call => widget.call;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: widget.core.directionality ?? Directionality.of(context),
child: Theme(
data: ThemeData(
brightness: widget.core.brightness,
),
child: StreamBuilder<List<AliceHttpCall>>(
stream: widget.core.callsSubject,
initialData: [widget.call],
builder: (context, callsSnapshot) {
if (callsSnapshot.hasData) {
final AliceHttpCall? call =
callsSnapshot.data!.firstWhereOrNull((snapshotCall) => snapshotCall.id == widget.call.id);
if (call != null) {
return _buildMainWidget();
} else {
return _buildErrorWidget();
}
} else {
return _buildErrorWidget();
}
},
),
),
);
}
Widget _buildMainWidget() {
return DefaultTabController(
length: 4,
child: Scaffold(
floatingActionButton: Column(mainAxisAlignment: MainAxisAlignment.end, children: [
FloatingActionButton(
backgroundColor: AliceConstants.lightRed,
heroTag: 'curl_key',
key: const Key('curl_key'),
onPressed: () async {
Share.share(await _getCurl(), subject: 'Request Details');
},
child: Text("Curl"),
),
SizedBox(
height: 10,
),
FloatingActionButton(
heroTag: 'share_key',
backgroundColor: AliceConstants.lightRed,
key: const Key('share_key'),
onPressed: () async {
Share.share(await _getSharableResponseString(), subject: 'Request Details');
},
child: const Icon(Icons.share),
),
]),
appBar: AppBar(
bottom: TabBar(
indicatorColor: AliceConstants.lightRed,
tabs: _getTabBars(),
),
title: const Text('Alice - HTTP Call Details'),
),
body: TabBarView(
children: _getTabBarViewList(),
),
),
);
}
Widget _buildErrorWidget() {
return const Center(child: Text("Failed to load data"));
}
Future<String> _getSharableResponseString() async {
return AliceSaveHelper.buildCallLog(widget.call);
}
Future<String> _getCurl() async {
return widget.call.getCurlCommand();
}
List<Widget> _getTabBars() {
final List<Widget> widgets = [];
widgets.add(const Tab(icon: Icon(Icons.info_outline), text: "Overview"));
widgets.add(const Tab(icon: Icon(Icons.arrow_upward), text: "Request"));
widgets.add(const Tab(icon: Icon(Icons.arrow_downward), text: "Response"));
widgets.add(
const Tab(
icon: Icon(Icons.warning),
text: "Error",
),
);
return widgets;
}
List<Widget> _getTabBarViewList() {
final List<Widget> widgets = [];
widgets.add(AliceCallOverviewWidget(widget.call));
widgets.add(AliceCallRequestWidget(widget.call));
widgets.add(AliceCallResponseWidget(widget.call));
widgets.add(AliceCallErrorWidget(widget.call));
return widgets;
}
}

View File

@ -0,0 +1,352 @@
import 'package:alice/model/alice_menu_item.dart';
import 'package:alice/helper/alice_alert_helper.dart';
import 'package:alice/model/alice_sort_option.dart';
import 'package:alice/ui/page/alice_call_details_screen.dart';
import 'package:alice/core/alice_core.dart';
import 'package:alice/model/alice_http_call.dart';
import 'package:alice/utils/alice_constants.dart';
import 'package:alice/ui/widget/alice_call_list_item_widget.dart';
import 'package:flutter/material.dart';
import 'alice_stats_screen.dart';
class AliceCallsListScreen extends StatefulWidget {
final AliceCore _aliceCore;
const AliceCallsListScreen(this._aliceCore);
@override
_AliceCallsListScreenState createState() => _AliceCallsListScreenState();
}
class _AliceCallsListScreenState extends State<AliceCallsListScreen> {
AliceCore get aliceCore => widget._aliceCore;
bool _searchEnabled = false;
final TextEditingController _queryTextEditingController = TextEditingController();
final List<AliceMenuItem> _menuItems = [];
AliceSortOption? _sortOption = AliceSortOption.time;
bool _sortAscending = false;
_AliceCallsListScreenState() {
_menuItems.add(AliceMenuItem("Sort", Icons.sort));
_menuItems.add(AliceMenuItem("Delete", Icons.delete));
_menuItems.add(AliceMenuItem("Stats", Icons.insert_chart));
_menuItems.add(AliceMenuItem("Save", Icons.save));
}
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: widget._aliceCore.directionality ?? Directionality.of(context),
child: Theme(
data: ThemeData(
brightness: widget._aliceCore.brightness,
),
child: Scaffold(
appBar: AppBar(
title: _searchEnabled ? _buildSearchField() : _buildTitleWidget(),
actions: [
_buildSearchButton(),
_buildMenuButton(),
],
),
body: _buildCallsListWrapper(),
),
),
);
}
@override
void dispose() {
super.dispose();
_queryTextEditingController.dispose();
}
Widget _buildSearchButton() {
return IconButton(
icon: const Icon(Icons.search),
onPressed: _onSearchClicked,
);
}
void _onSearchClicked() {
setState(() {
_searchEnabled = !_searchEnabled;
if (!_searchEnabled) {
_queryTextEditingController.text = "";
}
});
}
Widget _buildMenuButton() {
return PopupMenuButton<AliceMenuItem>(
onSelected: (AliceMenuItem item) => _onMenuItemSelected(item),
itemBuilder: (BuildContext context) {
return _menuItems.map((AliceMenuItem item) {
return PopupMenuItem<AliceMenuItem>(
value: item,
child: Row(children: [
Icon(
item.iconData,
color: AliceConstants.lightRed,
),
const Padding(
padding: EdgeInsets.only(left: 10),
),
Text(item.title)
]),
);
}).toList();
},
);
}
Widget _buildTitleWidget() {
return const Text("Alice - Inspector");
}
Widget _buildSearchField() {
return TextField(
controller: _queryTextEditingController,
autofocus: true,
decoration: InputDecoration(
hintText: "Search http request...",
hintStyle: TextStyle(fontSize: 16.0, color: AliceConstants.grey),
border: InputBorder.none,
),
style: const TextStyle(fontSize: 16.0),
onChanged: _updateSearchQuery,
);
}
void _onMenuItemSelected(AliceMenuItem menuItem) {
if (menuItem.title == "Sort") {
_showSortDialog();
}
if (menuItem.title == "Delete") {
_showRemoveDialog();
}
if (menuItem.title == "Stats") {
_showStatsScreen();
}
if (menuItem.title == "Save") {
_saveToFile();
}
}
Widget _buildCallsListWrapper() {
return StreamBuilder<List<AliceHttpCall>>(
stream: aliceCore.callsSubject,
builder: (context, snapshot) {
List<AliceHttpCall> calls = snapshot.data ?? [];
final String query = _queryTextEditingController.text.trim();
if (query.isNotEmpty) {
calls = calls.where((call) => call.endpoint.toLowerCase().contains(query.toLowerCase())).toList();
}
if (calls.isNotEmpty) {
return _buildCallsListWidget(calls);
} else {
return _buildEmptyWidget();
}
},
);
}
Widget _buildEmptyWidget() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 32),
child: Center(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(
Icons.error_outline,
color: AliceConstants.orange,
),
const SizedBox(height: 6),
const Text(
"There are no calls to show",
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 12),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: const [
Text(
"• Check if you send any http request",
style: TextStyle(fontSize: 12),
textAlign: TextAlign.center,
),
Text(
"• Check your Alice configuration",
style: TextStyle(fontSize: 12),
textAlign: TextAlign.center,
),
Text(
"• Check search filters",
style: TextStyle(fontSize: 12),
textAlign: TextAlign.center,
)
])
]),
),
);
}
Widget _buildCallsListWidget(List<AliceHttpCall> calls) {
final List<AliceHttpCall> callsSorted = List.of(calls);
switch (_sortOption) {
case AliceSortOption.time:
if (_sortAscending) {
callsSorted.sort((call1, call2) => call1.createdTime.compareTo(call2.createdTime));
} else {
callsSorted.sort((call1, call2) => call2.createdTime.compareTo(call1.createdTime));
}
break;
case AliceSortOption.responseTime:
if (_sortAscending) {
callsSorted.sort();
callsSorted.sort((call1, call2) => call1.response?.time.compareTo(call2.response!.time) ?? -1);
} else {
callsSorted.sort((call1, call2) => call2.response?.time.compareTo(call1.response!.time) ?? -1);
}
break;
case AliceSortOption.responseCode:
if (_sortAscending) {
callsSorted.sort((call1, call2) => call1.response?.status?.compareTo(call2.response!.status!) ?? -1);
} else {
callsSorted.sort((call1, call2) => call2.response?.status?.compareTo(call1.response!.status!) ?? -1);
}
break;
case AliceSortOption.responseSize:
if (_sortAscending) {
callsSorted.sort((call1, call2) => call1.response?.size.compareTo(call2.response!.size) ?? -1);
} else {
callsSorted.sort((call1, call2) => call2.response?.size.compareTo(call1.response!.size) ?? -1);
}
break;
case AliceSortOption.endpoint:
if (_sortAscending) {
callsSorted.sort((call1, call2) => call1.endpoint.compareTo(call2.endpoint));
} else {
callsSorted.sort((call1, call2) => call2.endpoint.compareTo(call1.endpoint));
}
break;
default:
break;
}
return ListView.builder(
itemCount: callsSorted.length,
itemBuilder: (context, index) {
return AliceCallListItemWidget(callsSorted[index], _onListItemClicked);
},
);
}
void _onListItemClicked(AliceHttpCall call) {
Navigator.push<void>(
widget._aliceCore.getContext()!,
MaterialPageRoute(
builder: (context) => AliceCallDetailsScreen(call, widget._aliceCore),
),
);
}
void _showRemoveDialog() {
AliceAlertHelper.showAlert(
context,
"Delete calls",
"Do you want to delete http calls?",
firstButtonTitle: "No",
firstButtonAction: () => <String, dynamic>{},
secondButtonTitle: "Yes",
secondButtonAction: () => _removeCalls(),
);
}
void _removeCalls() {
aliceCore.removeCalls();
}
void _showStatsScreen() {
Navigator.push<void>(
aliceCore.getContext()!,
MaterialPageRoute(
builder: (context) => AliceStatsScreen(widget._aliceCore),
),
);
}
void _saveToFile() async {
aliceCore.saveHttpRequests(context);
}
void _updateSearchQuery(String query) {
setState(() {});
}
void _showSortDialog() {
showDialog<void>(
context: context,
builder: (BuildContext buildContext) {
return Theme(
data: ThemeData(
brightness: Brightness.light,
),
child: AlertDialog(
title: const Text("Select filter"),
content: StatefulBuilder(builder: (context, setState) {
return Wrap(
children: [
...AliceSortOption.values
.map((AliceSortOption sortOption) => RadioListTile<AliceSortOption>(
title: Text(sortOption.name),
value: sortOption,
groupValue: _sortOption,
onChanged: (AliceSortOption? value) {
setState(() {
_sortOption = value;
});
},
))
.toList(),
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(() {});
}
}

View File

@ -0,0 +1,168 @@
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/utils/alice_constants.dart';
import 'package:flutter/material.dart';
class AliceStatsScreen extends StatelessWidget {
final AliceCore aliceCore;
const AliceStatsScreen(this.aliceCore);
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: aliceCore.directionality ?? Directionality.of(context),
child: Theme(
data: ThemeData(
brightness: aliceCore.brightness,
),
child: Scaffold(
appBar: AppBar(
title: const Text("Alice - HTTP Inspector - Stats"),
),
body: Container(
padding: const EdgeInsets.all(8),
child: ListView(
children: _buildMainListWidgets(),
),
),
),
),
);
}
List<Widget> _buildMainListWidgets() {
return [
_getRow("Total requests:", "${_getTotalRequests()}"),
_getRow("Pending requests:", "${_getPendingRequests()}"),
_getRow("Success requests:", "${_getSuccessRequests()}"),
_getRow("Redirection requests:", "${_getRedirectionRequests()}"),
_getRow("Error requests:", "${_getErrorRequests()}"),
_getRow("Bytes send:", AliceConversionHelper.formatBytes(_getBytesSent())),
_getRow("Bytes received:", AliceConversionHelper.formatBytes(_getBytesReceived())),
_getRow("Average request time:", AliceConversionHelper.formatTime(_getAverageRequestTime())),
_getRow("Max request time:", AliceConversionHelper.formatTime(_getMaxRequestTime())),
_getRow("Min request time:", AliceConversionHelper.formatTime(_getMinRequestTime())),
_getRow("Get requests:", "${_getRequests("GET")} "),
_getRow("Post requests:", "${_getRequests("POST")} "),
_getRow("Delete requests:", "${_getRequests("DELETE")} "),
_getRow("Put requests:", "${_getRequests("PUT")} "),
_getRow("Patch requests:", "${_getRequests("PATCH")} "),
_getRow("Secured requests:", "${_getSecuredRequests()}"),
_getRow("Unsecured requests:", "${_getUnsecuredRequests()}"),
];
}
Widget _getRow(String label, String value) {
return Row(
children: <Widget>[
Text(
label,
style: _getLabelTextStyle(),
),
const Padding(
padding: EdgeInsets.only(left: 10),
),
Text(
value,
style: _getValueTextStyle(),
)
],
);
}
TextStyle _getLabelTextStyle() {
return const TextStyle(fontSize: 16);
}
TextStyle _getValueTextStyle() {
return const TextStyle(fontSize: 16, fontWeight: FontWeight.bold);
}
int _getTotalRequests() {
return calls.length;
}
int _getSuccessRequests() => calls
.where((call) => call.response != null && call.response!.status! >= 200 && call.response!.status! < 300)
.toList()
.length;
int _getRedirectionRequests() => calls
.where((call) => call.response != null && call.response!.status! >= 300 && call.response!.status! < 400)
.toList()
.length;
int _getErrorRequests() => calls
.where((call) => call.response != null && call.response!.status! >= 400 && call.response!.status! < 600)
.toList()
.length;
int _getPendingRequests() => calls.where((call) => call.loading).toList().length;
int _getBytesSent() {
int bytes = 0;
calls.forEach((AliceHttpCall call) {
bytes += call.request!.size;
});
return bytes;
}
int _getBytesReceived() {
int bytes = 0;
calls.forEach((AliceHttpCall call) {
if (call.response != null) {
bytes += call.response!.size;
}
});
return bytes;
}
int _getAverageRequestTime() {
int requestTimeSum = 0;
int requestsWithDurationCount = 0;
calls.forEach((AliceHttpCall call) {
if (call.duration != 0) {
requestTimeSum = call.duration;
requestsWithDurationCount++;
}
});
if (requestTimeSum == 0) {
return 0;
}
return requestTimeSum ~/ requestsWithDurationCount;
}
int _getMaxRequestTime() {
int maxRequestTime = 0;
calls.forEach((AliceHttpCall call) {
if (call.duration > maxRequestTime) {
maxRequestTime = call.duration;
}
});
return maxRequestTime;
}
int _getMinRequestTime() {
int minRequestTime = 10000000;
if (calls.isEmpty) {
minRequestTime = 0;
} else {
calls.forEach((AliceHttpCall call) {
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<AliceHttpCall> get calls => aliceCore.callsSubject.value;
}

View File

@ -0,0 +1,42 @@
import 'dart:convert';
import 'package:alice/helper/alice_conversion_helper.dart';
import 'package:alice/utils/alice_parser.dart';
import 'package:flutter/material.dart';
abstract class AliceBaseCallDetailsWidgetState<T extends StatefulWidget>
extends State<T> {
final JsonEncoder encoder = const JsonEncoder.withIndent(' ');
Widget getListRow(String name, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(name,
style: const TextStyle(fontWeight: FontWeight.bold)),
const Padding(
padding: EdgeInsets.only(left: 5),
),
Flexible(
child: SelectableText(
value,
),
),
const Padding(
padding: EdgeInsets.only(bottom: 18),
)
],
);
}
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<String, dynamic>? headers) =>
AliceParser.getContentType(headers);
}

View File

@ -0,0 +1,39 @@
import 'package:alice/model/alice_http_call.dart';
import 'package:alice/ui/widget/alice_base_call_details_widget.dart';
import 'package:flutter/material.dart';
class AliceCallErrorWidget extends StatefulWidget {
final AliceHttpCall call;
const AliceCallErrorWidget(this.call);
@override
State<StatefulWidget> createState() {
return _AliceCallErrorWidgetState();
}
}
class _AliceCallErrorWidgetState
extends AliceBaseCallDetailsWidgetState<AliceCallErrorWidget> {
AliceHttpCall get _call => widget.call;
@override
Widget build(BuildContext context) {
if (_call.error != null) {
final List<Widget> rows = [];
final dynamic error = _call.error!.error;
var errorText = "Error is empty";
if (error != null) {
errorText = error.toString();
}
rows.add(getListRow("Error:", errorText));
return Container(
padding: const EdgeInsets.all(6),
child: ListView(children: rows),
);
} else {
return const Center(child: Text("Nothing to display here"));
}
}
}

View File

@ -0,0 +1,209 @@
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';
class AliceCallListItemWidget extends StatelessWidget {
final AliceHttpCall call;
final Function itemClickAction;
const AliceCallListItemWidget(this.call, this.itemClickAction);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => itemClickAction(call),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMethodAndEndpointRow(context),
const SizedBox(height: 4),
_buildServerRow(),
const SizedBox(height: 4),
_buildStatsRow()
],
),
),
_buildResponseColumn(context)
],
),
),
_buildDivider()
],
),
);
}
Widget _buildMethodAndEndpointRow(BuildContext context) {
final Color? textColor = _getEndpointTextColor(context);
return Row(children: [
Text(
call.method,
style: TextStyle(fontSize: 16, color: textColor),
),
const Padding(
padding: EdgeInsets.only(left: 10),
),
Flexible(
// ignore: avoid_unnecessary_containers
child: Container(
child: Text(
call.endpoint,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
color: textColor,
),
),
),
)
]);
}
Widget _buildServerRow() {
return Row(children: [
_getSecuredConnectionIcon(call.secure),
Expanded(
child: Text(
call.server,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: const TextStyle(
fontSize: 14,
),
),
),
]);
}
Widget _buildStatsRow() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(_formatTime(call.request!.time), 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)} / "
"${AliceConversionHelper.formatBytes(call.response!.size)}",
style: const TextStyle(fontSize: 12),
),
)
],
);
}
Widget _buildDivider() {
return Container(height: 1, color: AliceConstants.grey);
}
String _formatTime(DateTime time) {
return "${formatTimeUnit(time.hour)}:"
"${formatTimeUnit(time.minute)}:"
"${formatTimeUnit(time.second)}:"
"${formatTimeUnit(time.millisecond)}";
}
String formatTimeUnit(int timeUnit) {
return (timeUnit < 10) ? "0$timeUnit" : "$timeUnit";
}
Widget _buildResponseColumn(BuildContext context) {
final List<Widget> widgets = [];
if (call.loading) {
widgets.add(
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AliceConstants.lightRed),
),
),
);
widgets.add(
const SizedBox(
height: 4,
),
);
}
widgets.add(
Text(
_getStatus(call.response!),
style: TextStyle(
fontSize: 16,
color: _getStatusTextColor(context),
),
),
);
return Container(
width: 50,
child: Column(
children: widgets,
),
);
}
Color? _getStatusTextColor(BuildContext context) {
final int? status = call.response!.status;
if (status == -1) {
return AliceConstants.red;
} else if (status! < 200) {
return Theme.of(context).textTheme.bodyLarge?.color;
} else if (status >= 200 && status < 300) {
return AliceConstants.green;
} else if (status >= 300 && status < 400) {
return AliceConstants.orange;
} else if (status >= 400 && status < 600) {
return AliceConstants.red;
} else {
return Theme.of(context).textTheme.bodyLarge?.color;
}
}
Color? _getEndpointTextColor(BuildContext context) {
if (call.loading) {
return AliceConstants.grey;
} else {
return _getStatusTextColor(context);
}
}
String _getStatus(AliceHttpResponse response) {
if (response.status == -1) {
return "ERR";
} else if (response.status == 0) {
return "???";
} else {
return "${response.status}";
}
}
Widget _getSecuredConnectionIcon(bool secure) {
IconData iconData;
Color iconColor;
if (secure) {
iconData = Icons.lock_outline;
iconColor = AliceConstants.green;
} else {
iconData = Icons.lock_open;
iconColor = AliceConstants.red;
}
return Padding(
padding: const EdgeInsets.only(right: 3),
child: Icon(
iconData,
color: iconColor,
size: 12,
),
);
}
}

View File

@ -0,0 +1,38 @@
import 'package:alice/model/alice_http_call.dart';
import 'package:alice/ui/widget/alice_base_call_details_widget.dart';
import 'package:flutter/material.dart';
class AliceCallOverviewWidget extends StatefulWidget {
final AliceHttpCall call;
const AliceCallOverviewWidget(this.call);
@override
State<StatefulWidget> createState() {
return _AliceCallOverviewWidget();
}
}
class _AliceCallOverviewWidget
extends AliceBaseCallDetailsWidgetState<AliceCallOverviewWidget> {
AliceHttpCall get _call => widget.call;
@override
Widget build(BuildContext context) {
final List<Widget> rows = [];
rows.add(getListRow("Method: ", _call.method));
rows.add(getListRow("Server: ", _call.server));
rows.add(getListRow("Endpoint: ", _call.endpoint));
rows.add(getListRow("Started:", _call.request!.time.toString()));
rows.add(getListRow("Finished:", _call.response!.time.toString()));
rows.add(getListRow("Duration:", formatDuration(_call.duration)));
rows.add(getListRow("Bytes sent:", formatBytes(_call.request!.size)));
rows.add(getListRow("Bytes received:", formatBytes(_call.response!.size)));
rows.add(getListRow("Client:", _call.client));
rows.add(getListRow("Secure:", _call.secure.toString()));
return Container(
padding: const EdgeInsets.all(6),
child: ListView(children: rows),
);
}
}

View File

@ -0,0 +1,78 @@
import 'package:alice/model/alice_http_call.dart';
import 'package:alice/ui/widget/alice_base_call_details_widget.dart';
import 'package:flutter/material.dart';
class AliceCallRequestWidget extends StatefulWidget {
final AliceHttpCall call;
const AliceCallRequestWidget(this.call);
@override
State<StatefulWidget> createState() {
return _AliceCallRequestWidget();
}
}
class _AliceCallRequestWidget
extends AliceBaseCallDetailsWidgetState<AliceCallRequestWidget> {
AliceHttpCall get _call => widget.call;
@override
Widget build(BuildContext context) {
final List<Widget> rows = [];
rows.add(getListRow("Started:", _call.request!.time.toString()));
rows.add(getListRow("Bytes sent:", formatBytes(_call.request!.size)));
rows.add(
getListRow("Content type:", getContentType(_call.request!.headers)!));
final dynamic body = _call.request!.body;
var bodyContent = "Body is empty";
if (body != null) {
bodyContent = formatBody(body, getContentType(_call.request!.headers));
}
rows.add(getListRow("Body:", bodyContent));
final formDataFields = _call.request!.formDataFields;
if (formDataFields?.isNotEmpty == true) {
rows.add(getListRow("Form data fields: ", ""));
formDataFields!.forEach(
(field) {
rows.add(getListRow("${field.name}:", field.value));
},
);
}
final formDataFiles = _call.request!.formDataFiles;
if (formDataFiles?.isNotEmpty == true) {
rows.add(getListRow("Form data files: ", ""));
formDataFiles!.forEach(
(field) {
rows.add(getListRow("${field.fileName}:",
"${field.contentType} / ${field.length} B"));
},
);
}
final headers = _call.request!.headers;
var headersContent = "Headers are empty";
if (headers.isNotEmpty) {
headersContent = "";
}
rows.add(getListRow("Headers: ", headersContent));
_call.request!.headers.forEach((header, dynamic value) {
rows.add(getListRow("$header:", value.toString()));
});
final queryParameters = _call.request!.queryParameters;
var queryParametersContent = "Query parameters are empty";
if (queryParameters.isNotEmpty) {
queryParametersContent = "";
}
rows.add(getListRow("Query Parameters: ", queryParametersContent));
_call.request!.queryParameters.forEach((query, dynamic value) {
rows.add(getListRow("$query:", value.toString()));
});
return Container(
padding: const EdgeInsets.all(6),
child: ListView(children: rows),
);
}
}

View File

@ -0,0 +1,264 @@
import 'package:alice/model/alice_http_call.dart';
import 'package:alice/utils/alice_constants.dart';
import 'package:alice/ui/widget/alice_base_call_details_widget.dart';
import 'package:flutter/material.dart';
class AliceCallResponseWidget extends StatefulWidget {
final AliceHttpCall call;
const AliceCallResponseWidget(this.call);
@override
State<StatefulWidget> createState() {
return _AliceCallResponseWidgetState();
}
}
class _AliceCallResponseWidgetState extends AliceBaseCallDetailsWidgetState<AliceCallResponseWidget> {
static const _imageContentType = "image";
static const _videoContentType = "video";
static const _jsonContentType = "json";
static const _xmlContentType = "xml";
static const _textContentType = "text";
static const _kLargeOutputSize = 100000;
bool _showLargeBody = false;
bool _showUnsupportedBody = false;
AliceHttpCall get _call => widget.call;
@override
Widget build(BuildContext context) {
final List<Widget> rows = [];
if (!_call.loading) {
rows.addAll(_buildGeneralDataRows());
rows.addAll(_buildHeadersRows());
rows.addAll(_buildBodyRows());
return Container(
padding: const EdgeInsets.all(6),
child: ListView(children: rows),
);
} else {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [CircularProgressIndicator(), Text("Awaiting response...")],
),
);
}
}
@override
void dispose() {
super.dispose();
}
List<Widget> _buildGeneralDataRows() {
final List<Widget> rows = [];
rows.add(getListRow("Received:", _call.response!.time.toString()));
rows.add(getListRow("Bytes received:", formatBytes(_call.response!.size)));
final status = _call.response!.status;
var statusText = "$status";
if (status == -1) {
statusText = "Error";
}
rows.add(getListRow("Status:", statusText));
return rows;
}
List<Widget> _buildHeadersRows() {
final List<Widget> rows = [];
final headers = _call.response!.headers;
var headersContent = "Headers are empty";
if (headers != null && headers.isNotEmpty) {
headersContent = "";
}
rows.add(getListRow("Headers: ", headersContent));
if (_call.response!.headers != null) {
_call.response!.headers!.forEach((header, value) {
rows.add(getListRow("$header:", value.toString()));
});
}
return rows;
}
List<Widget> _buildBodyRows() {
final List<Widget> rows = [];
if (_isImageResponse()) {
rows.addAll(_buildImageBodyRows());
} else if (_isVideoResponse()) {
rows.addAll(_buildVideoBodyRows());
} else if (_isTextResponse()) {
if (_isLargeResponseBody()) {
rows.addAll(_buildLargeBodyTextRows());
} else {
rows.addAll(_buildTextBodyRows());
}
} else {
rows.addAll(_buildUnknownBodyRows());
}
return rows;
}
List<Widget> _buildImageBodyRows() {
final List<Widget> rows = [];
rows.add(
Column(
children: [
Row(
children: const [
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),
],
),
);
return rows;
}
List<Widget> _buildLargeBodyTextRows() {
final List<Widget> rows = [];
if (_showLargeBody) {
return _buildTextBodyRows();
} else {
rows.add(getListRow("Body:", "Too large to show (${_call.response!.body.toString().length} Bytes)"));
rows.add(const SizedBox(height: 8));
rows.add(
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(AliceConstants.lightRed),
),
onPressed: () {
setState(() {
_showLargeBody = true;
});
},
child: const Text("Show body"),
),
);
rows.add(const SizedBox(height: 8));
rows.add(const Text("Warning! It will take some time to render output."));
}
return rows;
}
List<Widget> _buildTextBodyRows() {
final List<Widget> rows = [];
final headers = _call.response!.headers;
final bodyContent = formatBody(_call.response!.body, getContentType(headers));
rows.add(getListRow("Body:", bodyContent));
return rows;
}
List<Widget> _buildVideoBodyRows() {
final List<Widget> rows = [];
rows.add(
Row(
children: const [
Text(
"Body: Video",
style: TextStyle(fontWeight: FontWeight.bold),
)
],
),
);
rows.add(const SizedBox(height: 8));
rows.add(const SizedBox(height: 8));
return rows;
}
List<Widget> _buildUnknownBodyRows() {
final List<Widget> rows = [];
final headers = _call.response!.headers;
final contentType = getContentType(headers) ?? "<unknown>";
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."));
rows.add(
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(AliceConstants.lightRed),
),
onPressed: () {
setState(() {
_showUnsupportedBody = true;
});
},
child: const Text("Show unsupported body"),
),
);
}
return rows;
}
Map<String, String> _buildRequestHeaders() {
final Map<String, String> requestHeaders = {};
if (_call.request?.headers != null) {
requestHeaders.addAll(
_call.request!.headers.map(
(String key, dynamic value) {
return MapEntry(key, value.toString());
},
),
);
}
return requestHeaders;
}
bool _isImageResponse() {
return _getContentTypeOfResponse()!.toLowerCase().contains(_imageContentType);
}
bool _isVideoResponse() {
return _getContentTypeOfResponse()!.toLowerCase().contains(_videoContentType);
}
bool _isTextResponse() {
final String responseContentTypeLowerCase = _getContentTypeOfResponse()!.toLowerCase();
return responseContentTypeLowerCase.contains(_jsonContentType) ||
responseContentTypeLowerCase.contains(_xmlContentType) ||
responseContentTypeLowerCase.contains(_textContentType);
}
String? _getContentTypeOfResponse() {
return getContentType(_call.response!.headers);
}
bool _isLargeResponseBody() {
return _call.response!.body != null && _call.response!.body.toString().length > _kLargeOutputSize;
}
}

View File

@ -0,0 +1,9 @@
import 'package:flutter/material.dart';
class AliceConstants {
static Color red = const Color(0xffff3f34);
static Color lightRed = const Color(0xffff5e57);
static Color green = const Color(0xff05c46b);
static Color grey = const Color(0xff808e9b);
static Color orange = const Color(0xffffa801);
}

View File

@ -0,0 +1,78 @@
import 'dart:convert';
class AliceParser {
static const String _emptyBody = "Body is empty";
static const String _unknownContentType = "Unknown";
static const String _jsonContentTypeSmall = "content-type";
static const String _jsonContentTypeBig = "Content-Type";
static const String _stream = "Stream";
static const String _applicationJson = "application/json";
static const String _parseFailedText = "Failed to parse ";
static const JsonEncoder encoder = JsonEncoder.withIndent(' ');
static String _parseJson(dynamic json) {
try {
return encoder.convert(json);
} catch (exception) {
return json.toString();
}
}
static dynamic _decodeJson(dynamic body) {
try {
return json.decode(body as String);
} catch (exception) {
return body;
}
}
static String formatBody(dynamic body, String? contentType) {
try {
if (body == null) {
return _emptyBody;
}
var bodyContent = _emptyBody;
if (contentType == null ||
!contentType.toLowerCase().contains(_applicationJson)) {
final bodyTemp = body.toString();
if (bodyTemp.isNotEmpty) {
bodyContent = bodyTemp;
}
} else {
if (body is String && body.contains("\n")) {
bodyContent = body;
} else {
if (body is String) {
if (body.isNotEmpty) {
//body is minified json, so decode it to a map and let the encoder pretty print this map
bodyContent = _parseJson(_decodeJson(body));
}
} else if (body is Stream) {
bodyContent = _stream;
} else {
bodyContent = _parseJson(body);
}
}
}
return bodyContent;
} catch (exception) {
return _parseFailedText + body.toString();
}
}
static String? getContentType(Map<String, dynamic>? headers) {
if (headers != null) {
if (headers.containsKey(_jsonContentTypeSmall)) {
return headers[_jsonContentTypeSmall] as String?;
}
if (headers.containsKey(_jsonContentTypeBig)) {
return headers[_jsonContentTypeBig] as String?;
}
}
return _unknownContentType;
}
}

View File

@ -0,0 +1,90 @@
///Code from https://github.com/deven98/shake
///Seems to be not maintained for almost 2 years... (01.03.2021).
import 'dart:async';
import 'dart:math';
import 'package:sensors_plus/sensors_plus.dart';
/// Callback for phone shakes
typedef PhoneShakeCallback = Null Function();
/// ShakeDetector class for phone shake functionality
class ShakeDetector {
/// User callback for phone shake
final PhoneShakeCallback? onPhoneShake;
/// Shake detection threshold
final double shakeThresholdGravity;
/// Minimum time between shake
final int shakeSlopTimeMS;
/// Time before shake count resets
final int shakeCountResetTime;
int mShakeTimestamp = DateTime.now().millisecondsSinceEpoch;
int mShakeCount = 0;
/// StreamSubscription for Accelerometer events
StreamSubscription? streamSubscription;
/// This constructor waits until [startListening] is called
ShakeDetector.waitForStart(
{this.onPhoneShake,
this.shakeThresholdGravity = 2.7,
this.shakeSlopTimeMS = 500,
this.shakeCountResetTime = 3000});
/// This constructor automatically calls [startListening] and starts detection and callbacks.\
ShakeDetector.autoStart(
{this.onPhoneShake,
this.shakeThresholdGravity = 2.7,
this.shakeSlopTimeMS = 500,
this.shakeCountResetTime = 3000}) {
startListening();
}
/// Starts listening to accelerometer events
void startListening() {
streamSubscription = accelerometerEvents.listen((AccelerometerEvent event) {
final double x = event.x;
final double y = event.y;
final double z = event.z;
final double gX = x / 9.80665;
final double gY = y / 9.80665;
final double gZ = z / 9.80665;
// gForce will be close to 1 when there is no movement.
final double gForce = sqrt(gX * gX + gY * gY + gZ * gZ);
if (gForce > shakeThresholdGravity) {
final now = DateTime.now().millisecondsSinceEpoch;
// ignore shake events too close to each other (500ms)
if (mShakeTimestamp + shakeSlopTimeMS > now) {
return;
}
// reset the shake count after 3 seconds of no shakes
if (mShakeTimestamp + shakeCountResetTime < now) {
mShakeCount = 0;
}
mShakeTimestamp = now;
mShakeCount++;
onPhoneShake!();
}
});
}
/// Stops listening to accelerometer events
void stopListening() {
if (streamSubscription != null) {
streamSubscription!.cancel();
}
}
void dispose() {
streamSubscription?.cancel();
}
}

BIN
alice/media/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

BIN
alice/media/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

BIN
alice/media/11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

BIN
alice/media/12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
alice/media/13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

BIN
alice/media/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

BIN
alice/media/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
alice/media/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

BIN
alice/media/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
alice/media/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

BIN
alice/media/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

BIN
alice/media/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

BIN
alice/media/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

BIN
alice/media/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

29
alice/pubspec.yaml Normal file
View File

@ -0,0 +1,29 @@
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: 0.2.4
author: Jakub Homlala <jhomlala@gmail.com>
homepage: https://github.com/jhomlala/alice
environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=1.12.0"
dependencies:
flutter:
sdk: flutter
http: ^1.2.2
dio: ^4.0.0
# flutter_local_notifications: ^9.9.1 # Disabled to avoid build conflicts
rxdart: ^0.27.1
path_provider: ^2.1.5
permission_handler: ^12.0.0+1
package_info_plus: 8.3.0
# open_file: ^3.2.1
sensors_plus: ^6.1.1
share_plus: 11.0.0
chopper: ^8.1.0
collection: ^1.19.1
dev_dependencies:
lint: ^1.5.3

29
analysis_options.yaml Normal file
View File

@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
android/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

69
android/app/build.gradle Normal file
View File

@ -0,0 +1,69 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def kotlin_version = rootProject.ext.has("kotlin_version") ? rootProject.ext.get("kotlin_version") : "2.1.20"
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
android {
namespace "com.example.baseproject"
compileSdkVersion 36
ndkVersion "26.1.10909125"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.baseproject"
minSdkVersion flutter.minSdkVersion
targetSdkVersion 36
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.baseproject">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,34 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.baseproject">
<application
android:label="baseproject"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@ -0,0 +1,6 @@
package com.example.baseproject
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.baseproject">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

18
android/build.gradle Normal file
View File

@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx4096M
android.useAndroidX=true
android.enableJetifier=true
org.gradle.java.home=C:\\Program Files\\Java\\jdk-17

View File

@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

25
android/settings.gradle Normal file
View File

@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.9.1" apply false
id "org.jetbrains.kotlin.android" version "2.1.20" apply false
}
include ":app"

12
build.yaml Normal file
View File

@ -0,0 +1,12 @@
targets:
$default:
builders:
injectable_generator:injectable_builder:
options:
auto_register: true
# auto registers any class with a name matches the given pattern
class_name_pattern:
"Service$|Repository$|Bloc$|UseCases$"
# auto registers any class inside a file with a
# name matches the given pattern
#file_name_pattern: "_service$|_repository$|_bloc$"

3
devtools_options.yaml Normal file
View File

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

34
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
</dict>
</plist>

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

41
ios/Podfile Normal file
View File

@ -0,0 +1,41 @@
# Uncomment this line to define a global platform for your project
platform :ios, '10.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@ -0,0 +1,552 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
FE15EF26263AF0E3589C177A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C769CC47EF6EE6238651A6B9 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A9D1A183C6BE536EA5C6A7E7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
B6F78449E779735190CB8C28 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
C769CC47EF6EE6238651A6B9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CB8A2F19EE92F5C4FB2CFA52 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FE15EF26263AF0E3589C177A /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
1139888A422B5187C08F9150 /* Pods */ = {
isa = PBXGroup;
children = (
CB8A2F19EE92F5C4FB2CFA52 /* Pods-Runner.debug.xcconfig */,
B6F78449E779735190CB8C28 /* Pods-Runner.release.xcconfig */,
A9D1A183C6BE536EA5C6A7E7 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
14C5FD2F79A8233FC0DD05E3 /* Frameworks */ = {
isa = PBXGroup;
children = (
C769CC47EF6EE6238651A6B9 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
1139888A422B5187C08F9150 /* Pods */,
14C5FD2F79A8233FC0DD05E3 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
B87CEC732CA2BE826259FF34 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
0B41A6C2C6540ED4CFDA2C6A /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0B41A6C2C6540ED4CFDA2C6A /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
B87CEC732CA2BE826259FF34 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = P3472XX8FH;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.baseproject;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = P3472XX8FH;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.baseproject;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = P3472XX8FH;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.baseproject;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,13 @@
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Some files were not shown because too many files have changed in this diff Show More