Commit đầu
140
.gitignore
vendored
Normal 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
@ -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
@ -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
@ -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
@ -0,0 +1,252 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/jhomlala/alice/master/media/logo.png" width="250px">
|
||||
</p>
|
||||
|
||||
# Alice
|
||||
|
||||
[](https://pub.dartlang.org/packages/alice)
|
||||
[](https://github.com/jhomlala/alice)
|
||||
[](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">
|
||||
21
alice/analysis_options.yaml
Normal 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
@ -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);
|
||||
}
|
||||
}
|
||||
123
alice/lib/core/alice_chopper_response_interceptor.dart
Normal 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;
|
||||
// }
|
||||
// }
|
||||
264
alice/lib/core/alice_core.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
144
alice/lib/core/alice_dio_interceptor.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
89
alice/lib/core/alice_http_adapter.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
84
alice/lib/core/alice_http_client_adapter.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
29
alice/lib/core/alice_http_client_extensions.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
12
alice/lib/core/alice_http_extensions.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
11
alice/lib/core/alice_utils.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
alice/lib/helper/alice_alert_helper.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
41
alice/lib/helper/alice_conversion_helper.dart
Normal 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";
|
||||
}
|
||||
}
|
||||
149
alice/lib/helper/alice_save_helper.dart
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
7
alice/lib/model/alice_form_data_file.dart
Normal file
@ -0,0 +1,7 @@
|
||||
class AliceFormDataFile {
|
||||
final String? fileName;
|
||||
final String contentType;
|
||||
final int length;
|
||||
|
||||
AliceFormDataFile(this.fileName, this.contentType, this.length);
|
||||
}
|
||||
6
alice/lib/model/alice_from_data_field.dart
Normal file
@ -0,0 +1,6 @@
|
||||
class AliceFormDataField {
|
||||
final String name;
|
||||
final String value;
|
||||
|
||||
AliceFormDataField(this.name, this.value);
|
||||
}
|
||||
83
alice/lib/model/alice_http_call.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
4
alice/lib/model/alice_http_error.dart
Normal file
@ -0,0 +1,4 @@
|
||||
class AliceHttpError {
|
||||
dynamic error;
|
||||
StackTrace? stackTrace;
|
||||
}
|
||||
16
alice/lib/model/alice_http_request.dart
Normal 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;
|
||||
}
|
||||
7
alice/lib/model/alice_http_response.dart
Normal file
@ -0,0 +1,7 @@
|
||||
class AliceHttpResponse {
|
||||
int? status = 0;
|
||||
int size = 0;
|
||||
DateTime time = DateTime.now();
|
||||
dynamic body;
|
||||
Map<String, String>? headers;
|
||||
}
|
||||
8
alice/lib/model/alice_menu_item.dart
Normal file
@ -0,0 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AliceMenuItem {
|
||||
final String title;
|
||||
final IconData iconData;
|
||||
|
||||
AliceMenuItem(this.title, this.iconData);
|
||||
}
|
||||
25
alice/lib/model/alice_sort_option.dart
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
135
alice/lib/ui/page/alice_call_details_screen.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
352
alice/lib/ui/page/alice_calls_list_screen.dart
Normal 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(() {});
|
||||
}
|
||||
}
|
||||
168
alice/lib/ui/page/alice_stats_screen.dart
Normal 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;
|
||||
}
|
||||
42
alice/lib/ui/widget/alice_base_call_details_widget.dart
Normal 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);
|
||||
}
|
||||
39
alice/lib/ui/widget/alice_call_error_widget.dart
Normal 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
209
alice/lib/ui/widget/alice_call_list_item_widget.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
38
alice/lib/ui/widget/alice_call_overview_widget.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
78
alice/lib/ui/widget/alice_call_request_widget.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
264
alice/lib/ui/widget/alice_call_response_widget.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
9
alice/lib/utils/alice_constants.dart
Normal 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);
|
||||
}
|
||||
78
alice/lib/utils/alice_parser.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
90
alice/lib/utils/shake_detector.dart
Normal 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
|
After Width: | Height: | Size: 327 KiB |
BIN
alice/media/10.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
alice/media/11.png
Normal file
|
After Width: | Height: | Size: 520 KiB |
BIN
alice/media/12.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
alice/media/13.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
alice/media/2.png
Normal file
|
After Width: | Height: | Size: 342 KiB |
BIN
alice/media/3.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
alice/media/4.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
alice/media/5.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
alice/media/6.png
Normal file
|
After Width: | Height: | Size: 387 KiB |
BIN
alice/media/7.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
alice/media/8.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
alice/media/9.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
alice/media/logo.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
29
alice/pubspec.yaml
Normal 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
@ -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
@ -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
@ -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"
|
||||
}
|
||||
7
android/app/src/debug/AndroidManifest.xml
Normal 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>
|
||||
34
android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||
@ -0,0 +1,6 @@
|
||||
package com.example.baseproject
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
}
|
||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal 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>
|
||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal 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>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal 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>
|
||||
18
android/app/src/main/res/values/styles.xml
Normal 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>
|
||||
7
android/app/src/profile/AndroidManifest.xml
Normal 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
@ -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
|
||||
}
|
||||
4
android/gradle.properties
Normal 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
|
||||
6
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
@ -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
@ -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
@ -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
@ -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
|
||||
26
ios/Flutter/AppFrameworkInfo.plist
Normal 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>
|
||||
2
ios/Flutter/Debug.xcconfig
Normal file
@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
2
ios/Flutter/Release.xcconfig
Normal file
@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
41
ios/Podfile
Normal 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
|
||||
552
ios/Runner.xcodeproj/project.pbxproj
Normal 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 */;
|
||||
}
|
||||
7
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -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>
|
||||
@ -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>
|
||||
87
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
Normal 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>
|
||||
10
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
13
ios/Runner/AppDelegate.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 564 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |