From 33b15daad41f4dd94692d16780c2c0e3ce191504 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud <68241710+a7medev@users.noreply.github.com> Date: Mon, 1 May 2023 09:40:22 +0200 Subject: [PATCH 01/34] [MOB-12122] Integrate Danger Coverage Plugin (#357) * Migrate Danger to TypeScript * Integrate Danger coverage plugin * Exclude generated files from coverage * Set Yarn network concurrency to 1 * Remove LCOV installation * Add TypeScript * Add coverage threshold --- .circleci/config.yml | 14 +++++- dangerfile.ts | 8 +++ example/pubspec.lock | 113 ++++++++++++++++--------------------------- package.json | 4 +- yarn.lock | 23 +++++++++ 5 files changed, 90 insertions(+), 72 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c28cb47a4..db6cdb9e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -80,6 +80,9 @@ jobs: - checkout - node/install-packages: pkg-manager: yarn + override-ci-command: yarn install --frozen-lockfile --network-concurrency 1 + - attach_workspace: + at: coverage - run: name: Run Danger command: yarn danger ci @@ -96,6 +99,13 @@ jobs: - run: sh ./scripts/pigeon.sh - run: flutter pub run build_runner build --delete-conflicting-outputs - run: flutter test --coverage + - run: + working_directory: coverage + command: lcov --remove lcov.info '*.g.dart' '*.mocks.dart' -o lcov.info + - persist_to_workspace: + root: coverage + paths: + - lcov.info test_android: executor: @@ -234,7 +244,9 @@ workflows: version: 2 build-test-and-approval-deploy: jobs: - - danger + - danger: + requires: + - test_flutter-stable - test_flutter: name: test_flutter-stable version: stable diff --git a/dangerfile.ts b/dangerfile.ts index 5268c527b..670ae0b05 100644 --- a/dangerfile.ts +++ b/dangerfile.ts @@ -1,4 +1,5 @@ import { danger, fail, schedule, warn } from 'danger'; +import collectCoverage, { ReportType } from '@instabug/danger-plugin-coverage'; const hasSourceChanges = danger.git.modified_files.some((file) => file.startsWith('lib/') @@ -27,3 +28,10 @@ async function hasDescription() { } schedule(hasDescription()); + +collectCoverage({ + label: 'Dart', + type: ReportType.LCOV, + filePath: 'coverage/lcov.info', + threshold: 80, +}); diff --git a/example/pubspec.lock b/example/pubspec.lock index c042872aa..acfcfbf5f 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,82 +5,72 @@ packages: dependency: transitive description: name: archive - sha256: "80e5141fafcb3361653ce308776cfd7d45e6e9fbb429e14eec571382c0c5fecb" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "3.3.2" + version: "3.3.0" async: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.10.0" + version: "2.9.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.0" characters: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.17.0" + version: "1.16.0" crypto: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" espresso: dependency: "direct dev" description: name: espresso - sha256: "641bdfcaec98b2fe2f5c90d61a16cdf6879ddac4d7333a6467ef03d60933596b" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0+5" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "6.1.4" + version: "6.1.2" flutter: dependency: "direct main" description: flutter @@ -120,48 +110,42 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.12.13" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.1.5" meta: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.8.0" path: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.8.2" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" process: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" sky_engine: @@ -173,90 +157,79 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.9.1" + version: "1.9.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.11.0" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.1.1" sync_http: dependency: transitive description: name: sync_http - sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "0.3.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.4.16" + version: "0.4.12" typed_data: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.2" vm_service: dependency: transitive description: name: vm_service - sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "9.4.0" + version: "9.0.0" webdriver: dependency: transitive description: name: webdriver - sha256: ef67178f0cc7e32c1494645b11639dd1335f1d18814aa8435113a92e9ef9d841 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.0" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=2.10.0" diff --git a/package.json b/package.json index d428bf57e..52b3a844e 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,8 @@ "name": "Instabug-Flutter", "version": "0.0.0", "devDependencies": { - "danger": "^11.2.5" + "@instabug/danger-plugin-coverage": "Instabug/danger-plugin-coverage", + "danger": "^11.2.5", + "typescript": "^5.0.4" } } diff --git a/yarn.lock b/yarn.lock index ffcc34e06..d86ee57b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32,6 +32,12 @@ query-string "^6.12.1" xcase "^2.0.1" +"@instabug/danger-plugin-coverage@Instabug/danger-plugin-coverage": + version "0.0.0-development" + resolved "git+ssh://git@github.com/Instabug/danger-plugin-coverage.git#a3941bd25421b0978ec636648a557b2280d0c9e6" + dependencies: + fast-xml-parser "^4.2.0" + "@octokit/auth-token@^2.4.4": version "2.5.0" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" @@ -410,6 +416,13 @@ fast-json-patch@^3.0.0-1: resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947" integrity sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ== +fast-xml-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.0.tgz#6db2ba33b95b8b4af93f94fe024d4b4d02a50855" + integrity sha512-+zVQv4aVTO+o8oRUyRL7PjgeVo1J6oP8Cw2+a8UTZQcj5V0yUK5T63gTN0ldgiHDPghUjKc4OpT6SwMTwnOQug== + dependencies: + strnum "^1.0.5" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -871,6 +884,11 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + supports-color@^5.0.0, supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -898,6 +916,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +typescript@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" From 01d3efb5cf6925efa289e24b8c7cd905b27e8baa Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud <68241710+a7medev@users.noreply.github.com> Date: Mon, 8 May 2023 13:58:25 +0300 Subject: [PATCH 02/34] [MOB-12372] Migrate Flutter Formatting to Dart (#362) --- .circleci/config.yml | 2 +- scripts/pigeon.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index db6cdb9e5..b4f66035d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -185,7 +185,7 @@ jobs: - run: flutter pub get - run: name: Check Format - command: flutter format . --set-exit-if-changed + command: dart format . --set-exit-if-changed lint_flutter: docker: diff --git a/scripts/pigeon.sh b/scripts/pigeon.sh index 4c948fe20..decc2eb21 100644 --- a/scripts/pigeon.sh +++ b/scripts/pigeon.sh @@ -26,7 +26,7 @@ generate_pigeon() { # Generated files are not formatted by default, # this affects pacakge score. - flutter format "$DIR_DART/$name_snake.api.g.dart" + dart format "$DIR_DART/$name_snake.api.g.dart" } for file in pigeons/** From fe22ecf4fe41037398f3b7fb8fe14ebbcf5bca7f Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:01:46 +0200 Subject: [PATCH 03/34] Update pubspec --- example/pubspec.lock | 32 ++++++++++++++++++++++++++++++++ example/pubspec.yaml | 10 +++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index acfcfbf5f..d0e368637 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -81,6 +81,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" + source: hosted + version: "2.0.1" flutter_test: dependency: "direct dev" description: flutter @@ -106,6 +114,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.5" + lints: + dependency: transitive + description: + name: lints + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" + source: hosted + version: "2.0.1" matcher: dependency: transitive description: @@ -127,6 +143,14 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -148,6 +172,14 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.4" + provider: + dependency: "direct main" + description: + name: provider + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" + source: hosted + version: "6.0.5" sky_engine: dependency: transitive description: flutter diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ab9ee0577..afae2f92f 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -18,11 +18,12 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.18.0 <3.0.0" dependencies: flutter: sdk: flutter + provider: ^6.0.5 instabug_flutter: path: ../ @@ -32,6 +33,7 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter + flutter_lints: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -42,6 +44,12 @@ flutter: # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true + assets: + - assets/images/ + fonts: + - family: Axiforma + fonts: + - asset: assets/fonts/Axiforma Regular.otf # To add assets to your application, add an assets section, like this: # assets: From e2c5ec41e5d3a2e2dac45539566d2f63df6c3dc1 Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:02:00 +0200 Subject: [PATCH 04/34] Add analysis_options --- example/analysis_options.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 example/analysis_options.yaml diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 000000000..b57be5d0a --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + require_trailing_commas: true From c46e77610eb56592b2be4064cc93139ac5e614eb Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:05:42 +0200 Subject: [PATCH 05/34] Add assets folder --- example/assets/fonts/Axiforma Regular.otf | Bin 0 -> 88096 bytes example/assets/images/APM.png | Bin 0 -> 852 bytes example/assets/images/Bug Reporting.png | Bin 0 -> 859 bytes example/assets/images/Core.png | Bin 0 -> 2367 bytes example/assets/images/Crash Reporting.png | Bin 0 -> 611 bytes example/assets/images/Feature Requests.png | Bin 0 -> 1031 bytes example/assets/images/Network.png | Bin 0 -> 1198 bytes example/assets/images/Replies.png | Bin 0 -> 464 bytes example/assets/images/Surveys.png | Bin 0 -> 774 bytes 9 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 example/assets/fonts/Axiforma Regular.otf create mode 100644 example/assets/images/APM.png create mode 100644 example/assets/images/Bug Reporting.png create mode 100644 example/assets/images/Core.png create mode 100644 example/assets/images/Crash Reporting.png create mode 100644 example/assets/images/Feature Requests.png create mode 100644 example/assets/images/Network.png create mode 100644 example/assets/images/Replies.png create mode 100644 example/assets/images/Surveys.png diff --git a/example/assets/fonts/Axiforma Regular.otf b/example/assets/fonts/Axiforma Regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..78c55a3c8519be643ed366c7a7b01914f1cd04a6 GIT binary patch literal 88096 zcmdSAcU%bfqXo?A=&miW)UB_AX*U zjV<=xd+#N7qlqPV*1L=EnOWN}&+qxX&->?Va`v9N<(ym2ZRdXJ(z9n5WRJ!m6>1$7 z9c@~53hzazZdZf~D%-bh7cCUJBq21xf)Lu=zH65re^#ALMrh&&gf{hR-y^=wVc{8v zs0b}WqEnY1{y|eWb$o~r?+*FXG78cMK4;6mN64`wLhK*8S?QVfHQHoB+C`wST!_#l zGii_?K9PHFLFtf<-)5dih_Xk>?rlN(kb(3_s0uy zAGV<&sBZwIn-J=X=#jut3Po0JYdJf$M)P+_rFsG}=;E0Vv7f@~^y&j@ta=6CDn!e% z@NXPK^b%zMztW;FX)65TZ+a2xqtYV3|3>sbkY)$@KZWV25B>i}sgS1ylxqYs|3!)% z1LEY6GD4mh2n{m-;XcpB+_4z+?Prp{el|O{~OKzlBRk9<>#R|IuYXj8~qCLKO!G`Cxi=t zGJyX-5cMr`rscB#rd-gK2VjGBl1 zsBK^m@;Wma;$RJdvFJm;N5)URXhmazLWG2YgRL9`t=As>(i8(9U=6AexHqk>DDj~Rq#Cw z)uT_rvi=^`rANWHOklsDkB5GWqb?ykSw3Bk;#6aR7DJiP%Fv_EnKU3$7ZvZI?rJ9AJ7h2eV;3V`*{=X1V*f91>m&wNxVsz=$OT2wFO zLUlkjsY0;ZWF$~2s4itfI%+huy&QQ^aWKZcK=}}=29D)1wtrNT^mKy z7lCv@YOqf=3I+RU2oy?Pfi>qQ@~6%tAM^tGqGzZvw8fQnME*d{s6iw1?#o?uflFOw^@UAF(u(0`p_p2VX_dIo9^>vn7CvuM?HsLR?9&*|G>gQH-* zS&1l^BR;Bu$d#!9<#r(x%yT~&hlcb|VBpCGyDjk1P_6;=AF57=UPI_H@N0nJtf)rSdl6!4RHuKa zt;WjfQORQaffJ>Q?m+#B7+5i;!u&<3gDs@`f_$T#i?L-zixBch$Yl8ViQ+pXc1Ep{ zgU4~zlMjwmFEyfCn-H2Kr&G7-BM_M2^o?Ns@cgV7&a)qR3TfeV3pIp)G}54^A9)or zpvaHB8abkrk355#qx_FN3-PNy@*30)ZTrY;!G7&N@^)b7H9zt^*uMCYw^w)-qSSVv zIg<*5GqTK6$dQ`+k*A?-#YY~_9I&U^@~DxCdijxO&=9bFTRaPP`OQaOgO<=MKJr@F zmtsEhc5nvm^^xb{3|#t=x0m^-qJcw;^Kx=aO{+~o0fFJBenU+i(o0IS@{0!hnIiM^ zO-gEssW_`7t9Wo$reEZcyzHXlg7ofLIc53j#eseS0fEirA!+t0rSZq4Pv54G-w9d8 zC3!`KCR>rNJxowiDVj$YO)B&ZV5=hMg3o1f`ksriG0-q1zm$D_wR~F>Vg4n_En+Z81 z(GZZH4dsea0m$hN@j0jr%BDk1Amj*8v^7KVpeGU5t!DqDOyhr*`nOEeztR(w8kB(E zB2);q`dUj@)B|E{I=|@7g`A}*L&-n*a|u7x1paHL)O!HPl=Eamjq^crKjNVkWQa%<&Y{I?az_EHM= z(-hVFDOj6 zqMOq#sW)^N&~Fd>Wo=X!xgb~M1^aC%jCfO6mm^Vou&SP5mAz3a^pCBl%Fr;HrJm9n zI)yURS>PM_7EMDl&}>-V7o!z0dwxUP(N44<9YDv?C3FQ{MK{p{*il}?ZAwCBR86bt zM4F-7LaP{Pvm>g9qM>hd&{#APjYkvE6f_zA0Q{=vX?2 z8cMgP>Ql~e;+%@kQ*Wui=yr4*q(%YNrR%}!489b^fwQC=x=7cg9q9(NKpSb1ZU(!* z1r4E2(t&h++F2eGI*EEln`i^w3ARZC>_QEY5rJ<4tRWD^z`YTnj$nt`VAU0LKe|7i zL#NW2bRL~fXVbZK8d9szLY`dcZ3cE*2PiiRZKl$xQ6Mv%K0@D8{iIs0+Ns*Brqp`1 zzdBUiMcvIf(fGz>XR2dzHW^K>CQnm0X`pGSX^N@R#nr{nrJGAnmn4_IuC!|n z*IKU5u0~g1*BI9{*DR0YA3lIa2e9S<7_TiR&|u@7GGlp|vqqS;2C(>%=5R;F=DWfPuqo;SRxMk84(x+H z(0!QMV_|Mjf!RG7X5J4lx92FcdjrgGd3Mi)`F#dv_$ip(r{Ro#9%lFxd5)v^=rYVZ z4U*4CVxRMjvPUVQed0{x*;zm*w z8bKk{7u|zB>k+IUX&+aQTv$o6DObvka;H2fPs)q(rW#T{lrPnYYE1c2{!{=JNCi_t zR0tJHg;PzaFe-v-N;RWmsW_?w)sgB%b*8#dU8!zVcd7@~lZvMjs6?t4l|=QXlBpD` zFO^F5q0(R_>Ib_bhCS{r?4AF>S;+c%O*Fvjn?yaQu2T15WsQ*6S!IWVdGg^m=-dDD zkAh1AqA057zmI`(q`TyZ%v&h)R$8j3wgi)N4b=Gkc&34Ptwsaar!^}Zq~_G3=0ka~ z1s#mJ0d}lLaQ0{pqY?w-&P1%i(f?eH#{(lawA)rk| z)ux=N2GDN-&^yhk80e7%Xm0^Eh$@HuasoA-`k7itEu$)^4b<;2GmcZ2s2kKhXfc5u zjDvot2m69M%%=$0Pus!%(v41rQOcu>=wb9X^pEsU^g?<$y^h{MZ>4wB2k8^^Ir=93 zgnmoES0RmLv=Bij#S5<;4MU|^6R1H>*Reh(Lu9~A-pjxJ?Qf*W1 zQ5{qrSDjVeQ2n8Lrh2C$up{tlgW6r~1G_?Nbysy?b)LFRJy!j#dbau(^*r@r^$PWB zb)|Z}dZYR`^m<4SF;<~t?X|05POonz+PwXv465=4Wp^45i|`o zp_)idCrxinmS&JspgdiYpS)3me)krA^oN*A{C>XeVgD)y~lVs$HsGqur?eU3*Y_QhQl@OZ!0kRQtD9 z(tfaG>>TX$b`9*@?0oD3?V8xNvWvFsWY^O!*{+}60J{>q;dT@4zPFoWx6p32-6p%8 zb_eW^+nuw!YIn!(k=>tmZ|w+2b9S5~SC=z#9$X_Xm}|;Kay_^lZW#9+w~$-GZRL(} zH@KHP#n<3n`9^#*z9XN+XY%FzSpEloK3~c2_6zLS+V8bLVt>s3l>HU^JND1*U)#TTU>#~WI6JsH_&5YQGU!J!x)FD4znGWJ8X2=>u}uRhQlL=za8G|)VdnF+B!kkNY_tS9NDipYj9R%L3&1U zQDJ0JPEldjfT;A0veK-mjC6>K%E&9uC@aX$&l(bySyY;yk&#tc+Ez(w3q{gP+bIIt zDKYIn=V)gW*v=-fy^^Q>7g6ourd5z`6W0E7p_qQ@#W7zp#m2=d-cp%i0rJSbym_kf0fp*FsF;6w@YqWVNQB+SwVh! zS!oxW>h4OR?no)d4)OgN;?x34T)bQCuZhl!PPylBr&HreQ;LqFDbp1h$N*z(wELtzEk7; z@{2O^J9ljRnMqe_k#19*Zd070WX@13oAE_*#%F1nN>Zj`0$GZHEF~uEa}KzW%Qlr| z6PTmq$@wBG=c|s%`CKR$x+wQcraYxoo>JwW-8pdPeEY;PC!RmHU4w zJm4!019FP9vI_Il3p4XF@|BGFN_F$U%99VZC{)r4)8(hq;-Z1MS%pfr!mqMr73LHv z)hhaW+=^@!D^|)ED}{=`DpaDxmwdF3k}s7k`6^Q>G`>_RYO{|vWs07%ul1DK^px2; zpiH(8@OHpFC|5L;f011N$v(;za~S$1WvCJ{OerwzbJyo7->LmS*~e!l-LIr1AP8nF zEDAPOsg{lXBGvZ&NvZ8iQDkFn^|tZ0%G-Fk1vXA;hQem2_RCJqhW}IZocE=^a-W3+2H1i?TM%Rmf^9*FEeKVDfR92nFMQBfd4i(8ijVxl2ixi$Y?Bmht9P)i-odtd2ixi$`Z0^G z-odt31ly_~Y^#2-t=_@5dI#GY6Ktz~u&w&Rw(5u2)P~q(hS=1G*wlvD)P~s9hS(H^ z*wlvD^o7{;h1fKN*wlvD)P~sfh1m3k*z^Sle%0ilulV4v_^-MvIP9zV@UQqLU-1!N z@n3i8S3?jS{UuMxS9(Lf(iQTRu8^hMG!EGBsr8tK`6_T14S4xHfg{p;voDeCq2JZkz7=wR1gALvK%Oqfl(yO zp)46fS+X1`l7X>F21bz#p(3v&KfNSZ;Yt<3rHWkmRm7Isq(UGIEmdT~uOhPaqr_61 zz|w4(*+BtKWK)GeHdP2@Q-we_RS4uRhd?$}2xJ?DK(<&2WK)G8La9q;VUf~sIh5O} zgi4KMPHCya$hDFgxvfg5)J*1Vt(7^YxiX_D2v??3KpSP)1KKE4DWHw5e&M$Ih1=>E zZmVCot$yLQ`i0Bof&xR80)e4Qfxu9uKwzj+ATU%Z5E!Zy2n_vLK%R6^E&y7Snhm?2 z>~T+3I#TKBf5piy{C5VL)9lmz~V61n~VDNl*{4{>sgQkgIEO5xO!UAhK;%K_*NPTYCyWv$;d%qmB}rQwAs4tb(F1dOpW|5hh2Kz{U+ZKi2hg z%$H1q%8E*}GW+HCgN{|&`YA#wBscd{gssfynEy=wSE8c!vq;%M0sdT2clV7kSv>$tf=bqT$Gvm!6-Qmz|woGz`v$S+Z3?maNQl z+bAjrIzL$NYzHXW%xvc;nX@G-obBqMjKNojDmm%%nM(QibP2PKmVESAxUUb1O2o$l zmt5xKc}r%M3=MrFdtgH2nYgsQBrr z5S@|zHBR7lYXbhWf$;M3E%@agp!ZY_suuXILc!ORMh&IbQJcZH@|t>2t7tv#MaO}M zvM)UxUXo|hzta2Y=in=;r}9({QGE*@kVmTLDoL$U$AZ@*MV+IbuAZY_q+Sbtj&15a z>Vs+mz70D@$LN^`Oc;1IVwe=BACt=zC?1Wo%oXM~^Ne}NRI?hk5gW!Pv)Sw@b`m>_ zUCVA^@3K$Xx9kUvoyGw?6KotK>-_!wgB`hag?n%z9RMc`G~XLrc% zq1`(>3x_xZ=f;I_QCxSf7dMcb3?76z++uDyw*kBe2e^~mCGIx&H?QXHcs=jM2l8#e zm(ZK9B8`4WB@KZc*c&){eCOZheYdj5C*2!E14%U|U0@DIVi@P@bAQ}$|m2YV;` z2KFxYp7y@>0rsKxP3>FSx3Q0o;fU!ramA_6?r*62-PrFU04<0xyMqahAgq9hjV&&aC(Tk?=+i#UcGj!^P%`1t&@|77Umn*^=6i>T|RS} zp|WCVZf^e}*41B2IFuS}bH`F6K6h zydJyVzmHu-e6dlqKuiyVu*s;?dkXq@&kx+%D$W)n1QL$zh#hV!>h$*x|92;2%SUl2FQJ%s?7v>eG8Olm#&Kx3+4rfL! zT0MT9;pX9OxW;x7mq||;oqmzf2u7!J#qten2QKfQQ#2?qd*z_@B5v=r))tqSYl~Ya z@>|yTJ#5&!eeJP%VwaW7pwYvI6sBiw<@Ga#vuvEe>sN8)X^32t?83s_0ZoK0`wuz) zR#2H?)GZ&lTKtY3QCwJ-J8(^Ey<2)V-o{e1Sh8b}Mm z6FbrbS}PDIQjh6|=bqH)q_il3)#?8}fBM~bViUIGl-!)2hMu|W?+FpC%lBtv{xefF%mH@e&%?`0hLmRx8Zc{isaV{R8Cq2} zrpoZg<^%hW_O3{P+}By`A$eeLiwARq#qFgvj2BCE(6^1Qb1^LO1g`dV&Xd(0U~z)9 z6DL}BGW%Kl<1LOQQI;yw=eEF-Ucf|4Ro8_c0=7J}r1ARBxPylHk~_>`L8CKI&=YT0 zSK>yDFFkSnKhEzswnikrQ@asw($LT)e%Xp_F{>q$@%#C*iw2x~5BJ2mI4czUl4x;~ z#X}zwck;oL)2Hv>JJl{II3_kk)RA+Vkfa`=VMzz>8}T_`a#6#N(_^oLJ6IaX5#xhG~|=OM~@&(nk*) zATGnb8NB@u3(KZkm{VEIrmCF_b{d|XiU|&mY2UDG`{g^kiYYCaZfEc2-!|aJn0kxd zu^T~u6BAg`Z`{gAj$e_JBYRR-4qPwt7Xp|!M>rgD0*no7xBvb6O`pGsmT3EUxVdll zz*_N)IWRD>q~bqV5^7;F0*=Gjb0P;w92t z;9Cph*||K?ktX>9FG>Ca|5%Uv;d18Qvw5d>Ib&zv2c&+JCOOgVjQiU%d-oryIAOSS zq<8DK@m)H^h-568!8DQ5A8Wuk*$WT5b36&xr*QoH?x9dk%Vldh!|QLePx`>zwJgxRjk~(Yjsg>cK@OjewuU^SC|A`EY;J$zH{rJw*CH%>Ue zUT7OUn`=vcXU?#H;@=q_*J8=f_(Unk=>9o(78^wAkS|YOwH2NpIeq`yp?E?Sab6dQ ziqGxUJyqaIHfc>-k$j#Z9Lpc80i0l^b}!h$oh$=TZ_s1wVoQ`xe@@9vioDq^UT8e9#jU zUW^;F54Xg{#H6)z7jZ%50!3Z3?M%;t=zcK<-rQ6l$sH?XV8t-- zgpG}J4Nu+q#E8%PlFJ&>`mdh<+~=|Q_8zWvBW+)YDv>`Ul)_Z6+zEzv?1|G$X%dTP zSnQbP5J+~6Wzs8id6I^lv*eKrQXZ2GOTDz)2|KdzX_z5^nGs)Hg#%e^x20WFv-pm) zxfrRb6V|b0IzEA?;WCEkSu%kXF}P?a_Z;`t?AZZL)?B;z@I>x!JywbQ>Q$RJ6s+P` za);HYxl{5c)rhQRe-wGjiycc6Vsf%M9St{}H-(kTl#d*iM-JIq1OLIA- z_rHYGa5RpN#A&2IzolSv;(Y!Hiyul(jKv9#jNLTkk)=K()%Vlr4zp6Uw41T)hN3+* zmRQR^M%w4C(VZ10f~DVJZ{cQ)3(HFrUsznsPF^gJDVAp+u1ZQxEa>Aa>QeDYvs0Cz z|AWO8-Y(lGzj9e=;eg`OBHrrs%It)TUO;Oe_0fv?eZ}VrlwHr_HQWeve*RNZ-j{~dYNtrW@AR=A(<5NUF)2JhKua;f&i&yYu3YkXw zl62l&7uv#;akw*&y`MPoWHgTD$z0r>CkwC}e-O@z{DXIIU|VEDMa@9)JWtJ}00K?TQo%Km z`h@}r8ucqeTM+y;2--?f^AP~bs0B2@GpNNBJo!^g5x_R6Whwxk0X~{qr2@PL05|}% zK~*BOlcv^EfB*pK24F(~paNJEfFe`tX#jmt8xcS`s7*A$+^8)yzy|=;L2XB9KTYjW zqi~Aaqe2lBfIFxI2tXdxK?Fb$_&Xc`eNcyKfEobigF1%LF&Y2?=p+R&6cj~Kz???Z zX$mkQ04@R448UOk&qAH2&{+y#_JC&r_y8d4sVgeLi@;@9Mu$yDfJ&eM#B`COZX>|C zP-grgqNfTf`xQGhC;o~Qs_hdNW#3pIf00G9@UI_htPuF-H`2G|Us>j3@&*b@M` zpzaj)mIllofcpV^LP<2BVgSGghy?(q00M=oM(7SreNY3|oK~w)ZwjC}fHa3IISpv$ zWJGfa04g+J7cT06(xKh@qMCN}xb}em11KI1O1%MD0ivT17$Rm}80YnoThEz~t7X*kBfVlvA1VBK5 z69U)}VB-L~gBBq)kW%RoI4@MtmXQchOaNd4G!p>z(0Jt!3`QV;CZRD1fJuOELZgw& zu_irI_It>9DLpcQskMet?F%X$=zCIE_Lq3SC5OrEITsH)X=>N;vs-ALV3y%0PmR`mzQ2M`{! zn8nOy<}6@4?lR9<9b1p}W_{QWfaSwCUKSACT8TdXH1D0YNzl%Q#9*^t%E5K7=d&a(wy~*CszN>wA`$YQ;`y%_{ z_H*pd*x$6jZ~wyn9|zjO-l3j@JD@1$I4pC(4rW~oUB0eZH(EDUw^+AIw@J54cUSjJ z_r6A#8og=^t}(sF;uPoY)O%FlzkXu<-u1Jcot+0dmphMlp5naN`Ka?f=f}=3oL@S>b^gcMY7h*rhR%i( z!ve##23&(y4SF`1-(W?9XW#~zD9jel8Fj`qW0tYRSZ1s;9yDGxN@7jXN9-W>5KF}o z;sWuF$;A|IiZC@ZwKlag#hJR8dYTeU$)+?@KU1bD&s1nCF%2<|G>tY*0G#R%ra7kh zre&rzrYh4`({9s2({a;z7kd|9mrR$fE_YnLT!*>Nbd}uV+`e-=p| ztGnLa*weh%l=}tuYwmxz|Ly+4qlSmcqoqfzM=y_Dk1-zeJ=S^b@;K>n)8nnD z+SA{&sb^QuRL^|RVV+Yx7kh5>+~@hHm*5rP)x@itS6{Deufbk3y?*s(y>;GQyc4`L zz4N_?dQbMA>Alpu(tEr2KJTO6XS{ECKk$C;ZEo1OVb6xi4f{3xq2cC+Pkd_lIQa-Z zaXtk;!+gH+nc=g*XN}J`pMyTPd>;5b_j%*<-dE@A$lxs=ik=9pMRnMT>nk}NButp7y?2A zk^{yBtP1!o;AX&!z|Mi+1+EM{8TcY7BB)JJLC~C_2f_Kl(?i@smWMiqwht{19Thq= zv@-NeSc|a4uw`MDVVlBsgzXI57j``CLfFl)2Vu{{UWG|vAHp@^HNxwKo5EegeZqso zn}3zw_=$gFuBZm0*pWft}#9=P*hv8Y|VP=)m~|2cW=UVRvEa(g^VH9-DS36Cyw;4?$(YQ1i5dF=g27QCWx1G864?jT_Mb$sLnIf9Gu+ZUEztPb~;nvV$+AZTM=*%mJJ8|4rDlX+b$ zVMTRcB_&L1tqlk{vGRZ8x49XWbnJZJE#EPUfGaYP}zJ@FETt4R-J(4)TS! z@-KWx(vUl^q{=5Of!nnn5-CXoI=yi^v}1a8H~oB$Tzz6L|5lj7ksE)S*LC4Kkdki& zaus`+SGDDml4QP6uFP;UQ0DiVe}fb4ijC`vR>EsmQEuj{fmI^z;gwJ!P)ce0D8|c@m{WM|EP)81n zrf-ErTsFEXmnhYMVacmb2Nq9-iicJAY0AwP4qze~^fLtfGr7vh*Uppu!-WY?oOTGh z34bMUG1c*UV5ds{W@@`&;jF9V&j~pGg&uA=FeVdUXvjCDZn!W>E`#4%0`&3Rcew8h zJ)Wb%>=g#m*;WkR=tbUXB9j(I%@c4->8`oFub{&%@#6#zcPGWRkJ4lT z&m~35hb;@)X#Ej}4GJ9RELGs!x(z1rQd-PPCQBKUsWEj(XqJ;bYjK`2GmI(PwsXW@ z1I!KT0l4bTWpMRri5HqTcjTnnfgm(j0AEwB1a4S$I{2HWNKxtB1Z$Pd;vYM471ezd z_Rmnx%X(PJFf@Z(V%?#{9x$)}iF2#YP(FT=?G!h!c9Fl#EzNHFqxa*sbq;0L^oUy! zZGeXeyItuUniGEvv~U{a|kAw$Xz(8Ob$EIupOU z|HV7bxPq+L_eqf)`;w*}oHO^Y1`=lQQ(ZdM{kt zPsFYVUST5+bH>@EmF$=Wzf&Z5p0Y_ch7f1Zn)U6pS;mMfkMph>*8VnQ&T4V=9cJ91 z0TT)gmP1k_J#K;>u@3u#FRVs_03IqwY>&f@x(aEyQ*W+9Y7I+qBkNi{@c6lTojy+R zU}p+A&$>>IlDzRzeVKrJlD!PB!CFGi^=5F`)e;dd^ucRJ2XHBPR021$I$h>!B@4T& z`vNB|#3_2kOJnrnwuD^oeQ$LAfzk282ABtP9^S%s&bk!r(21K@-AB$HB5C!$+%E!Y zU%msosP+Udx)Zmyy008JKa>lw9sy4B&fqGozsqrh zWaIOz&XoB$#r9fNyUJPS9p~UVD(oF>u9J+3S;eVIsY3P{ra!lJ%dVrV#J?&}+{d+? zrBNE)bNr8a@e*OG^{Fgmm|{(xtharIKc#!%*Bfa$K}Ja@7{apn0_-sdS$z6iPA7%q z`A%&FDO@Unb!0g#3-Skk3M)wgEP+Z&D{z@UweGNe9EQbQ9-cPBbYYd5HkbF}R^cTu z$G4MWnRV;L;YXxUW{1P(nHvL~q=mine$QUq%nGMsVJ2P#s&;}ZnRk2NBUB*$t9vM4 zXJOZjNaHgDD8f?xvCj9E*D(sO?!5$=4`nO>Hx$j_s zB^&YKaBe@o{VPmB-THDu<8D8vCWpN30Pxz@?c%n?_%0z zCObK@q>F*5x?a6MT9jI{)8>3Pcd;{>fur=d&h~CJLnL?Dd-F~ler&L$f%82&Y0d6k zTPvQ5xCz^MblY~G2Hhc8=C=>!#^9@?8wq3Zx@2ycb&JgTm{)}g7p$9Q{^}Jyyc>Ex zAruyE$AAqlX58Di`AYrSf)27y64qKb${DXqW`ALdm1!g#lqQ;|=*b?& zx7J?CiP-CFAf7}G|M|e$10nXqZNa(k504`*-7p(-Pb6(D7QJWN8_!=}y7ubXwMcKz zmdzWAx`*aybA`SKX$B8yvj2_jfpd>s8D9-)E7#_TIo#YxAMwQ3kAy_y5F)T+1?llE z4f)41Z-|gBOp|&j_EAXwGzru2`JMuvK_1HN8?*Y;Io2g6=?;_73Manj39~0gYBJ=QQ0#DZBEm%uarWoV12KDW&%+f4w z_lZ^F&R;e!Ub(p6#d`3FO$OjQ<`(*8izkj+Bd)s6%+E`oooXO;M!5ze>rE0M1juA3{&YS{uoE|Fk=cVWR4XakIs~EJRK%BzPUA79Gc>}iAi%3ZC z+1)vPb^nb<9MjR3lR*7rntO25FF7sXX5dHQL%aru(V95sEQxvI=NA`!((-s6HzUEk zwv{jnPJ9dCF9mQ*CwMQ=S?C+dT!pn_Du-+I6n0q1R4zgK&|A1|T{@Lp8Y?7PEK}v& z&E$@`XQifcjWPwiz{!sz11(ZNp}_38ox>^C?5SL)Fxh0V2oLnhR{Z*I_ z$39rf#*Z8^X|zE%*=*NIz@x36Q(>{eUWxEt1OGwF^w==$+Bxj*jNfXopC`QXwC^@9Z2*r zl82h68T})t8+9{rS2Np5NVK|vf_3=HC^z_BmduT}Zk4&KJz*cv%AAjx=`B=Rx5)h9 zSi#e32b`pq2dv7flVf|E>7Rs#wj3?ZHDJIRTZ`pPGo&*8p36Pf_in>1%xbV|E$}ggd!cyY3b(T9T`ObMwKQRVyZpUTP$HUHR{O zoN*_Tpf4OaV@`>P^YAfdP42?vUd}E}gWZg}bvQ{G_b%38H%`iYzYtgCa}ArPwoNgf z>)mvlw}C{!>ufFg$p%A+BVOLxc5bH_v+ZgMpjZ@mh^W9rEJ>ESdiQ4c-@d+a|Mi>e zA?~itL)=8&K<*I6(sq4RT>tLLBHry=oh^5Nj~yrb!=rdGX5^Pfc#;OgA;IDVOQD`H z@{8F6NKsu_FGK|sm8d%+O}-#B(;P~PTi4tGbzzkn1!5L_`hM3=Rvz3TlHJecg>r=@ zeTYB?U2-aiT@puwZG?ku$R9gLa;?A|fX&4h!5;Q-^5AaJOb|$ttn`BOccYD3^?ql>8)^^;VduVk1qA2-H^Lpmlb9bC|B=F!QVTE<0 zBH=pTsBgqA5`4-17DA;|D2e(*>(-yR?2H}!?-L_25l4TLXtetOX|AimUZ<}-zjmm1 z>sFn+1{s@a{<)gaB&u7q*MQ6)VZ=(pn2~F@j@x2@mwM(guJ=c;3oTaxRG@=Y@11_G zpd(R|@tyvRpgfj7z+G;VE^n4`ZjGBhZMqy%W2FLE#$8z3LhfzZrY{w8E-_~yse{yf zjUM>9(7ezV!U();ln?y=n9Pl|Zk4&+iV@44qZ!FYTqX0#v4WS?PUc=OlQiIU0HJ)TT9?WYI{Kd98LXOA-K+XBmB8cpI*wkoWk8-z56Og&iJB62a2To zOSq|4AGvs>nSo`fv(?{6NRhf-5J;HCgEWym7!!-fSk^G7Apkgs4iCeL(<0%NBGraL z{?@u$=JC`?0_kC0uJD&);Vx$(N}hZ(oym~|D-|htNaLn+J>kKrGEnF#H8QV)GYlUF zJlt=)<+uM5T=q$U3~g8RTF0 zD>s4^$t4HP5w;V(iIaMza^@O?hjKXB+#R;Qm5b!9Z?(MjrOobZAets;1=}m-_4Uig z57}W{3$K*DQ>MWJev7m`%Wc|t(WsLeo1>uY4o+MqC~1keNa8KQh;zGlEvgXkEvSAM%W3^EVO39V#WiMr|Gsn5UT|&O!BZK2jl1aKp$=xg zZVJ~L_P+V@-d7~=eIyAdnoIR5y+)K4h)a7h^OkLzz1DDaZGN}!#p$`ZKjaz05;8k= z+R^Lg&Q0@IE)%nl;BK0~;`Rp{N0M#&^rD4pR;^mFYfa_k2^C`eg#I~k1|4aM2TC>d ze_g%u>hG3UJw02t^zgdc>MwEX5q(_JuG43B?ml^Hcdzy_y?O&?#pdj)!mp)t`KhiG zHb1eXSj+K37Pq>IX{^1QhJ8p8ta&Q=DSEl>DLT4+>-f&I;co_G;7!|HA3uC*i2bbjV}BVnZ|E<(x2<@zY?t=p`uXb*8D{-7^_wN)+* z{Qe+bA&v9nI~un~GFwibT6)xQ{dB*!Q^m;W-u_0S4uS*7%hM?>TE@h?#kO0#HBwBA zVESCVRrt)H!!4dTZG}sUjI*{a+Zb5d>-6V$72qvs5$xM%`WjCZhsSZxr@7K3C)t0mIeSD zYrqfuw?KO66PhiXXn zC=viK%)On#di??Im^0EC^76h0=l%jd17Do%Y|VGVJ{Y~j0ke&kwHgvIo4_A0V_#?d zfh^HWYc)7v%s(!~$C>z=z-19I#;BVvoRfxuZ>OeaSX#o676wzoR;(BCWG}KzLz@2G z3J0Dz^6P@#M)ET>P^0@>I!S!=FY(kUc(pzNuh!s0Ov?uy_I2L6)d#Q;r|(^5drShvmJ`YU z(TFwPM57_4lcDSCkiO2O4-O`JJopyyfFzrY9RrBF zGe9D1G$O&1jh32tq8=Q&4)<~B_eM*JhBWz}aF4K;vu+K3t(d@E4mXgJE`73+x!_qi zj6Xp)%Xj~H=xEmcdamK!X8V> zB#j+l@LQ4w&q*EQNK2xgpyr8%^Hp6=wy7XPRQ^a-f zpmn;NcmVc>YIv71>&RZ{7nId(qWUw4;FukVt!cg)3lj}-9P8{UL?*-pO>_~ zX!zJMqsEWX62a%2(iFp}A>aKlLY&lw8NXz~9aH(WiQ7TKnAH@Fr=gkT5u{)xU3P2;+Hbm_EUXN;K9l1V;&tKfk_ zw?x`6RW22l;B(UXN&!#IS}NGCt7)^H-m7;saIfqBak=xC-ZOumpyPS5a57Ot@i zn3WLV1q^xlRI2Q*AwQD)WG223o3Cz~)CmryC1fcskx^m8@KQ4D86e={CM912l}qWs zkN%(nIA^w#&zVOB@O+U>4e2_W7+px~s{+Zrk8}46*zEz=sFL%P#!u&ZOU@v9k!DO@ z>i7gh%RUv4L>$qGwAPS@x8rf0?Yn=Pz1c`&pUcWlOZ#yhz0?Q*XPTdd8(a3RG^YHp zBlU=ZL_WZiG~Ij7+Ar!bHm?VX>oo2?Tl=?M&;8T$<)s@dFRVAVTXm}7jN#&isy(a3 zmJelllKcA_ia=k;?$$HXuhn|)UC+PH-&%Qoy)k;l@q)94J7>4-TOp?WxHa`CXt^(I zNtCtp#`~lweV?S!1Jgw8?o0M*eAd8uCjJUX>Kn%sGk}&xzdS6S)2zP1rFQZYb(K;b zD3Fpgx(Et*kkKK47VK{;)DsHTvlZgyLT5Df}f+fvVxv`?4- z2k7m3Q^Q_Csp7Rh%z?Fsw_Gtix!5syzDS0L++3sNEgz9w=#q!Q?{9F{ch6dn?@A?98&98Cy7W#NEO7x!YS zs-{g}FOt+}8j^-r>3s^S_UzlR_Kt`(q!l|MBkkKk2HhUG@GjAZ1!cBK7uVsrII9m% z{zD7THD!L}@LG#MK(ua=u7Cv(gFCJRZiu5ugQrAq949YXD%+Cv^u_h24Lw_gdx^R{ zsRg)1cK*6*<4)%jiDwC8B=anlxRS;E(W|$Q=N^unCkEjF{egu81`HV7zZW?4u%5+n z7EVv%aC02To?6~7EvbKQS6H6fNiCp#Z{X%D^~N(<-BM}oUA>e@UTaE8U@>V~+_EsG zerJ;Q5c5(!!au}mEV+&E=%=q|a2tz?%wks^T>5M&Zm-q-V@`J>lkiG-8vO+jkC8KNPncnHMcQREjgnXF{~VR057$B=P%@i-06BI6jH8y*FxCzn6YK0McRSF2ye z?=@!^ZryUw`D*i1MAxQSX=Z|Pb6aNRuHB1{8jkK9+&iUjzMt4M0^ViM?-?AQ7@rs3 zJz;KTCo!i5lXvvO;A;lm%1cgLg#U-V^8kzL+8+JPz?p#=9a{z!oEb!eHTK?n7f}&g zRH8yI5oIy`U%^0R{URJm`79eP$3;%r&?C-~0aGoB7tM zyR5eNUc0TcP`=l>AqFuI@Qr4t&{Zg?9DorWcW#Z*B6z#GJ=1R-Vvt2(b?QGZNS@m4u zm9oQ?A6?;5W>-h*n>Al*>az_-$$9V-c{PA$g@n=Hf!vk zDf5N;KN5>D3(M`5ck0Q0*}dhrx0-oFiDRW>ktq8_{nFl8kJ(3Ys)Oz^=F*e$TIM%oLNuFkV`-v#jL#4 z&rw%Cm!LE--QKrPW;wIJ{Hjt%t}o`JSU+B0c~vR6U$NW{btnyu*6FUEuBA3*jY;wD z*rj&EUR!TD+w5Ny6?&-%2qSy?96$r`uc{&#sNI^6aIEGj3LZ$kszR%zQBR+U%Eoy4kd>x}a* z+wU5)Z-q8ryK`$x71q~VU#-FlZkE;_jj_4DtZ!02Qfh8#iOpe>vR_g<)OXd7Fb+e;hvZ=10_;MD%t z+ji>OzoD~w-DG3p!B<>>gep>Oo}*u`mNXi>p0gIInY+$)roBYj_lWF zca!9JrB*tFA;#VQPMk2pIah8c$s1y_4aV1$ zYz;^`8q%m?yUvwD+s)m?mXQ`x`0?1$F#(utx^&FQ;-#;N9V4H~huHEGd^b}rC6}mh zT`5_ub^lJIoZpyapMzN7uS9JdKZ-3cr=^WuTY9#wyFb=r3jyK0OxTxV=`mUzuu*?B2YwKRai1wb_<7$<@$SJAC`WBfGc8UfI>R zL+4(-YB{GUDYo)$cAq; z0+ix6D$6C4PHoyx|F>*c7QR#r37<26h~xEU(vYpE#+?aREMI4r(7`M5#}2WDr+vM8 z?K(jZ`yA-ftTnH*J8OIITKDd-#ZF~Ic2()Us89AC@|SDwmhH0Lk)7z>XYkC$kxqGy ztCBRM-*-$*D7AVhr4)z5Izirs>h?S3UZUNKCEERzS6i`lsEyL*6fN7UVhcr)-iU=Q z6x-N9QA94f&Ao-9H(Mx*I;|62UMaS$fJP=(d{0;ES9LXH^UMT!a4$v!XNZD&Ad;)C zoj4q7YoBo9khzA-izOlt9g?hL8F*G>X|IT|t-g7Jyr#81*{;-fmrD8XqEuEFg;J@l z6-qx*ByWlmc~dKp^4~>)RBDR?DNieHPPTupMXlf}wV$X*MAXl;m~vIEILkkX$np^7l!0*C1t589U8%g{)#WZNYk& ziM9W0dPuG_a;>iRRz#XC*K1;T&6X3p+Dm7Tt7ex!mtO(BnV_eP_<$uNQD8q$?myqG zjOTWmT$MZ*YI%;~ewI9eJl~YJbhSUMC-Ri9Duz~A-Ew1+{aGZdyid5vth$J_iCvG{ z4oF7}`pv5R8?mY?vus~#`yd^zE)9tKBLvSlV*jEki@vBMsGbKXsWy$zA3yvsSUeRd z&k||BMWBeCd+n}<2O{hra`Qcrf5@C~ZmYzXHp@S41o^oF`5^6ntA_2z@@DdFuS|qP zqbm~ZV=ckyLI7ozI39H0L6VjB&o|fAZa>c5tQ2<`l)ecgrjEO-t+_bxbMTd8!;NH8)OUQnt{+;q7Gq zE|nF7Wc}QAMJZ&<9`T@^#Ez&DkJ?FAMV4QhAZEh4V>}JAojtJs?75Kr^nAKBYkr{1 z8LG8gvY&i!kv&`T?qSaMrZv`$Hz&xZ35$@mUrRH4sEB<$LvCkFH-5`jlG4Uq4?nwV zXTyouinmHjA;>jnfju=Vh%w3;(UPJwu9o{sHI1(;eI=#2=Vq|DF&QT`5*vbsD^11C zhuj#A1C^mtl5wp(RFaz;2XT|A-P||+8qIM1QkkFym8LdQTE;bDtl3uCM^MH3Ho2iK z>9p~lJaC=d&|fYbbVae$ZSY!`o{oL(q|NJhFWVh(ZeQP)PNlf=>JTN!f3xz=lESSx+;mt){W+H<&f=Tb9VVy*}_>#b7$XI`B2<_f|0+@_O)r} zjNMVE>=@Lh&Y_CQ+3fqP=Juvt6Az4U+Vhk8&HWYMgMZjhhiwg?$>xhsC}Qhu;p4;l zwhZ~cPUpSmv!BN7@ISwQ=I-f^ql?WgcK6vifncA;E8`BZC~&4SZXd(ma+)&6w(hi~ zRJ&83(ZVkNa%c?aYIhuico@#EOeG}NF83%))X`fBD~I1^@UV;gw!i!i0a(6r~L^#tk%{;;13+oGX9 zfs(1%!Y73FZ4vT)-A;SW=iI2z*!`2^$YOI#P#;hJ6Rs)MKz+jRN;Oa)A*U;qJ*YSE zpnjQRU+uPIAgG6x1@$hV9wty9v7NATkK6Y>m38>gD;HJd*6zF{x%?VdD#?!1_CL(>X}>?1Z!aqw zv(BtBlTCnlY9$|Q))}Fz-dE-FLOWG!_9825b0^E{C7a7?`s3p*Ll^gs-dtkHh59?H z{NQt?Z9N7cCcLb8DV6<|%C}fTQsDzfqvMxM$*JY`?)&S2c|#p-+V1Fi;;1xw_2voN0=93RxoEZXWZRuPTAq~Vt^8%g@dB-5u9BdM&B3YVcg2H)fAoyxIT2_SJ{C z?tZuHM#u5P{SJjJ?KHh)YV)clMkSinFXP{MQ}-hS9a~2pcsuG%pLK7q7}d+a+pFQV z+O}VDWssv;$fn8(eUd*Z8GQDRNtR|_j+VXst*poUO#a>`M;Vt~l9U_9Nz*4ypYH$D zl%J+dceuQaD=%Tn8}F~Zep>blIJNh!H?}x8M@t(*>dmhkpwv^GrIot z6x)`yOOH%FG-du5^FEv7BM*;JJ~w{2>TfI8_@Cds=fbXS!}~38oZUI#bfe8RCY4lP znpCmsB&DFwz@G>HGRP59Kgsy*yI+qR=ierxZufzXMy^N*mbut*UXn9U)x!pAFTS;zqRw+TJ*y0)_*RUNJx~|(2 z$7^$1&x`nn&!FGmp1Z_<^X7SbHf$T;Zw;;)25s#d zaL1;x+dkOzj?b$1m%cyPzwdzIt=o57dU%+lUD(Q2Cn7HYy`z zFHkYpQc4VS_B4*}Ae|g<{A}{4Q$BO_Ge&lhVvUMzne0_d4i4D6|ChO&ofl1S4e0t_ z*MPE3_r^xjN;>3nF}Br9=SFSu|Lw~epUrT{y^bsI7(f2Ur*r1}AKM&tZ2N|>-4{BJ zuZ_6UdU-=Q?lY+tYt;P5eE0SbZ#(+7y=eUI{cqlX-@o^;ZeedYI=EWVQ&UR0-f>0S zDi|3mpG{RVTPkmqYb)z%TieRzhbZMaDg{zlt6BcIQht><8jaTGu1PM3jj^My)s?PF zx9TkG?Pg4xB2gKHnyjCklsDU41Cz1`8m-k_ue(05h3=s*%3m(uJ(LAi<;3-#E{r%; zlq)C|_v{yMBx@IWfve+gvocnB#pdcBm)+eck5!iKHj^MLGhGAYNKpC3+kIjE#Kmet zTbxqLv|W~@rp8{1Bq`Hs%fpO=YDh}6+Hy%_Z^c_WZ`>w(OY*mI%5dY_M9JFmAh`9B zKbQ8KvirziNWmsKj{VbdN;Ap&y6cO5wwoqJWdcZ*n@gFd?AMj|rCKKIxC?Ta&DBGG zmCizT54n$2(IlV1p~MNLxTJjK?do?myB}i&t{(|@!=#*GjP8W&$I|HR9!ejntI5?v zNwg8Bu8Drw4cB`%59g}ExvnO`xllPY*_1sjJJ{wL7N0%LXx-{+;d0J2PgQ!)G+Xz& zI>^&(dk@dPxW(Cdv-H~Vx857jsmt*i0o~dkvoD)BYr~;^ZIf6!by8kH-Kkh(!t4vU0t(&LzuD+4vtdO|p^^*a&lH%{)sSzLS z%x3Vf$sOAdu3hz3^;l<(2Cu)_H^9}xFJjo-a^`m)>2v21glI7IbvU8f$x4sbcy=HcEdX&wcnsM%EYCjn;*mm9{q5u8iz9MrP9# zWw88Ea<$3G-c{2mN3z5yGTSK816`q*@rNK^d4SAyM&;Lszc4mtl$4IuWG|!jS9v4d z%lc?K;5E(?Pt9An5|jOhuU3@{M_yhVsLLz-|gNYyU`ij9C2)CmZ~e8Xc}$x@Mg+J!TA8ILOf=qEiUBo)p|7@UJgUxM=eR`^m#*_azBOvUV?_2HTebH4FP&byVdrw^Z|~0<|NHxL3#Cel zUOmG{b`G#^bKOO{N*2qbDpg~4t5A9sxsY>F_BYqE63;zMTYz@ z-B@?^IsaW7x1Qg<_RWZuj^q2JqrFP>ruA#Hs>|Zt z{&RntGxsOQ+EH8I-tdmk;o&<+_Vge7(K~Pb%`s%bYqJN>_OZ&-TrJR>VRCaSf+6xy zr8z6!hw&FH-dXw1iub1SFy}FEr3F=H5JG9eu{6h)auCBOEeICGzom1UthXK7JAd5@ zXa5b-s5f659}&>7``)Xuf9$=qam~0wco^wp`2eVz})JZ=z}_QAuWH4zM**-YVA;59hG$ zJM)!HZpdnu&?D%F~|rJeK3DU+s7@n;cy3)8f(r%rRuHnE0# z07Ik`*-jqLK=BxQE8$8#`74L=oh#VJY|_*zQ>RREY&j7>KRqBl{>@j9JEu;XGU+RS ze+uQdiqg?P!M@ocFS=~M;x54T+yz*vy*5kNkN#sld3c!J8nRlf z;kJHm9{R9slI z3(3WrHnwMXFtE7++Xmd-6+s z*yU3;h;QO^1sQxQpa~xYc$d$_Oww=TqYp_qy3fGLeFZ)ZF%oz7KNuE!74)jY#^P|V zK3=1}CVGA2H3PTw`*BF0;$7FfxA$Q0QQq%)f95>}hiuEd*W!xpxOcR7ns*k?=Jk?M z3X*Ezj!n3kZzqLFeQ?P(R2nONAbloHlD=o-ak6wv%El#IVPh|2gz*jIOygYRR^ua+ z&EznZ!wFj*)7z%6O+T1^!3oFui3YVufw+)j@}3Q4)q=5JI?p-zCZfT^<9RO_kC9+v)= zF`5gu?{UDk5EpElEW0d+EGI2zEf*~@mRL(NPS`Rm4+P#~UV#q^Of2xJO#rtunFQPIm8xsyj$gO#U_Y^ z8AlE$;WMP2{Ub9gWPMn7+rXL7$o$s0&p(~u{3%6}tM69^vBsqD+DPg5-{<^z(!ONm z;DK+9>|<$bF4Ll^QlMqa#VcNQ^lvH+I2beHS^#ytkvg@d)Ade>*|x&&tYLP@UwD6Q z{@U(TI(M`8T3mD0Ai8kBM=svve_+R&BU|^r(`|#}*kS2nw}wj^1}LR!;kT&amaxME zo!dt49~U)--rLGiz5PS`gx6}*Ze`3sNAnQNR`!G{fiB-jGn=;x$c5xu@0#gB7Y@gX z)JEnPH|_rYaKM$lL#uu7{A6Iy$)N#C_2$j1lt0%*E*yJ!#m-euOF6Uj^0v(0cqS`v zml|xmH0)e}rR4#sY1f*4UJg)dDvW1g=rodxGRRZyR&1}Pe>l$`l+rpYmSc(|KpCX8 zQmQNOSynBx9h# z@dx%?INN=9BTH3zvQo~}sY|Va)dN^fR+y2&dSEEdHB+w+?y%Q+>WFlygW~&#QZzt$ zU17SLG|(q)I&| zxz~3-{f3xT8Q#4~eSf8(tgEigs&rviW#^IOySFo|(yMKUUg2U^#bqnsc5n2#LwlK3 z>DsJOyROWt+_2w`7rp|&D3|@QDX>#m-SDn+F+)Q5TE(8|$n)UYx$ly_7f${l>ozQ( z^6et$%b#^Dm;I5+ zl0Dy6!@R=oXd^qO+LfqoZ0Xu&>}u9_5jf7jVzSnezj^_q(A(g!j8uarSz~ zz-cEn?{^LDaIfO+M7d~UkJHt~y2o>KcOJg%FYBtsD*^TD4DZ1NUTbN~fvDeh1f1Ug zTJu(2`!sM?uaRs_IvC!xMfdPmx^|ttu7k6G1F7HfGh^alL~GNI{+*UI3&3tIh{{!` z_?lSvQ02mr#k)2;2be4)88pHQFQC-4xXRnQn_sp&Ew$`ja)@(PunF&dN=;K0uwQRp z)WK}YTGrH#N11u7SSa1(vapUWT)KDnVnaojVQ&zGm!PZ0Ydy`DL1GTWsEpk+$!<+Q za6JCvzA!~M+AKC$SWZ`$ZXY(w_3#5Xa^&>cL!G0#OJmor`!FiN!rFslwl_xG@lvzJ z?69=N^k3cuH&2alAjJv^qY{>FR=T>(lJJr^Eqj!t%*qt=;;e~N2FeGdcUY`_{W}`S z_v@Imy!zQKubah=7aWwag3e-eotbR5gqau5HU~Bni->WQB_DhENdt4%{8r?WVZ)_Q zm~oof5^L_#)0~RG9g8irpZOYFp)AAAn>SlJE^n-DLa-ckeE`P}&RKo%h<{SMG^Lb7 z*~9|Ky+*n6g?sm6`(J)#zLP1#&uwSd_URKoIIOW#X)O;l%H6V~Y)UuzdpX3Iv86+Y z&b>QTg=Z7YBPjaasIYLJg<~`Kz8tY*$CB%467M=q6vIuWf`!kfwPzbco#unO40RqE z)pkYefR^6C?tF-h-cJ2D|I+L<5yOb2-3T3pzHq7nhY6bhS zSFDa)Fw`k}nblhwODh8hDh>UW!guS*maCW7@897FJtp;gEo^vLK(o-Tr&(1gzq(2e zV$kht;gHKuIOG~7xjKMWglYR`i#(Ere{>cs3``Z~U&X?f4&$fx9cI*_!a!VQHR_1h zPp7U4zaXB&`}LKiYTu@$4IKRHgWfEpNPi~n%Ak?WlFcIE%u$*hK(Bi z#>n|@r)r}|zB6>pl86#9JJ$cP8jq#j&6d(FaUz$s5GR3319}m~lqwe6IXkOpEp$u< zC_xul;Kjn2GN;U~Ui-*ydEai$T8nQ&`8PV#^e2L}rK-gV^STqn-#%kh0)ChEFUu7J z+S#4iqZ?s#%kBHxJ~1vh+I;HZ3eD_R8>Vj=#)Hc+9vq|$kSrUPE#A0(8J|)aJ|e;* zk7v7lg!vQl5grRyF4|>|6m`y#?NNsg_1e~c?`? zez;SUD;wiBbZy^hU?)ZAWMI$|$GEi;$OyTS-WkjQTX~t^M6)f%c%dh&K9xpX#mbF@ z2RX}<*=9xOE{=*fo&FJh?4dKID*gL+>o9=n&rBPWp@Zy3%QyDZmL=v-vkT3#yE3Ml z^p_)wY`Ua_YBgEV34MB=<%8X-8vkzFJ+MC;NEtL0hSzx18nA3j^J$I2;M)_ z?qYM_h&3x0 zEnL2I?EFy*zM!DH#7KvFB z2FR}{kx+?a!PEPlVSA;}z7g;A2ndh($G$$!@UBwJy@y6#2(YwJ0_jklXZy8WB=w{m zG}Vz^+N9X0mOm-K%%GyBihI%Ng3|6=(?RwHR2UDQq+}gKONM8%)ORL>&z|mW> zwC)+%afCCvnq}13IZGGKo3q%mVD$6kHZyuLuVAU%b@$F{}_3SJ+( z^x3@4+^@6K;vX`4css}Nh8vBtbZDRK3$Nwh9?VgGYa};(*rkE><<&vTD%L09i_861jq< zS*PKzj8|&a$Z>H3$C)cvPJ_u!3#>w3+%9rwGi!wuQWnm(# z9O&6B0gBEdQ`yi#)M;N|#-$q$z5Kav1+?Ltpn5!veZ_Gyp2_q$AHy{oU++7^@q)e} zpK=S(+c^g6OLHu%59U~2k5f0kPFICvb$xY?HT5+)*5T{PdcIHBh+{K-3yy8{?Ktv< zI*xpuj$@cUoMSJ2FOGxtgE_vTAHg@as~AYl@V?=FoQrWLck$Qh%=(wSKGJ!$YSSr9 z_tK#E$BxvM9r@bZk$fKxhh%)4P8H6`a5Gj4Uu1?BZM*SJ^KL!52l7=w_d7ja%j}+O zgZ8CE_j514Imma?b-vB}v<}p@Y1uq1P}i+xH;(-~Ht!Rt8{WA~r$F7id~7LD_i?u_ zodb2#!bF%~`Ib?ja7w1-?9EpT13lLg-(4;3xi-=r5As}_@ETUebM1ro^s=67U%t}% zvgg{&_ZNdb*A`t#zTE0gzW|cTp`;)l*(&HNj(B}+r0$V8>W$jbsvZ5bqeD9eYsV_u zv5t0Zq8;04#}Mrpt{wYp$06eT%lb&|_?~wBSUXP9j?=Z{Z0)#2JFe4?+qL5X?RZK% zUe=Du+VSS75u--vABdyDpdHQHv8Z-N{NBmowa0G z^ETZ#MNAylX7sCLYK&Pk=FphyA6)pX{FJd%CTTp=Px-)ep6)(Rz0B8HOV+~?_h zJ$ZT+?OkQu{td(T;Cqy=3k+fTPxcMPv~_0KhzugIY6pgRHOO}$3wDAG$Ky)<&lG}H&e)$;;B z()$&heixK*J%{sLaCPT#&`?jkN$5C3Lt=)j(cm|OP&3tA(0@;(-=#j%nYcTs(U_{H zz=;#&c8%Op$SqChL-;$M@DIQxgAk8A_#_e{9_NoU!B$W{TfI--sf60$;lK?-+#v5= zIxj*!Ae|U;%0eDUM(3^nmMh$g>hb)Ek51};?YaAi^wdZCC~)v4-V)-a>#q^wuDTr@ zHs~LzF$SHw&)`iCp>W>_H?PBw^Y9}Mem?@24f@nacZl;Fcv^V>2k$SS1?R!$6i3Q zjeo#rZ=~a+CKLafrc;@4Qg9(n3xA35F@#Sgd^GtbYS{azvC#Mhcuwa0i+Y(*R|s{T zP^SqMPpC9fjn;X=*JM(RCB?(esW-SXb7fT%w5uCjT_J{rxCPY&Lf}6X zx>b(9s@FBH9M|NMPF|4BxF2rF#0^pC-~a*PuPSu4ahbv_F;Df8(bSJE1YyacPYYD1z)cHsrjZvf3Tk0+~1|3LHPpc_vB6|5B`MG7TUKNR{)T!8J_#A)~eG1t^>gt*P9_=_K`mn$7hz z;?Ur|=#NsVqvSpQsaN>#dwlyJyHI0Q+%4z+)F0I}RaK9u>E!>ribrBKNxh~~7ESBI zkGI>9TQbyLBkTtCsJd8Pqh6%Vc;+Xb5&m?ZJm7&k6k*H>8o@8U##M)=1Xo5NE${3IPSsiSnrB7@@!EzMMons_t< zdc;rNM_nW+N$0#tJ)K%Hw>68s%SC7xTWQR>)Vco($!Vk=wrIO(FN15>Gi6KNWZDxj*&B zb7Zd7QMuv7mAd)K`={@6TOjp#-gne*=kng?Tt6Ag{Z!qP^EUr8^%%7|Sc|sef9&Vh z=KpSX|BY5D2jg7Hh$ne!{hR>WIX_KMH$4&LnH$Xpb;r(aOP&e!=MU7qfBs&3Yxp8V z*uT?4rnwlyc?BvSkas$@N)l~v0(y~8yZHj?zEBXXt3-6}lF&Qt`f&~VLecXHq_w9v z^20f&oaPhhFFg_^1Z<-LS-q*=QRTlB{c&LRK32pnQW0%@I@IUGXZ4<%CH{{i{figK zQD80X8Nulr|7wchbAwtZ0nLa;Gfw7LD+?5%&4vE_%bKgDf#of9Lc=-X$&$vif5R#l zI`ZH6smq?9s=7|y2G$Rd_EGG}WH@*i{fXf|7A?31uk?S$FT&lx;=GrC4u2LizZjaO ziB*Oy^V7OJe>J`Q@*s6h{{N1oa?+)w(6^*ciidMCdHWqXA^uZmG>#SG0_U6(^RK3y zA7}qrTIhl3*F5f5y_lwgvsQZwod4C@NMJ^7mO`mbp!ai8Jx^cfYX10A3*67&O3Ocd zeoy~BO!G_euRRmJqWm=z%>GG#<Y=RB$W zg>ivAvHr;=82`6MrL^AqKgs@IdQHg?Wy0{$U12kQ(d~;Vd~2tGR_>mwGwLbz#FKpk zch;1)KWQJI-{Yd6{5amz)_Ts!gL>&n^5~_+=4b!prj!$2otAU;&16+CKj7zi?D6sOE~N; zvOVzA`5E~dPEo(ger^1|EZ(6+V2Rr$3zQ5l*~;JQUoF58P&S}RK(A62OEoJM?kMYM z;b`XwaSV1uI^J{q>X_}==eXd^aNc!32-F4E2yDmKYd#2E5O^XmqjcfYRZ54J9#VQ@ z>Ca2gF1?`is?r-u?<}1Wqzf_z6$tVRsvI;nXi?CWpzA@8%9JP*T&8lF24$L+X;UV& zOn8}jW$u>^EgM<(2hW#ij+8xB_Cnb!e2XTn?9H- z5}6d3qcAW>q1R{WiW?+dA%jWx5;Ft$n6uCu0>#Kxo_vg~11P9l&fMk-#^={CFTIZ0 z>rKq-S#=imwl0#I{B3lb`3>D|>{=J&`t)6xYq{>35sD}6%giUFFrRRh`GgDTE+0!H zhJ&;$q$TD}|65P=x(Ir?&mNyo*Z;ORKKV*;J!=GzB08XF-GN2@LH9lF?n3I*C0ISn z_#EGI);+Ay)ko&FkW&*)PP?eF;(^=heI%Wr{|sGWya;Kp#(ciV4A)*mB{j*=gId{} z6bz)`MQkr(>vg`QU-apx#4oI8wvD-0J@Lf%)1Hp^>=TJFLLFNM;#jfAspm-J3Js~GYe?!%!8ec`8jwR% za%c;_I&$#>UmZDlfv=9-yuepSj$W)U$Sb4u;49>tOF=I7rAWUt=@%sZj-=m}^y`qe zfpiU|X&^nZULiku=Kr)TQu9J;UP#Rgsd+t)yOQfwz-mC~#ahm8sTq)(7gE#o5UF_~ zH7}&*h19$>?USI|1ctu4dLB+R^Kil&Y)x=N@Z1C^1m8_?Lh#;%s%iQ!(3iOSQVT9maU>xus@IG)`&DLcB%sc56ARBn7X6s9;8}Rz~_)8A94 z>hA;ON^SmtIb;j&J{R|x*$xQZRkNBx%lUI z^=#SwPd#~BSKW2dv#CGZM!fiw=eHRzQMWEcJ{h!N&)Pq7VcBzWa1)tsZ zsGfRs6?Nzz@cJga7ArT>X*m;UIWN$1o`j!QS&wE>ca!23Qrw9&WoqKPw7KW>ceHj_ ztTsp{m8+z3fm9BW%4SkIM=A$MWi8T3XRVtU?_jP>=LL8J5?}<7vCaqZ1=8*yU@xk>ITLe$Wf&fiLZ+4$x$W8L~=|u1hB#&5FUtDU(D=3 zeH79V^|`3aOK6<=?>#A~TZk32gi-&c%=)@ZZ6)$z%RhBzzkp}z?vQ1 zEZ}!wHuc6oIL`s*0`q|Rz>9EaD=Rm41AhQ}fPKJz-~ezCI7B*!fg`|C;23ZmI02jl zP64NZGr(EGpXYpm^F<&UxCC5g{5%G@0$c@zMB+Gmq@qhvkLs=gsX!W#4luf>y8&bX znZQlp7I2$*cUaMN7q|!92V@|N=MMk{$Oax#?@6p{G6E*R2k-^VfCVT36a)&fey1={ z1Skf)1fV6_N~7!QS<#~CW%aDV3lL?YFr}~vP!uQzyrdpA*Z@DExO&`BLOo_E$=RQC zY07&L^=28)WjV7-$A_q_0$jz zshx^aL%iZ?*UM2$G@_O;P=9Blmw%v>*QhBn(X-psjcKBtr!Jqbf57^(Z1pBpt*DVV zP&fLZ4@DlgpOw^DLl;)0g|dpUJ3Xc_>dYQGyP*%~exz3bOt4THjaY^yzMQ&o1)scj zTNf{bMFX(#BKLdbu4>lB|CSgfkVCRZ4l2~w^Ry7pDF-L?P)ka;fFXrGh6HC%M$sBz=sq zr-4{n?09CX6Im@NthxW6O8$H?!OapLZkC{BwZqA>8YdI61O9t&|4RL2z@G8bdcb9@4kEd`bV%YhZZN?;YR8dw9Y1=a!U`HcMr&KrSEz-H3e0&Jzn z7sYuSupQU|>;!i6{10Fcun*V|8~_diXMuBs5u+~`foR|o5YMv&C`ts9fMg(r=huK# zAPq0Z*`2N=sT zh}|HP?j1_}c!RHQjDa4x!5=6^&(^MMO#dyA{##@EZ-MmR8qC8Ml)hYd zAdK3%2j8vtdqV1>wq1wQ1nPUR5aS&|V4wp7J2~pf(Ljz~~I8;q|Zb7B9k02pcExD;5X8_DyUJROD}R7D4RlH*&*UyL#Ya9pqTT%l3BdiH2U zzr0CD^uR^xMMSb1*`@HW@|YM>;zpKxZVhC0>t|6EkG2o4cHFs0CocB zfOz;%e;bXlpcxi4!+|`Cfo&<|Q5lT8fN>ZY_dp(nz`88*CsWLxC7u5IuWAfzS#;fes4vP@snbJrsCB zffp2bL4g+(ctL>|6nH@aZ8gA3PpHsCg&r#OP~nYcHH4B5P~wD=rs#Gtbh{W7Nl;{j zA`=v~fTH41)B=i%Ls1JTDh@?0pr|p(~3PyRZ*xa3ROitav7j$$x7~p)K_~n zW-aGjxfF#e(N8Vw2D?nv?`)zFNHpo zLLW+@57nVH09q@f4^^PGH5#6y51pa8D>R3DXs+#{xwePq+8&x~LvtbY;k7@d*&7VK zUr=M7)QkMeO-^kfYU$kC)m7F{fi35*IvGIIGgGpXM*#=nbQ zfMSOm>(Jriex#K|s3iEF!g%p@#um_3Vq_Bf8lmqKG7bqa+WXv=Z;q zea7eWK%4V~dy%yMqrq68ebPRVV>sEK1lKOTcO=undm?%fVQTov&h-`WjtRFz-wXI#MY0FUIoH=*sEw zc!U4X;JX`KQotn!zSq<>08^3Ad*t&TC0I{epaVk-YYHM6zk8cdZZ7c*E=00ILCjz1 zxk}fLH=sp~_C*SLX*m|-{u*>PqX*`NeORA-Y*5#fFh(%A4DH>ZIRu&|Xs!&+WudtY zxCBFYMeykbKE1#+Nhjug;z+F(saZ&^F{za#wN|8NCbbK2Q}mg_NwFI#8cDG_DT*Fg zc~Y!Mid9LmA1U_JQuNbOEKjT|^t@^?qEZuS)gu20Xo;jh_BJEL?vzDerUfbYAY~IN z*CpjZQm#hIHA%S+Dfc1eJ{ng|V5nzwvnnaqK#dwJ<+_a&D;9Ct?Z$u zIB&a>ua10mk3$tFl;EI)P^~>_bSIRaP@?A>!5-=Sk4LrVd5_1mC&#s8&UJ2IxCpaW z+00NW;KpbIbXG+-YXG(2<{~g!rV}H#RiVED^m{|UpuHh<7iU)C5o39epz+UAamz@o zpb(?44M|B@J0hieq|}p?$}nG%{y4S&3{FKKHq)d>6NsA6T{&@WBCP zGh9#3EAXgbt%73<{!lQ(THe~ny3V@Ay3_igP@_U?3Y{r*yO67}qwsr$&lRavq+5~q ziu_t+UXc|=HWk@jv~bbjqG60rRH4PMs>=FmYO=nDdRSkdcCD$JsBeii&Ubxp!FX&#o;Sk#Kx5uD!P;rbyY@Wq zK%5Zn@wuSy#XkSuJnv7qfxH{U^$=Cj55?QSFz!ckrp*A$yI?6+v^~PMdIVP57%%hM zH<;H>)0G38>tK@tHrK)C2H4z$HxKmHG>mS7kpe~!z$g=p62a(}zA5i=vAP9Tcfl$P ztW*zHsy>1+1BvU#>^7LG9?b55nF?nAe4SlYu$vEdzk%6eq;&>K32bhI%|0+$fz%Eo zwS!3QroOfMoxYv=6}(yuufEWCMuH(~V=!9*X3LP|d?Yyso~_VFsNd^fVq9z2>$)(nVX4YJ}jE~u-VwdVt%HuN5V18g`LD+ zjYHC@x{~m6lSj|a!pF^Ep8@tl*N%a8JXl``yW?PX49xC=*-5ZTB=@7_o=NVPklt@d zZyswN_2^j|dbV85$YB`^eM+ZCRt--c?vzzh-b%^|r0nu&@pf`JN)CUJgG>&m-1>rM zN2({tC7E23Ni9>;g&S}?9bL$P;~C(8&!d@l^msM^cLmPhL=RHn{yj|xTJhYiop<5( z9dc72>p?%>_2)hU7>Itjbs;m4F5J$e3z?!1McENPM1&WPB+8EP;Si6eUSX!XQXuX5%Mfo`3;pTp5%_Qg3XbIF$KBbiAKF`@fvqTs9gt5r^6JgHm45K1KeKm(z$TLO zXy}ErsdTZN$8y*Ljf%n)@QIy?sq?r8n07g>`NNRD^O@isZ$CbxUge4pH? zjkMgd$W73G7up4SK7e-G@BP)luqf)pq?<)Pp+D7Xg&sZfvt1*f1uhJrmF3U)w2G!!g=0vQTULBR|t zaCs=WPj6k!=!jYSS!lNvDsDkV3RJ8#NIDCY6h_NSvNllX(R2e8U4^1cP!t128M?Ai zlng~@(Skitl?qkYC{rzsKF!BCWX;f3fFTCoqUxQteuFce~Sm?-rkSJ5h}TFGxp zu5Mo5Cg(@+a6dUl!?&aG?HYW$3g2At>ymyL7>xuXX{$%`{B7#CKbu__Wlt|^+9!?z zhm+uN-IM`imrL{L&+7Isar=(&(k^c$uJxRW2$Tx|6O&_-kqJaV|DWa@#SPq;j8BT;QvDVM4cs?a_gdAv_JIF48O^o{9E_{v144^z`Y7 zaa?Gcr%e$f8Xb6(%BX`cG8DF+%#+)sLH#8(mg_-0AC2z79ol?R+ldy=Z4urhk1P4fjc&73w$F;TWbfo9tH^A6s^)V^i7OuiB|X} zVqFEZB;blPrLYu7YrHxkc!itaZ{5_N*75<_-ztnYN_DDnoLr2d3Yk)5wz2T&mg2c8hwEej5pq3 zRkPa*mBA25@BP^)FFi*iQ{mAKQq9rGD^QuEkx6+p@|LEN+ImmVx(7dxO)GRl*fPR4 zyAHo(xSvg`G8AJ?XuL}y)l4Y9`&c{OBZ<4=q0n`~!+0pa3gz)=LTge8p|yzMJP=+D zL9QcdqudnVAjVE&2ybVp#7Ku%37Yo7E1^5&8BVF`4UgP*hN$0M=@q5X<+0&iV47pUU-Qs=Ps`Jt z-yKpq0KKATA?Qs6d$Ct0NtDn((fpxk`?J~~uW5e)52p@zxO4?B9Rq9OJuaPmj*(9~ z`G}Rp*EKHrdGz2CRAfL!Dx6A%3eghW%%caQ?h|@&hZKeFdmXNc@n6kv8&n7%aH9T; zCiQcq&TM_2mcUBOxRDxJ^hBa)=k-`&dTQSt)VwRe`8GMz4#CfXTo0injD$CoO6t2R z*cesO@@hP-fh{6h5_c> zKQ)hS5e>IRjd24!P9x8Ya3UIfX2S_lN5`NyS4jN=97%^GC%|!+mdg+@8%ovuq3GkK zL)lFziiM&?C`yB(6ezk5MF~)m?x7+YD$;euNjaL7FCjCD)K8Im8aSkpx-Y3;Aob&< zE_zHN^)!z(Vn{vSlX?=Vi_#NE>KUY-29M5>dYYDcBB>{mdLdHZ2akNoA=Q&ZF><&} z4!g)9jvO+`LD)%V@E4;BNgjz@BZph$;MPlb>Nhn03f^Xt@+A%L#^_fQO?xhCoV~@{ zo4h?izB*)+L8wG1qkja(*vxsQNQi3cPC}x)VAGh^uqmVI1}sn&E0eYKe|l96P70eO z5t}5Fe3OuP40$G^iAmTXspKiFgEZtLtOLf<jC#C)cN?}+RWZ`v5Vb> zUyfM(nCwzFs;kr}JO|vu%Sa|qvIxBmKU#RiQ1`NPez|&8+chi3hX3-9IZW+G3c?ea zwl5c71$QZ3p1YhR#XBt@;pxrC6YDx7di>naIcvV1c!1%rJ&)ztU2wz$UQQVC{Lg=^ z0M7G+Ud=xVT)}q)d=dLGJwH_u_>1s)pSoY-13~QC%=7zqcdlz3x=!no&kySGLy_aV zfIDy&Aq9Vs6Z<*yMq(_~^YhSlQG`U^sZe#YtZcdmNJG(ia6ZT2k!RIMntUJQ`~z2EGlaa(7=ntWHuVs}sQ|lhWJRGATG_mWw!~|`bs~6rl@wmbugC!WK;EF$$^98~FLSP_H9F>8KY50a zG30-X{z*PRA$s1%=e!a=cr?xkUxTsKWKYCmmP-3kk7;#`x(8ZP;QBeu^G=rLIp&0x zqE=Us*7YYacgGgxn|*X}DoN-I9w?GIUJz2t(H0Rs_U*pQ- z8IP+>^g-0ZE_C5BrX&i-^Z&uk$M4XJOX?qZS=i(DNT`bSx1Jwblt~*Cg~vvbuW!zC z_O7Ln?@CYITS#Z+E(RT60q^iNM_OAsu4N_$Z*S_a+VyI#R;i0}d^M=MX_u1l4Mc7B z>@SxN@$Tt5=U=P~Zh~E$R>!*AFZOVt!&y(Ko{J5-{n&qrR-2tdmr^KSal|~8C!EkP z^oV0lYvkskQIqj8w@%~nUW73-IfSe>{9$Zs7PpWgZ{TozbLz-kq-&JBiYo zj3#GzYX5V&{?(9s0xkYVd@QYo=eLoFTXRM0f`>bGC+}nFp}Kz_xl;C2YzK6VALDD> zCFqJ1PI#ZvV{Hi8E!CuUPIzwBe0@gYjq`vgCxp(T&O1rTJb@nF)atp%B}F6&9-^kj zv!Re}E(f{3&;K^`#Jcemp>uyigLCi1l^Xlx{a?KkT1%=493Pj9C({shy~gD~DJ@UN z$h~u0Gwyp~KZu$qZ%+AW>0Paz@FW2rwR%U?3&hLOoSe6Uhkx1E8vFVl|J2V&;cH|h z{M)&uE%L!Ga{r#>2^6@kGfi9EzL0Xm=EV6|&Urnjsvn@|Q$>l($vw9w%9}P)V?8gJ zAI|$U=bE~o7Cq;OM*ej^UCN0~E(`Hl^Sjn2sL#qQCmf|it8v9Wx_ygw@esV-O*xP$ z!J6i3nv|-wz%qLNM5)M0=lN%KXYM<#+|8z5K1vQ(wRE$flXl!4#-nSOb4v_wZ&4q_ zVi}xe&62QHa&f{M=3NqX%j5PVH?O~TMN7l)xp55j=7@ew%UP{jv6`nUcx(ZaD6F3| zXki*P@V|{g-n`Y{plSn>I6=LPXJjo0Ve32)wUEG6{PM>5E7!DGr>T**BkLsOpX9M7 zqDeWG|09hTF61qv5%@Ryi#gS1l}mMYHU_hvs}pN`I^)UIkCorRH9$spAFT0a0IjYJcJ#cn^ za7VN5&>^?Y>y9gG`~T+8Z5_}{y+Dh$i$7mGK&Halh@l)P)Cj`%cGpz*kuoLmui}Fh za*CF66RBS&pYxhkif4D~oHYIkfbbTMr%URY8wgd{?T>$GvFFZR2Epbr2BN;ie!@1> zVx<#S^gOnCBEQ77;O=hip7pHaOnE&*?Q>sirBaZuOS1`t6-&GI^pE#f(f9qdzrwp3 z_!2H1EU_;6i*{e@y}_wGy_noo+_`Bq!l{A|*62mv15|jgrT$l=cLxD);x#>J81`B)ttycGLr0J(dgCi2%IUer~?w zffShwPn5z&IEuVIEpCo{bMq#|THp#{lC?iALEhS3lWL9~>rVX=T-@%~6{NEVO%id0 zotQ_La&o}JdPE%-mtzZqRi5@KC)BfNjY_RO*ECP7@AC;IO4G_GpKHCU2k!ok`wbe1 z>|W&eVpai~L=Wqq{b;Z9;fFicpT$RFn(gEX$tYL;IsHlM*dWjTp0>iC4Ur>3tT*b0 z{NCq0Q&$l7YR;WF=UVQ0pSrK%*ICN;8uUig8EKT3JQU`J&&gAG(H1py?vHPc{L>#b zrWj`ux|lESo~MTDg+(jgB(li^ot>4<)LFEC$6rh1|F9isFm(?(~K66ruqzVttxDkV3C`Z)%<924JwAU#hxe-T{B`y?^N zsH_;dO`*M^_eR;frL{75;L4w1A-;Zq<#A4BH8$&)Mc&wT+F85fUh6rypFMq~_q`g+ z_ILMl(WfK4unCR;LIA)FZ&H`$hR?au z+6U1BUGU%sFxXFWaPAcy6Kdp7Hk`ne(&QB}rH#J^kd5bbgF9KY| z4=nQ3F-WQ)YPhG zCkZ{?DN4KeiTnT#3&iR+!P5*9jn#*%+}=CD`>-A{t3LvL~0(JogH6NPE5^;(0u25 z#{XO*<(#(y2Y{8R;F0I4cu%;@oD|*R2_M04HdrAsH$8)lv3S{Yyk9o@ow?(dpPZh9zmoY>offa-#;LnN2F73xEku26v@b!_;`RfZ4+z7v^tz?P@ORSc4s@D7BlTFD=EyMMis`-367lEX#O?2s z2Da+HQK{((b9!yC13%{;)$hknX+mxN=6}Tp1KbR)#If z8o)`^!r8a_-4Dz9uhq}~k+z_Q8v2tXzt`ve(Au=EntiqP`7`t1u3Y)D=6`V2Zb^J_ zd3}!K&I|T(BQKuI`cd5@Z6@{W=QzC2T-K2Wyi*F)GJd83J*dN}+|q=SYbou;rH5wJ zVL4;0@dnF|ZRewWrFFZ45`GOU`dNP^{T9#!^6S4k{{0%f+6CgiBkLc!V8(e+BG)4f z*?*4zUR_2}DjoKbx|f{+sbA1I9_x@svQGK;Sbx_3je7k_T88w>lNWE03NNF(c%pW@ zS<^%JVV4$sLmkB3T2k9$cjbNshtFEg*x<7UOLEZ5SID{Dw zb=hc0^BkJ|b+*Myj%iHv@SDQ|CE=yk7b?1ZiMNVnSPYI zoHKH6W_2IdP2WlD>7hfWAyHcZUFsUfX00MT z*<(AoDy>C&Z)TJDaqrKB^;PKR9a7y!e%T00dIMF)n@09-@@_d=RU5HunxjB?pRN(! zJ!w~QZG}Q*n&#cKdwxrvzD2vo+FdWF+kS-CdN`#yG`q)h)UF#dm(Zac=K)pjK3Y4Q zb?kU$VM0``_scrgAa^*1otckxs&2{qNSk%NXLUYxeLr4v^8byaYm%B;g5z=f*n8Ke z$LAf7yZ_>!!hi9-&d+Eq-l`=X{%tBb_TIO(jVh=!i2sof)GWWrwlu3fth=tMwsY*K zW43=oDLaff*Li)Ho~#pe0^IouCHcFQK`gG)gvJ@eh<GB5o z#3)&Sb%*dDgBs9>uCA|3r0R=lyX4Xv#HJ=!Fj}wv zE>+t+x;C|3to~mACuxBX*57_jo7Mfj_)mNMSgBU?&${2B~|{-(S_X=;_HE@ESUUJh)kL*MP{y?d=iF)F=V^bXz>JfOk20ewwwr0I+-=lq>O#j^uZ2XaBJ6KWE zxz-cfSJ76WrxpKw_9*ZF~pc)pz&34*NGEFmBdgDLh{|Bx0R5e|z{DcvtiSc!kVg63!AnD!fRzOjsd2 zL)cY#WyHFH;b36ZNdGxA3Cm@Erf_iDPOv>8Hw!<(y0hL7wfOrph5drf=sr8JKACSJ zlXElXX6J1^w-S2c*}{ST9lj0<%;Uf{(zIZ7!K&)i48=YkiUVt0hYq1@=+3%k#i4H) z7|svF!>BMej0bH;(`n)QFq`X^FhATA7KTM(DKU5~JP}rhwPAhO#D1si!zv@j}R2h_@_W6mz9gQB6)1<`15Xmm+5A-Xb}63qZ-MsuQh(Vgs&`mJbj zv@BW?t&CPh&qnK_4be-{)@VDpBYG>^4Zg>>jV1FFyOT|mWyy+URkCyP zT81<^D;+7W%8FeCZ^U zluWwGq>J27ms08Ssg=yf%jSi$IYA+rAe$2ulJPPhC;W^;a+z{}l7%D${rgY*8shd> zi0Q8nWj|dO4v?L0GC4;s_m`diGU=_aUG;o+w1xPdWjPpD%0xNh)8*m~Q7K z<)c4a_RrSy;J^yDzF%a270OPZV70_)p8Cw~kY7V>FJIp$<4k?B= zMBRn9_UjGymwVLgV{Vi)Q}$;D%5OJ9?%W!g%^S@x?0*0*kjdBe?>hav!2ZhA4=V3j z#r$1tP7yD(i<m2WQd3(QSF zST)RGO95&d!bZ)a)UX_JuPcOq3@v#6lT6-L$`AD)V)6%l{j{rfxp`;dJ3uKt=6m0`!}YlU!-TxhKnYpHZT zAeTGZzwVHplgj0v$mDC5+P7P+OYqWc)@+^)`+%$2SjOaMywF%zMy}HsN8!+<=#8dFEHO` zp>;ovZ1u3KqyFH-#&_;VGJiQr@cckD0esWS$yi|wN;@2*8gkpAPV%Fhzr%mSf79RR z@Au#Ki~IwAv47An@!#=F{dfJ7{%QZ5|GD4fSu@GAnvws9Kfpj;)@cukz%Fg(@63iqDYkl&4b6Z!X8IWFP<#!i?if53m>3T(hHyN-pB zV84n~C}4l>!q64Hj{P~)&*aZp{uXj=p8qDRncU~^W2Lrl`^BtPv&1iP?JR`sGW3{V z#hP+Y`4?H?n6+UQhO>~5&JH68$3@6SpHyf_4_1P9u9Lfo`&T$;y04OlH@F4v8}3Hr z(r>#jBbz?tZe|7jr`+wVfBzio>p#!=fZM>i7)f=rd(i!ca|!z^Y-hjA-@6^|yU4CP z`R@;`fd4T2DC~kF{=)ePE0e#+I^g>`|H}tl6B8l zkqdXYA6xpnrxd%@#BQm3Mk)9#Dfk!noZsX3xaa*|f6%RCM|AtT&+n?jl^@IFiKg55` z5A~n$!~G~f+K=&L{Uv^)pX5L5FZWmYEB)vF6n~AM>Zgf{_LKVuJ*#3uMMgvUqMz2G zZRixb5JJnfOQ8Xq8Fv#jQ0~jQ2gW$gabF|Zi3c#`ClyPIBuhI8OC3 z-`HJEDmQUo9|3;Ka7Ivw;^K@1X>$M< z50b}?EREn23tgc?SESGtD|F2iI^&o^SE|sJ;cnyyYQ3$&tXCEMdH!0>Eey(W3&ocd zmXpEGnLMua?Py`0KpuDYU1({g^V#yam+$S*^nLtURPt4JDcQ>IC9jY}zr=Ptj0@vQ zj0xcicJKIHn8I3xQ^U2cN%&&8o>{22f55qvU5zs<(IgM|IVCA0Yz}-Quq9VvcC8wo zTX9+6R1;SOeK_1HXS8=QE6qD*X*OqkZ2C;F`ni>skuKIrq(y62l8~`O30JO}r!;z# z{~HmSn5$&|XGF-lW4|^tysG6e`eE-jLfewLIdNv$w&L&qTaLU;K-v*+OTP|Wc4yBG z&UUot9Xw_$$Xf~8lpVY6dplA$eVo9pdFHt__ma$6N)D8g7bog63NgMmS}7)f?8-IE zIPh=Z+Pf;b!Omw#xn*sGnNVmEUz#XCZEtdmf1*~iV&dPFT#GButq!$xF$!!>>`O#t zmX8+8+K!|Dcf|W>H)}JqkiI}FH-l12p`T9Bo~=bwQma`pp)4Y-1;nQXam%-LY-&B) z)oNPX|MfMi)xmlgvhSbGRO2Ym&=PyJ>Ajnc7g4^rj14n38cTnVc#3mbZp63}Tq)+g zrO!6xe0AQJvs1*0y0Y|Yt{iEWrWu^cIXc1#C$N7(D^BDRq>J8^^Jj)L*=_Eua2BQ5 zh%mwh>Z}VWUoQ$5QI?Gg+FvNJ7jYOJMyot&?QifmP^QoFvnbzh^fw~o%=WXXiRSn@ zNJL-uUxqGk^|xYYo}Y)EulTQE=Qe*EcINx}*fANwBcVM&+5Q(~s&rjuGSxo9!9kc> z(WXg+3ii&4LTh%)W!*g49(20f0P|4R5!UkbavB-v;PC>#J&>e zkW2lk7h>)__7hA|FINjHWZ{}g(|eD@hQ0skkutMPvdX9>H= zOeY*OI7{8N?n_YnOyssENaHuLqMOMH%zi*RH|=B(h5qzRxGj`kVd_l?F67z#^YSUk zr$|1riwtKg`4pE=z2s9uKAnj_HxZ{f)ND*Ha7jMd!JduP<}Um$~QUk4EEakC*1v^_pTq(PK+H93{O$gB{x#Yx z6>XOIJN=#fdzZh9e@!ZCiJbE`QakHkBDL?LHP9bfcDFlM^xH?0Papq2ErTwi;%5Hu ztjKyU?Tr1bd~LE&8~*`qh5nL=PVtBQA@=_xlijJJ_dZC#5j#f3S?#!ONV48>JK7|9 z?ETk>HIFNUt+##}ZIdRhJ2Fx;q^Xin#wzUPv=8&qs5Wb!vAC*8TvaTt>LdBv zB-{~hh)RsXDlw{5VsxdQ!oHNq;-6xF!?8%@ACp9WGSO`%YO{Uk?tqi-L=Hw1#@Q4( z_(8rK&Djo}%i*@Cp*h=g@Hx1_+AhVSJlnbJ=X~AhevJ!TIRn(Ujc230lqfGx?U*9b zU%u$Ck?60P=&wNZS0egrEcz=H{Tbyo!FS_l@Odx^eHxWhFx^ zxM&E^#*5wf?;@U!BfF^t=p?>uF23~Q%g*AEB?(C|3bz8WZHBQ z6lz>pAuenyE-VojmZ(JPL?1?T>=;L~+b6Aoa`8l&cp@&II7!@4>EC62_4eY4O7TRI zc%qBAp;&xylBm8?^qvsCCq?fmQFp%Ry0s{}wP?9gwA@IvTp(I*Bw8*IEmw+WD@C)7 zM6(5=*+!z-0?}-xXttwhGf%C>uAyXb)&bAu4T;;FH=M|LGDd@oJ?iS=v`g;@Z zCNGt8HRUSiGAZi#bXX=jY$7@=6W_KG zHMWL(S5xy^y-@)VuOsPlB1T7CMU4BBjt_lD^{Bs zAL@CeaLcq+CSjYj!G6l*S)Ot$seH;)8Ida=*p6OP@ZThZNaaF8xe`;kkWzlozM#BC zGnolTM5wbzjDPL0DkUYqlNstT=>sh+?uMLvj%9M#>R?)r>9##KlHy=5-Zd>dsUrUK z;xjKk^WrCK`(hn88q0IvkNrYg|qT7 zHsiSov~FM7kDFL^p|tJ-t#*(dqtH@4bs^SWNQsV_w=VD`yWIPMP<2|5kZqycA2oT{ zB&q`Prx|zSVbfM6#3flCP7%UF;?|t7+P_7@VsgRWjO(pE6kIQKcLUjx>|B4Yt3+cv z$aO%McRnT`W6kR#bhkrU4g2GM7;?&RKb%&?h5kamj`FsT*=SarZ{jcZ7xO&EkD+Dy zDc0_9X<6$Ca$9UFIkI z$<#Je*g>NZo$xiT%FpmKSa;jn36W$Xc444xTduOSTyju}4wUJ_7H$y~0InLXlZ3wudGK@yOc1Qb+amrDY& z-qR6ERZ`TS;pvM;CpthTP=0v3Xp)Nt4H-7dl}!B7w8?JJ-rbC_ zp^V0E3Di{t1)oG&@-cU@`z#}MZbV|ak6kyPL{s}KdN=;!KJa9=LV6oUsUDTVs7 zM*0|f(PkqN-A`*}6>Z`zZU^Ij4tmNCg(*&2wWE|e+kG4<>cYF|n|Z|j*!`5& z%j@h){ZEhXSGE(xqKXo*pBv`Jl3vr0zV4=1ZUsF!>uKfeWJlr;JtdZGC*iX8Xm8J9 zU(`>#&r#og8NKua?9=)*8tRv+Z~y8J(V9-%PQh0lsD1j=|96SI3i)C#{Y8rzyR_PE zK<;|ey$737uF35@xc&t8&K%%Iuovm)smE`oo%SF*89qbH>o@ccz3)9OrL=7;t2$B& z3`8F?j$Xy<=uupNRQMQD-$o?4x7=Q2GRjN2ZTdWGIh~8N`WZALUv#%3O@7Dy!2QI% zNKe!s-QRpf9iFybK-t!rvV0J7?Rey)>)kx0&ZX?hxyEgRCf|1Zkn|{@<#uDr#V*tz zgK33bhQ#wF^!4AMPwR)YAb&yI@K5gVNQCrQU3S^z8Ht&~xx)Fv1;YD!#lnXtP5#^!$;X6G2v-Z&3fBua3AYGe5xypTQ@BgG zN4QUTaMF}(u1*DEN?0f?5w;Xo3OfqB33~{83;PKN35N>%ta>@Msj>?<57Jb&_( zYp>29E*vEsD;zJpTsT=cO?bUN!qvjH!u7(J zgj>%tqowA~!yRetAuW+F7eBp56DB)P)c;V&3 z$--&E>xHwYPw#$e!7al1!h3`Zg^Pqsh0BGH37-(I7OoYp7j6=60ecj@B79Bwrf`>V zk8q#xpkd=6ObH8xCBl}%N?}Lvw8q_pJ%qi5{e**rLxmRzM++|zP7q!xoFbedoH_m4 z3DXZb22H{K7ubn!*@mAq>;SS+j!rj96g!_db z8WzTc`NF2cGGT?VY6f&t*jae8u&1z(u)lDKaF}qUaEx%AaFXyU;Z)&uNKS#v{|E10 zt>1>lK%H!DvH#TFBWqf!H7r^S&NK}ThiN@UGvv5ZX$MS8*b5;Y>;Ku* z@W{cI;|+_}@9SxQG$fjCf>B_?n9y_*>9)4DpN;=a^I>MD`}5LQX{9orO)t^VSS2?b z7ERygY2`H}R?4-8#S`Ra!=mZJyfk7Bho&d<(v~$Go+vjP7EN1Z+VO@&)602j=Nb;% z$<2ntQ)tItiFV?8TKDs5C*MzNb_JS~)#x-fqCwbBJAW6wYgLS3>%o|{f#|eGqM@3= z$h2wnfZsyT^u3I(T;?D3Pxxp33yf&p=69fV+T-^lT$pU(Q;d_)eXF!K-hV4??$@Ww z4Zp#^nlX7RbAK~)-H*V@3Df+ zy-nucBXd7Lb043%&&=ExX6{RL56rq*lMZ2QedeBtX)F`dSSDp+`_gF_pOCf}UzxdQ z>?fM0U1UBDBT)!rNOVs0N&k}=n6{gkoqkWPNWZ7nX6{>^&$AwjLUbopXt+G1G0Q2< zPq!y@N|`76TLbzl`)XzVlP;uoaKFi$wi_Q)ucgShR669{GG^OSn=#uapKv*~fj@j| zo$v)h>QifkPYE9vJ_?4^qi8#AG;BYAj>bvmj(pX~NOYc8XGXw&0cxC%#`9X-TVXoj z)H1YmKD8L>$EUu97Ata%QVVK17x8@&p|^fEayB&+dsAyU88wZK{M7KH&N2Ko#vI7F z&>LGPA9eQD_qf}zbH2VLl9wMor}D?toIl36L~{7y(_$XEc(PaK?2qmK zIkvmDG|$X7o0*tV z!*n>|oTPJH4f9@1m>ZTkht&8|;S$Vi%bvzx)ghq|CsCGjHqHGS-|15E{WZ?_YJ6Yk z@|aau^W0Ly#uAqwUv<=(Ejsl&mu$?s*`1M6(*!kd`^zpXx{IvVQUa3a}S$@ z`1SNW%w#;&EXG02X8h9}#y!pTH*5YvkHBn-_~o`QZ#cF_4$>HiSC0xUZ=r1s@b9$H&t_|1O$mlSOuyrTK))!VnE2#tF?4F&eg2p&<}A5E|d$9`@Fs^oTD3pY)~R)80moKkGuY^SDZ~5Un__|9osJI&X3| z2HkSyYie~)$Drz1zr%KXtU_s$q;1N0n{28akq-YN1!9akpnoX} z)??q1{-DnE13?9`rtT8P@7zRx|2TBv37_;SpXc*^BjzbLW5or>AfjveX!Qy;}GfV@@CQ>zHkGH?q`yy#2ucH*L2p=^R^PnYPG+|KkUEF z+YkLu{WJb+{(F8AZ;$)+NL~y4a>lPF{Ez(4{7?MX{UglhNg$JL@N1CJR`>^bdy*MP z&oL|MzdZ7XU*$LYwSFOz+jn^Tv47D&&wQotGdm`jWCz~BsM!EN&jW8{)NFvSZv$^) zd}4sV=YwBn>}-I~?*wmVeB!_N?;%DV2EWN9$FK$N$+!9&8KfVkyY#eHPa!-@zv$hh z&po8W0^)lw@mfguH!)^-C8NfeAEN6iQgJn@^(?8fhS;z5&y&-2mICslD>Bd{^j%qh z@ad9xJbX--l)~WYwX-jm(2Iu|Ha&c5GkQANe0usYb7$|+`>Po|Bbn84F_QDAnZt7_ z{koUo0;A9w4Ql-9{eB^#=lO2tWg$-y)2ICNeBMN?^6fjbUCd{Xv~KtFkkQT7edL*g zde6X?{lCnAKl8ubhn`QKVmt?|oyL|NeZegeVp?}e7X3kCiI{!s_N(mb& zf4yRyM{o4sDXm-4kKCI6A2syFQ{r;n; zw8hkBdJ)UL_?Vt!mt@XQ9`P$;&VM;0R$DMqH76|eP20RX>aD}2!U*E_nJ_+F#>|?D zv>`sr+5lICE16+)6{*3z8**h1F}5`KO6f*Q=~AU=TVfWV9O?sdO!=aIcgrzGA8|V3 ZeI9pnB@ok%^xK=gnEJ^Jm9pqj{ujZ7$=v_| literal 0 HcmV?d00001 diff --git a/example/assets/images/APM.png b/example/assets/images/APM.png new file mode 100644 index 0000000000000000000000000000000000000000..bc864c9e2d90eb1b12b401a7500b41c60d1fd8a6 GIT binary patch literal 852 zcmV-a1FQUrP)Y+EuuLIRWW{CcF9u?G2KifO3K=C!je2;RfXdm|cMdxdFKWS<#k7jHEc7 zJWH}0`Ybyge>0iHPfDWSlHPlg40LpKbTA=GOJ&jnZpW%x53M6xs&-vN-Yk4I{Ng6V z5wg`)JB4i_#2A-1pPWU`FUVo6?#wUj2dW1TUPGqKVnO7tpPgZU$fu5tM_kK?qJ_J? z5uP#6THFgXhH>S}%L(>5OT>_%KsP8%zSiTa;>IEucxCc$0DyzCT@!rjXPT*!eAz`n*T zA-}z%^^a%j??+pfsj71UMi_^sD0AVC*d8I8&1Ls(c9R{;rX*yZxU6$)Gdcw#S1 z!*0`aZSD2)%w9fNT$?~Zupz18Oux$G;yNo~%|NQ)%usrESRj;@)dF++RhB-5KNjO& ztR~LhTkn+woo|{NR^r(Q+jHt34S(iW$3cj48n-`H^1X?1iF&<31JrfO6NCB0=6~o; zR`(U((+p4EGYpHny&iG*v;7k2KKgQtGi$QwJz9@BS3mbXB31G1mkGXS?S1y^VCH&q esH3B!gPng0a?y3^Rpl7~0000NgAj^Yf@)U09-d4_?CdT3G;?Y*;gpT)q>X29 z&hY2kF%+U!oa1e><1X?=rQxH?1%e4Yf+Ez3ON0$j4p*f>HEYiCI_!7=Wmu~!jEw~~ zY(goKl0T}vxMb99sKv(0$2Ll$VC2Yq=zv;1l%%{k!cDmb6tr8;pd=6LHR$5}=^nzt zDGY{(gLuuwYbn#=&F37ah1QG4z8b>HiIFJxVCLSX)Awh3{IRIZ&%lL?vNW*;f$OKr zLzzL;0WQVfMl6iF(1>Pn945f&yHiE+cfGDBea%r=pbzwsf|T| zpa*9oeQ$>%Bh#4Cgk6OPF&C7F=4rZA7z#9N?!NK}4N9DSA>+Hni?9xb_Z5n2ip3?C z$G=nVc)y`O09zcwT*9B*t3|8JPRm2oNfb#XNaVy#FhZRW?8#J@6Zebn7DRKt5_|-^ zq09yH)-oddr~^X3KoU7|JbAyQCcPvNIf(n0ptv~RFvZuL1in6azr245fyKntE$kXe zf%)Qi@H9&ph0-?={|jhOgO47f1(`<|d4D;wL+7 zbcp!5%I~K@sJ_^=Ogxrust2}*+UeZ<9t>Y3b>@9k#MgD>cCM5C!FER=uv;`hB$s@= zd28X8Fk!uQ8&RVLa^=FZp0gD2{?ywEFZ*rwx4r|tIs>teS(0&-ITFr>&-&yW)WX8% l!&!Gx2kDT!!C){{$zOoW<$T^hWk~=4002ovPDHLkV1oXHlhFVG literal 0 HcmV?d00001 diff --git a/example/assets/images/Core.png b/example/assets/images/Core.png new file mode 100644 index 0000000000000000000000000000000000000000..4a52210b2e770f9644bee4e861917411be45c6f0 GIT binary patch literal 2367 zcmV-F3BdM=P)nPsC%($(y~~RzEq@t2%dh!A0SfxN6p@|Z z85?}p+!palQoF#@m!Z`*3KBQ(zYus<&+qJL*^Mdw+7%e6$CAy5;F+Ao5M2QAgjx9FS$sN>Eqtg|@a3-k5f&?2NyD zrnU2>EH^y0_Zm?)l24awO}Qe{<@OTb_9nN99Bxv|AxY6cHv^F*aBixaYW!i!6UZ!+ z{wR6CB9RStdh5jChNlk7lqFMYCel+YkTEOx(X$+bQh+;!3?A)feP7H&)DJutG=Bu_ zDrg|W+!{ngaHGv?0g<3{)v>_}z7BAd##1l^Vnc%!;>qr8P;NP64Wadxdi64S6gStE zQW-oBF^a2i2%~pv*umi^yAT+yjt^cxOxF{E`_+{fmspS2lovWzlvB-YXLz)m^MUFi zv&Mwt)+TxvEBcu=LW^~d0z{62O`i~BJL7{RN9o%&xVQiQ;@6bXZ=65jGT9lNVtGPMO#CaSeT_uv?sqJ#w6i@sYjD>1V z;G>l+zmf(n^z*2hqHp#t1T&=2Ijl9OsaF&@>&~rXh4)S*s)c6ZboFxQIP>LL%jLil zO#6(OPa_E=V*+_9foU=i7GI!e=?Pozz<$d-3J67r+={U3`@RZ!D3j-+|(zEWxN=a9&C7BH#A_ zzdpKi`HidSQjP?7c5<5&HNEaI2}=qOwo-gn%>pHi<)Zx>6UdW*_NXN}3-VF`xfI|t z3z;05U!tqKlW&aR1ac_2UruZct3<7|QDmhi-HI99W~yDx<)HkQ0D zut>=5AK3Gby-9P|HXrk$@JVJ4G z8Gg)=Fl}M-Q!6_CR9M&b$2PwB1kIJ13JTuqSGWR8a3dAAoyvV)Fu5>qABv;jzGcwTaoyCQPgl9AAj|06mWv;Hn5Oh{(}9`LSiw zfvG-pLf2zxe-n+Ry9z!7L(~iZHMcAsfPo9?%1G6Md+Yt@L&(w*(#Vto>Qz}U{&zhI zzo_$*h#Tnr-@Tx)HJ0;hOZ!l?wnG)=vQb<^{o%*Tfc-yUX6Sd*n(JX!%ji57{#238 z9Ne1|&&rgFOFke|>B|KD@p9}wt(V>?-uwCgnITo8CX@0Un`#f`7i3xJA(Ek%I0~EWBg$U~y>b$pC86G!H4OxDZw$Ip@vQ82e0(;XWZF#VrytMio7JDazw@0aN~T$#0cG(>DHo|+^>~j^&udKXkJt+e1*=dp zcMF+}Pf^ghO#XV1*Aj(3P}CO|JmjLlx~F+$yn>iOIskhqaNjQB&wME><&-~5Wu@F% z&#=W{a7x%TZa1%UOzScaN|)1$2bQo}cWegt9+8Y<{^A%8krRP?`-(jA%7!TL8RSbl zznx-^lf2Doo)Xw&H@x*P2gH?>Z3^owVxODgwi41e9JALk|HqLd)a@P|?+|nzRMUFr zQJe=e++It<L4DH6iOdr@OMz zUcwvJ<%CbX^Z}1i%;+}#Pk;}GTG{SUCi4bRhi=uEojxqieCcKKx`Y&Hv7GNj7uS#S}UaL`&?2)@ipgkADvj-+32Z%8_n)c9IKe;CFTk&zBzW zWVp+57WkLYWm}Eo?8XNw&$&mA&P^x%9_v}d<b47A(vCagN>9a>2eP&m1=Z0cPgjNa+Phou7h3UjHa!dg~?}k9S0@uirX;!wZ zati66n(5|BjJ;SN49P7|rq4>`fCsmM<;PYh)96&u8&GBncyw=>l1nSTp2OV5`*;ay z1NYY7)-ORG<(U&HGa>((-AJ&!*Hm0d)@I6jOAPA4ryVCTp6eURu%JHGv$$Z{QCz2p z29K&hj*}qfelfoCTduD4kvZ%(Kwh5irVK8&V}t5qkN=b4?!Ut-!T|HGL2TLD(0vcG lx|fv)#5m^va2>BGUjYFeyIIwvek1?@002ovPDHLkV1g1Xe1!l2 literal 0 HcmV?d00001 diff --git a/example/assets/images/Crash Reporting.png b/example/assets/images/Crash Reporting.png new file mode 100644 index 0000000000000000000000000000000000000000..c6290ff04e7c0cb399b987f69c1086cd572ef9b0 GIT binary patch literal 611 zcmV-p0-XJcP)-`#Af(ndVqt+ZJd$6pd`5#5(EbI|Fpom=T@rSYA z-KCV=+Gb>-2FOgIHK#h+X?J>B21h7~3|#1DQ5e}!IPih;2h7nja0W{PXcI-IrGlLv zYdik^s9pHmywz&xHkuJK5)nB2M$s@(Ppi)sP%j|jX2Cq9Ttwh(?m?u2Jzsx@A&9uP z+=i5kI#SK*s&S19J}jr}jz;Qp7w-N!b#DR54F-Iz6XgikOGxA|!`ks+foE3X4;aL(Ic^`N$#2CFaR- xZ9H%}&asNjoA{8*M?w97WUC+uf*=Sx&Nsf1w_(u6m9hW;002ovPDHLkV1oCO`h)-g literal 0 HcmV?d00001 diff --git a/example/assets/images/Feature Requests.png b/example/assets/images/Feature Requests.png new file mode 100644 index 0000000000000000000000000000000000000000..318b00676d0040e2df6471ef2ed4f2ab4cdc1b4d GIT binary patch literal 1031 zcmV+i1o-=jP)xDLEVQN3^$MskLbd10_6n235X{E780Ht^aR8cz+NBB4a6NR=(4&e z5hKY=S5J?+m~TPCB;7qV)m{Bpb%2(ZmX=5m8iqf<=;7A%v*6K#|ET$q+!G=VXIc!MoG_i(cC)M!Tx!lqkoWG(!M<-2GmvJZ482 z)5hiMy*@_WQ}}w{Dw&7erM3JD3e=u+Q_|nCbGfOtxu0Fjb%tM{u;X2$2POJx$;NXK zJ`}@N7d>s-l-s~8AnDpnB;s@@$06eHV2%FZY4p+O{U0L)u`B{5tu6F2%yG3 zCyA&3+OtUN@@xQ2s^m(4VP3NY8|R*;VdcO^=QVSeDuqx>p*Wev^d61ir%%g>Q`$S~ zo=Hz?)m}nQ{mU+D5=UE-bje3RH--(y^azc?=wPR`hv)WH~N?r@{0tSv473DFPyd0(>bPNF|o>zJXoTwX;pzh=;!B@}mYCFcoG5SjQ}fkjh> zaD+%4K z!^np20e=Da%OXlLcMibfEh(^u)FE41T3Y@YKLLfxSgT+SgH-?k002ovPDHLkV1m>a B<_`b> literal 0 HcmV?d00001 diff --git a/example/assets/images/Network.png b/example/assets/images/Network.png new file mode 100644 index 0000000000000000000000000000000000000000..04e0cae2ed21656ba542fbd60128ab41c3088322 GIT binary patch literal 1198 zcmV;f1X25mP)~ugXfD{mmRA6-wO)QekAl+v7 zKpu=GS)jWFC;Da>{oLv2JMGVRdjQm^QKN<-U{NFa?elthlcxJJigzW@bCM*GYg`V#c!CNzxCZd5e}H_e|*;tR753m zb5iiO<+Lua7lu#~i-D0JL_P-zf;Ux2Zsc=9iU|I1Ve4T3Fg{y0Ia6qPv#VBbcEA?# zn!j}b8(@}-D5hEtrO0XD zLO9;<&BmGS{Dr*)9AVxfSCK?>?*6HxWE^`F>welp+saY z=W{|VXH>=UBZ_XA*qMIOO5osubjc)dqL10C<8Ln+QUb?i?pBH%5}beb?~jz|Pd2L& zkNTx~ah;NMNAjF|Eo>#&!1FS9DH|=jO>##mLGi1YVxM>WU>E&_k`Y^nwg?@dXgcD( zQWmmKD$oSK`psATB}YGRD#Txr$Y(P>U5A)p-Bi$3Jja7y0i;r8iev;ML6F{vV7$-PIvUg7?OTfsg(s|Q=UyFnIz~`YRUCr z5QfNMz>>_Xsx#&K2D%5EYROd?AcvDZ-1Ca6Pyz|#VuG?Q>L>SUVBu9dMu;OqgYx^Uz(Q6-@oVjyNaz zi}3wmhAqch;b%xKxexLEOyp5h(K02-=0q=v)M@!kMQXw2)OY`pFKLRHz+@^AfWYy~C-avuIXZHXnq_%sPO5NB{r; literal 0 HcmV?d00001 diff --git a/example/assets/images/Replies.png b/example/assets/images/Replies.png new file mode 100644 index 0000000000000000000000000000000000000000..122b6674e7665c9991ef262638610028bd6da136 GIT binary patch literal 464 zcmeAS@N?(olHy`uVBq!ia0vp^Iv~u!1|;QLq8Nb`XMsm#F#`j)FbFd;%$g$s6l5$8 za(7}_cTVOdki(Mh=?6;pa=YBr8?PtPmh%kGzq^68x%jBj< z$}$Ia4z4;7cT97ceyHmEDQ)LcHXPcr*!PWBRPe9A?;h&aP18=zJXW+<=ZSj6mVX|H z+RwD}Y(MsAsg}yKBiG_DKQQQ4F4$dfHuJ~dr5B=k658gSIdhjsB>T%={^cPbq?lg` z&xuuIV!NUoVdllwXS$AS+TOOi%dT*`75El>ih9AJ{8B0>?*XIR71bY`Zr!OXe6hfC z#=mt@sV83>s@<&bJ*eg0^y{b6WOb{*ZB?&Ja_qkbm#ALd^T!()0Sun5elF{r5}E)f C;J)<$ literal 0 HcmV?d00001 diff --git a/example/assets/images/Surveys.png b/example/assets/images/Surveys.png new file mode 100644 index 0000000000000000000000000000000000000000..b4151f725cd778f4838088302ce518f5de62f39a GIT binary patch literal 774 zcmV+h1Nr=kP)L*|bbo$q67QsBe%Qfg7Mz$ui5ho#yi; zJ40ZU*ak_&pf{6oKypSeU)Gm?06ZR##}K%!k-AA6viC92vJJZi26G-)in9~3vCF0g z>ipQ}4;TW`?up%lVw|0hbuC&s>iq2Ts=P^@m~WIrx7!9Y9RC(Z%&8sNQ&qRmfYv7k-e=Mxty3joVWS- zaiBYXl#w5dqYM6`jGai#K67$lS!<#OTIV(;)|!|UlbByn2zzw|@5J~%!`N9Zm0*}u z>bnRJZHP%M<~b4-J67#i8+&q8F@L>TTFZ0x**j*&^VA~ zILH(C+4^3SI_E+zEnpZ+3ks;H$(4}BSW1=&m6$|culfHM#jZpa7_CfAOVg5slFA}k z`%E@;iXoRAfFz>%07*qoM6N<$ Eg04MaqyPW_ literal 0 HcmV?d00001 From 0469a3fa8d9625efb8a1458dbfb1374863764e0f Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:05:54 +0200 Subject: [PATCH 06/34] Add models folder --- example/lib/models/app_flow.dart | 13 ++++++ example/lib/models/app_theme.dart | 59 ++++++++++++++++++++++++++++ example/lib/models/product_item.dart | 15 +++++++ 3 files changed, 87 insertions(+) create mode 100644 example/lib/models/app_flow.dart create mode 100644 example/lib/models/app_theme.dart create mode 100644 example/lib/models/product_item.dart diff --git a/example/lib/models/app_flow.dart b/example/lib/models/app_flow.dart new file mode 100644 index 000000000..524810b4d --- /dev/null +++ b/example/lib/models/app_flow.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; + +class AppFlow { + final String title; + final Widget page; + final IconData icon; + + const AppFlow({ + required this.title, + required this.page, + required this.icon, + }); +} diff --git a/example/lib/models/app_theme.dart b/example/lib/models/app_theme.dart new file mode 100644 index 000000000..ef6a52fa2 --- /dev/null +++ b/example/lib/models/app_theme.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + static final lightTheme = ThemeData( + colorScheme: ColorScheme.fromSwatch().copyWith( + brightness: Brightness.light, + primary: const Color(0xFF00287a), + secondary: const Color(0xFF5DAAF0), + ), + scaffoldBackgroundColor: Colors.grey[100], + appBarTheme: const AppBarTheme( + color: Color(0xFF00287a), + ), + visualDensity: VisualDensity.adaptivePlatformDensity, + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: Color(0xFF00287a), + unselectedItemColor: Colors.white, + selectedItemColor: Color(0xFF5DAAF0), + ), + chipTheme: const ChipThemeData( + selectedColor: Color(0xFF5DAAF0), + ), + iconTheme: IconThemeData(color: Colors.grey[600]), + textTheme: const TextTheme( + headlineMedium: TextStyle( + fontFamily: 'Axiforma', + fontSize: 16.0, + color: Color(0xFF00287a), + fontWeight: FontWeight.w600, + ), + ), + ); + + static final darkTheme = ThemeData.dark().copyWith( + colorScheme: ColorScheme.fromSwatch().copyWith( + secondary: const Color(0xFF5DAAF0), + ), + appBarTheme: const AppBarTheme( + color: Color(0xFF212121), + ), + visualDensity: VisualDensity.adaptivePlatformDensity, + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: Color(0xFF212121), + ), + chipTheme: const ChipThemeData( + selectedColor: Color(0xFF5DAAF0), + backgroundColor: Color(0xFFB3B3B3), + ), + iconTheme: const IconThemeData(color: Colors.white), + textTheme: const TextTheme( + headlineMedium: TextStyle( + fontFamily: 'Axiforma', + fontSize: 16.0, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ); +} diff --git a/example/lib/models/product_item.dart b/example/lib/models/product_item.dart new file mode 100644 index 000000000..cfe274c7b --- /dev/null +++ b/example/lib/models/product_item.dart @@ -0,0 +1,15 @@ +import 'package:flutter/widgets.dart'; + +class ProductItem { + final String title; + final String imageUrl; + final Color color; + final Widget screen; + + const ProductItem({ + required this.title, + required this.imageUrl, + required this.color, + required this.screen, + }); +} From cfaaed993fd4088db18b78928a8208922ad6054c Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:06:14 +0200 Subject: [PATCH 07/34] Add re-usable widgets --- example/lib/widgets/chip_picker.dart | 48 ++++++++++++++ example/lib/widgets/feature_tile.dart | 61 +++++++++++++++++ example/lib/widgets/product_card.dart | 70 ++++++++++++++++++++ example/lib/widgets/section_card.dart | 34 ++++++++++ example/lib/widgets/separated_list_view.dart | 20 ++++++ 5 files changed, 233 insertions(+) create mode 100644 example/lib/widgets/chip_picker.dart create mode 100644 example/lib/widgets/feature_tile.dart create mode 100644 example/lib/widgets/product_card.dart create mode 100644 example/lib/widgets/section_card.dart create mode 100644 example/lib/widgets/separated_list_view.dart diff --git a/example/lib/widgets/chip_picker.dart b/example/lib/widgets/chip_picker.dart new file mode 100644 index 000000000..5a006cd26 --- /dev/null +++ b/example/lib/widgets/chip_picker.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +typedef LabelBuilder = String Function(T value); + +class ChipPicker extends StatefulWidget { + final LabelBuilder labelBuilder; + final Set items; + final Set values; + final ValueChanged> onChanged; + + const ChipPicker({ + super.key, + required this.labelBuilder, + required this.items, + required this.values, + required this.onChanged, + }); + + @override + _ChipPickerState createState() => _ChipPickerState(); +} + +class _ChipPickerState extends State> { + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 4.0, + children: widget.items + .map( + (item) => FilterChip( + label: Text( + widget.labelBuilder(item), + ), + selected: widget.values.contains(item), + onSelected: (selected) { + if (selected) { + widget.values.add(item); + } else { + widget.values.remove(item); + } + widget.onChanged(widget.values); + }, + ), + ) + .toList(), + ); + } +} diff --git a/example/lib/widgets/feature_tile.dart b/example/lib/widgets/feature_tile.dart new file mode 100644 index 000000000..0eeb69ccd --- /dev/null +++ b/example/lib/widgets/feature_tile.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +class FeatureTile extends StatelessWidget { + final Widget leading; + final Widget title; + final Function? onTap; + final Widget? right; + final Widget? bottom; + final int? leftFlex; + final int? rightFlex; + final Widget? trailing; + + const FeatureTile({ + super.key, + required this.leading, + required this.title, + this.onTap, + this.right, + this.bottom, + this.leftFlex, + this.rightFlex, + this.trailing, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + Expanded( + flex: leftFlex ?? 1, + child: ListTile( + leading: IconTheme( + data: Theme.of(context).iconTheme, + child: leading, + ), + title: title, + trailing: trailing, + onTap: onTap != null ? () => onTap!() : null, + ), + ), + if (right != null) + Expanded( + flex: rightFlex ?? 1, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: right!, + ), + ), + ], + ), + if (bottom != null) + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 8.0), + child: bottom!, + ) + ], + ); + } +} diff --git a/example/lib/widgets/product_card.dart b/example/lib/widgets/product_card.dart new file mode 100644 index 000000000..f2db62f18 --- /dev/null +++ b/example/lib/widgets/product_card.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../models/product_item.dart'; +import '../providers/theme_state.dart'; +import '../screens/products_screen.dart'; + +class ProductCard extends StatelessWidget { + final String title; + final String imageUrl; + final Color color; + + const ProductCard({ + super.key, + required this.title, + required this.imageUrl, + required this.color, + }); + + void navToSelectedProduct(BuildContext ctx) { + ProductItem selectedProduct = + ProductsScreen.products.firstWhere((product) => product.title == title); + + Navigator.push( + ctx, + MaterialPageRoute(builder: (_) => selectedProduct.screen), + ); + } + + @override + Widget build(BuildContext context) { + final state = Provider.of(context); + return InkWell( + onTap: () => navToSelectedProduct(context), + splashColor: state.getThemeData().splashColor, + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.0), + color: state.getThemeData().cardColor, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80.0, + height: 80.0, + margin: const EdgeInsets.only(top: 20.0), + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + image: DecorationImage( + image: AssetImage(imageUrl), + ), + ), + ), + Expanded( + child: Center( + child: Text( + title, + textAlign: TextAlign.center, + style: state.getThemeData().textTheme.headlineMedium, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/widgets/section_card.dart b/example/lib/widgets/section_card.dart new file mode 100644 index 000000000..e62b22f33 --- /dev/null +++ b/example/lib/widgets/section_card.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../providers/theme_state.dart'; +import '../widgets/separated_list_view.dart'; + +class SectionCard extends StatelessWidget { + final List children; + + const SectionCard({ + super.key, + required this.children, + }); + + @override + Widget build(BuildContext context) { + final state = Provider.of(context); + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: state.getThemeData().cardColor, + ), + child: SeparatedListView( + separator: const Divider( + height: 4.0, + thickness: 1.0, + ), + primary: false, + shrinkWrap: true, + children: children, + ), + ); + } +} diff --git a/example/lib/widgets/separated_list_view.dart b/example/lib/widgets/separated_list_view.dart new file mode 100644 index 000000000..45346b950 --- /dev/null +++ b/example/lib/widgets/separated_list_view.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class SeparatedListView extends ListView { + final List children; + final Widget separator; + + SeparatedListView({ + super.key, + super.padding, + super.primary, + super.shrinkWrap, + super.physics = const ClampingScrollPhysics(), + required this.children, + required this.separator, + }) : super.separated( + itemBuilder: (_, index) => children[index], + separatorBuilder: (_, __) => separator, + itemCount: children.length, + ); +} From f2d90536cc42e114364c58c8117647b0a51f765f Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:06:57 +0200 Subject: [PATCH 08/34] Add providers folder --- .../lib/providers/bug_reporting_state.dart | 97 +++++++++++++++++++ example/lib/providers/core_state.dart | 11 +++ example/lib/providers/settings_state.dart | 26 +++++ example/lib/providers/theme_state.dart | 17 ++++ 4 files changed, 151 insertions(+) create mode 100644 example/lib/providers/bug_reporting_state.dart create mode 100644 example/lib/providers/core_state.dart create mode 100644 example/lib/providers/settings_state.dart create mode 100644 example/lib/providers/theme_state.dart diff --git a/example/lib/providers/bug_reporting_state.dart b/example/lib/providers/bug_reporting_state.dart new file mode 100644 index 000000000..82f824fe7 --- /dev/null +++ b/example/lib/providers/bug_reporting_state.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +class BugReportingState with ChangeNotifier { + final _extraAttachments = { + 'Screenshot': true, + 'Extra Screenshot': true, + 'Gallery Image': true, + 'Screen Recording': true + }; + Map get extraAttachments => _extraAttachments; + + var _selectedInvocationOptions = {}; + final _invocationOptions = { + InvocationOption.commentFieldRequired: 'Comment Required', + InvocationOption.emailFieldHidden: 'Email Hidden', + InvocationOption.emailFieldOptional: 'Email Optional', + InvocationOption.disablePostSendingDialog: 'Disable Post Sending Dialog', + }; + Set get selectedInvocationOptions => + _selectedInvocationOptions; + set selectedInvocationOptions(Set value) { + _selectedInvocationOptions = value; + notifyListeners(); + } + + Map get invocationOptions => _invocationOptions; + + var _selectedInvocationEvents = { + InvocationEvent.floatingButton + }; + final _invocationEvents = { + InvocationEvent.floatingButton: 'Floating Button', + InvocationEvent.shake: 'Shake', + InvocationEvent.screenshot: 'Screenshot', + InvocationEvent.twoFingersSwipeLeft: 'Two Finger Swipe Left', + InvocationEvent.none: 'None', + }; + Set get selectedInvocationEvents => + _selectedInvocationEvents; + set selectedInvocationEvents(Set value) { + _selectedInvocationEvents = value; + notifyListeners(); + } + + Map get invocationEvents => _invocationEvents; + + var _selectedExtendedMode = 'Disabled'; + final _extendedMode = { + ExtendedBugReportMode.disabled: 'Disabled', + ExtendedBugReportMode.enabledWithOptionalFields: 'Optional Fields', + ExtendedBugReportMode.enabledWithRequiredFields: 'Required Fields', + }; + String get selectedExtendedMode => _selectedExtendedMode; + set selectedExtendedMode(String value) { + _selectedExtendedMode = value; + notifyListeners(); + } + + Map get extendedMode => _extendedMode; + + var _selectedVideoRecordingPosition = 'Bottom Right'; + final _videoRecordingPosition = { + Position.topLeft: 'Top Left', + Position.topRight: 'Top Right', + Position.bottomLeft: 'Bottom Left', + Position.bottomRight: 'Bottom Right', + }; + String get selectedVideoRecordingPosition => _selectedVideoRecordingPosition; + set selectedVideoRecordingPosition(String value) { + _selectedVideoRecordingPosition = value; + notifyListeners(); + } + + Map get videoRecordingPosition => _videoRecordingPosition; + + var _selectedFloatingButtonEdge = 'Right'; + var _selectedFloatingButtonOffset = 100; + final _floatingButtonEdge = { + FloatingButtonEdge.right: 'Right', + FloatingButtonEdge.left: 'Left', + }; + String get selectedFloatingButtonEdge => _selectedFloatingButtonEdge; + set selectedFloatingButtonEdge(String value) { + _selectedFloatingButtonEdge = value; + notifyListeners(); + } + + Map get floatingButtonEdge => _floatingButtonEdge; + + int get selectedFloatingButtonOffset => _selectedFloatingButtonOffset; + set selectedFloatingButtonOffset(int value) { + _selectedFloatingButtonOffset = value; + notifyListeners(); + } +} diff --git a/example/lib/providers/core_state.dart b/example/lib/providers/core_state.dart new file mode 100644 index 000000000..e2abeaf5e --- /dev/null +++ b/example/lib/providers/core_state.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class CoreState extends ChangeNotifier { + bool _isDisabled = false; + + bool get isDisabled => _isDisabled; + set isDisabled(bool value) { + _isDisabled = value; + notifyListeners(); + } +} diff --git a/example/lib/providers/settings_state.dart b/example/lib/providers/settings_state.dart new file mode 100644 index 000000000..b873ef744 --- /dev/null +++ b/example/lib/providers/settings_state.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class SettingsState extends ChangeNotifier { + final Map _colors = { + 'Default': const Color(0xFF1D82DC), + 'Red': Colors.red, + 'Green': Colors.green, + 'Blue': Colors.blue, + 'Yellow': Colors.yellow, + 'Orange': Colors.orange, + 'Purple': Colors.purple, + 'Pink': Colors.pink, + }; + + String _selectedColorName = 'Default'; + + Map get colors => _colors; + + Color get selectedColor => _colors[_selectedColorName]!; + + String get selectedColorName => _selectedColorName; + void selectColor(String colorName) { + _selectedColorName = colorName; + notifyListeners(); + } +} diff --git a/example/lib/providers/theme_state.dart b/example/lib/providers/theme_state.dart new file mode 100644 index 000000000..6149fca0d --- /dev/null +++ b/example/lib/providers/theme_state.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +import '../models/app_theme.dart'; + +class ThemeState with ChangeNotifier { + bool _isDarkTheme = false; + ThemeData _themeData = AppTheme.lightTheme; + + bool get isDarkTheme => _isDarkTheme; + + ThemeData getThemeData() => _themeData; + void setThemeData(bool isDarkMode) { + _isDarkTheme = !_isDarkTheme; + _themeData = isDarkMode ? AppTheme.darkTheme : AppTheme.lightTheme; + notifyListeners(); + } +} From f28b02f09002f50f9e5156a723816c43a4e3854a Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:20:43 +0200 Subject: [PATCH 09/34] Add Main screen --- example/lib/screens/main_screen.dart | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 example/lib/screens/main_screen.dart diff --git a/example/lib/screens/main_screen.dart b/example/lib/screens/main_screen.dart new file mode 100644 index 000000000..267481a5d --- /dev/null +++ b/example/lib/screens/main_screen.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +import './products_screen.dart'; +import './settings_screen.dart'; +import '../models/app_flow.dart'; + +class MainScreen extends StatefulWidget { + const MainScreen({super.key}); + + @override + _MainScreenState createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + int selectedPageIndex = 0; + + static const flows = [ + AppFlow( + title: 'Home', + page: ProductsScreen(), + icon: Icons.home, + ), + AppFlow( + title: 'Settings', + page: SettingsScreen(), + icon: Icons.settings, + ), + ]; + + void _selectPage(int index) { + setState(() { + selectedPageIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(flows[selectedPageIndex].title), + ), + body: IndexedStack( + index: selectedPageIndex, + children: flows.map((flow) => flow.page).toList(), + ), + bottomNavigationBar: BottomNavigationBar( + onTap: _selectPage, + currentIndex: selectedPageIndex, + items: flows + .map( + (flow) => BottomNavigationBarItem( + icon: Icon(flow.icon), + label: flow.title, + ), + ) + .toList(), + ), + ); + } +} From 1f2221b1f333774d2c6bb6aff5ccc2355b06633c Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:20:56 +0200 Subject: [PATCH 10/34] Add Settings screen --- example/lib/screens/settings_screen.dart | 98 ++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 example/lib/screens/settings_screen.dart diff --git a/example/lib/screens/settings_screen.dart b/example/lib/screens/settings_screen.dart new file mode 100644 index 000000000..82f8f1bad --- /dev/null +++ b/example/lib/screens/settings_screen.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../providers/settings_state.dart'; +import '../providers/theme_state.dart'; +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({super.key}); + + @override + _SettingsScreenState createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + @override + Widget build(BuildContext context) { + final themeState = Provider.of(context); + final settingsState = Provider.of(context); + return Scaffold( + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: Icon( + themeState.isDarkTheme + ? Icons.dark_mode + : Icons.dark_mode_outlined, + ), + title: const Text('Dark Theme'), + trailing: Switch( + value: themeState.isDarkTheme, + onChanged: (value) { + themeState.setThemeData(value); + Instabug.setColorTheme( + themeState.isDarkTheme + ? ColorTheme.dark + : ColorTheme.light, + ); + }, + ), + ), + FeatureTile( + leading: const Icon(Icons.palette), + title: const Text('Primary Color'), + bottom: Wrap( + spacing: 4.0, + children: settingsState.colors.keys.map((colorName) { + final color = settingsState.colors[colorName]!; + final isSelected = + colorName == settingsState.selectedColorName; + return ChoiceChip( + label: Text(colorName), + labelStyle: TextStyle( + color: isSelected ? Colors.white : Colors.grey[700], + ), + selected: isSelected, + selectedColor: color, + onSelected: (selected) { + if (selected) { + settingsState.selectColor(colorName); + Instabug.setPrimaryColor(color); + } + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + side: isSelected + ? BorderSide.none + : BorderSide( + color: color, + width: 2, + ), + ), + ); + }).toList(), + ), + ), + ], + ), + ], + ), + ), + ); + } +} From 3a129ccb4364183bc7960728c11e3ef473926dda Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:25:31 +0200 Subject: [PATCH 11/34] Add Products screen --- example/lib/screens/products_screen.dart | 89 ++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 example/lib/screens/products_screen.dart diff --git a/example/lib/screens/products_screen.dart b/example/lib/screens/products_screen.dart new file mode 100644 index 000000000..91db6c23e --- /dev/null +++ b/example/lib/screens/products_screen.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; + +import '../models/product_item.dart'; +import '../screens/apm_screen.dart'; +import '../screens/bug_reporting_screen.dart'; +import '../screens/core_screen.dart'; +import '../screens/crash_reporting_screen.dart'; +import '../screens/feature_requests_screen.dart'; +import '../screens/network_logger_screen.dart'; +import '../screens/replies_screen.dart'; +import '../screens/surveys_screen.dart'; +import '../widgets/product_card.dart'; + +class ProductsScreen extends StatelessWidget { + static const products = [ + ProductItem( + title: 'Bug Reporting', + imageUrl: 'assets/images/Bug Reporting.png', + color: Color(0x3000287A), + screen: BugReportingScreen(), + ), + ProductItem( + title: 'Crash Reporting', + imageUrl: 'assets/images/Crash Reporting.png', + color: Color(0x30E91002), + screen: CrashReportingScreen(), + ), + ProductItem( + title: 'APM', + imageUrl: 'assets/images/APM.png', + color: Color(0x30008037), + screen: APMScreen(), + ), + ProductItem( + title: 'Replies', + imageUrl: 'assets/images/Replies.png', + color: Color(0x309D03A0), + screen: RepliesScreen(), + ), + ProductItem( + title: 'Surveys', + imageUrl: 'assets/images/Surveys.png', + color: Color(0x30FF1887), + screen: SurveysScreen(), + ), + ProductItem( + title: 'Feature Requests', + imageUrl: 'assets/images/Feature Requests.png', + color: Color(0x30FFA721), + screen: FeatureRequestsScreen(), + ), + ProductItem( + title: 'Core', + imageUrl: 'assets/images/Core.png', + color: Color(0x305DABF0), + screen: CoreScreen(), + ), + ProductItem( + title: 'Network Logger', + imageUrl: 'assets/images/Network.png', + color: Color(0x30F4CE04), + screen: NetworkLoggerScreen(), + ), + ]; + + const ProductsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: GridView.count( + physics: const ClampingScrollPhysics(), + crossAxisCount: 2, + padding: const EdgeInsets.all(20.0), + crossAxisSpacing: 10, + mainAxisSpacing: 10, + children: products + .map( + (product) => ProductCard( + title: product.title, + imageUrl: product.imageUrl, + color: product.color, + ), + ) + .toList(), + ), + ); + } +} From 1362b11e1672eeb0307218cc43cd0e049910207c Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:23:09 +0200 Subject: [PATCH 12/34] Add Bug Reporting screen --- example/lib/screens/bug_reporting_screen.dart | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 example/lib/screens/bug_reporting_screen.dart diff --git a/example/lib/screens/bug_reporting_screen.dart b/example/lib/screens/bug_reporting_screen.dart new file mode 100644 index 000000000..74a067e3c --- /dev/null +++ b/example/lib/screens/bug_reporting_screen.dart @@ -0,0 +1,338 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../providers/bug_reporting_state.dart'; +import '../widgets/chip_picker.dart'; +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class BugReportingScreen extends StatefulWidget { + const BugReportingScreen({super.key}); + + @override + _BugReportingScreenState createState() => _BugReportingScreenState(); +} + +class _BugReportingScreenState extends State { + final characterCountController = TextEditingController(); + final disclaimerTextController = TextEditingController(); + final floatingButtonOffsetController = TextEditingController(); + + @override + void dispose() { + characterCountController.dispose(); + disclaimerTextController.dispose(); + floatingButtonOffsetController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final state = Provider.of(context); + floatingButtonOffsetController.text = + state.selectedFloatingButtonOffset.toString(); + return GestureDetector( + onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); + }, + child: Scaffold( + appBar: AppBar( + title: const Text( + 'Bug Reporting', + ), + ), + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: const Icon(Icons.bug_report), + title: const Text('Report a bug'), + onTap: () => BugReporting.show( + ReportType.bug, + state.selectedInvocationOptions.toList(), + ), + ), + FeatureTile( + leading: const Icon(Icons.feedback), + title: const Text('Suggest an improvement'), + onTap: () => BugReporting.show( + ReportType.feedback, + state.selectedInvocationOptions.toList(), + ), + ), + FeatureTile( + leading: const Icon(Icons.question_mark), + title: const Text('Ask a question'), + onTap: () => BugReporting.show( + ReportType.question, + state.selectedInvocationOptions.toList(), + ), + ), + ], + ), + SectionCard( + children: [ + FeatureTile( + leading: const Icon(Icons.touch_app), + title: const Text('Invocation Events'), + bottom: ChipPicker( + items: state.invocationEvents.keys.toSet(), + values: state.selectedInvocationEvents, + labelBuilder: (value) => state.invocationEvents[value]!, + onChanged: (values) { + state.selectedInvocationEvents = values; + BugReporting.setInvocationEvents( + state.selectedInvocationEvents.toList(), + ); + }, + ), + ), + FeatureTile( + leading: const Icon(Icons.attachment), + title: const Text('Attachments'), + bottom: Wrap( + spacing: 4.0, + children: state.extraAttachments.keys + .toList() + .map((String attachment) { + return FilterChip( + label: Text(attachment), + selected: state.extraAttachments[attachment]!, + onSelected: (bool value) { + setState(() { + state.extraAttachments[attachment] = value; + BugReporting.setEnabledAttachmentTypes( + state.extraAttachments['Screenshot']!, + state.extraAttachments['Extra Screenshot']!, + state.extraAttachments['Gallery Image']!, + state.extraAttachments['Screen Recording']!, + ); + }); + }, + ); + }).toList(), + ), + ), + FeatureTile( + leading: const Icon(Icons.dynamic_form), + title: const Text('Invocation Options'), + bottom: ChipPicker( + items: state.invocationOptions.keys.toSet(), + values: state.selectedInvocationOptions, + labelBuilder: (value) => state.invocationOptions[value]!, + onChanged: (values) { + state.selectedInvocationOptions = values; + BugReporting.setInvocationOptions( + state.selectedInvocationOptions.toList(), + ); + }, + ), + ), + Tooltip( + message: + 'Minimum number of character required for the comments field', + child: FeatureTile( + leftFlex: 2, + leading: const Icon(Icons.comment), + title: const Text('Comment Minimum'), + right: Focus( + onFocusChange: (hasFocus) { + if (!hasFocus && + characterCountController.text.isNotEmpty) { + BugReporting.setCommentMinimumCharacterCount( + int.parse(characterCountController.text), + ); + } + }, + child: TextField( + controller: characterCountController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(vertical: 8.0), + border: OutlineInputBorder(), + hintText: 'Characters', + ), + enableInteractiveSelection: true, + ), + ), + ), + ), + FeatureTile( + leftFlex: 5, + leading: const Icon(Icons.keyboard_control), + title: const Text('Extended Report'), + rightFlex: 4, + right: DropdownMenu( + width: 150, + initialSelection: state.extendedMode.keys.firstWhere( + (element) => + state.extendedMode[element] == + state.selectedExtendedMode, + ), + label: const Text('Mode'), + dropdownMenuEntries: state.extendedMode.keys + .map>( + (ExtendedBugReportMode mode) { + return DropdownMenuEntry( + value: mode, + label: state.extendedMode[mode]!, + ); + }).toList(), + onSelected: (ExtendedBugReportMode? mode) { + state.selectedExtendedMode = state.extendedMode[mode]!; + BugReporting.setExtendedBugReportMode(mode!); + }, + ), + ), + FeatureTile( + leading: const Icon(Icons.info_outline), + title: const Text('Disclaimer Text'), + bottom: TextField( + controller: disclaimerTextController, + keyboardType: TextInputType.multiline, + textAlign: TextAlign.left, + textAlignVertical: TextAlignVertical.top, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 8.0, + ), + border: OutlineInputBorder(), + ), + onSubmitted: (String value) => + BugReporting.setDisclaimerText( + disclaimerTextController.text, + ), + enableInteractiveSelection: true, + ), + ), + ], + ), + SectionCard( + children: [ + FeatureTile( + leftFlex: 5, + leading: const Icon(Icons.videocam), + title: const Text('Recording Button'), + rightFlex: 4, + right: DropdownMenu( + width: 150, + initialSelection: + state.videoRecordingPosition.keys.firstWhere( + (element) => + state.videoRecordingPosition[element] == + state.selectedVideoRecordingPosition, + ), + label: const Text('Position'), + dropdownMenuEntries: state.videoRecordingPosition.keys + .map>( + (Position position) { + return DropdownMenuEntry( + value: position, + label: state.videoRecordingPosition[position]!, + ); + }).toList(), + onSelected: (Position? position) { + state.selectedVideoRecordingPosition = + state.videoRecordingPosition[position]!; + BugReporting.setVideoRecordingFloatingButtonPosition( + position!, + ); + }, + ), + ), + FeatureTile( + leftFlex: 5, + leading: const Icon(Icons.message), + title: const Text('Floating Button'), + rightFlex: 4, + right: DropdownMenu( + width: 150, + initialSelection: + state.floatingButtonEdge.keys.firstWhere( + (element) => + state.floatingButtonEdge[element] == + state.selectedFloatingButtonEdge, + ), + label: const Text('Edge'), + dropdownMenuEntries: state.floatingButtonEdge.keys + .map>( + (FloatingButtonEdge edge) { + return DropdownMenuEntry( + value: edge, + label: state.floatingButtonEdge[edge]!, + ); + }).toList(), + onSelected: (FloatingButtonEdge? edge) { + state.selectedFloatingButtonEdge = + state.floatingButtonEdge[edge]!; + BugReporting.setFloatingButtonEdge( + edge!, + state.selectedFloatingButtonOffset, + ); + }, + ), + ), + Tooltip( + message: 'Offset from top', + child: FeatureTile( + leftFlex: 2, + leading: const Icon(Icons.border_top), + title: const Text('Floating Button Offset'), + right: Focus( + onFocusChange: (hasFocus) { + if (!hasFocus && + floatingButtonOffsetController.text.isNotEmpty) { + state.selectedFloatingButtonOffset = int.parse( + floatingButtonOffsetController.text, + ); + } else if (!hasFocus && + floatingButtonOffsetController.text.isEmpty) { + state.selectedFloatingButtonOffset = 100; + } + BugReporting.setFloatingButtonEdge( + state.floatingButtonEdge.keys.firstWhere( + (element) => + state.floatingButtonEdge[element] == + state.selectedFloatingButtonEdge, + ), + state.selectedFloatingButtonOffset, + ); + }, + child: TextField( + controller: floatingButtonOffsetController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(vertical: 8.0), + border: OutlineInputBorder(), + ), + enableInteractiveSelection: true, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} From d9a9ab81a2b6d850e4b29935fa1dd0cd8f6ff9c8 Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:23:24 +0200 Subject: [PATCH 13/34] Add Crash Reporting screen --- .../lib/screens/crash_reporting_screen.dart | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 example/lib/screens/crash_reporting_screen.dart diff --git a/example/lib/screens/crash_reporting_screen.dart b/example/lib/screens/crash_reporting_screen.dart new file mode 100644 index 000000000..fe4623998 --- /dev/null +++ b/example/lib/screens/crash_reporting_screen.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class CrashReportingScreen extends StatefulWidget { + const CrashReportingScreen({super.key}); + + @override + State createState() => _CrashReportingScreenState(); +} + +class _CrashReportingScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Crash Reporting', + ), + ), + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: const Icon(Icons.warning_amber_outlined), + title: const Text('Handled Crash'), + onTap: () { + try { + throw Exception( + 'This is a handled crash from Instabug Example App', + ); + } catch (e, st) { + CrashReporting.reportHandledCrash(e, st); + const snackBar = SnackBar( + content: Text( + 'A handled crash has been successfully reported!', + ), + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + }, + ), + FeatureTile( + leading: const Icon(Icons.warning), + title: const Text('Unhandled Crash'), + onTap: () { + try { + final arr = [1, 2]; + arr[2]; + } finally { + const snackBarText = kDebugMode + ? 'Unhandled Crashes will only be reported in release mode and not in debug mode.' + : 'An unhandled crash has been successfully reported!'; + const snackBar = SnackBar( + content: Text(snackBarText), + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + }, + ), + ], + ), + ], + ), + ), + ); + } +} From 5148dfbd37b38976eafe72c8a4c7b853214221ac Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:23:33 +0200 Subject: [PATCH 14/34] Add APM screen --- example/lib/screens/apm_screen.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 example/lib/screens/apm_screen.dart diff --git a/example/lib/screens/apm_screen.dart b/example/lib/screens/apm_screen.dart new file mode 100644 index 000000000..f6a8f43f2 --- /dev/null +++ b/example/lib/screens/apm_screen.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class APMScreen extends StatelessWidget { + const APMScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'APM', + ), + ), + ); + } +} From e4bea0affce374a4efede716d6532734f9852104 Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:23:49 +0200 Subject: [PATCH 15/34] Add Replies screen --- example/lib/screens/replies_screen.dart | 90 +++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 example/lib/screens/replies_screen.dart diff --git a/example/lib/screens/replies_screen.dart b/example/lib/screens/replies_screen.dart new file mode 100644 index 000000000..d7fe38aba --- /dev/null +++ b/example/lib/screens/replies_screen.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class RepliesScreen extends StatefulWidget { + const RepliesScreen({super.key}); + + @override + State createState() => _RepliesScreenState(); +} + +class _RepliesScreenState extends State { + var hasChats = false; + var count = 0; + + @override + void initState() { + super.initState(); + callRepliesAsyncAPIs(); + } + + Future callRepliesAsyncAPIs() async { + hasChats = await Replies.hasChats(); + count = await Replies.getUnreadRepliesCount(); + setState(() { + hasChats = hasChats; + count = count; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Replies', + ), + ), + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: const Icon(Icons.chat_bubble), + title: const Text('Show'), + onTap: () => Replies.show(), + ), + FeatureTile( + leading: const Icon(Icons.chat), + title: const Text('Has Chats'), + trailing: hasChats + ? const Icon( + Icons.check, + color: Colors.green, + ) + : const Icon( + Icons.close, + color: Colors.red, + ), + ), + FeatureTile( + leading: const Icon(Icons.mark_chat_unread), + title: const Text('Unread Replies Count'), + trailing: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + count.toString(), + ), + ), + ) + ], + ) + ], + ), + ), + ); + } +} From 58b6b1a43dec78725cd8ca016c06f24dc66a132f Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:24:18 +0200 Subject: [PATCH 16/34] Add Surveys screen --- example/lib/screens/surveys_screen.dart | 79 +++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 example/lib/screens/surveys_screen.dart diff --git a/example/lib/screens/surveys_screen.dart b/example/lib/screens/surveys_screen.dart new file mode 100644 index 000000000..3f8fe6de6 --- /dev/null +++ b/example/lib/screens/surveys_screen.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class SurveysScreen extends StatefulWidget { + const SurveysScreen({super.key}); + + @override + State createState() => _SurveysScreenState(); +} + +class _SurveysScreenState extends State { + final surveyTokenController = TextEditingController(); + + @override + void dispose() { + surveyTokenController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Surveys', + ), + ), + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: const Icon(Icons.rate_review), + title: const Text('Show if available'), + onTap: () { + Surveys.showSurveyIfAvailable(); + }, + ), + FeatureTile( + leading: const Icon(Icons.generating_tokens), + title: const Text('Manual Survey'), + bottom: TextField( + controller: surveyTokenController, + keyboardType: TextInputType.multiline, + textAlign: TextAlign.left, + textAlignVertical: TextAlignVertical.top, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 8.0, + ), + border: OutlineInputBorder(), + ), + onSubmitted: (String value) => + Surveys.showSurvey(surveyTokenController.text), + enableInteractiveSelection: true, + ), + ), + ], + ), + ], + ), + ), + ); + } +} From f0bcca31457f7da611f8e8cd86ff521b49eca30c Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:24:48 +0200 Subject: [PATCH 17/34] Add Feature Requests screen --- .../lib/screens/feature_requests_screen.dart | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 example/lib/screens/feature_requests_screen.dart diff --git a/example/lib/screens/feature_requests_screen.dart b/example/lib/screens/feature_requests_screen.dart new file mode 100644 index 000000000..38dc7e948 --- /dev/null +++ b/example/lib/screens/feature_requests_screen.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class FeatureRequestsScreen extends StatelessWidget { + const FeatureRequestsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Feature Requests', + ), + ), + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: const Icon(Icons.lightbulb), + title: const Text('Show Feature Requests'), + onTap: () { + FeatureRequests.show(); + }, + ), + ], + ), + ], + ), + ), + ); + } +} From acc909fdab0978e0901bf6c5fc2d7dfe1c804c93 Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:25:04 +0200 Subject: [PATCH 18/34] Add Core screen --- example/lib/screens/core_screen.dart | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 example/lib/screens/core_screen.dart diff --git a/example/lib/screens/core_screen.dart b/example/lib/screens/core_screen.dart new file mode 100644 index 000000000..cd8e00bb5 --- /dev/null +++ b/example/lib/screens/core_screen.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../providers/core_state.dart'; +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class CoreScreen extends StatefulWidget { + const CoreScreen({super.key}); + + @override + State createState() => _CoreScreenState(); +} + +class _CoreScreenState extends State { + @override + Widget build(BuildContext context) { + final state = Provider.of(context); + return Scaffold( + appBar: AppBar( + title: const Text( + 'Core', + ), + ), + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: const Icon( + Icons.disabled_by_default_outlined, + ), + title: const Text('Disable SDK'), + trailing: Switch( + value: state.isDisabled, + onChanged: (value) { + Instabug.setEnabled(!value); + state.isDisabled = value; + }, + ), + ), + ], + ), + ], + ), + ), + ); + } +} From 7bb80d84831a7be19344c4d4a892c8b3d8797aff Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:25:16 +0200 Subject: [PATCH 19/34] Add Network Logger screen --- example/lib/screens/network_logger_screen.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 example/lib/screens/network_logger_screen.dart diff --git a/example/lib/screens/network_logger_screen.dart b/example/lib/screens/network_logger_screen.dart new file mode 100644 index 000000000..667d75c65 --- /dev/null +++ b/example/lib/screens/network_logger_screen.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class NetworkLoggerScreen extends StatelessWidget { + const NetworkLoggerScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Network Logger', + ), + ), + ); + } +} From 5d9fcc6e57933c11c4b8bfa3c7e2470205dc0074 Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:29:57 +0200 Subject: [PATCH 20/34] Update `main.dart` --- example/lib/main.dart | 381 ++++-------------------------------------- 1 file changed, 33 insertions(+), 348 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index b42390e9d..d864c65b6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,13 +1,21 @@ import 'dart:async'; - import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + import 'package:instabug_flutter/instabug_flutter.dart'; -void main() { +import '../providers/bug_reporting_state.dart'; +import '../providers/core_state.dart'; +import '../providers/settings_state.dart'; +import '../providers/theme_state.dart'; +import '../screens/main_screen.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); Instabug.init( - token: 'ed6f659591566da19b67857e1b9d40ab', + token: '0174a800719ebdebf7b248fa6ae2ef17', invocationEvents: [InvocationEvent.floatingButton], debugLogsLevel: LogLevel.verbose, ); @@ -18,361 +26,38 @@ void main() { Zone.current.handleUncaughtError(details.exception, details.stack!); }; - runZonedGuarded(() => runApp(MyApp()), CrashReporting.reportCrash); + runZonedGuarded(() => runApp(const MyApp()), CrashReporting.reportCrash); } class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - navigatorObservers: [ - InstabugNavigatorObserver(), - ], - theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class InstabugButton extends StatelessWidget { - String text; - void Function()? onPressed; - - InstabugButton({required this.text, this.onPressed}); + const MyApp({super.key}); @override Widget build(BuildContext context) { - return Container( - width: double.infinity, - margin: const EdgeInsets.only(left: 20.0, right: 20.0), - child: ElevatedButton( - onPressed: onPressed, - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.lightBlue), - foregroundColor: MaterialStateProperty.all(Colors.white), + return MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (ctx) => BugReportingState(), ), - child: Text(text), - ), - ); - } -} - -class InstabugTextField extends StatelessWidget { - String label; - TextEditingController controller; - - InstabugTextField({required this.label, required this.controller}); - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - margin: const EdgeInsets.only(left: 20.0, right: 20.0), - child: TextField( - controller: controller, - decoration: InputDecoration( - labelText: label, + ChangeNotifierProvider( + create: (_) => CoreState(), ), - ), - ); - } -} - -class SectionTitle extends StatelessWidget { - String text; - - SectionTitle(this.text); - - @override - Widget build(BuildContext context) { - return Container( - alignment: Alignment.centerLeft, - margin: const EdgeInsets.only(top: 20.0, left: 20.0), - child: Text( - text, - textAlign: TextAlign.left, - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key, required this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - final buttonStyle = ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.lightBlue), - foregroundColor: MaterialStateProperty.all(Colors.white), - ); - - List reportTypes = []; - - final primaryColorController = TextEditingController(); - final screenNameController = TextEditingController(); - - void restartInstabug() { - Instabug.setEnabled(false); - Instabug.setEnabled(true); - BugReporting.setInvocationEvents([InvocationEvent.floatingButton]); - } - - void setOnDismissCallback() { - BugReporting.setOnDismissCallback((dismissType, reportType) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('On Dismiss'), - content: Text( - 'onDismiss callback called with $dismissType and $reportType', - ), + ChangeNotifierProvider( + create: (_) => SettingsState(), + ), + ChangeNotifierProvider( + create: (_) => ThemeState(), + ), + ], + child: Consumer( + builder: (context, themeState, child) { + return MaterialApp( + title: 'Instabug Flutter Example', + theme: themeState.getThemeData(), + home: const MainScreen(), ); }, - ); - }); - } - - void show() { - Instabug.show(); - } - - void reportScreenChange() { - Instabug.reportScreenChange(screenNameController.text); - } - - void sendBugReport() { - BugReporting.show(ReportType.bug, [InvocationOption.emailFieldOptional]); - } - - void sendFeedback() { - BugReporting.show( - ReportType.feedback, [InvocationOption.emailFieldOptional]); - } - - void askQuestion() { - BugReporting.show( - ReportType.question, [InvocationOption.emailFieldOptional]); - } - - void showNpsSurvey() { - Surveys.showSurvey('pcV_mE2ttqHxT1iqvBxL0w'); - } - - void showManualSurvey() { - Surveys.showSurvey('PMqUZXqarkOR2yGKiENB4w'); - } - - void showFeatureRequests() { - FeatureRequests.show(); - } - - void toggleReportType(ReportType reportType) { - if (reportTypes.contains(reportType)) { - reportTypes.remove(reportType); - } else { - reportTypes.add(reportType); - } - BugReporting.setReportTypes(reportTypes); - } - - void changeFloatingButtonEdge() { - BugReporting.setFloatingButtonEdge(FloatingButtonEdge.left, 200); - } - - void setInvocationEvent(InvocationEvent invocationEvent) { - BugReporting.setInvocationEvents([invocationEvent]); - } - - void changePrimaryColor() { - String text = 'FF' + primaryColorController.text.replaceAll('#', ''); - Color color = Color(int.parse(text, radix: 16)); - Instabug.setPrimaryColor(color); - } - - void setColorTheme(ColorTheme colorTheme) { - Instabug.setColorTheme(colorTheme); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(widget.title)), - body: SingleChildScrollView( - physics: ClampingScrollPhysics(), - padding: const EdgeInsets.only(top: 20.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - margin: const EdgeInsets.only( - left: 20.0, right: 20.0, bottom: 20.0), - child: const Text( - 'Hello Instabug\'s awesome user! The purpose of this application is to show you the different options for customizing the SDK and how easy it is to integrate it to your existing app', - textAlign: TextAlign.center, - ), - ), - InstabugButton( - onPressed: restartInstabug, - text: 'Restart Instabug', - ), - SectionTitle('Primary Color'), - InstabugTextField( - controller: primaryColorController, - label: 'Enter primary color', - ), - InstabugButton( - text: 'Change Primary Color', - onPressed: changePrimaryColor, - ), - SectionTitle('Change Invocation Event'), - ButtonBar( - mainAxisSize: MainAxisSize.min, - alignment: MainAxisAlignment.start, - children: [ - ElevatedButton( - onPressed: () => setInvocationEvent(InvocationEvent.none), - style: buttonStyle, - child: const Text('None'), - ), - ElevatedButton( - onPressed: () => setInvocationEvent(InvocationEvent.shake), - style: buttonStyle, - child: const Text('Shake'), - ), - ElevatedButton( - onPressed: () => - setInvocationEvent(InvocationEvent.screenshot), - style: buttonStyle, - child: const Text('Screenshot'), - ), - ], - ), - ButtonBar( - mainAxisSize: MainAxisSize.min, - alignment: MainAxisAlignment.start, - children: [ - ElevatedButton( - onPressed: () => - setInvocationEvent(InvocationEvent.floatingButton), - style: buttonStyle, - child: const Text('Floating Button'), - ), - ElevatedButton( - onPressed: () => - setInvocationEvent(InvocationEvent.twoFingersSwipeLeft), - style: buttonStyle, - child: const Text('Two Fingers Swipe Left'), - ), - ], - ), - InstabugButton( - onPressed: show, - text: 'Invoke', - ), - InstabugButton( - onPressed: setOnDismissCallback, - text: 'Set On Dismiss Callback', - ), - SectionTitle('Repro Steps'), - InstabugTextField( - controller: screenNameController, - label: 'Enter screen name', - ), - InstabugButton( - text: 'Report Screen Change', - onPressed: reportScreenChange, - ), - InstabugButton( - onPressed: sendBugReport, - text: 'Send Bug Report', - ), - InstabugButton( - onPressed: showManualSurvey, - text: 'Show Manual Survey', - ), - SectionTitle('Change Report Types'), - ButtonBar( - mainAxisSize: MainAxisSize.min, - alignment: MainAxisAlignment.start, - children: [ - ElevatedButton( - onPressed: () => toggleReportType(ReportType.bug), - style: buttonStyle, - child: const Text('Bug'), - ), - ElevatedButton( - onPressed: () => toggleReportType(ReportType.feedback), - style: buttonStyle, - child: const Text('Feedback'), - ), - ElevatedButton( - onPressed: () => toggleReportType(ReportType.question), - style: buttonStyle, - child: const Text('Question'), - ), - ], - ), - InstabugButton( - onPressed: changeFloatingButtonEdge, - text: 'Move Floating Button to Left', - ), - InstabugButton( - onPressed: sendFeedback, - text: 'Send Feedback', - ), - InstabugButton( - onPressed: askQuestion, - text: 'Ask a Question', - ), - InstabugButton( - onPressed: showNpsSurvey, - text: 'Show NPS Survey', - ), - InstabugButton( - onPressed: showManualSurvey, - text: 'Show Multiple Questions Survey', - ), - InstabugButton( - onPressed: showFeatureRequests, - text: 'Show Feature Requests', - ), - SectionTitle('Color Theme'), - ButtonBar( - mainAxisSize: MainAxisSize.max, - alignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () => setColorTheme(ColorTheme.light), - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.white), - foregroundColor: - MaterialStateProperty.all(Colors.lightBlue), - ), - child: const Text('Light'), - ), - ElevatedButton( - onPressed: () => setColorTheme(ColorTheme.dark), - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.black), - foregroundColor: MaterialStateProperty.all(Colors.white), - ), - child: const Text('Dark'), - ), - ], - ), - ], - )), // This trailing comma makes auto-formatting nicer for build methods. + ), ); } } From bc8c300d25112ab8a193002b440cb1241e2f568c Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:32:53 +0200 Subject: [PATCH 21/34] Update `Info.plist` --- example/ios/Runner/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 808130cd2..4e0eac976 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -47,5 +47,7 @@ Instabug needs access to your photo library so you can attach images. CADisableMinimumFrameDurationOnPhone + UIApplicationSupportsIndirectInputEvents + From e89e8f7172b9ce36bbe57a7848c0a166af1e7397 Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:33:04 +0200 Subject: [PATCH 22/34] Sync iOS project --- example/ios/Runner.xcodeproj/project.pbxproj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 432b32d6f..097d7cee1 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -444,6 +444,7 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -475,6 +476,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); From 5f9a7610746cc5383d92d53c75a79cb95268fe11 Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 7 Mar 2023 17:38:51 +0200 Subject: [PATCH 23/34] Fix format --- example/lib/main.dart | 1 - example/lib/screens/main_screen.dart | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index d864c65b6..1fd853fd1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -11,7 +11,6 @@ import '../providers/theme_state.dart'; import '../screens/main_screen.dart'; void main() async { - WidgetsFlutterBinding.ensureInitialized(); Instabug.init( diff --git a/example/lib/screens/main_screen.dart b/example/lib/screens/main_screen.dart index 267481a5d..4330223eb 100644 --- a/example/lib/screens/main_screen.dart +++ b/example/lib/screens/main_screen.dart @@ -6,7 +6,7 @@ import '../models/app_flow.dart'; class MainScreen extends StatefulWidget { const MainScreen({super.key}); - + @override _MainScreenState createState() => _MainScreenState(); } From d07acd1d7aa48e047f91f3b62c2f48221fd7d8d4 Mon Sep 17 00:00:00 2001 From: David Mina Date: Wed, 8 Mar 2023 11:29:22 +0200 Subject: [PATCH 24/34] Exclude `example` from build process --- build.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 build.yaml diff --git a/build.yaml b/build.yaml new file mode 100644 index 000000000..1a4ea87c5 --- /dev/null +++ b/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + # Exclude the 'example' folder from the build + exclude: + - example/** From 4bc9059a3e7df92a3a5292bb4db9e4546836201e Mon Sep 17 00:00:00 2001 From: David Mina Date: Wed, 8 Mar 2023 12:33:26 +0200 Subject: [PATCH 25/34] Update flutter version in setup_flutter job --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b4f66035d..fb49027d0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,7 +9,7 @@ commands: setup_flutter: steps: - flutter/install_sdk_and_pub: - flutter_version: 3.3.0 + flutter_version: 3.7.3 - run: name: Generate Pigeons command: sh ./scripts/pigeon.sh From a6ebdc030af50b3fcd5b29df567329c0f3def070 Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 14 Mar 2023 13:36:31 +0200 Subject: [PATCH 26/34] Fix Flutter IDE issue --- analysis_options.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 9e956095e..ce8381e19 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,7 +2,6 @@ include: package:lint/analysis_options_package.yaml analyzer: exclude: - - "example/**" - "**/*.g.dart" linter: From 3c11119cd68e2b8e64b8e0080aaa96c57f2783b3 Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 14 Mar 2023 13:36:44 +0200 Subject: [PATCH 27/34] Update `gitignore` --- example/.gitignore | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/example/.gitignore b/example/.gitignore index 9d532b18a..24476c5d1 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +migrate_working_dir/ # IntelliJ related *.iml @@ -31,11 +32,13 @@ .pub/ /build/ -# Web related -lib/generated_plugin_registrant.dart - # Symbolication related app.*.symbols # Obfuscation related app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release From 11b13e0fe547bc1f33b72b081845fe8af31f8d5b Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 14 Mar 2023 13:36:59 +0200 Subject: [PATCH 28/34] Sync `.metadata` --- example/.metadata | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/example/.metadata b/example/.metadata index cd984dd00..262ceed06 100644 --- a/example/.metadata +++ b/example/.metadata @@ -1,10 +1,45 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + revision: 135454af32477f815a7525073027a3ff9eff1bfd channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: android + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: ios + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: linux + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: macos + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: web + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: windows + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' From 2a2c231c2f7f87fd99b19cde1654d730b0158565 Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 14 Mar 2023 13:56:22 +0200 Subject: [PATCH 29/34] Fix lint issue --- example/analysis_options.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index b57be5d0a..d00720089 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -3,3 +3,4 @@ include: package:flutter_lints/flutter.yaml linter: rules: require_trailing_commas: true + library_private_types_in_public_api: false From f2b003de277da5a0aea5c9d36da13c0e16224610 Mon Sep 17 00:00:00 2001 From: David Mina Date: Wed, 15 Mar 2023 18:21:55 +0200 Subject: [PATCH 30/34] Apply several enhancements --- example/lib/models/app_theme.dart | 28 ++-- .../lib/providers/bug_reporting_state.dart | 96 +++++------ example/lib/screens/bug_reporting_screen.dart | 151 +++++++----------- example/lib/screens/core_screen.dart | 7 +- .../lib/screens/crash_reporting_screen.dart | 7 +- example/lib/screens/settings_screen.dart | 7 +- example/lib/utils/enum_extensions.dart | 15 ++ example/lib/widgets/chip_picker.dart | 19 +-- 8 files changed, 139 insertions(+), 191 deletions(-) create mode 100644 example/lib/utils/enum_extensions.dart diff --git a/example/lib/models/app_theme.dart b/example/lib/models/app_theme.dart index ef6a52fa2..35c145721 100644 --- a/example/lib/models/app_theme.dart +++ b/example/lib/models/app_theme.dart @@ -1,31 +1,37 @@ import 'package:flutter/material.dart'; +abstract class AppColors { + static const primaryColor = Color(0xFF00287a); + static const secondaryColor = Color(0xFF5DAAF0); + static const primaryColorDark = Color(0xFF212121); +} + class AppTheme { static final lightTheme = ThemeData( colorScheme: ColorScheme.fromSwatch().copyWith( brightness: Brightness.light, - primary: const Color(0xFF00287a), - secondary: const Color(0xFF5DAAF0), + primary: AppColors.primaryColor, + secondary: AppColors.secondaryColor, ), scaffoldBackgroundColor: Colors.grey[100], appBarTheme: const AppBarTheme( - color: Color(0xFF00287a), + color: AppColors.primaryColor, ), visualDensity: VisualDensity.adaptivePlatformDensity, bottomNavigationBarTheme: const BottomNavigationBarThemeData( - backgroundColor: Color(0xFF00287a), + backgroundColor: AppColors.primaryColor, unselectedItemColor: Colors.white, - selectedItemColor: Color(0xFF5DAAF0), + selectedItemColor: AppColors.secondaryColor, ), chipTheme: const ChipThemeData( - selectedColor: Color(0xFF5DAAF0), + selectedColor: AppColors.secondaryColor, ), iconTheme: IconThemeData(color: Colors.grey[600]), textTheme: const TextTheme( headlineMedium: TextStyle( fontFamily: 'Axiforma', fontSize: 16.0, - color: Color(0xFF00287a), + color: AppColors.primaryColor, fontWeight: FontWeight.w600, ), ), @@ -33,17 +39,17 @@ class AppTheme { static final darkTheme = ThemeData.dark().copyWith( colorScheme: ColorScheme.fromSwatch().copyWith( - secondary: const Color(0xFF5DAAF0), + secondary: AppColors.secondaryColor, ), appBarTheme: const AppBarTheme( - color: Color(0xFF212121), + color: AppColors.primaryColorDark, ), visualDensity: VisualDensity.adaptivePlatformDensity, bottomNavigationBarTheme: const BottomNavigationBarThemeData( - backgroundColor: Color(0xFF212121), + backgroundColor: AppColors.primaryColorDark, ), chipTheme: const ChipThemeData( - selectedColor: Color(0xFF5DAAF0), + selectedColor: AppColors.secondaryColor, backgroundColor: Color(0xFFB3B3B3), ), iconTheme: const IconThemeData(color: Colors.white), diff --git a/example/lib/providers/bug_reporting_state.dart b/example/lib/providers/bug_reporting_state.dart index 82f824fe7..41859860b 100644 --- a/example/lib/providers/bug_reporting_state.dart +++ b/example/lib/providers/bug_reporting_state.dart @@ -3,95 +3,77 @@ import 'package:flutter/material.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; class BugReportingState with ChangeNotifier { - final _extraAttachments = { + var _extraAttachments = { 'Screenshot': true, 'Extra Screenshot': true, 'Gallery Image': true, 'Screen Recording': true }; Map get extraAttachments => _extraAttachments; + set extraAttachments(Map attachments) { + _extraAttachments = attachments; + notifyListeners(); + } var _selectedInvocationOptions = {}; - final _invocationOptions = { - InvocationOption.commentFieldRequired: 'Comment Required', - InvocationOption.emailFieldHidden: 'Email Hidden', - InvocationOption.emailFieldOptional: 'Email Optional', - InvocationOption.disablePostSendingDialog: 'Disable Post Sending Dialog', - }; Set get selectedInvocationOptions => _selectedInvocationOptions; - set selectedInvocationOptions(Set value) { - _selectedInvocationOptions = value; + set selectedInvocationOptions(Set options) { + _selectedInvocationOptions = options; notifyListeners(); } - Map get invocationOptions => _invocationOptions; - var _selectedInvocationEvents = { InvocationEvent.floatingButton }; - final _invocationEvents = { - InvocationEvent.floatingButton: 'Floating Button', - InvocationEvent.shake: 'Shake', - InvocationEvent.screenshot: 'Screenshot', - InvocationEvent.twoFingersSwipeLeft: 'Two Finger Swipe Left', - InvocationEvent.none: 'None', - }; Set get selectedInvocationEvents => _selectedInvocationEvents; - set selectedInvocationEvents(Set value) { - _selectedInvocationEvents = value; + set selectedInvocationEvents(Set events) { + _selectedInvocationEvents = events; notifyListeners(); } - Map get invocationEvents => _invocationEvents; - - var _selectedExtendedMode = 'Disabled'; - final _extendedMode = { - ExtendedBugReportMode.disabled: 'Disabled', - ExtendedBugReportMode.enabledWithOptionalFields: 'Optional Fields', - ExtendedBugReportMode.enabledWithRequiredFields: 'Required Fields', - }; - String get selectedExtendedMode => _selectedExtendedMode; - set selectedExtendedMode(String value) { - _selectedExtendedMode = value; + var _selectedExtendedMode = ExtendedBugReportMode.disabled; + ExtendedBugReportMode get selectedExtendedMode => _selectedExtendedMode; + set selectedExtendedMode(ExtendedBugReportMode mode) { + _selectedExtendedMode = mode; notifyListeners(); } - Map get extendedMode => _extendedMode; - - var _selectedVideoRecordingPosition = 'Bottom Right'; - final _videoRecordingPosition = { - Position.topLeft: 'Top Left', - Position.topRight: 'Top Right', - Position.bottomLeft: 'Bottom Left', - Position.bottomRight: 'Bottom Right', - }; - String get selectedVideoRecordingPosition => _selectedVideoRecordingPosition; - set selectedVideoRecordingPosition(String value) { - _selectedVideoRecordingPosition = value; + var _selectedVideoRecordingPosition = Position.bottomRight; + Position get selectedVideoRecordingPosition => + _selectedVideoRecordingPosition; + set selectedVideoRecordingPosition(Position position) { + _selectedVideoRecordingPosition = position; notifyListeners(); } - Map get videoRecordingPosition => _videoRecordingPosition; + var _selectedFloatingButtonEdge = FloatingButtonEdge.right; + FloatingButtonEdge get selectedFloatingButtonEdge => + _selectedFloatingButtonEdge; + set selectedFloatingButtonEdge(FloatingButtonEdge edge) { + _selectedFloatingButtonEdge = edge; + notifyListeners(); + } - var _selectedFloatingButtonEdge = 'Right'; - var _selectedFloatingButtonOffset = 100; - final _floatingButtonEdge = { - FloatingButtonEdge.right: 'Right', - FloatingButtonEdge.left: 'Left', - }; - String get selectedFloatingButtonEdge => _selectedFloatingButtonEdge; - set selectedFloatingButtonEdge(String value) { - _selectedFloatingButtonEdge = value; + var _disclaimerText = ''; + String get disclaimerText => _disclaimerText; + set disclaimerText(String text) { + _disclaimerText = text; notifyListeners(); } - Map get floatingButtonEdge => _floatingButtonEdge; + var _characterCount = ''; + String get characterCount => _characterCount; + set characterCount(String count) { + _characterCount = count; + notifyListeners(); + } - int get selectedFloatingButtonOffset => _selectedFloatingButtonOffset; - set selectedFloatingButtonOffset(int value) { - _selectedFloatingButtonOffset = value; + var _floatingButtonOffset = 100; + int get floatingButtonOffset => _floatingButtonOffset; + set floatingButtonOffset(int offset) { + _floatingButtonOffset = offset; notifyListeners(); } } diff --git a/example/lib/screens/bug_reporting_screen.dart b/example/lib/screens/bug_reporting_screen.dart index 74a067e3c..b4083db01 100644 --- a/example/lib/screens/bug_reporting_screen.dart +++ b/example/lib/screens/bug_reporting_screen.dart @@ -4,36 +4,18 @@ import 'package:provider/provider.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; import '../providers/bug_reporting_state.dart'; +import '../utils/enum_extensions.dart'; import '../widgets/chip_picker.dart'; import '../widgets/feature_tile.dart'; import '../widgets/section_card.dart'; import '../widgets/separated_list_view.dart'; -class BugReportingScreen extends StatefulWidget { +class BugReportingScreen extends StatelessWidget { const BugReportingScreen({super.key}); - @override - _BugReportingScreenState createState() => _BugReportingScreenState(); -} - -class _BugReportingScreenState extends State { - final characterCountController = TextEditingController(); - final disclaimerTextController = TextEditingController(); - final floatingButtonOffsetController = TextEditingController(); - - @override - void dispose() { - characterCountController.dispose(); - disclaimerTextController.dispose(); - floatingButtonOffsetController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { final state = Provider.of(context); - floatingButtonOffsetController.text = - state.selectedFloatingButtonOffset.toString(); return GestureDetector( onTap: () { FocusManager.instance.primaryFocus?.unfocus(); @@ -88,9 +70,9 @@ class _BugReportingScreenState extends State { leading: const Icon(Icons.touch_app), title: const Text('Invocation Events'), bottom: ChipPicker( - items: state.invocationEvents.keys.toSet(), + items: InvocationEvent.values.toSet(), values: state.selectedInvocationEvents, - labelBuilder: (value) => state.invocationEvents[value]!, + labelBuilder: (value) => value.capitalizedName(), onChanged: (values) { state.selectedInvocationEvents = values; BugReporting.setInvocationEvents( @@ -111,15 +93,16 @@ class _BugReportingScreenState extends State { label: Text(attachment), selected: state.extraAttachments[attachment]!, onSelected: (bool value) { - setState(() { - state.extraAttachments[attachment] = value; - BugReporting.setEnabledAttachmentTypes( - state.extraAttachments['Screenshot']!, - state.extraAttachments['Extra Screenshot']!, - state.extraAttachments['Gallery Image']!, - state.extraAttachments['Screen Recording']!, - ); - }); + state.extraAttachments = { + ...state.extraAttachments, + attachment: value + }; + BugReporting.setEnabledAttachmentTypes( + state.extraAttachments['Screenshot']!, + state.extraAttachments['Extra Screenshot']!, + state.extraAttachments['Gallery Image']!, + state.extraAttachments['Screen Recording']!, + ); }, ); }).toList(), @@ -129,9 +112,9 @@ class _BugReportingScreenState extends State { leading: const Icon(Icons.dynamic_form), title: const Text('Invocation Options'), bottom: ChipPicker( - items: state.invocationOptions.keys.toSet(), + items: InvocationOption.values.toSet(), values: state.selectedInvocationOptions, - labelBuilder: (value) => state.invocationOptions[value]!, + labelBuilder: (value) => value.capitalizedName('Field'), onChanged: (values) { state.selectedInvocationOptions = values; BugReporting.setInvocationOptions( @@ -149,15 +132,14 @@ class _BugReportingScreenState extends State { title: const Text('Comment Minimum'), right: Focus( onFocusChange: (hasFocus) { - if (!hasFocus && - characterCountController.text.isNotEmpty) { + if (!hasFocus && state.characterCount.isNotEmpty) { BugReporting.setCommentMinimumCharacterCount( - int.parse(characterCountController.text), + int.parse(state.characterCount), ); } }, - child: TextField( - controller: characterCountController, + child: TextFormField( + initialValue: state.characterCount, keyboardType: TextInputType.number, textAlign: TextAlign.center, textAlignVertical: TextAlignVertical.center, @@ -167,6 +149,7 @@ class _BugReportingScreenState extends State { hintText: 'Characters', ), enableInteractiveSelection: true, + onChanged: (value) => state.characterCount = value, ), ), ), @@ -178,31 +161,27 @@ class _BugReportingScreenState extends State { rightFlex: 4, right: DropdownMenu( width: 150, - initialSelection: state.extendedMode.keys.firstWhere( - (element) => - state.extendedMode[element] == - state.selectedExtendedMode, - ), + initialSelection: state.selectedExtendedMode, label: const Text('Mode'), - dropdownMenuEntries: state.extendedMode.keys + dropdownMenuEntries: ExtendedBugReportMode.values .map>( (ExtendedBugReportMode mode) { return DropdownMenuEntry( value: mode, - label: state.extendedMode[mode]!, + label: mode.capitalizedName('enabledWith'), ); }).toList(), onSelected: (ExtendedBugReportMode? mode) { - state.selectedExtendedMode = state.extendedMode[mode]!; - BugReporting.setExtendedBugReportMode(mode!); + state.selectedExtendedMode = mode!; + BugReporting.setExtendedBugReportMode(mode); }, ), ), FeatureTile( leading: const Icon(Icons.info_outline), title: const Text('Disclaimer Text'), - bottom: TextField( - controller: disclaimerTextController, + bottom: TextFormField( + initialValue: state.disclaimerText, keyboardType: TextInputType.multiline, textAlign: TextAlign.left, textAlignVertical: TextAlignVertical.top, @@ -213,11 +192,13 @@ class _BugReportingScreenState extends State { ), border: OutlineInputBorder(), ), - onSubmitted: (String value) => - BugReporting.setDisclaimerText( - disclaimerTextController.text, - ), enableInteractiveSelection: true, + onFieldSubmitted: (String value) { + state.disclaimerText = value; + BugReporting.setDisclaimerText( + value, + ); + }, ), ), ], @@ -231,26 +212,20 @@ class _BugReportingScreenState extends State { rightFlex: 4, right: DropdownMenu( width: 150, - initialSelection: - state.videoRecordingPosition.keys.firstWhere( - (element) => - state.videoRecordingPosition[element] == - state.selectedVideoRecordingPosition, - ), + initialSelection: state.selectedVideoRecordingPosition, label: const Text('Position'), - dropdownMenuEntries: state.videoRecordingPosition.keys + dropdownMenuEntries: Position.values .map>( (Position position) { return DropdownMenuEntry( value: position, - label: state.videoRecordingPosition[position]!, + label: position.capitalizedName(), ); }).toList(), onSelected: (Position? position) { - state.selectedVideoRecordingPosition = - state.videoRecordingPosition[position]!; + state.selectedVideoRecordingPosition = position!; BugReporting.setVideoRecordingFloatingButtonPosition( - position!, + position, ); }, ), @@ -262,27 +237,21 @@ class _BugReportingScreenState extends State { rightFlex: 4, right: DropdownMenu( width: 150, - initialSelection: - state.floatingButtonEdge.keys.firstWhere( - (element) => - state.floatingButtonEdge[element] == - state.selectedFloatingButtonEdge, - ), + initialSelection: state.selectedFloatingButtonEdge, label: const Text('Edge'), - dropdownMenuEntries: state.floatingButtonEdge.keys + dropdownMenuEntries: FloatingButtonEdge.values .map>( (FloatingButtonEdge edge) { return DropdownMenuEntry( value: edge, - label: state.floatingButtonEdge[edge]!, + label: edge.capitalizedName(), ); }).toList(), onSelected: (FloatingButtonEdge? edge) { - state.selectedFloatingButtonEdge = - state.floatingButtonEdge[edge]!; + state.selectedFloatingButtonEdge = edge!; BugReporting.setFloatingButtonEdge( - edge!, - state.selectedFloatingButtonOffset, + edge, + state.floatingButtonOffset, ); }, ), @@ -295,26 +264,15 @@ class _BugReportingScreenState extends State { title: const Text('Floating Button Offset'), right: Focus( onFocusChange: (hasFocus) { - if (!hasFocus && - floatingButtonOffsetController.text.isNotEmpty) { - state.selectedFloatingButtonOffset = int.parse( - floatingButtonOffsetController.text, + if (!hasFocus) { + BugReporting.setFloatingButtonEdge( + state.selectedFloatingButtonEdge, + state.floatingButtonOffset, ); - } else if (!hasFocus && - floatingButtonOffsetController.text.isEmpty) { - state.selectedFloatingButtonOffset = 100; } - BugReporting.setFloatingButtonEdge( - state.floatingButtonEdge.keys.firstWhere( - (element) => - state.floatingButtonEdge[element] == - state.selectedFloatingButtonEdge, - ), - state.selectedFloatingButtonOffset, - ); }, - child: TextField( - controller: floatingButtonOffsetController, + child: TextFormField( + initialValue: state.floatingButtonOffset.toString(), keyboardType: TextInputType.number, textAlign: TextAlign.center, textAlignVertical: TextAlignVertical.center, @@ -323,6 +281,13 @@ class _BugReportingScreenState extends State { border: OutlineInputBorder(), ), enableInteractiveSelection: true, + onChanged: (value) { + if (value.isEmpty) { + state.floatingButtonOffset = 100; + } else { + state.floatingButtonOffset = int.parse(value); + } + }, ), ), ), diff --git a/example/lib/screens/core_screen.dart b/example/lib/screens/core_screen.dart index cd8e00bb5..56b4b0064 100644 --- a/example/lib/screens/core_screen.dart +++ b/example/lib/screens/core_screen.dart @@ -8,14 +8,9 @@ import '../widgets/feature_tile.dart'; import '../widgets/section_card.dart'; import '../widgets/separated_list_view.dart'; -class CoreScreen extends StatefulWidget { +class CoreScreen extends StatelessWidget { const CoreScreen({super.key}); - @override - State createState() => _CoreScreenState(); -} - -class _CoreScreenState extends State { @override Widget build(BuildContext context) { final state = Provider.of(context); diff --git a/example/lib/screens/crash_reporting_screen.dart b/example/lib/screens/crash_reporting_screen.dart index fe4623998..2192a79cd 100644 --- a/example/lib/screens/crash_reporting_screen.dart +++ b/example/lib/screens/crash_reporting_screen.dart @@ -7,14 +7,9 @@ import '../widgets/feature_tile.dart'; import '../widgets/section_card.dart'; import '../widgets/separated_list_view.dart'; -class CrashReportingScreen extends StatefulWidget { +class CrashReportingScreen extends StatelessWidget { const CrashReportingScreen({super.key}); - @override - State createState() => _CrashReportingScreenState(); -} - -class _CrashReportingScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( diff --git a/example/lib/screens/settings_screen.dart b/example/lib/screens/settings_screen.dart index 82f8f1bad..733825a1e 100644 --- a/example/lib/screens/settings_screen.dart +++ b/example/lib/screens/settings_screen.dart @@ -9,14 +9,9 @@ import '../widgets/feature_tile.dart'; import '../widgets/section_card.dart'; import '../widgets/separated_list_view.dart'; -class SettingsScreen extends StatefulWidget { +class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); - @override - _SettingsScreenState createState() => _SettingsScreenState(); -} - -class _SettingsScreenState extends State { @override Widget build(BuildContext context) { final themeState = Provider.of(context); diff --git a/example/lib/utils/enum_extensions.dart b/example/lib/utils/enum_extensions.dart new file mode 100644 index 000000000..2f94a5f84 --- /dev/null +++ b/example/lib/utils/enum_extensions.dart @@ -0,0 +1,15 @@ +extension EnumLabel on Enum { + String capitalizedName([String? substringToRemove]) { + return name + .replaceAll(substringToRemove ?? '', '') + .replaceAllMapped( + RegExp('([A-Z]+)'), + (match) => ' ${match.group(0)}', + ) + .toLowerCase() + .replaceAllMapped( + RegExp('(^|\\s)[a-z]'), + (match) => match.group(0)!.toUpperCase(), + ); + } +} diff --git a/example/lib/widgets/chip_picker.dart b/example/lib/widgets/chip_picker.dart index 5a006cd26..cb43ac6c9 100644 --- a/example/lib/widgets/chip_picker.dart +++ b/example/lib/widgets/chip_picker.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; typedef LabelBuilder = String Function(T value); -class ChipPicker extends StatefulWidget { +class ChipPicker extends StatelessWidget { final LabelBuilder labelBuilder; final Set items; final Set values; @@ -16,29 +16,24 @@ class ChipPicker extends StatefulWidget { required this.onChanged, }); - @override - _ChipPickerState createState() => _ChipPickerState(); -} - -class _ChipPickerState extends State> { @override Widget build(BuildContext context) { return Wrap( spacing: 4.0, - children: widget.items + children: items .map( (item) => FilterChip( label: Text( - widget.labelBuilder(item), + labelBuilder(item), ), - selected: widget.values.contains(item), + selected: values.contains(item), onSelected: (selected) { if (selected) { - widget.values.add(item); + values.add(item); } else { - widget.values.remove(item); + values.remove(item); } - widget.onChanged(widget.values); + onChanged(values); }, ), ) From bc7a6d20dcdf0d2328d6c593e6da2e8a05e1f687 Mon Sep 17 00:00:00 2001 From: David Mina Date: Thu, 16 Mar 2023 10:29:53 +0200 Subject: [PATCH 31/34] Move theme management to settings --- example/lib/main.dart | 6 +----- example/lib/providers/settings_state.dart | 14 ++++++++++++++ example/lib/providers/theme_state.dart | 17 ----------------- example/lib/screens/settings_screen.dart | 23 +++++++++-------------- example/lib/widgets/product_card.dart | 4 ++-- example/lib/widgets/section_card.dart | 4 ++-- 6 files changed, 28 insertions(+), 40 deletions(-) delete mode 100644 example/lib/providers/theme_state.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 1fd853fd1..175f64389 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -7,7 +7,6 @@ import 'package:instabug_flutter/instabug_flutter.dart'; import '../providers/bug_reporting_state.dart'; import '../providers/core_state.dart'; import '../providers/settings_state.dart'; -import '../providers/theme_state.dart'; import '../screens/main_screen.dart'; void main() async { @@ -44,11 +43,8 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider( create: (_) => SettingsState(), ), - ChangeNotifierProvider( - create: (_) => ThemeState(), - ), ], - child: Consumer( + child: Consumer( builder: (context, themeState, child) { return MaterialApp( title: 'Instabug Flutter Example', diff --git a/example/lib/providers/settings_state.dart b/example/lib/providers/settings_state.dart index b873ef744..04ba55c7c 100644 --- a/example/lib/providers/settings_state.dart +++ b/example/lib/providers/settings_state.dart @@ -1,6 +1,20 @@ import 'package:flutter/material.dart'; +import '../models/app_theme.dart'; + class SettingsState extends ChangeNotifier { + bool _isDarkTheme = false; + ThemeData _themeData = AppTheme.lightTheme; + + bool get isDarkTheme => _isDarkTheme; + + ThemeData getThemeData() => _themeData; + void setThemeData(bool isDarkMode) { + _isDarkTheme = !_isDarkTheme; + _themeData = isDarkMode ? AppTheme.darkTheme : AppTheme.lightTheme; + notifyListeners(); + } + final Map _colors = { 'Default': const Color(0xFF1D82DC), 'Red': Colors.red, diff --git a/example/lib/providers/theme_state.dart b/example/lib/providers/theme_state.dart deleted file mode 100644 index 6149fca0d..000000000 --- a/example/lib/providers/theme_state.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../models/app_theme.dart'; - -class ThemeState with ChangeNotifier { - bool _isDarkTheme = false; - ThemeData _themeData = AppTheme.lightTheme; - - bool get isDarkTheme => _isDarkTheme; - - ThemeData getThemeData() => _themeData; - void setThemeData(bool isDarkMode) { - _isDarkTheme = !_isDarkTheme; - _themeData = isDarkMode ? AppTheme.darkTheme : AppTheme.lightTheme; - notifyListeners(); - } -} diff --git a/example/lib/screens/settings_screen.dart b/example/lib/screens/settings_screen.dart index 733825a1e..70d752dd9 100644 --- a/example/lib/screens/settings_screen.dart +++ b/example/lib/screens/settings_screen.dart @@ -4,7 +4,6 @@ import 'package:provider/provider.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; import '../providers/settings_state.dart'; -import '../providers/theme_state.dart'; import '../widgets/feature_tile.dart'; import '../widgets/section_card.dart'; import '../widgets/separated_list_view.dart'; @@ -14,8 +13,7 @@ class SettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final themeState = Provider.of(context); - final settingsState = Provider.of(context); + final state = Provider.of(context); return Scaffold( body: MediaQuery.removePadding( context: context, @@ -31,19 +29,17 @@ class SettingsScreen extends StatelessWidget { children: [ FeatureTile( leading: Icon( - themeState.isDarkTheme + state.isDarkTheme ? Icons.dark_mode : Icons.dark_mode_outlined, ), title: const Text('Dark Theme'), trailing: Switch( - value: themeState.isDarkTheme, + value: state.isDarkTheme, onChanged: (value) { - themeState.setThemeData(value); + state.setThemeData(value); Instabug.setColorTheme( - themeState.isDarkTheme - ? ColorTheme.dark - : ColorTheme.light, + state.isDarkTheme ? ColorTheme.dark : ColorTheme.light, ); }, ), @@ -53,10 +49,9 @@ class SettingsScreen extends StatelessWidget { title: const Text('Primary Color'), bottom: Wrap( spacing: 4.0, - children: settingsState.colors.keys.map((colorName) { - final color = settingsState.colors[colorName]!; - final isSelected = - colorName == settingsState.selectedColorName; + children: state.colors.keys.map((colorName) { + final color = state.colors[colorName]!; + final isSelected = colorName == state.selectedColorName; return ChoiceChip( label: Text(colorName), labelStyle: TextStyle( @@ -66,7 +61,7 @@ class SettingsScreen extends StatelessWidget { selectedColor: color, onSelected: (selected) { if (selected) { - settingsState.selectColor(colorName); + state.selectColor(colorName); Instabug.setPrimaryColor(color); } }, diff --git a/example/lib/widgets/product_card.dart b/example/lib/widgets/product_card.dart index f2db62f18..62b055367 100644 --- a/example/lib/widgets/product_card.dart +++ b/example/lib/widgets/product_card.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/product_item.dart'; -import '../providers/theme_state.dart'; +import '../providers/settings_state.dart'; import '../screens/products_screen.dart'; class ProductCard extends StatelessWidget { @@ -29,7 +29,7 @@ class ProductCard extends StatelessWidget { @override Widget build(BuildContext context) { - final state = Provider.of(context); + final state = Provider.of(context); return InkWell( onTap: () => navToSelectedProduct(context), splashColor: state.getThemeData().splashColor, diff --git a/example/lib/widgets/section_card.dart b/example/lib/widgets/section_card.dart index e62b22f33..83dda1818 100644 --- a/example/lib/widgets/section_card.dart +++ b/example/lib/widgets/section_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../providers/theme_state.dart'; +import '../providers/settings_state.dart'; import '../widgets/separated_list_view.dart'; class SectionCard extends StatelessWidget { @@ -14,7 +14,7 @@ class SectionCard extends StatelessWidget { @override Widget build(BuildContext context) { - final state = Provider.of(context); + final state = Provider.of(context); return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), From 0c8b31c7b73ee09727b542eb8021f6e26ed06ab2 Mon Sep 17 00:00:00 2001 From: David Mina Date: Thu, 16 Mar 2023 11:40:51 +0200 Subject: [PATCH 32/34] Set default floatingButtonOffset based on platform --- example/lib/providers/bug_reporting_state.dart | 4 +++- example/lib/screens/bug_reporting_screen.dart | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/example/lib/providers/bug_reporting_state.dart b/example/lib/providers/bug_reporting_state.dart index 41859860b..ad25db09a 100644 --- a/example/lib/providers/bug_reporting_state.dart +++ b/example/lib/providers/bug_reporting_state.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; @@ -70,7 +72,7 @@ class BugReportingState with ChangeNotifier { notifyListeners(); } - var _floatingButtonOffset = 100; + var _floatingButtonOffset = Platform.isIOS ? 100 : 250; int get floatingButtonOffset => _floatingButtonOffset; set floatingButtonOffset(int offset) { _floatingButtonOffset = offset; diff --git a/example/lib/screens/bug_reporting_screen.dart b/example/lib/screens/bug_reporting_screen.dart index b4083db01..ab3ba2c03 100644 --- a/example/lib/screens/bug_reporting_screen.dart +++ b/example/lib/screens/bug_reporting_screen.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -283,7 +285,8 @@ class BugReportingScreen extends StatelessWidget { enableInteractiveSelection: true, onChanged: (value) { if (value.isEmpty) { - state.floatingButtonOffset = 100; + state.floatingButtonOffset = + Platform.isIOS ? 100 : 250; } else { state.floatingButtonOffset = int.parse(value); } From 459f171f1f82b502da89861a8abafa72ef832224 Mon Sep 17 00:00:00 2001 From: David Mina Date: Tue, 28 Mar 2023 10:37:11 +0200 Subject: [PATCH 33/34] Apply some refactoring --- example/lib/main.dart | 20 +++-- example/lib/models/app_theme.dart | 32 ++++---- example/lib/models/attachment_type.dart | 6 ++ .../lib/providers/bug_reporting_state.dart | 61 +++++++------- example/lib/providers/core_state.dart | 8 +- example/lib/providers/settings_state.dart | 19 +++-- example/lib/screens/bug_reporting_screen.dart | 82 +++++++++---------- example/lib/screens/core_screen.dart | 4 +- .../lib/screens/crash_reporting_screen.dart | 4 +- .../lib/screens/feature_requests_screen.dart | 10 +-- example/lib/screens/main_screen.dart | 12 ++- example/lib/screens/settings_screen.dart | 10 +-- example/lib/utils/enum_extensions.dart | 3 +- example/lib/widgets/product_card.dart | 13 ++- example/lib/widgets/section_card.dart | 5 +- 15 files changed, 143 insertions(+), 146 deletions(-) create mode 100644 example/lib/models/attachment_type.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 175f64389..5968b5f5c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; +import '../models/app_theme.dart'; import '../providers/bug_reporting_state.dart'; import '../providers/core_state.dart'; import '../providers/settings_state.dart'; @@ -24,18 +25,21 @@ void main() async { Zone.current.handleUncaughtError(details.exception, details.stack!); }; - runZonedGuarded(() => runApp(const MyApp()), CrashReporting.reportCrash); + runZonedGuarded( + () => runApp(const InstabugApp()), + CrashReporting.reportCrash, + ); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class InstabugApp extends StatelessWidget { + const InstabugApp({super.key}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider( - create: (ctx) => BugReportingState(), + create: (_) => BugReportingState(), ), ChangeNotifierProvider( create: (_) => CoreState(), @@ -45,10 +49,14 @@ class MyApp extends StatelessWidget { ), ], child: Consumer( - builder: (context, themeState, child) { + builder: (context, state, child) { return MaterialApp( title: 'Instabug Flutter Example', - theme: themeState.getThemeData(), + themeMode: state.colorTheme == ColorTheme.light + ? ThemeMode.light + : ThemeMode.dark, + theme: AppTheme.light, + darkTheme: AppTheme.dark, home: const MainScreen(), ); }, diff --git a/example/lib/models/app_theme.dart b/example/lib/models/app_theme.dart index 35c145721..551d3dfd9 100644 --- a/example/lib/models/app_theme.dart +++ b/example/lib/models/app_theme.dart @@ -1,55 +1,55 @@ import 'package:flutter/material.dart'; abstract class AppColors { - static const primaryColor = Color(0xFF00287a); - static const secondaryColor = Color(0xFF5DAAF0); - static const primaryColorDark = Color(0xFF212121); + static const primary = Color(0xFF00287a); + static const secondary = Color(0xFF5DAAF0); + static const primaryDark = Color(0xFF212121); } class AppTheme { - static final lightTheme = ThemeData( + static final light = ThemeData( colorScheme: ColorScheme.fromSwatch().copyWith( brightness: Brightness.light, - primary: AppColors.primaryColor, - secondary: AppColors.secondaryColor, + primary: AppColors.primary, + secondary: AppColors.secondary, ), scaffoldBackgroundColor: Colors.grey[100], appBarTheme: const AppBarTheme( - color: AppColors.primaryColor, + color: AppColors.primary, ), visualDensity: VisualDensity.adaptivePlatformDensity, bottomNavigationBarTheme: const BottomNavigationBarThemeData( - backgroundColor: AppColors.primaryColor, + backgroundColor: AppColors.primary, unselectedItemColor: Colors.white, - selectedItemColor: AppColors.secondaryColor, + selectedItemColor: AppColors.secondary, ), chipTheme: const ChipThemeData( - selectedColor: AppColors.secondaryColor, + selectedColor: AppColors.secondary, ), iconTheme: IconThemeData(color: Colors.grey[600]), textTheme: const TextTheme( headlineMedium: TextStyle( fontFamily: 'Axiforma', fontSize: 16.0, - color: AppColors.primaryColor, + color: AppColors.primary, fontWeight: FontWeight.w600, ), ), ); - static final darkTheme = ThemeData.dark().copyWith( + static final dark = ThemeData.dark().copyWith( colorScheme: ColorScheme.fromSwatch().copyWith( - secondary: AppColors.secondaryColor, + secondary: AppColors.secondary, ), appBarTheme: const AppBarTheme( - color: AppColors.primaryColorDark, + color: AppColors.primaryDark, ), visualDensity: VisualDensity.adaptivePlatformDensity, bottomNavigationBarTheme: const BottomNavigationBarThemeData( - backgroundColor: AppColors.primaryColorDark, + backgroundColor: AppColors.primaryDark, ), chipTheme: const ChipThemeData( - selectedColor: AppColors.secondaryColor, + selectedColor: AppColors.secondary, backgroundColor: Color(0xFFB3B3B3), ), iconTheme: const IconThemeData(color: Colors.white), diff --git a/example/lib/models/attachment_type.dart b/example/lib/models/attachment_type.dart new file mode 100644 index 000000000..fe0749296 --- /dev/null +++ b/example/lib/models/attachment_type.dart @@ -0,0 +1,6 @@ +enum AttachmentType { + screenshot, + extraScreenshot, + galleryImage, + screenRecording +} diff --git a/example/lib/providers/bug_reporting_state.dart b/example/lib/providers/bug_reporting_state.dart index ad25db09a..4b1926589 100644 --- a/example/lib/providers/bug_reporting_state.dart +++ b/example/lib/providers/bug_reporting_state.dart @@ -3,58 +3,53 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; +import '../models/attachment_type.dart'; class BugReportingState with ChangeNotifier { - var _extraAttachments = { - 'Screenshot': true, - 'Extra Screenshot': true, - 'Gallery Image': true, - 'Screen Recording': true + var _extraAttachments = { + AttachmentType.screenshot, + AttachmentType.extraScreenshot, + AttachmentType.galleryImage, + AttachmentType.screenRecording, }; - Map get extraAttachments => _extraAttachments; - set extraAttachments(Map attachments) { + Set get extraAttachments => _extraAttachments; + set extraAttachments(Set attachments) { _extraAttachments = attachments; notifyListeners(); } - var _selectedInvocationOptions = {}; - Set get selectedInvocationOptions => - _selectedInvocationOptions; - set selectedInvocationOptions(Set options) { - _selectedInvocationOptions = options; + var _invocationOptions = {}; + Set get invocationOptions => _invocationOptions; + set invocationOptions(Set options) { + _invocationOptions = options; notifyListeners(); } - var _selectedInvocationEvents = { - InvocationEvent.floatingButton - }; - Set get selectedInvocationEvents => - _selectedInvocationEvents; - set selectedInvocationEvents(Set events) { - _selectedInvocationEvents = events; + var _invocationEvents = {InvocationEvent.floatingButton}; + Set get invocationEvents => _invocationEvents; + set invocationEvents(Set events) { + _invocationEvents = events; notifyListeners(); } - var _selectedExtendedMode = ExtendedBugReportMode.disabled; - ExtendedBugReportMode get selectedExtendedMode => _selectedExtendedMode; - set selectedExtendedMode(ExtendedBugReportMode mode) { - _selectedExtendedMode = mode; + var _extendedMode = ExtendedBugReportMode.disabled; + ExtendedBugReportMode get extendedMode => _extendedMode; + set extendedMode(ExtendedBugReportMode mode) { + _extendedMode = mode; notifyListeners(); } - var _selectedVideoRecordingPosition = Position.bottomRight; - Position get selectedVideoRecordingPosition => - _selectedVideoRecordingPosition; - set selectedVideoRecordingPosition(Position position) { - _selectedVideoRecordingPosition = position; + var _videoRecordingPosition = Position.bottomRight; + Position get videoRecordingPosition => _videoRecordingPosition; + set videoRecordingPosition(Position position) { + _videoRecordingPosition = position; notifyListeners(); } - var _selectedFloatingButtonEdge = FloatingButtonEdge.right; - FloatingButtonEdge get selectedFloatingButtonEdge => - _selectedFloatingButtonEdge; - set selectedFloatingButtonEdge(FloatingButtonEdge edge) { - _selectedFloatingButtonEdge = edge; + var _floatingButtonEdge = FloatingButtonEdge.right; + FloatingButtonEdge get floatingButtonEdge => _floatingButtonEdge; + set floatingButtonEdge(FloatingButtonEdge edge) { + _floatingButtonEdge = edge; notifyListeners(); } diff --git a/example/lib/providers/core_state.dart b/example/lib/providers/core_state.dart index e2abeaf5e..c15ea2133 100644 --- a/example/lib/providers/core_state.dart +++ b/example/lib/providers/core_state.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; class CoreState extends ChangeNotifier { - bool _isDisabled = false; + bool _isEnabled = true; - bool get isDisabled => _isDisabled; - set isDisabled(bool value) { - _isDisabled = value; + bool get isEnabled => _isEnabled; + set isEnabled(bool value) { + _isEnabled = value; notifyListeners(); } } diff --git a/example/lib/providers/settings_state.dart b/example/lib/providers/settings_state.dart index 04ba55c7c..e2a8d2952 100644 --- a/example/lib/providers/settings_state.dart +++ b/example/lib/providers/settings_state.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart'; - -import '../models/app_theme.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; class SettingsState extends ChangeNotifier { - bool _isDarkTheme = false; - ThemeData _themeData = AppTheme.lightTheme; + ColorTheme _theme = ColorTheme.light; + // bool _isDarkTheme = false; + // ThemeData _themeData = AppTheme.lightTheme; - bool get isDarkTheme => _isDarkTheme; + // bool get isDarkTheme => _isDarkTheme; - ThemeData getThemeData() => _themeData; - void setThemeData(bool isDarkMode) { - _isDarkTheme = !_isDarkTheme; - _themeData = isDarkMode ? AppTheme.darkTheme : AppTheme.lightTheme; + ColorTheme get colorTheme => _theme; + void setColorTheme(ColorTheme theme) { + // _isDarkTheme = !_isDarkTheme; + // _themeData = isDarkMode ? AppTheme.dark : AppTheme.light; + _theme = theme; notifyListeners(); } diff --git a/example/lib/screens/bug_reporting_screen.dart b/example/lib/screens/bug_reporting_screen.dart index ab3ba2c03..e60293107 100644 --- a/example/lib/screens/bug_reporting_screen.dart +++ b/example/lib/screens/bug_reporting_screen.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; +import '../models/attachment_type.dart'; import '../providers/bug_reporting_state.dart'; import '../utils/enum_extensions.dart'; import '../widgets/chip_picker.dart'; @@ -45,7 +46,7 @@ class BugReportingScreen extends StatelessWidget { title: const Text('Report a bug'), onTap: () => BugReporting.show( ReportType.bug, - state.selectedInvocationOptions.toList(), + state.invocationOptions.toList(), ), ), FeatureTile( @@ -53,7 +54,7 @@ class BugReportingScreen extends StatelessWidget { title: const Text('Suggest an improvement'), onTap: () => BugReporting.show( ReportType.feedback, - state.selectedInvocationOptions.toList(), + state.invocationOptions.toList(), ), ), FeatureTile( @@ -61,7 +62,7 @@ class BugReportingScreen extends StatelessWidget { title: const Text('Ask a question'), onTap: () => BugReporting.show( ReportType.question, - state.selectedInvocationOptions.toList(), + state.invocationOptions.toList(), ), ), ], @@ -73,12 +74,12 @@ class BugReportingScreen extends StatelessWidget { title: const Text('Invocation Events'), bottom: ChipPicker( items: InvocationEvent.values.toSet(), - values: state.selectedInvocationEvents, - labelBuilder: (value) => value.capitalizedName(), + values: state.invocationEvents, + labelBuilder: (value) => value.capitalizedName, onChanged: (values) { - state.selectedInvocationEvents = values; + state.invocationEvents = values; BugReporting.setInvocationEvents( - state.selectedInvocationEvents.toList(), + state.invocationEvents.toList(), ); }, ), @@ -86,28 +87,23 @@ class BugReportingScreen extends StatelessWidget { FeatureTile( leading: const Icon(Icons.attachment), title: const Text('Attachments'), - bottom: Wrap( - spacing: 4.0, - children: state.extraAttachments.keys - .toList() - .map((String attachment) { - return FilterChip( - label: Text(attachment), - selected: state.extraAttachments[attachment]!, - onSelected: (bool value) { - state.extraAttachments = { - ...state.extraAttachments, - attachment: value - }; - BugReporting.setEnabledAttachmentTypes( - state.extraAttachments['Screenshot']!, - state.extraAttachments['Extra Screenshot']!, - state.extraAttachments['Gallery Image']!, - state.extraAttachments['Screen Recording']!, - ); - }, + bottom: ChipPicker( + items: AttachmentType.values.toSet(), + values: state.extraAttachments, + labelBuilder: (value) => value.capitalizedName, + onChanged: (values) { + state.extraAttachments = values; + BugReporting.setEnabledAttachmentTypes( + state.extraAttachments + .contains(AttachmentType.screenshot), + state.extraAttachments + .contains(AttachmentType.extraScreenshot), + state.extraAttachments + .contains(AttachmentType.galleryImage), + state.extraAttachments + .contains(AttachmentType.screenRecording), ); - }).toList(), + }, ), ), FeatureTile( @@ -115,12 +111,13 @@ class BugReportingScreen extends StatelessWidget { title: const Text('Invocation Options'), bottom: ChipPicker( items: InvocationOption.values.toSet(), - values: state.selectedInvocationOptions, - labelBuilder: (value) => value.capitalizedName('Field'), + values: state.invocationOptions, + labelBuilder: (value) => + value.capitalizedName.replaceAll('Field ', ''), onChanged: (values) { - state.selectedInvocationOptions = values; + state.invocationOptions = values; BugReporting.setInvocationOptions( - state.selectedInvocationOptions.toList(), + state.invocationOptions.toList(), ); }, ), @@ -163,18 +160,19 @@ class BugReportingScreen extends StatelessWidget { rightFlex: 4, right: DropdownMenu( width: 150, - initialSelection: state.selectedExtendedMode, + initialSelection: state.extendedMode, label: const Text('Mode'), dropdownMenuEntries: ExtendedBugReportMode.values .map>( (ExtendedBugReportMode mode) { return DropdownMenuEntry( value: mode, - label: mode.capitalizedName('enabledWith'), + label: mode.capitalizedName + .replaceAll('enabledWith', ''), ); }).toList(), onSelected: (ExtendedBugReportMode? mode) { - state.selectedExtendedMode = mode!; + state.extendedMode = mode!; BugReporting.setExtendedBugReportMode(mode); }, ), @@ -214,18 +212,18 @@ class BugReportingScreen extends StatelessWidget { rightFlex: 4, right: DropdownMenu( width: 150, - initialSelection: state.selectedVideoRecordingPosition, + initialSelection: state.videoRecordingPosition, label: const Text('Position'), dropdownMenuEntries: Position.values .map>( (Position position) { return DropdownMenuEntry( value: position, - label: position.capitalizedName(), + label: position.capitalizedName, ); }).toList(), onSelected: (Position? position) { - state.selectedVideoRecordingPosition = position!; + state.videoRecordingPosition = position!; BugReporting.setVideoRecordingFloatingButtonPosition( position, ); @@ -239,18 +237,18 @@ class BugReportingScreen extends StatelessWidget { rightFlex: 4, right: DropdownMenu( width: 150, - initialSelection: state.selectedFloatingButtonEdge, + initialSelection: state.floatingButtonEdge, label: const Text('Edge'), dropdownMenuEntries: FloatingButtonEdge.values .map>( (FloatingButtonEdge edge) { return DropdownMenuEntry( value: edge, - label: edge.capitalizedName(), + label: edge.capitalizedName, ); }).toList(), onSelected: (FloatingButtonEdge? edge) { - state.selectedFloatingButtonEdge = edge!; + state.floatingButtonEdge = edge!; BugReporting.setFloatingButtonEdge( edge, state.floatingButtonOffset, @@ -268,7 +266,7 @@ class BugReportingScreen extends StatelessWidget { onFocusChange: (hasFocus) { if (!hasFocus) { BugReporting.setFloatingButtonEdge( - state.selectedFloatingButtonEdge, + state.floatingButtonEdge, state.floatingButtonOffset, ); } diff --git a/example/lib/screens/core_screen.dart b/example/lib/screens/core_screen.dart index 56b4b0064..56974ec82 100644 --- a/example/lib/screens/core_screen.dart +++ b/example/lib/screens/core_screen.dart @@ -38,10 +38,10 @@ class CoreScreen extends StatelessWidget { ), title: const Text('Disable SDK'), trailing: Switch( - value: state.isDisabled, + value: !state.isEnabled, onChanged: (value) { Instabug.setEnabled(!value); - state.isDisabled = value; + state.isEnabled = !value; }, ), ), diff --git a/example/lib/screens/crash_reporting_screen.dart b/example/lib/screens/crash_reporting_screen.dart index 2192a79cd..828e0ffe1 100644 --- a/example/lib/screens/crash_reporting_screen.dart +++ b/example/lib/screens/crash_reporting_screen.dart @@ -38,8 +38,8 @@ class CrashReportingScreen extends StatelessWidget { throw Exception( 'This is a handled crash from Instabug Example App', ); - } catch (e, st) { - CrashReporting.reportHandledCrash(e, st); + } catch (error, stacktrace) { + CrashReporting.reportHandledCrash(error, stacktrace); const snackBar = SnackBar( content: Text( 'A handled crash has been successfully reported!', diff --git a/example/lib/screens/feature_requests_screen.dart b/example/lib/screens/feature_requests_screen.dart index 38dc7e948..51cd4a72b 100644 --- a/example/lib/screens/feature_requests_screen.dart +++ b/example/lib/screens/feature_requests_screen.dart @@ -26,15 +26,13 @@ class FeatureRequestsScreen extends StatelessWidget { separator: const SizedBox( height: 8.0, ), - children: [ + children: const [ SectionCard( children: [ FeatureTile( - leading: const Icon(Icons.lightbulb), - title: const Text('Show Feature Requests'), - onTap: () { - FeatureRequests.show(); - }, + leading: Icon(Icons.lightbulb), + title: Text('Show Feature Requests'), + onTap: FeatureRequests.show, ), ], ), diff --git a/example/lib/screens/main_screen.dart b/example/lib/screens/main_screen.dart index 4330223eb..fefa7ab47 100644 --- a/example/lib/screens/main_screen.dart +++ b/example/lib/screens/main_screen.dart @@ -27,12 +27,6 @@ class _MainScreenState extends State { ), ]; - void _selectPage(int index) { - setState(() { - selectedPageIndex = index; - }); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -44,7 +38,11 @@ class _MainScreenState extends State { children: flows.map((flow) => flow.page).toList(), ), bottomNavigationBar: BottomNavigationBar( - onTap: _selectPage, + onTap: (index) { + setState(() { + selectedPageIndex = index; + }); + }, currentIndex: selectedPageIndex, items: flows .map( diff --git a/example/lib/screens/settings_screen.dart b/example/lib/screens/settings_screen.dart index 70d752dd9..f37644882 100644 --- a/example/lib/screens/settings_screen.dart +++ b/example/lib/screens/settings_screen.dart @@ -29,18 +29,18 @@ class SettingsScreen extends StatelessWidget { children: [ FeatureTile( leading: Icon( - state.isDarkTheme + state.colorTheme == ColorTheme.dark ? Icons.dark_mode : Icons.dark_mode_outlined, ), title: const Text('Dark Theme'), trailing: Switch( - value: state.isDarkTheme, + value: state.colorTheme == ColorTheme.dark, onChanged: (value) { - state.setThemeData(value); - Instabug.setColorTheme( - state.isDarkTheme ? ColorTheme.dark : ColorTheme.light, + state.setColorTheme( + value ? ColorTheme.dark : ColorTheme.light, ); + Instabug.setColorTheme(state.colorTheme); }, ), ), diff --git a/example/lib/utils/enum_extensions.dart b/example/lib/utils/enum_extensions.dart index 2f94a5f84..90ba43b24 100644 --- a/example/lib/utils/enum_extensions.dart +++ b/example/lib/utils/enum_extensions.dart @@ -1,7 +1,6 @@ extension EnumLabel on Enum { - String capitalizedName([String? substringToRemove]) { + String get capitalizedName { return name - .replaceAll(substringToRemove ?? '', '') .replaceAllMapped( RegExp('([A-Z]+)'), (match) => ' ${match.group(0)}', diff --git a/example/lib/widgets/product_card.dart b/example/lib/widgets/product_card.dart index 62b055367..9016ba1c6 100644 --- a/example/lib/widgets/product_card.dart +++ b/example/lib/widgets/product_card.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import '../models/product_item.dart'; -import '../providers/settings_state.dart'; import '../screens/products_screen.dart'; class ProductCard extends StatelessWidget { @@ -17,26 +15,25 @@ class ProductCard extends StatelessWidget { required this.color, }); - void navToSelectedProduct(BuildContext ctx) { + void navToSelectedProduct(BuildContext context) { ProductItem selectedProduct = ProductsScreen.products.firstWhere((product) => product.title == title); Navigator.push( - ctx, + context, MaterialPageRoute(builder: (_) => selectedProduct.screen), ); } @override Widget build(BuildContext context) { - final state = Provider.of(context); return InkWell( onTap: () => navToSelectedProduct(context), - splashColor: state.getThemeData().splashColor, + splashColor: Theme.of(context).splashColor, child: Ink( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20.0), - color: state.getThemeData().cardColor, + color: Theme.of(context).cardColor, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -58,7 +55,7 @@ class ProductCard extends StatelessWidget { child: Text( title, textAlign: TextAlign.center, - style: state.getThemeData().textTheme.headlineMedium, + style: Theme.of(context).textTheme.headlineMedium, ), ), ), diff --git a/example/lib/widgets/section_card.dart b/example/lib/widgets/section_card.dart index 83dda1818..3db4a1663 100644 --- a/example/lib/widgets/section_card.dart +++ b/example/lib/widgets/section_card.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import '../providers/settings_state.dart'; import '../widgets/separated_list_view.dart'; class SectionCard extends StatelessWidget { @@ -14,11 +12,10 @@ class SectionCard extends StatelessWidget { @override Widget build(BuildContext context) { - final state = Provider.of(context); return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), - color: state.getThemeData().cardColor, + color: Theme.of(context).cardColor, ), child: SeparatedListView( separator: const Divider( From bf87c598600eed078c9055f21598e194b5742f28 Mon Sep 17 00:00:00 2001 From: David Mina Date: Mon, 1 May 2023 10:13:11 +0300 Subject: [PATCH 34/34] Configure Repro Steps --- example/lib/main.dart | 1 + example/lib/widgets/product_card.dart | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 5968b5f5c..428161322 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -51,6 +51,7 @@ class InstabugApp extends StatelessWidget { child: Consumer( builder: (context, state, child) { return MaterialApp( + navigatorObservers: [InstabugNavigatorObserver()], title: 'Instabug Flutter Example', themeMode: state.colorTheme == ColorTheme.light ? ThemeMode.light diff --git a/example/lib/widgets/product_card.dart b/example/lib/widgets/product_card.dart index 9016ba1c6..75225a36f 100644 --- a/example/lib/widgets/product_card.dart +++ b/example/lib/widgets/product_card.dart @@ -21,7 +21,10 @@ class ProductCard extends StatelessWidget { Navigator.push( context, - MaterialPageRoute(builder: (_) => selectedProduct.screen), + MaterialPageRoute( + builder: (_) => selectedProduct.screen, + settings: RouteSettings(name: selectedProduct.title), + ), ); }