├── .gitignore ├── .metadata ├── LICENSE.md ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── lv │ │ │ │ └── daria │ │ │ │ └── example │ │ │ │ └── bunny_search │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-mdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-v21 │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxxhdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── data ├── .gitignore ├── .metadata ├── lib │ ├── brands │ │ ├── persisted_brands_repository.dart │ │ └── search_service.dart │ ├── database │ │ ├── brands_dao.dart │ │ ├── database.dart │ │ ├── database.g.dart │ │ ├── list_type_converter.dart │ │ └── model │ │ │ ├── brand_entity.dart │ │ │ ├── brand_with_organization_entity.dart │ │ │ └── organization_entity.dart │ ├── organizations │ │ ├── model │ │ │ ├── firebase_organization.dart │ │ │ └── firebase_organization_brand.dart │ │ └── repository │ │ │ ├── assets_organizations_repository.dart │ │ │ └── firebase_organizations_repository.dart │ └── storage │ │ └── shared_preferences_key_value_storage.dart ├── pubspec.lock └── pubspec.yaml ├── domain ├── .gitignore ├── .metadata ├── lib │ ├── brands │ │ ├── model │ │ │ └── brand.dart │ │ └── repository │ │ │ └── brands_repository.dart │ ├── organizations │ │ ├── model │ │ │ ├── organization.dart │ │ │ ├── organization_brand.dart │ │ │ └── organization_type.dart │ │ └── repository │ │ │ └── organizations_repository.dart │ ├── result │ │ ├── delayed_result.dart │ │ └── result.dart │ └── storage │ │ └── key_value_storage.dart ├── pubspec.lock └── pubspec.yaml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ ├── LaunchBackground.imageset │ │ ├── Contents.json │ │ └── background.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── analytics │ ├── bloc_error_delegate.dart │ └── crashlytics_fimber_tree.dart ├── app.dart ├── app_routes.dart ├── brand │ ├── bloc │ │ └── popular_brands_bloc.dart │ └── widget │ │ ├── brand_details_page.dart │ │ ├── brand_list_item.dart │ │ └── popular_brands_page.dart ├── generated │ ├── codegen_loader.g.dart │ └── locale_keys.g.dart ├── home │ ├── bloc │ │ └── home_bloc.dart │ └── widget │ │ ├── background_wave_clipper.dart │ │ ├── home_brands_list.dart │ │ ├── home_content_screen.dart │ │ ├── home_organizations_section.dart │ │ ├── home_page.dart │ │ ├── home_popular_brands_section.dart │ │ ├── home_splash_screen.dart │ │ ├── no_overscroll_behaviour.dart │ │ ├── organizations_list.dart │ │ ├── search_bar.dart │ │ ├── search_bunny_icon_clipper.dart │ │ ├── show_all_button.dart │ │ ├── sliver_search_app_bar.dart │ │ └── support_dialog.dart ├── main.dart ├── organization │ ├── bloc │ │ ├── organization_brands_bloc.dart │ │ └── organizations_bloc.dart │ ├── model │ │ ├── organization_brand_details.dart │ │ ├── organization_details.dart │ │ └── organizations_mapper.dart │ └── widget │ │ ├── organization_brands_page.dart │ │ ├── organization_list_card.dart │ │ └── organizations_page.dart ├── theme │ ├── app_colors.dart │ ├── app_typography.dart │ ├── bunny_appbar_back_button.dart │ ├── bunny_back_button.dart │ ├── bunny_cached_logo_image.dart │ ├── bunny_snack_bar.dart │ └── images_provider.dart └── utils │ ├── cache │ └── image_cache_manager.dart │ └── widget │ └── focus_utils.dart ├── pubspec.lock ├── pubspec.yaml ├── resources ├── database │ └── bunny-search-database.json ├── fonts │ ├── Inter-Black.ttf │ ├── Inter-Bold.ttf │ ├── Inter-ExtraBold.ttf │ ├── Inter-ExtraLight.ttf │ ├── Inter-Light.ttf │ ├── Inter-Medium.ttf │ ├── Inter-Regular.ttf │ ├── Inter-SemiBold.ttf │ └── Inter-Thin.ttf ├── icons │ ├── background.png │ ├── foreground.png │ ├── ic_back.svg │ ├── ic_bunny.png │ ├── ic_check.svg │ ├── ic_chevron_right.svg │ ├── ic_cross.svg │ ├── ic_leaping_bunny.png │ ├── ic_question.svg │ ├── ic_stop.svg │ └── launcher.png ├── images │ ├── 2.0x │ │ ├── ic_bunny_search.png │ │ └── ic_peta.png │ ├── 3.0x │ │ ├── ic_bunny_search.png │ │ └── ic_peta.png │ ├── ic_bunny_search.png │ └── ic_peta.png ├── langs │ ├── en.json │ └── ru.json ├── scripts │ ├── clean.sh │ ├── generate_icons.sh │ ├── generate_json_models.sh │ ├── generate_splash.sh │ ├── run_clean.sh │ ├── run_generate_json_models.sh │ └── update_translations.sh └── splash │ └── splash_bg.png └── test └── data └── brands └── search_service_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: b1395592de68cc8ac4522094ae59956dd21a91db 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | ## “Commons Clause” License Condition v1.0 4 | 5 | The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition. Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software. For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice. 6 | 7 | Software: Bunny Search 8 | 9 | License: The 3-Clause BSD License 10 | 11 | Licensor: Darja Orlova 12 | 13 | ## The 3-Clause BSD License 14 | 15 | Copyright 2022 Darja Orlova 16 | 17 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 18 | 19 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 20 | 21 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 22 | 23 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bunny Search App 2 | 3 | This is the copy of the source code of the “Bunny Search” app. 4 | 5 | The only difference of this copy with the original is that it uses local database read from a json 6 | file, instead of Firebase. The code used in the original copy can be found by searching the comment 7 | “Used in real app”. 8 | 9 | ## Articles 10 | 11 | Flutter pet project roadmap **[based on Bunny Search](https://medium.com/@daria.orlova/flutter-pet-project-roadmap-31247c8eb015)**. 12 | 13 | Custom shaped AppBar **[as seen in the Bunny Search app](https://medium.com/flutter-community/custom-shaped-appbar-as-seen-in-the-bunny-search-app-6312d067485c)**. 14 | 15 | How to implement "fuzzy search" **[as in Bunny search app](https://dariadroid.substack.com/p/implementing-fuzzy-search-in-a-flutter)**. 16 | 17 | ## Contact me 18 | 19 | You can reach out to me via [Twitter](https://twitter.com/dariadroid) if you have any questions. 20 | 21 | ## Download app 22 | 23 | You can download the app 24 | for [android](https://play.google.com/store/apps/details?id=lv.chi.bunny_search) 25 | & [ios](https://apps.apple.com/lv/app/bunny-search/id1592571643). 26 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - [ build/** ] 6 | - data/lib/**/*.g.dart #TODO and other codegen places 7 | 8 | linter: 9 | rules: 10 | - require_trailing_commas 11 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 34 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | applicationId "lv.daria.example.bunny_search" 37 | minSdkVersion 21 38 | targetSdkVersion 34 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | } 42 | 43 | buildTypes { 44 | release { 45 | // TODO: Add your own signing config for the release build. 46 | // Signing with the debug keys for now, so `flutter run --release` works. 47 | signingConfig signingConfigs.debug 48 | } 49 | } 50 | } 51 | 52 | flutter { 53 | source '../..' 54 | } 55 | 56 | dependencies { 57 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 58 | 59 | implementation platform('com.google.firebase:firebase-bom:28.2.0') 60 | implementation 'com.google.firebase:firebase-analytics-ktx' 61 | } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/lv/daria/example/bunny_search/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package lv.daria.example.bunny_search 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/drawable-v21/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/drawable/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 16 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 16 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.5.20' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.4.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | -------------------------------------------------------------------------------- /data/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: b1395592de68cc8ac4522094ae59956dd21a91db 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /data/lib/brands/search_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:data/database/model/brand_entity.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | 7 | class SearchService { 8 | Future> search({ 9 | required List allBrands, 10 | required String query, 11 | }) async => 12 | compute( 13 | searchSync, 14 | SearchQuery(allBrands, query), 15 | ); 16 | 17 | @visibleForTesting 18 | static List searchSync(SearchQuery query) { 19 | final searchTerm = query.query.toLowerCase(); 20 | final filtered = query.allBrands.expand((brand) { 21 | final title = brand.title.toLowerCase(); 22 | if (title == searchTerm) { 23 | return [SearchResult(0, brand)]; 24 | } 25 | if (title.contains(searchTerm)) { 26 | return [SearchResult(1, brand)]; 27 | } 28 | final maxDistance = title.length > 7 ? 4 : 2; 29 | final nameDistance = levenshteinDistance(title, searchTerm, maxDistance); 30 | if (nameDistance <= maxDistance) { 31 | return [SearchResult(nameDistance, brand)]; 32 | } else { 33 | return []; 34 | } 35 | }).toList(); 36 | filtered.sort((a, b) { 37 | return a.distance.compareTo(b.distance); 38 | }); 39 | return filtered.map((result) => result.brand).toList(); 40 | } 41 | 42 | @visibleForTesting 43 | static int levenshteinDistance(String a, String b, int maxDistance) { 44 | if (a == b) { 45 | return 0; 46 | } 47 | if (a.isEmpty) { 48 | return b.length <= maxDistance ? b.length : maxDistance + 1; 49 | } 50 | if (b.isEmpty) { 51 | return a.length <= maxDistance ? a.length : maxDistance + 1; 52 | } 53 | 54 | int aLength = a.length; 55 | int bLength = b.length; 56 | List> matrix = List.generate( 57 | bLength + 1, 58 | (i) => List.generate(aLength + 1, (j) => 0, growable: false), 59 | growable: false, 60 | ); 61 | 62 | for (int i = 0; i <= aLength; i++) { 63 | matrix[0][i] = i; 64 | } 65 | for (int i = 0; i <= bLength; i++) { 66 | matrix[i][0] = i; 67 | } 68 | 69 | for (int i = 1; i <= bLength; i++) { 70 | int minRowValue = maxDistance + 1; 71 | for (int j = 1; j <= aLength; j++) { 72 | int cost = (a[j - 1] == b[i - 1]) ? 0 : 1; 73 | matrix[i][j] = _min( 74 | matrix[i - 1][j] + 1, // deletion 75 | matrix[i][j - 1] + 1, // insertion 76 | matrix[i - 1][j - 1] + cost, // substitution 77 | ); 78 | 79 | minRowValue = min(minRowValue, matrix[i][j]); 80 | } 81 | 82 | if (minRowValue > maxDistance) { 83 | return maxDistance + 1; 84 | } 85 | } 86 | 87 | return matrix[bLength][aLength]; 88 | } 89 | 90 | static int _min(int a, int b, int c) { 91 | return (a < b) ? (a < c ? a : c) : (b < c ? b : c); 92 | } 93 | 94 | @visibleForTesting 95 | static int hammingDistance(String a, String b) { 96 | if (a.length != b.length) { 97 | throw ArgumentError('Strings must be of equal length'); 98 | } 99 | 100 | int distance = 0; 101 | for (int i = 0; i < a.length; i++) { 102 | if (a[i] != b[i]) { 103 | distance++; 104 | } 105 | } 106 | return distance; 107 | } 108 | } 109 | 110 | // TODO: Update to Dart 3 & use records 111 | class SearchQuery extends Equatable { 112 | final List allBrands; 113 | final String query; 114 | 115 | const SearchQuery(this.allBrands, this.query); 116 | 117 | @override 118 | List get props => [ 119 | allBrands, 120 | query, 121 | ]; 122 | } 123 | 124 | class SearchResult extends Equatable { 125 | final int distance; 126 | final BrandEntity brand; 127 | 128 | const SearchResult(this.distance, this.brand); 129 | 130 | @override 131 | List get props => [ 132 | distance, 133 | brand, 134 | ]; 135 | } 136 | -------------------------------------------------------------------------------- /data/lib/database/brands_dao.dart: -------------------------------------------------------------------------------- 1 | import 'package:data/database/model/brand_entity.dart'; 2 | import 'package:data/database/model/brand_with_organization_entity.dart'; 3 | import 'package:data/database/model/organization_entity.dart'; 4 | import 'package:floor/floor.dart'; 5 | 6 | @dao 7 | abstract class BrandsDao { 8 | @Query('SELECT * FROM brands') 9 | Future> getAllBrands(); 10 | 11 | @Query('SELECT * FROM brands WHERE popular = 1') 12 | Future> getAllPopularBrands(); 13 | 14 | @Query('SELECT * FROM organizations') 15 | Future> getAllOrganizations(); 16 | 17 | @Query( 18 | 'SELECT * FROM brands WHERE title IN (SELECT brand_title FROM brands_with_organizations WHERE org_id = :orgId)', 19 | ) 20 | Future> getAllOrganizationBrands(String orgId); 21 | 22 | @Query('SELECT * FROM brands WHERE title LIKE :query') 23 | Future> findBrands(String query); 24 | 25 | @Insert(onConflict: OnConflictStrategy.replace) 26 | Future insertBrands(List brands); 27 | 28 | @Insert(onConflict: OnConflictStrategy.replace) 29 | Future insertBrandsWithOrganizations( 30 | List brands, 31 | ); 32 | 33 | @Insert(onConflict: OnConflictStrategy.replace) 34 | Future insertOrganizations(List organizations); 35 | 36 | @Query('DELETE FROM organizations') 37 | Future deleteAllOrganizations(); 38 | 39 | @Query('DELETE FROM brands') 40 | Future deleteAllBrands(); 41 | 42 | @Query('DELETE FROM brands_with_organizations') 43 | Future deleteAllBrandsWithOrganizations(); 44 | 45 | @transaction 46 | Future updateBrands( 47 | List brands, 48 | List brandsWithOrgz, 49 | List orgz, 50 | ) { 51 | deleteAllBrands(); 52 | deleteAllOrganizations(); 53 | deleteAllBrandsWithOrganizations(); 54 | insertBrands(brands); 55 | insertOrganizations(orgz); 56 | insertBrandsWithOrganizations(brandsWithOrgz); 57 | return Future.value(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /data/lib/database/database.dart: -------------------------------------------------------------------------------- 1 | import 'package:data/database/brands_dao.dart'; 2 | import 'package:data/database/model/brand_entity.dart'; 3 | import 'package:data/database/model/brand_with_organization_entity.dart'; 4 | import 'package:data/database/model/organization_entity.dart'; 5 | import 'package:floor/floor.dart'; 6 | import 'list_type_converter.dart'; 7 | 8 | // TODO: fix this when dependencies are upgraded 9 | // ignore: depend_on_referenced_packages 10 | import 'package:sqflite/sqflite.dart' as sqflite; 11 | import 'dart:async'; 12 | 13 | part 'database.g.dart'; 14 | 15 | @TypeConverters([ListTypeConverter]) 16 | @Database( 17 | version: 1, 18 | entities: [BrandEntity, BrandWithOrganizationEntity, OrganizationEntity], 19 | ) 20 | abstract class BunnySearchDatabase extends FloorDatabase { 21 | BrandsDao get brandsDao; 22 | } 23 | -------------------------------------------------------------------------------- /data/lib/database/list_type_converter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:floor/floor.dart'; 4 | 5 | class ListTypeConverter extends TypeConverter, String> { 6 | @override 7 | List decode(String databaseValue) { 8 | return List.from(jsonDecode(databaseValue)); 9 | } 10 | 11 | @override 12 | String encode(List value) { 13 | return jsonEncode(value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /data/lib/database/model/brand_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:floor/floor.dart'; 3 | 4 | @Entity(tableName: 'brands') 5 | class BrandEntity extends Equatable { 6 | @PrimaryKey(autoGenerate: true) 7 | final int? id; 8 | final String title; 9 | final String description; 10 | final List organizationsIds; 11 | final bool? hasVeganProducts; 12 | final String? logoUrl; 13 | final bool popular; 14 | 15 | const BrandEntity({ 16 | this.id, 17 | required this.title, 18 | required this.description, 19 | required this.organizationsIds, 20 | required this.hasVeganProducts, 21 | required this.logoUrl, 22 | required this.popular, 23 | }); 24 | 25 | @override 26 | List get props => [ 27 | title, 28 | description, 29 | organizationsIds, 30 | hasVeganProducts, 31 | logoUrl, 32 | popular, 33 | ]; 34 | } 35 | -------------------------------------------------------------------------------- /data/lib/database/model/brand_with_organization_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:floor/floor.dart'; 3 | 4 | @Entity( 5 | tableName: 'brands_with_organizations', 6 | primaryKeys: ['brand_title', 'org_id'], 7 | ) 8 | class BrandWithOrganizationEntity extends Equatable { 9 | @ColumnInfo(name: 'brand_title') 10 | final String brandTitle; 11 | @ColumnInfo(name: 'org_id') 12 | final String orgId; 13 | 14 | const BrandWithOrganizationEntity({ 15 | required this.brandTitle, 16 | required this.orgId, 17 | }); 18 | 19 | @override 20 | List get props => [brandTitle, orgId]; 21 | } 22 | -------------------------------------------------------------------------------- /data/lib/database/model/organization_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/organizations/model/organization.dart'; 2 | import 'package:domain/organizations/model/organization_type.dart'; 3 | import 'package:equatable/equatable.dart'; 4 | import 'package:floor/floor.dart'; 5 | 6 | @Entity(tableName: 'organizations') 7 | class OrganizationEntity extends Equatable { 8 | @PrimaryKey(autoGenerate: true) 9 | final int? id; 10 | final String orgId; 11 | final String type; 12 | final int brandsCount; 13 | final String website; 14 | 15 | const OrganizationEntity({ 16 | this.id, 17 | required this.orgId, 18 | required this.type, 19 | required this.brandsCount, 20 | required this.website, 21 | }); 22 | 23 | Organization toOrganization() { 24 | return Organization( 25 | id: orgId, 26 | type: _typeFromString(type), 27 | brandsCount: brandsCount, 28 | website: website, 29 | ); 30 | } 31 | 32 | @override 33 | List get props => [id, orgId, type, brandsCount, website]; 34 | } 35 | 36 | OrganizationType _typeFromString(String type) { 37 | switch (type) { 38 | case 'peta_white': 39 | return OrganizationType.petaWhite; 40 | case 'peta_black': 41 | return OrganizationType.petaBlack; 42 | case 'bunny_search': 43 | return OrganizationType.bunnySearch; 44 | } 45 | throw StateError('Unknown type: $type'); 46 | } 47 | -------------------------------------------------------------------------------- /data/lib/organizations/model/firebase_organization.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/organizations/model/organization.dart'; 2 | import 'package:domain/organizations/model/organization_type.dart'; 3 | import 'package:equatable/equatable.dart'; 4 | 5 | class FirebaseOrganisation extends Equatable { 6 | final String id; 7 | final String type; 8 | final int brandsCount; 9 | final String website; 10 | 11 | const FirebaseOrganisation({ 12 | required this.id, 13 | required this.type, 14 | required this.brandsCount, 15 | required this.website, 16 | }); 17 | 18 | @override 19 | List get props => [id, type, brandsCount]; 20 | 21 | factory FirebaseOrganisation.fromJson(Map json) => 22 | FirebaseOrganisation( 23 | id: json['id'] as String, 24 | type: json['type'] as String, 25 | brandsCount: json['brandsCount'] as int, 26 | website: json['website'] as String, 27 | ); 28 | 29 | Map toJson() => { 30 | 'id': id, 31 | 'type': type, 32 | 'brandsCount': brandsCount, 33 | 'website': website 34 | }; 35 | 36 | Organization toOrganization() => Organization( 37 | id: id, 38 | type: _toOrganizationTypeFromString(type), 39 | brandsCount: brandsCount, 40 | website: website, 41 | ); 42 | } 43 | 44 | OrganizationType _toOrganizationTypeFromString(String type) { 45 | switch (type) { 46 | case 'peta_white': 47 | return OrganizationType.petaWhite; 48 | case 'peta_black': 49 | return OrganizationType.petaBlack; 50 | case 'bunny_search': 51 | return OrganizationType.bunnySearch; 52 | } 53 | throw StateError('Unknown type: $type'); 54 | } 55 | -------------------------------------------------------------------------------- /data/lib/organizations/model/firebase_organization_brand.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/organizations/model/organization_brand.dart'; 2 | import 'package:domain/organizations/model/organization_type.dart'; 3 | import 'package:equatable/equatable.dart'; 4 | 5 | class FirebaseOrganizationBrand extends Equatable { 6 | final String id; 7 | final String title; 8 | final String organizationType; 9 | final String organizationWebsite; 10 | final bool? hasVeganProducts; 11 | final String? logoUrl; 12 | 13 | const FirebaseOrganizationBrand({ 14 | required this.id, 15 | required this.title, 16 | required this.organizationType, 17 | required this.organizationWebsite, 18 | required this.hasVeganProducts, 19 | required this.logoUrl, 20 | }); 21 | 22 | factory FirebaseOrganizationBrand.fromJson(Map json) => 23 | FirebaseOrganizationBrand( 24 | id: json['id'] as String, 25 | title: json['title'] as String, 26 | organizationType: json['organizationType'] as String, 27 | organizationWebsite: json['organizationWebsite'] as String, 28 | hasVeganProducts: json['hasVeganProducts'] as bool?, 29 | logoUrl: json['logoUrl'] as String?, 30 | ); 31 | 32 | Map toJson() => { 33 | 'id': id, 34 | 'title': title, 35 | 'organizationType': organizationType, 36 | 'organizationWebsite': organizationWebsite, 37 | 'hasVeganProducts': hasVeganProducts, 38 | 'logoUrl': logoUrl 39 | }; 40 | 41 | OrganizationBrand toOrganizationBrand() => OrganizationBrand( 42 | id: id, 43 | title: title, 44 | organizationType: _toOrganizationTypeFromString(organizationType), 45 | organizationWebsite: organizationWebsite, 46 | hasVeganProducts: hasVeganProducts, 47 | logoUrl: logoUrl, 48 | ); 49 | 50 | @override 51 | List get props => [ 52 | id, 53 | title, 54 | organizationType, 55 | organizationWebsite, 56 | hasVeganProducts, 57 | logoUrl 58 | ]; 59 | } 60 | 61 | OrganizationType _toOrganizationTypeFromString(String type) { 62 | switch (type) { 63 | case 'peta_white': 64 | return OrganizationType.petaWhite; 65 | case 'peta_black': 66 | return OrganizationType.petaBlack; 67 | case 'bunny_search': 68 | return OrganizationType.bunnySearch; 69 | } 70 | throw StateError('Unknown type: $type'); 71 | } 72 | -------------------------------------------------------------------------------- /data/lib/organizations/repository/assets_organizations_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:data/organizations/model/firebase_organization.dart'; 2 | import 'package:data/organizations/model/firebase_organization_brand.dart'; 3 | import 'package:domain/organizations/model/organization.dart'; 4 | import 'package:domain/organizations/model/organization_brand.dart'; 5 | import 'package:domain/organizations/model/organization_type.dart'; 6 | import 'package:domain/organizations/repository/organizations_repository.dart'; 7 | 8 | class AssetsOrganizationsRepository extends OrganizationsRepository { 9 | final dynamic databaseJson; 10 | 11 | AssetsOrganizationsRepository({required this.databaseJson}); 12 | 13 | @override 14 | Future> getAll() async { 15 | final organizations = databaseJson['organizations'] as Map; 16 | return organizations.values 17 | .map( 18 | (e) => FirebaseOrganisation.fromJson(Map.from(e)) 19 | .toOrganization(), 20 | ) 21 | .toList(); 22 | } 23 | 24 | @override 25 | Future> getBrandsById(String id) { 26 | // TODO: implement getBrandsById 27 | throw UnimplementedError(); 28 | } 29 | 30 | @override 31 | Future> getBrandsByType(OrganizationType type) async { 32 | var mappedType = ''; 33 | switch (type) { 34 | case OrganizationType.petaWhite: 35 | mappedType = 'peta_white'; 36 | break; 37 | case OrganizationType.petaBlack: 38 | mappedType = 'peta_black'; 39 | break; 40 | case OrganizationType.bunnySearch: 41 | mappedType = 'bunny_search'; 42 | break; 43 | } 44 | final brands = databaseJson['brands'][mappedType] as Map; 45 | return brands.values 46 | .map( 47 | (e) => 48 | FirebaseOrganizationBrand.fromJson(Map.from(e)) 49 | .toOrganizationBrand(), 50 | ) 51 | .toList(); 52 | } 53 | 54 | @override 55 | Future getById(String id) { 56 | // TODO: implement getById 57 | throw UnimplementedError(); 58 | } 59 | 60 | @override 61 | Future getByType(OrganizationType type) { 62 | // TODO: implement getByType 63 | throw UnimplementedError(); 64 | } 65 | 66 | @override 67 | Future> getPopular() async { 68 | final brands = databaseJson['brands']['popular'] as Map; 69 | return brands.values 70 | .map( 71 | (e) => 72 | FirebaseOrganizationBrand.fromJson(Map.from(e)) 73 | .toOrganizationBrand(), 74 | ) 75 | .toList(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /data/lib/organizations/repository/firebase_organizations_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:data/organizations/model/firebase_organization.dart'; 2 | import 'package:data/organizations/model/firebase_organization_brand.dart'; 3 | import 'package:domain/organizations/model/organization.dart'; 4 | import 'package:domain/organizations/model/organization_brand.dart'; 5 | import 'package:domain/organizations/model/organization_type.dart'; 6 | import 'package:domain/organizations/repository/organizations_repository.dart'; 7 | import 'package:fimber/fimber.dart'; 8 | import 'package:firebase_database/firebase_database.dart'; 9 | 10 | class FirebaseOrganizationsRepository extends OrganizationsRepository { 11 | @override 12 | Future> getAll() async { 13 | try { 14 | final organizations = await FirebaseDatabase.instance 15 | .reference() 16 | .child('organizations') 17 | .once() 18 | .then( 19 | (value) => List.from( 20 | Map.from(value.value).values, 21 | ) 22 | .map( 23 | (e) => FirebaseOrganisation.fromJson( 24 | Map.from(e), 25 | ).toOrganization(), 26 | ) 27 | .toList(), 28 | ); 29 | return organizations; 30 | } catch (ex, st) { 31 | Fimber.e('Failed to load organizations', ex: ex, stacktrace: st); 32 | rethrow; 33 | } 34 | } 35 | 36 | @override 37 | Future getById(String id) { 38 | // TODO: implement getById 39 | throw UnimplementedError(); 40 | } 41 | 42 | @override 43 | Future getByType(OrganizationType type) async { 44 | final orgz = await getAll(); 45 | return orgz.firstWhere((element) => element.type == type); 46 | } 47 | 48 | @override 49 | Future> getBrandsById(String id) async { 50 | try { 51 | final brands = await FirebaseDatabase.instance 52 | .reference() 53 | .child('brands') 54 | .child(id) 55 | .once() 56 | .then( 57 | (value) => List.from( 58 | Map.from(value.value).values, 59 | ) 60 | .map( 61 | (e) => FirebaseOrganizationBrand.fromJson( 62 | Map.from(e), 63 | ).toOrganizationBrand(), 64 | ) 65 | .toList(), 66 | ); 67 | return brands; 68 | } catch (ex, st) { 69 | Fimber.e('Failed to load brands', ex: ex, stacktrace: st); 70 | rethrow; 71 | } 72 | } 73 | 74 | @override 75 | Future> getBrandsByType(OrganizationType type) async { 76 | var mappedType = ''; 77 | switch (type) { 78 | case OrganizationType.petaWhite: 79 | mappedType = 'peta_white'; 80 | break; 81 | case OrganizationType.petaBlack: 82 | mappedType = 'peta_black'; 83 | break; 84 | case OrganizationType.bunnySearch: 85 | mappedType = 'bunny_search'; 86 | break; 87 | } 88 | try { 89 | final brands = await FirebaseDatabase.instance 90 | .reference() 91 | .child('brands') 92 | .child(mappedType) 93 | .once() 94 | .then( 95 | (value) => List.from( 96 | Map.from(value.value).values, 97 | ) 98 | .map( 99 | (e) => FirebaseOrganizationBrand.fromJson( 100 | Map.from(e), 101 | ).toOrganizationBrand(), 102 | ) 103 | .toList(), 104 | ); 105 | return brands; 106 | } catch (ex, st) { 107 | Fimber.e('Failed to load brands', ex: ex, stacktrace: st); 108 | rethrow; 109 | } 110 | } 111 | 112 | @override 113 | Future> getPopular() async { 114 | try { 115 | final brands = await FirebaseDatabase.instance 116 | .reference() 117 | .child('brands') 118 | .child('popular') 119 | .once() 120 | .then( 121 | (value) => List.from( 122 | Map.from(value.value).values, 123 | ) 124 | .map( 125 | (e) => FirebaseOrganizationBrand.fromJson( 126 | Map.from(e), 127 | ).toOrganizationBrand(), 128 | ) 129 | .toList(), 130 | ); 131 | return brands; 132 | } catch (ex, st) { 133 | Fimber.e('Failed to load brands', ex: ex, stacktrace: st); 134 | rethrow; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /data/lib/storage/shared_preferences_key_value_storage.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | import 'package:domain/storage/key_value_storage.dart'; 3 | 4 | class SharedPreferencesKeyValueStorage implements KeyValueStorage { 5 | final SharedPreferences _sharedPreferences; 6 | 7 | SharedPreferencesKeyValueStorage(this._sharedPreferences); 8 | 9 | @override 10 | Future> getKeys() async { 11 | return _sharedPreferences.getKeys(); 12 | } 13 | 14 | @override 15 | Future getString(String key) async { 16 | return _sharedPreferences.getString(key); 17 | } 18 | 19 | @override 20 | Future remove(String key) async { 21 | await _sharedPreferences.remove(key); 22 | } 23 | 24 | @override 25 | Future setString(String key, String value) async { 26 | await _sharedPreferences.setString(key, value); 27 | } 28 | 29 | @override 30 | Future getBool(String key) async { 31 | return _sharedPreferences.getBool(key); 32 | } 33 | 34 | @override 35 | Future getInt(String key) async { 36 | return _sharedPreferences.getInt(key); 37 | } 38 | 39 | @override 40 | Future setBool(String key, bool value) async { 41 | await _sharedPreferences.setBool(key, value); 42 | } 43 | 44 | @override 45 | Future setInt(String key, int value) async { 46 | await _sharedPreferences.setInt(key, value); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /data/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: data 2 | description: data 3 | version: 0.0.1 4 | publish_to: none 5 | 6 | environment: 7 | sdk: ">=2.12.0 <3.0.0" 8 | flutter: ">=1.17.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | domain: 14 | path: ../domain 15 | equatable: ^2.0.3 16 | flutter_secure_storage: ^4.2.1 17 | shared_preferences: ^2.0.7 18 | fimber: ^0.6.1 19 | firebase_core: ^1.6.0 20 | firebase_database: ^7.2.1 21 | collection: ^1.15.0 22 | floor: ^1.2.0 23 | 24 | dev_dependencies: 25 | flutter_test: 26 | sdk: flutter 27 | floor_generator: ^1.2.0 28 | build_runner: ^2.0.5 29 | 30 | # For information on the generic Dart part of this file, see the 31 | # following page: https://dart.dev/tools/pub/pubspec 32 | 33 | # The following section is specific to Flutter. 34 | flutter: 35 | 36 | # To add assets to your package, add an assets section, like this: 37 | # assets: 38 | # - images/a_dot_burr.jpeg 39 | # - images/a_dot_ham.jpeg 40 | # 41 | # For details regarding assets in packages, see 42 | # https://flutter.dev/assets-and-images/#from-packages 43 | # 44 | # An image asset can refer to one or more resolution-specific "variants", see 45 | # https://flutter.dev/assets-and-images/#resolution-aware. 46 | 47 | # To add custom fonts to your package, add a fonts section here, 48 | # in this "flutter" section. Each entry in this list should have a 49 | # "family" key with the font family name, and a "fonts" key with a 50 | # list giving the asset and other descriptors for the font. For 51 | # example: 52 | # fonts: 53 | # - family: Schyler 54 | # fonts: 55 | # - asset: fonts/Schyler-Regular.ttf 56 | # - asset: fonts/Schyler-Italic.ttf 57 | # style: italic 58 | # - family: Trajan Pro 59 | # fonts: 60 | # - asset: fonts/TrajanPro.ttf 61 | # - asset: fonts/TrajanPro_Bold.ttf 62 | # weight: 700 63 | # 64 | # For details regarding fonts in packages, see 65 | # https://flutter.dev/custom-fonts/#from-packages 66 | -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | -------------------------------------------------------------------------------- /domain/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: b1395592de68cc8ac4522094ae59956dd21a91db 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /domain/lib/brands/model/brand.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/organizations/model/organization.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | class Brand extends Equatable { 5 | final String title; 6 | final String description; 7 | final Map organizations; 8 | final bool? hasVeganProducts; 9 | final String? logoUrl; 10 | 11 | const Brand({ 12 | required this.title, 13 | required this.description, 14 | required this.organizations, 15 | required this.hasVeganProducts, 16 | required this.logoUrl, 17 | }); 18 | 19 | Brand copyWith({ 20 | String? title, 21 | String? description, 22 | Map? organizations, 23 | bool? hasVeganProducts, 24 | String? logoUrl, 25 | }) => 26 | Brand( 27 | title: title ?? this.title, 28 | description: description ?? this.description, 29 | organizations: organizations ?? this.organizations, 30 | hasVeganProducts: hasVeganProducts ?? this.hasVeganProducts, 31 | logoUrl: logoUrl, 32 | ); 33 | 34 | @override 35 | List get props => 36 | [title, description, organizations, hasVeganProducts, logoUrl]; 37 | } 38 | -------------------------------------------------------------------------------- /domain/lib/brands/repository/brands_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/brands/model/brand.dart'; 2 | import 'package:domain/organizations/model/organization.dart'; 3 | import 'package:domain/organizations/model/organization_type.dart'; 4 | 5 | abstract class BrandsRepository { 6 | Future loadAllBrands(); 7 | 8 | Future> search(String searchTerm); 9 | 10 | Future> getAllOrganizations(); 11 | 12 | Future> getBrandsByOrganizationType(OrganizationType type); 13 | 14 | Future> getAllPopularBrands(); 15 | } 16 | -------------------------------------------------------------------------------- /domain/lib/organizations/model/organization.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/organizations/model/organization_type.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | class Organization extends Equatable { 5 | final String id; 6 | final OrganizationType type; 7 | final int brandsCount; 8 | final String website; 9 | 10 | const Organization({ 11 | required this.id, 12 | required this.type, 13 | required this.brandsCount, 14 | required this.website, 15 | }); 16 | 17 | @override 18 | List get props => [id, type, brandsCount, website]; 19 | } 20 | -------------------------------------------------------------------------------- /domain/lib/organizations/model/organization_brand.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/organizations/model/organization_type.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | class OrganizationBrand extends Equatable { 5 | final String id; 6 | final String title; 7 | final OrganizationType organizationType; 8 | final String organizationWebsite; 9 | final bool? hasVeganProducts; 10 | final String? logoUrl; 11 | 12 | const OrganizationBrand({ 13 | required this.id, 14 | required this.title, 15 | required this.organizationType, 16 | required this.organizationWebsite, 17 | required this.hasVeganProducts, 18 | required this.logoUrl, 19 | }); 20 | 21 | Map toJson() => { 22 | 'id': id, 23 | 'title': title, 24 | 'organizationType': _toOrganizationTypeString(organizationType), 25 | 'organizationWebsite': organizationWebsite, 26 | 'hasVeganProducts': hasVeganProducts, 27 | 'logoUrl': logoUrl 28 | }; 29 | 30 | @override 31 | List get props => [ 32 | id, 33 | title, 34 | organizationType, 35 | organizationWebsite, 36 | hasVeganProducts, 37 | logoUrl 38 | ]; 39 | } 40 | 41 | String _toOrganizationTypeString(OrganizationType type) { 42 | switch (type) { 43 | case OrganizationType.petaWhite: 44 | return 'peta_white'; 45 | case OrganizationType.petaBlack: 46 | return 'peta_black'; 47 | case OrganizationType.bunnySearch: 48 | return 'bunny_search'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /domain/lib/organizations/model/organization_type.dart: -------------------------------------------------------------------------------- 1 | enum OrganizationType { petaWhite, petaBlack, bunnySearch } 2 | -------------------------------------------------------------------------------- /domain/lib/organizations/repository/organizations_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/organizations/model/organization.dart'; 2 | import 'package:domain/organizations/model/organization_brand.dart'; 3 | import 'package:domain/organizations/model/organization_type.dart'; 4 | 5 | abstract class OrganizationsRepository { 6 | Future> getAll(); 7 | 8 | Future getById(String id); 9 | 10 | Future getByType(OrganizationType type); 11 | 12 | Future> getBrandsById(String id); 13 | 14 | Future> getBrandsByType(OrganizationType type); 15 | 16 | Future> getPopular(); 17 | } 18 | -------------------------------------------------------------------------------- /domain/lib/result/delayed_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/result/result.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | class DelayedResult extends Equatable { 5 | final Result? result; 6 | final bool isInProgress; 7 | 8 | DelayedResult.error(Exception e) 9 | : result = Result.error(e), 10 | isInProgress = false; 11 | 12 | DelayedResult.success([T? result]) 13 | : result = Result.success(result), 14 | isInProgress = false; 15 | 16 | const DelayedResult.inProgress() 17 | : result = null, 18 | isInProgress = true; 19 | 20 | bool get isSuccessful => result != null && result?.isSuccessful == true; 21 | 22 | bool get isError => result != null && result?.isError == true; 23 | 24 | T? get value => result?.value; 25 | 26 | DelayedResult map(R Function(T val) f) { 27 | if (isSuccessful) { 28 | return DelayedResult.success(f(result!.value as T)); 29 | } else if (isError) { 30 | return DelayedResult.error(result!.exception!); 31 | } 32 | 33 | return DelayedResult.inProgress(); 34 | } 35 | 36 | @override 37 | List get props => [result, isInProgress]; 38 | } 39 | -------------------------------------------------------------------------------- /domain/lib/result/result.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Result extends Equatable { 4 | final T? value; 5 | final Exception? exception; 6 | 7 | const Result.error(Exception e) 8 | : value = null, 9 | exception = e; 10 | 11 | const Result.success([T? result]) 12 | : value = result, 13 | exception = null; 14 | 15 | bool get isSuccessful => value != null && exception == null; 16 | 17 | bool get isError => exception != null; 18 | 19 | @override 20 | List get props => [value, exception]; 21 | } 22 | -------------------------------------------------------------------------------- /domain/lib/storage/key_value_storage.dart: -------------------------------------------------------------------------------- 1 | abstract class KeyValueStorage { 2 | Future getString(String key); 3 | 4 | Future setString(String key, String value); 5 | 6 | Future getBool(String key); 7 | 8 | Future setBool(String key, bool value); 9 | 10 | Future getInt(String key); 11 | 12 | Future setInt(String key, int value); 13 | 14 | Future remove(String key); 15 | 16 | Future> getKeys(); 17 | } 18 | -------------------------------------------------------------------------------- /domain/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.18.0" 44 | equatable: 45 | dependency: "direct main" 46 | description: 47 | name: equatable 48 | sha256: c6094fd1efad3046334a9c40bee022147e55c25401ccd89b94e373e3edadd375 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "2.0.3" 52 | fake_async: 53 | dependency: transitive 54 | description: 55 | name: fake_async 56 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.1" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | matcher: 71 | dependency: transitive 72 | description: 73 | name: matcher 74 | sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" 75 | url: "https://pub.dev" 76 | source: hosted 77 | version: "0.12.16" 78 | material_color_utilities: 79 | dependency: transitive 80 | description: 81 | name: material_color_utilities 82 | sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "0.5.0" 86 | meta: 87 | dependency: transitive 88 | description: 89 | name: meta 90 | sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "1.10.0" 94 | path: 95 | dependency: transitive 96 | description: 97 | name: path 98 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "1.8.3" 102 | sky_engine: 103 | dependency: transitive 104 | description: flutter 105 | source: sdk 106 | version: "0.0.99" 107 | source_span: 108 | dependency: transitive 109 | description: 110 | name: source_span 111 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 112 | url: "https://pub.dev" 113 | source: hosted 114 | version: "1.10.0" 115 | stack_trace: 116 | dependency: transitive 117 | description: 118 | name: stack_trace 119 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 120 | url: "https://pub.dev" 121 | source: hosted 122 | version: "1.11.1" 123 | stream_channel: 124 | dependency: transitive 125 | description: 126 | name: stream_channel 127 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 128 | url: "https://pub.dev" 129 | source: hosted 130 | version: "2.1.2" 131 | string_scanner: 132 | dependency: transitive 133 | description: 134 | name: string_scanner 135 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 136 | url: "https://pub.dev" 137 | source: hosted 138 | version: "1.2.0" 139 | term_glyph: 140 | dependency: transitive 141 | description: 142 | name: term_glyph 143 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "1.2.1" 147 | test_api: 148 | dependency: transitive 149 | description: 150 | name: test_api 151 | sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "0.6.1" 155 | vector_math: 156 | dependency: transitive 157 | description: 158 | name: vector_math 159 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "2.1.4" 163 | web: 164 | dependency: transitive 165 | description: 166 | name: web 167 | sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "0.3.0" 171 | sdks: 172 | dart: ">=3.2.0-194.0.dev <4.0.0" 173 | flutter: ">=1.17.0" 174 | -------------------------------------------------------------------------------- /domain/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: domain 2 | description: domain 3 | version: 0.0.1 4 | publish_to: none 5 | 6 | environment: 7 | sdk: ">=2.12.0 <3.0.0" 8 | flutter: ">=1.17.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | equatable: ^2.0.3 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | # For information on the generic Dart part of this file, see the 20 | # following page: https://dart.dev/tools/pub/pubspec 21 | 22 | # The following section is specific to Flutter. 23 | flutter: 24 | 25 | # To add assets to your package, add an assets section, like this: 26 | # assets: 27 | # - images/a_dot_burr.jpeg 28 | # - images/a_dot_ham.jpeg 29 | # 30 | # For details regarding assets in packages, see 31 | # https://flutter.dev/assets-and-images/#from-packages 32 | # 33 | # An image asset can refer to one or more resolution-specific "variants", see 34 | # https://flutter.dev/assets-and-images/#resolution-aware. 35 | 36 | # To add custom fonts to your package, add a fonts section here, 37 | # in this "flutter" section. Each entry in this list should have a 38 | # "family" key with the font family name, and a "fonts" key with a 39 | # list giving the asset and other descriptors for the font. For 40 | # example: 41 | # fonts: 42 | # - family: Schyler 43 | # fonts: 44 | # - asset: fonts/Schyler-Regular.ttf 45 | # - asset: fonts/Schyler-Italic.ttf 46 | # style: italic 47 | # - family: Trajan Pro 48 | # fonts: 49 | # - asset: fonts/TrajanPro.ttf 50 | # - asset: fonts/TrajanPro_Bold.ttf 51 | # weight: 700 52 | # 53 | # For details regarding fonts in packages, see 54 | # https://flutter.dev/custom-fonts/#from-packages 55 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '10.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Firebase/CoreOnly (8.8.0): 3 | - FirebaseCore (= 8.8.0) 4 | - Firebase/Crashlytics (8.8.0): 5 | - Firebase/CoreOnly 6 | - FirebaseCrashlytics (~> 8.8.0) 7 | - Firebase/Database (8.8.0): 8 | - Firebase/CoreOnly 9 | - FirebaseDatabase (~> 8.8.0) 10 | - firebase_core (1.8.0): 11 | - Firebase/CoreOnly (= 8.8.0) 12 | - Flutter 13 | - firebase_crashlytics (2.2.4): 14 | - Firebase/Crashlytics (= 8.8.0) 15 | - firebase_core 16 | - Flutter 17 | - firebase_database (7.2.1): 18 | - Firebase/Database (= 8.8.0) 19 | - firebase_core 20 | - Flutter 21 | - FirebaseCore (8.8.0): 22 | - FirebaseCoreDiagnostics (~> 8.0) 23 | - GoogleUtilities/Environment (~> 7.4) 24 | - GoogleUtilities/Logger (~> 7.4) 25 | - FirebaseCoreDiagnostics (8.8.0): 26 | - GoogleDataTransport (~> 9.0) 27 | - GoogleUtilities/Environment (~> 7.4) 28 | - GoogleUtilities/Logger (~> 7.4) 29 | - nanopb (~> 2.30908.0) 30 | - FirebaseCrashlytics (8.8.0): 31 | - FirebaseCore (~> 8.0) 32 | - FirebaseInstallations (~> 8.0) 33 | - GoogleDataTransport (~> 9.0) 34 | - GoogleUtilities/Environment (~> 7.4) 35 | - nanopb (~> 2.30908.0) 36 | - PromisesObjC (< 3.0, >= 1.2) 37 | - FirebaseDatabase (8.8.0): 38 | - FirebaseCore (~> 8.0) 39 | - leveldb-library (~> 1.22) 40 | - FirebaseInstallations (8.8.0): 41 | - FirebaseCore (~> 8.0) 42 | - GoogleUtilities/Environment (~> 7.4) 43 | - GoogleUtilities/UserDefaults (~> 7.4) 44 | - PromisesObjC (< 3.0, >= 1.2) 45 | - Flutter (1.0.0) 46 | - flutter_secure_storage (3.3.1): 47 | - Flutter 48 | - FMDB (2.7.5): 49 | - FMDB/standard (= 2.7.5) 50 | - FMDB/standard (2.7.5) 51 | - GoogleDataTransport (9.1.2): 52 | - GoogleUtilities/Environment (~> 7.2) 53 | - nanopb (~> 2.30908.0) 54 | - PromisesObjC (< 3.0, >= 1.2) 55 | - GoogleUtilities/Environment (7.6.0): 56 | - PromisesObjC (< 3.0, >= 1.2) 57 | - GoogleUtilities/Logger (7.6.0): 58 | - GoogleUtilities/Environment 59 | - GoogleUtilities/UserDefaults (7.6.0): 60 | - GoogleUtilities/Logger 61 | - leveldb-library (1.22.1) 62 | - nanopb (2.30908.0): 63 | - nanopb/decode (= 2.30908.0) 64 | - nanopb/encode (= 2.30908.0) 65 | - nanopb/decode (2.30908.0) 66 | - nanopb/encode (2.30908.0) 67 | - path_provider (0.0.1): 68 | - Flutter 69 | - PromisesObjC (2.0.0) 70 | - shared_preferences (0.0.1): 71 | - Flutter 72 | - sqflite (0.0.2): 73 | - Flutter 74 | - FMDB (>= 2.7.5) 75 | 76 | DEPENDENCIES: 77 | - firebase_core (from `.symlinks/plugins/firebase_core/ios`) 78 | - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) 79 | - firebase_database (from `.symlinks/plugins/firebase_database/ios`) 80 | - Flutter (from `Flutter`) 81 | - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) 82 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 83 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) 84 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 85 | 86 | SPEC REPOS: 87 | trunk: 88 | - Firebase 89 | - FirebaseCore 90 | - FirebaseCoreDiagnostics 91 | - FirebaseCrashlytics 92 | - FirebaseDatabase 93 | - FirebaseInstallations 94 | - FMDB 95 | - GoogleDataTransport 96 | - GoogleUtilities 97 | - leveldb-library 98 | - nanopb 99 | - PromisesObjC 100 | 101 | EXTERNAL SOURCES: 102 | firebase_core: 103 | :path: ".symlinks/plugins/firebase_core/ios" 104 | firebase_crashlytics: 105 | :path: ".symlinks/plugins/firebase_crashlytics/ios" 106 | firebase_database: 107 | :path: ".symlinks/plugins/firebase_database/ios" 108 | Flutter: 109 | :path: Flutter 110 | flutter_secure_storage: 111 | :path: ".symlinks/plugins/flutter_secure_storage/ios" 112 | path_provider: 113 | :path: ".symlinks/plugins/path_provider/ios" 114 | shared_preferences: 115 | :path: ".symlinks/plugins/shared_preferences/ios" 116 | sqflite: 117 | :path: ".symlinks/plugins/sqflite/ios" 118 | 119 | SPEC CHECKSUMS: 120 | Firebase: 629510f1a9ddb235f3a7c5c8ceb23ba887f0f814 121 | firebase_core: 3b4c707f5a8eff38f52fd5580895bcd89357bf42 122 | firebase_crashlytics: 5e4c7b5695a7ffe144a55dacfddebbf8eb36028a 123 | firebase_database: bdd8c57a8613616cb6749b8656e38820e596d9ec 124 | FirebaseCore: 98b29e3828f0a53651c363937a7f7d92a19f1ba2 125 | FirebaseCoreDiagnostics: fe77f42da6329d6d83d21fd9d621a6b704413bfc 126 | FirebaseCrashlytics: 3660c045c8e45cc4276110562a0ef44cf43c8157 127 | FirebaseDatabase: bc4610ff3a816dcc680c1dbccf7f9f4bd94ac07d 128 | FirebaseInstallations: 2563cb18a723ef9c6ef18318a49519b75dce613c 129 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 130 | flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec 131 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 132 | GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 133 | GoogleUtilities: 684ee790a24f73ebb2d1d966e9711c203f2a4237 134 | leveldb-library: 50c7b45cbd7bf543c81a468fe557a16ae3db8729 135 | nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 136 | path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c 137 | PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 138 | shared_preferences: 5033afbb22d372e15aff8ff766df9021b845f273 139 | sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 140 | 141 | PODFILE CHECKSUM: fe0e1ee7f3d1f7d00b11b474b62dd62134535aea 142 | 143 | COCOAPODS: 1.14.3 144 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "LaunchImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "LaunchImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleLocalizations 14 | 15 | en 16 | ru 17 | 18 | CFBundleName 19 | Bunny Search 20 | CFBundlePackageType 21 | APPL 22 | CFBundleShortVersionString 23 | $(FLUTTER_BUILD_NAME) 24 | CFBundleSignature 25 | ???? 26 | CFBundleVersion 27 | $(FLUTTER_BUILD_NUMBER) 28 | LSRequiresIPhoneOS 29 | 30 | UILaunchStoryboardName 31 | LaunchScreen 32 | UIMainStoryboardFile 33 | Main 34 | UISupportedInterfaceOrientations 35 | 36 | UIInterfaceOrientationPortrait 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UIViewControllerBasedStatusBarAppearance 48 | 49 | CADisableMinimumFrameDurationOnPhone 50 | 51 | UIApplicationSupportsIndirectInputEvents 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/analytics/bloc_error_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:fimber/fimber.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | class BlocErrorObserver extends BlocObserver { 5 | @override 6 | void onError(BlocBase bloc, Object error, StackTrace stackTrace) { 7 | Fimber.w('${bloc.runtimeType} err: $error', stacktrace: stackTrace); 8 | super.onError(bloc, error, stackTrace); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/analytics/crashlytics_fimber_tree.dart: -------------------------------------------------------------------------------- 1 | import 'package:fimber/fimber.dart'; 2 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 3 | 4 | // Fimber tree for logging non-fatal errors in release builds. 5 | class CrashlyticsFimberTree extends LogTree { 6 | // Only report Warnings and Errors. 7 | static const List defaultLevels = ['W', 'E']; 8 | 9 | @override 10 | void log( 11 | String level, 12 | String message, { 13 | String? tag, 14 | dynamic ex, 15 | StackTrace? stacktrace, 16 | }) { 17 | FirebaseCrashlytics.instance.log(message); 18 | FirebaseCrashlytics.instance 19 | .recordError(ex ?? Exception(message), stacktrace); 20 | } 21 | 22 | @override 23 | List getLevels() => defaultLevels; 24 | } 25 | -------------------------------------------------------------------------------- /lib/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/app_routes.dart'; 2 | import 'package:bunny_search/home/widget/home_page.dart'; 3 | import 'package:bunny_search/theme/app_colors.dart'; 4 | import 'package:bunny_search/utils/widget/focus_utils.dart'; 5 | import 'package:easy_localization/easy_localization.dart'; 6 | import 'package:bunny_search/generated/locale_keys.g.dart'; 7 | import 'package:flutter/cupertino.dart'; 8 | import 'package:flutter/material.dart'; 9 | 10 | class App extends StatefulWidget { 11 | const App({Key? key}) : super(key: key); 12 | 13 | @override 14 | State createState() => _AppState(); 15 | } 16 | 17 | class _AppState extends State with WidgetsBindingObserver { 18 | @override 19 | Widget build(BuildContext context) { 20 | final easyLocalization = EasyLocalization.of(context)!; 21 | return GestureDetector( 22 | onTap: () => FocusUtils.unfocus(context), 23 | child: MaterialApp( 24 | onGenerateTitle: (BuildContext context) => tr(LocaleKeys.app_title), 25 | localizationsDelegates: easyLocalization.delegates, 26 | supportedLocales: easyLocalization.supportedLocales, 27 | locale: easyLocalization.locale, 28 | home: HomePage.withBloc(), 29 | theme: ThemeData( 30 | useMaterial3: false, 31 | cupertinoOverrideTheme: CupertinoThemeData().copyWith( 32 | primaryColor: AppColors.rose, 33 | textTheme: 34 | CupertinoTextThemeData().copyWith(primaryColor: AppColors.rose), 35 | ), 36 | textSelectionTheme: TextSelectionThemeData( 37 | cursorColor: AppColors.rose, 38 | selectionColor: AppColors.rose.withOpacity(0.5), 39 | selectionHandleColor: AppColors.rose.withOpacity(0.5), 40 | ), 41 | colorScheme: ColorScheme.fromSwatch().copyWith( 42 | primary: AppColors.rose, 43 | secondary: AppColors.inactive, 44 | ), 45 | ), 46 | initialRoute: AppRoutes.root, 47 | onGenerateRoute: (settings) { 48 | switch (settings.name) { 49 | case AppRoutes.root: 50 | return MaterialPageRoute( 51 | settings: const RouteSettings(name: AppRoutes.root), 52 | builder: (settings) => _PlaceholderContainer(), 53 | ); 54 | default: 55 | return MaterialPageRoute( 56 | settings: const RouteSettings(name: AppRoutes.root), 57 | builder: (settings) => _PlaceholderContainer(), 58 | ); 59 | } 60 | }, 61 | ), 62 | ); 63 | } 64 | } 65 | 66 | class _PlaceholderContainer extends StatelessWidget { 67 | const _PlaceholderContainer({Key? key}) : super(key: key); 68 | 69 | @override 70 | Widget build(BuildContext context) { 71 | return Container( 72 | width: MediaQuery.of(context).size.width, 73 | decoration: BoxDecoration( 74 | gradient: LinearGradient( 75 | begin: Alignment.topLeft, 76 | end: Alignment.bottomRight, 77 | colors: [Color(0xFFD8CCFF), Color(0xFFA3E3FF)], 78 | tileMode: TileMode.clamp, 79 | ), 80 | ), 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/app_routes.dart: -------------------------------------------------------------------------------- 1 | class AppRoutes { 2 | static const root = '/'; 3 | static const main = '/main'; 4 | } -------------------------------------------------------------------------------- /lib/brand/bloc/popular_brands_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/brands/model/brand.dart'; 2 | import 'package:domain/brands/repository/brands_repository.dart'; 3 | import 'package:domain/result/delayed_result.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:fimber/fimber.dart'; 6 | import 'package:flutter_bloc/flutter_bloc.dart'; 7 | 8 | abstract class PopularBrandsBlocEvent extends Equatable { 9 | const PopularBrandsBlocEvent(); 10 | 11 | @override 12 | List get props => []; 13 | } 14 | 15 | class LoadBrandsEvent extends PopularBrandsBlocEvent {} 16 | 17 | class PopularBrandsState extends Equatable { 18 | final DelayedResult> brandsResult; 19 | 20 | const PopularBrandsState({required this.brandsResult}); 21 | 22 | PopularBrandsState copyWith({DelayedResult>? brandsResult}) => 23 | PopularBrandsState(brandsResult: brandsResult ?? this.brandsResult); 24 | 25 | @override 26 | List get props => [brandsResult]; 27 | } 28 | 29 | class PopularBrandBloc 30 | extends Bloc { 31 | final BrandsRepository brandsRepository; 32 | 33 | PopularBrandBloc({required this.brandsRepository}) 34 | : super( 35 | const PopularBrandsState(brandsResult: DelayedResult.inProgress()), 36 | ); 37 | 38 | @override 39 | Stream mapEventToState( 40 | PopularBrandsBlocEvent event, 41 | ) async* { 42 | if (event is LoadBrandsEvent) { 43 | yield* _mapLoadBrandsEventToState(event); 44 | } 45 | } 46 | 47 | Stream _mapLoadBrandsEventToState( 48 | LoadBrandsEvent event, 49 | ) async* { 50 | yield state.copyWith(brandsResult: const DelayedResult.inProgress()); 51 | 52 | try { 53 | final brands = await brandsRepository.getAllPopularBrands(); 54 | yield state.copyWith(brandsResult: DelayedResult.success(brands)); 55 | } on Exception catch (ex, st) { 56 | Fimber.e('Failed to load popular brands', ex: ex, stacktrace: st); 57 | yield state.copyWith(brandsResult: DelayedResult.error(ex)); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/brand/widget/brand_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/theme/app_colors.dart'; 2 | import 'package:bunny_search/theme/app_typography.dart'; 3 | import 'package:bunny_search/theme/bunny_cached_logo_image.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_svg/flutter_svg.dart'; 7 | 8 | class BrandListItem extends StatelessWidget { 9 | final String title; 10 | final String filters; 11 | final String logoUrl; 12 | 13 | const BrandListItem({ 14 | Key? key, 15 | required this.title, 16 | required this.filters, 17 | required this.logoUrl, 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Container( 23 | margin: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4), 24 | child: Row( 25 | mainAxisAlignment: MainAxisAlignment.start, 26 | children: [ 27 | Container( 28 | width: 72, 29 | height: 72, 30 | padding: const EdgeInsets.all(12), 31 | decoration: BoxDecoration( 32 | color: AppColors.background, 33 | borderRadius: BorderRadius.circular(20), 34 | ), 35 | child: Center( 36 | child: logoUrl.isEmpty 37 | ? Text( 38 | title.substring(0, 1), 39 | style: AppTypography.header 40 | .copyWith(fontWeight: FontWeight.bold), 41 | ) 42 | : logoUrl.endsWith('svg') 43 | ? SvgPicture.network( 44 | logoUrl, 45 | placeholderBuilder: (context) => Text( 46 | title.substring(0, 1), 47 | style: AppTypography.header 48 | .copyWith(fontWeight: FontWeight.bold), 49 | ), 50 | ) 51 | : BunnyCachedLogoImage( 52 | logoUrl: logoUrl, 53 | title: title, 54 | ), 55 | ), 56 | ), 57 | const SizedBox( 58 | width: 16, 59 | ), 60 | Expanded( 61 | child: Column( 62 | mainAxisAlignment: MainAxisAlignment.center, 63 | crossAxisAlignment: CrossAxisAlignment.start, 64 | mainAxisSize: MainAxisSize.min, 65 | children: [ 66 | Flexible( 67 | fit: FlexFit.loose, 68 | child: Text( 69 | title, 70 | style: AppTypography.medium, 71 | maxLines: 1, 72 | overflow: TextOverflow.ellipsis, 73 | ), 74 | ), 75 | const SizedBox( 76 | height: 4, 77 | ), 78 | Text( 79 | filters, 80 | style: AppTypography.caption, 81 | ) 82 | ], 83 | ), 84 | ) 85 | ], 86 | ), 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/brand/widget/popular_brands_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/brand/bloc/popular_brands_bloc.dart'; 2 | import 'package:bunny_search/brand/widget/brand_details_page.dart'; 3 | import 'package:bunny_search/brand/widget/brand_list_item.dart'; 4 | import 'package:bunny_search/theme/app_colors.dart'; 5 | import 'package:bunny_search/theme/app_typography.dart'; 6 | import 'package:bunny_search/theme/bunny_appbar_back_button.dart'; 7 | import 'package:bunny_search/theme/bunny_snack_bar.dart'; 8 | import 'package:domain/brands/model/brand.dart'; 9 | import 'package:domain/organizations/model/organization.dart'; 10 | import 'package:domain/organizations/model/organization_type.dart'; 11 | import 'package:flutter/material.dart'; 12 | import 'package:flutter_bloc/flutter_bloc.dart'; 13 | import 'package:easy_localization/easy_localization.dart'; 14 | import 'package:bunny_search/generated/locale_keys.g.dart'; 15 | 16 | class PopularBrandsPage extends StatefulWidget { 17 | const PopularBrandsPage({Key? key}) : super(key: key); 18 | 19 | @override 20 | State createState() => _PopularBrandsPageState(); 21 | 22 | static Widget withBloc() => BlocProvider( 23 | create: (context) => PopularBrandBloc(brandsRepository: context.read()) 24 | ..add(LoadBrandsEvent()), 25 | child: const PopularBrandsPage(), 26 | ); 27 | } 28 | 29 | class _PopularBrandsPageState extends State { 30 | late PopularBrandBloc _bloc; 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | _bloc = context.read(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return BlocConsumer( 41 | listener: (context, state) { 42 | final isError = state.brandsResult.isError == true; 43 | if (isError) { 44 | _handleError(); 45 | } 46 | }, 47 | builder: (context, state) { 48 | final progress = state.brandsResult.isInProgress; 49 | final brands = state.brandsResult.isSuccessful 50 | ? state.brandsResult.value ?? [] 51 | : []; 52 | return Scaffold( 53 | backgroundColor: AppColors.white, 54 | appBar: AppBar( 55 | centerTitle: true, 56 | backgroundColor: AppColors.white, 57 | elevation: 0, 58 | leading: const BunnyAppBarBackButton(), 59 | title: Column( 60 | children: [ 61 | Text( 62 | LocaleKeys.popular_brands_title.tr(), 63 | style: AppTypography.h4, 64 | ), 65 | ], 66 | ), 67 | ), 68 | body: progress 69 | ? const Center( 70 | child: CircularProgressIndicator( 71 | color: AppColors.rose, 72 | ), 73 | ) 74 | : SafeArea( 75 | child: ListView.builder( 76 | padding: const EdgeInsets.only(bottom: 24), 77 | itemBuilder: (context, pos) { 78 | Brand brand = brands[pos]; 79 | return TextButton( 80 | style: ButtonStyle( 81 | overlayColor: MaterialStateProperty.all( 82 | AppColors.rose.withOpacity(0.05), 83 | ), 84 | ), 85 | onPressed: () { 86 | Navigator.of(context).push( 87 | MaterialPageRoute( 88 | builder: (context) => 89 | BrandDetailsPage(brand: brand), 90 | ), 91 | ); 92 | }, 93 | child: BrandListItem( 94 | title: brand.title, 95 | filters: _buildFiltersString( 96 | brand.organizations.values.toList(), 97 | ), 98 | logoUrl: brand.logoUrl ?? '', 99 | ), 100 | ); 101 | }, 102 | itemCount: brands.length, 103 | ), 104 | ), 105 | ); 106 | }, 107 | ); 108 | } 109 | 110 | String _buildFiltersString(List organizations) { 111 | return organizations.map((o) => _organizationTypeToString(o.type)).join(' • '); 112 | } 113 | 114 | String _organizationTypeToString(OrganizationType type) { 115 | switch (type) { 116 | case OrganizationType.petaWhite: 117 | return LocaleKeys.organization_peta_dont_test.tr(); 118 | case OrganizationType.petaBlack: 119 | return LocaleKeys.organization_peta_do_test.tr(); 120 | case OrganizationType.bunnySearch: 121 | return LocaleKeys.organization_bunny_search.tr(); 122 | } 123 | } 124 | 125 | void _handleError() { 126 | ScaffoldMessenger.of(context).showSnackBar( 127 | BunnyDefaultSnackBar( 128 | text: LocaleKeys.general_error.tr(), 129 | onRetry: () => _bloc.add(LoadBrandsEvent()), 130 | ), 131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/generated/codegen_loader.g.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:easy_localization/generate.dart 2 | 3 | // ignore_for_file: prefer_single_quotes 4 | 5 | import 'dart:ui'; 6 | 7 | import 'package:easy_localization/easy_localization.dart' show AssetLoader; 8 | 9 | class CodegenLoader extends AssetLoader{ 10 | const CodegenLoader(); 11 | 12 | @override 13 | Future> load(String path, Locale locale ) { 14 | return Future.value(mapLocales[locale.toString()]); 15 | } 16 | 17 | static const Map ru = { 18 | "app_title": "Bunny Search", 19 | "splash_loading_brands": "Загружаем бренды...", 20 | "home_organizations": "Организации", 21 | "home_show_all": "Все", 22 | "home_popular_brands": "Популярные бренды", 23 | "home_results": "Результаты", 24 | "home_support_dialog_title": "Привет!", 25 | "home_support_dialog_content": "Thanks for using the app and making the effort to choose cruelty-free! 💖️\n️\nWe are very happy to help make that choice easier for you, but there is one little thing we ask you to keep in mind.\n\nWe are a very small team of 3: a data manager, a designer, and an app developer. \n\nFurthermore, we are working on this app only in our free time, and we’re trying our best to keep the information up to date, communicate with the brands, get the official permissions from the organizations to show their data, and fix bugs! \n\nThe app is and will be completely free and doesn't contain any ads or affiliate links.\n\nWe are sorry if the brand you are looking for is missing from our lists, or you encounter any problems while using the app. \n\nPlease feel free to reach out to us about any concerns via bunnysearchmobileapp@gmail.com, and we will be happy to help!\n\nStay tuned for the updates, and thank you for choosing cruelty-free 🐰", 26 | "home_support_dialog_team": "Твоя команда Bunny Search 🐇🔍", 27 | "home_support_dialog_close_button": "Понятно!", 28 | "home_search_prompt": "Давай найдем\nэтичные бренды", 29 | "home_search_hint": "Начать поиск...", 30 | "organization_peta_do_test": "PETA Неэтичный", 31 | "organization_peta_dont_test": "PETA Этичный", 32 | "organization_bunny_search": "Bunny Search", 33 | "brand_details_cf_markers": "Маркеры этичности", 34 | "brand_details_peta_dont_test_marker": "В этичном списке PETA", 35 | "brand_details_peta_do_test_marker": "В неэтичном списке PETA", 36 | "brand_details_bunny_search_marker": "Проверен командой Bunny Search", 37 | "brand_details_other_markers": "Другие маркеры", 38 | "brand_details_vegan_marker": "У бренда есть vegan продукты 🌱", 39 | "brand_details_based_on": "Источник: {source}", 40 | "organization_brands_count": "{count} брендов", 41 | "organizations_title": "Организации", 42 | "popular_brands_title": "Популярные бренды", 43 | "general_error": "Что-то пошло не так :(" 44 | }; 45 | static const Map en = { 46 | "app_title": "Bunny Search", 47 | "splash_loading_brands": "Loading beauty brands...", 48 | "home_organizations": "Organizations", 49 | "home_show_all": "Show All", 50 | "home_popular_brands": "Popular brands", 51 | "home_results": "Results", 52 | "home_support_dialog_title": "Welcome!", 53 | "home_support_dialog_content": "Thanks for using the app and making the effort to choose cruelty-free! 💖️\n️\nWe are very happy to help make that choice easier for you, but there is one little thing we ask you to keep in mind.\n\nWe are a very small team of 3: a data manager, a designer, and an app developer. \n\nFurthermore, we are working on this app only in our free time, and we’re trying our best to keep the information up to date, communicate with the brands, get the official permissions from the organizations to show their data, and fix bugs! \n\nThe app is and will be completely free and doesn't contain any ads or affiliate links.\n\nWe are sorry if the brand you are looking for is missing from our lists, or you encounter any problems while using the app. \n\nPlease feel free to reach out to us about any concerns via bunnysearchmobileapp@gmail.com, and we will be happy to help!\n\nStay tuned for the updates, and thank you for choosing cruelty-free 🐰", 54 | "home_support_dialog_team": "Your Bunny Search Team 🐇🔍", 55 | "home_support_dialog_close_button": "Got it!", 56 | "home_search_prompt": "Let's find\ncruelty free brands", 57 | "home_search_hint": "Start brand search", 58 | "organization_peta_do_test": "PETA Do Test", 59 | "organization_peta_dont_test": "PETA Don't Test", 60 | "organization_bunny_search": "Bunny Search", 61 | "brand_details_cf_markers": "Cruelty-free markers", 62 | "brand_details_peta_dont_test_marker": "Is in PETA Don't Test list", 63 | "brand_details_peta_do_test_marker": "Is in PETA Do Test list", 64 | "brand_details_bunny_search_marker": "Approved by the Bunny Search team", 65 | "brand_details_other_markers": "Other markers", 66 | "brand_details_vegan_marker": "Brand has vegan products 🌱", 67 | "brand_details_based_on": "Based on: {source}", 68 | "organization_brands_count": "{count} brands", 69 | "organizations_title": "Organizations", 70 | "popular_brands_title": "Popular brands", 71 | "general_error": "Something went wrong :(" 72 | }; 73 | static const Map> mapLocales = {"ru": ru, "en": en}; 74 | } 75 | -------------------------------------------------------------------------------- /lib/generated/locale_keys.g.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:easy_localization/generate.dart 2 | 3 | // ignore_for_file: constant_identifier_names 4 | 5 | abstract class LocaleKeys { 6 | static const app_title = 'app_title'; 7 | static const splash_loading_brands = 'splash_loading_brands'; 8 | static const home_organizations = 'home_organizations'; 9 | static const home_show_all = 'home_show_all'; 10 | static const home_popular_brands = 'home_popular_brands'; 11 | static const home_results = 'home_results'; 12 | static const home_support_dialog_title = 'home_support_dialog_title'; 13 | static const home_support_dialog_content = 'home_support_dialog_content'; 14 | static const home_support_dialog_team = 'home_support_dialog_team'; 15 | static const home_support_dialog_close_button = 'home_support_dialog_close_button'; 16 | static const home_search_prompt = 'home_search_prompt'; 17 | static const home_search_hint = 'home_search_hint'; 18 | static const organization_peta_do_test = 'organization_peta_do_test'; 19 | static const organization_peta_dont_test = 'organization_peta_dont_test'; 20 | static const organization_bunny_search = 'organization_bunny_search'; 21 | static const brand_details_cf_markers = 'brand_details_cf_markers'; 22 | static const brand_details_peta_dont_test_marker = 'brand_details_peta_dont_test_marker'; 23 | static const brand_details_peta_do_test_marker = 'brand_details_peta_do_test_marker'; 24 | static const brand_details_bunny_search_marker = 'brand_details_bunny_search_marker'; 25 | static const brand_details_other_markers = 'brand_details_other_markers'; 26 | static const brand_details_vegan_marker = 'brand_details_vegan_marker'; 27 | static const brand_details_based_on = 'brand_details_based_on'; 28 | static const organization_brands_count = 'organization_brands_count'; 29 | static const organizations_title = 'organizations_title'; 30 | static const popular_brands_title = 'popular_brands_title'; 31 | static const general_error = 'general_error'; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /lib/home/bloc/home_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/organization/model/organization_details.dart'; 2 | import 'package:bunny_search/organization/model/organizations_mapper.dart'; 3 | import 'package:domain/brands/model/brand.dart'; 4 | import 'package:domain/brands/repository/brands_repository.dart'; 5 | import 'package:domain/result/delayed_result.dart'; 6 | import 'package:domain/storage/key_value_storage.dart'; 7 | import 'package:equatable/equatable.dart'; 8 | import 'package:fimber/fimber.dart'; 9 | import 'package:flutter_bloc/flutter_bloc.dart'; 10 | import 'package:quiver/core.dart'; 11 | import 'package:rxdart/rxdart.dart'; 12 | 13 | const _keySupportDialogShown = 'key_show_support_dialog'; 14 | 15 | abstract class HomeBlocEvent extends Equatable { 16 | @override 17 | List get props => []; 18 | } 19 | 20 | class LoadEvent extends HomeBlocEvent {} 21 | 22 | class SearchEvent extends HomeBlocEvent { 23 | final String searchTerm; 24 | 25 | SearchEvent({required this.searchTerm}); 26 | 27 | @override 28 | List get props => [searchTerm]; 29 | } 30 | 31 | class SetSupportDialogShownEvent extends HomeBlocEvent {} 32 | 33 | class HomeBlocState extends Equatable { 34 | final String searchTerm; 35 | final Optional>> searchResult; 36 | final DelayedResult> organizationsResult; 37 | final List popularBrands; 38 | final bool showSupportDialog; 39 | 40 | const HomeBlocState({ 41 | required this.searchTerm, 42 | required this.searchResult, 43 | required this.organizationsResult, 44 | required this.popularBrands, 45 | required this.showSupportDialog, 46 | }); 47 | 48 | HomeBlocState copyWith({ 49 | String? searchTerm, 50 | Optional>>? searchResult, 51 | DelayedResult>? organizationsResult, 52 | List? popularBrands, 53 | bool? showSupportDialog, 54 | }) => 55 | HomeBlocState( 56 | searchTerm: searchTerm ?? this.searchTerm, 57 | searchResult: searchResult ?? this.searchResult, 58 | organizationsResult: organizationsResult ?? this.organizationsResult, 59 | popularBrands: popularBrands ?? this.popularBrands, 60 | showSupportDialog: showSupportDialog ?? this.showSupportDialog, 61 | ); 62 | 63 | @override 64 | List get props => [ 65 | searchTerm, 66 | searchResult, 67 | organizationsResult, 68 | popularBrands, 69 | showSupportDialog 70 | ]; 71 | } 72 | 73 | class HomeBloc extends Bloc { 74 | final BrandsRepository brandsRepository; 75 | final KeyValueStorage keyValueStorage; 76 | 77 | HomeBloc({required this.brandsRepository, required this.keyValueStorage}) 78 | : super( 79 | const HomeBlocState( 80 | searchTerm: '', 81 | searchResult: Optional.absent(), 82 | organizationsResult: DelayedResult.inProgress(), 83 | popularBrands: [], 84 | showSupportDialog: false, 85 | ), 86 | ); 87 | 88 | @override 89 | Stream mapEventToState(HomeBlocEvent event) async* { 90 | if (event is LoadEvent) { 91 | yield* _mapLoadEventToState(); 92 | } else if (event is SearchEvent) { 93 | yield* _mapSearchEventToState(event); 94 | } else if (event is SetSupportDialogShownEvent) { 95 | await keyValueStorage.setBool(_keySupportDialogShown, true); 96 | yield state.copyWith(showSupportDialog: false); 97 | } 98 | } 99 | 100 | @override 101 | Stream> transformEvents( 102 | Stream events, 103 | TransitionFunction transitionFn, 104 | ) { 105 | final nonTransformedStream = events.where((event) => event is! SearchEvent); 106 | 107 | final debounceSetSearchTermStream = events 108 | .where((event) => event is SearchEvent) 109 | .debounceTime(const Duration(milliseconds: 500)); 110 | 111 | return super.transformEvents( 112 | MergeStream([ 113 | nonTransformedStream, 114 | debounceSetSearchTermStream, 115 | ]), 116 | transitionFn, 117 | ); 118 | } 119 | 120 | Stream _mapLoadEventToState() async* { 121 | yield state.copyWith(organizationsResult: const DelayedResult.inProgress()); 122 | 123 | try { 124 | // TODO when searching also make sure db is available 125 | final supportDialogShown = 126 | (await keyValueStorage.getBool(_keySupportDialogShown)) == true; 127 | await brandsRepository.loadAllBrands(); 128 | final organizations = await brandsRepository.getAllOrganizations(); 129 | final mapped = organizations 130 | .map((o) => OrganizationsMapper.toOrganizationDetails(o)) 131 | .toList(); 132 | final popular = await brandsRepository.getAllPopularBrands(); 133 | yield state.copyWith( 134 | organizationsResult: DelayedResult.success(mapped), 135 | popularBrands: popular.take(15).toList(), 136 | showSupportDialog: !supportDialogShown, 137 | ); 138 | } on Exception catch (ex, st) { 139 | Fimber.e('Failed to load organizations', ex: ex, stacktrace: st); 140 | yield state.copyWith(organizationsResult: DelayedResult.error(ex)); 141 | } 142 | } 143 | 144 | Stream _mapSearchEventToState(SearchEvent event) async* { 145 | if (event.searchTerm.isEmpty) { 146 | yield state.copyWith( 147 | searchTerm: event.searchTerm, 148 | searchResult: const Optional.absent(), 149 | ); 150 | return; 151 | } 152 | 153 | yield state.copyWith( 154 | searchTerm: event.searchTerm, 155 | searchResult: Optional.of(const DelayedResult.inProgress()), 156 | ); 157 | 158 | try { 159 | final results = await brandsRepository.search(event.searchTerm); 160 | yield state.copyWith( 161 | searchResult: Optional.of(DelayedResult.success(results)), 162 | ); 163 | } on Exception catch (ex, st) { 164 | Fimber.w( 165 | 'Failed to find brand: ${event.searchTerm}', 166 | ex: ex, 167 | stacktrace: st, 168 | ); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /lib/home/widget/background_wave_clipper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BackgroundWaveClipper extends CustomClipper { 4 | BackgroundWaveClipper(); 5 | 6 | @override 7 | Path getClip(Size size) { 8 | var path = Path(); 9 | final p1Diff = ((140 - size.height) * 0.28).truncate().abs(); 10 | path.lineTo(0.0, size.height - p1Diff); 11 | final p2Diff = ((140 - size.height) * 1).truncate().abs(); 12 | final endPoint = Offset(size.width, size.height - p2Diff); 13 | final p3Diff = ((140 - size.height) * 0.72).truncate().abs(); 14 | final firstControlPoint = Offset(-100, size.height - p3Diff); 15 | final p4Diff = ((140 - size.height) * 0.575).abs(); 16 | final secondControlPoint = Offset(size.width / 2.5, size.height + p4Diff); 17 | path.cubicTo( 18 | firstControlPoint.dx, 19 | firstControlPoint.dy, 20 | secondControlPoint.dx, 21 | secondControlPoint.dy, 22 | endPoint.dx, 23 | endPoint.dy, 24 | ); 25 | path.lineTo(size.width, 0.0); 26 | 27 | path.close(); 28 | 29 | return path; 30 | } 31 | 32 | @override 33 | bool shouldReclip(CustomClipper oldClipper) => oldClipper != this; 34 | } 35 | -------------------------------------------------------------------------------- /lib/home/widget/home_brands_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/brand/widget/brand_details_page.dart'; 2 | import 'package:bunny_search/brand/widget/brand_list_item.dart'; 3 | import 'package:bunny_search/organization/model/organizations_mapper.dart'; 4 | import 'package:bunny_search/theme/app_colors.dart'; 5 | import 'package:domain/brands/model/brand.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class HomeBrandsList extends StatelessWidget { 9 | final List brands; 10 | 11 | const HomeBrandsList({Key? key, required this.brands}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return SliverList( 16 | delegate: SliverChildBuilderDelegate( 17 | (BuildContext context, int index) { 18 | final brand = brands[index]; 19 | return TextButton( 20 | style: ButtonStyle( 21 | overlayColor: 22 | MaterialStateProperty.all(AppColors.rose.withOpacity(0.05)), 23 | ), 24 | onPressed: () { 25 | Navigator.of(context).push( 26 | MaterialPageRoute( 27 | builder: (context) => BrandDetailsPage(brand: brand), 28 | ), 29 | ); 30 | }, 31 | child: BrandListItem( 32 | title: brand.title, 33 | filters: OrganizationsMapper.organizationsToString( 34 | brand.organizations.values.toList(), 35 | ), 36 | logoUrl: brand.logoUrl ?? '', 37 | ), 38 | ); 39 | }, 40 | childCount: brands.length, 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/home/widget/home_content_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/generated/locale_keys.g.dart'; 2 | import 'package:bunny_search/home/widget/home_brands_list.dart'; 3 | import 'package:bunny_search/home/widget/home_organizations_section.dart'; 4 | import 'package:bunny_search/home/widget/home_popular_brands_section.dart'; 5 | import 'package:bunny_search/home/widget/no_overscroll_behaviour.dart'; 6 | import 'package:bunny_search/home/widget/search_bar.dart'; 7 | import 'package:bunny_search/home/widget/sliver_search_app_bar.dart'; 8 | import 'package:bunny_search/organization/model/organization_details.dart'; 9 | import 'package:bunny_search/theme/app_colors.dart'; 10 | import 'package:bunny_search/theme/app_typography.dart'; 11 | import 'package:domain/brands/model/brand.dart'; 12 | import 'package:easy_localization/easy_localization.dart'; 13 | import 'package:flutter/material.dart'; 14 | 15 | class HomeContentScreen extends StatelessWidget { 16 | final bool showProgress; 17 | final bool showOrganizations; 18 | final bool showPopularBrands; 19 | final bool showSearchResults; 20 | final List searchBrands; 21 | final List popularBrands; 22 | final List organizations; 23 | final OnSearchTermChanged onSearchTermChanged; 24 | 25 | const HomeContentScreen({ 26 | Key? key, 27 | required this.showProgress, 28 | required this.showOrganizations, 29 | required this.showPopularBrands, 30 | required this.showSearchResults, 31 | required this.searchBrands, 32 | required this.popularBrands, 33 | required this.organizations, 34 | required this.onSearchTermChanged, 35 | }) : super(key: key); 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return Scaffold( 40 | backgroundColor: Colors.white, 41 | body: NestedScrollView( 42 | headerSliverBuilder: (BuildContext context, bool innerBoxScrolled) { 43 | return [ 44 | SliverOverlapAbsorber( 45 | handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), 46 | sliver: SliverPersistentHeader( 47 | delegate: SliverSearchAppBar( 48 | onSearchTermChanged: onSearchTermChanged, 49 | ), 50 | pinned: true, 51 | ), 52 | ), 53 | ]; 54 | }, 55 | body: Builder( 56 | builder: (BuildContext context) => CustomScrollView( 57 | key: const ValueKey('home_scroll_view'), 58 | scrollBehavior: NoOverscrollBehavior(), 59 | slivers: [ 60 | SliverOverlapInjector( 61 | // This is the flip side of the SliverOverlapAbsorber 62 | // above. 63 | handle: 64 | NestedScrollView.sliverOverlapAbsorberHandleFor(context), 65 | ), 66 | SliverToBoxAdapter( 67 | child: Column( 68 | mainAxisAlignment: MainAxisAlignment.start, 69 | crossAxisAlignment: CrossAxisAlignment.start, 70 | mainAxisSize: MainAxisSize.max, 71 | children: [ 72 | const SizedBox( 73 | height: 12, 74 | ), 75 | if (showOrganizations) 76 | HomeOrganizationsSection( 77 | organizations: organizations, 78 | ), 79 | if (showPopularBrands) const HomePopularBrandsSection(), 80 | if (showSearchResults || showProgress) 81 | Padding( 82 | padding: const EdgeInsets.only( 83 | left: 16, 84 | right: 16, 85 | bottom: 12, 86 | ), 87 | child: Text( 88 | LocaleKeys.home_results.tr(), 89 | style: AppTypography.headerMedium, 90 | textAlign: TextAlign.start, 91 | ), 92 | ), 93 | if (showProgress) 94 | const Center( 95 | child: CircularProgressIndicator( 96 | color: AppColors.rose, 97 | ), 98 | ) 99 | ], 100 | ), 101 | ), 102 | if (!showProgress) 103 | HomeBrandsList( 104 | brands: showSearchResults ? searchBrands : popularBrands, 105 | ), 106 | const SliverToBoxAdapter( 107 | child: SizedBox( 108 | height: 32, 109 | ), 110 | ) 111 | ], 112 | ), 113 | ), 114 | ), 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/home/widget/home_organizations_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/generated/locale_keys.g.dart'; 2 | import 'package:bunny_search/home/widget/organizations_list.dart'; 3 | import 'package:bunny_search/home/widget/show_all_button.dart'; 4 | import 'package:bunny_search/organization/model/organization_details.dart'; 5 | import 'package:bunny_search/organization/widget/organizations_page.dart'; 6 | import 'package:bunny_search/theme/app_typography.dart'; 7 | import 'package:easy_localization/easy_localization.dart'; 8 | import 'package:flutter/material.dart'; 9 | 10 | class HomeOrganizationsSection extends StatelessWidget { 11 | final List organizations; 12 | 13 | const HomeOrganizationsSection({Key? key, required this.organizations}) 14 | : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Column( 19 | mainAxisSize: MainAxisSize.min, 20 | mainAxisAlignment: MainAxisAlignment.start, 21 | crossAxisAlignment: CrossAxisAlignment.start, 22 | children: [ 23 | Padding( 24 | padding: const EdgeInsets.only(left: 16, right: 16), 25 | child: Row( 26 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 27 | children: [ 28 | Text( 29 | LocaleKeys.home_organizations.tr(), 30 | style: AppTypography.headerMedium, 31 | ), 32 | ShowAllButton( 33 | onShowAll: () => _showAllOrganizations(context), 34 | ) 35 | ], 36 | ), 37 | ), 38 | const SizedBox(height: 20), 39 | OrganizationsList( 40 | organizations: organizations, 41 | ), 42 | const SizedBox( 43 | height: 40, 44 | ), 45 | ], 46 | ); 47 | } 48 | 49 | void _showAllOrganizations(BuildContext context) { 50 | Navigator.of(context).push( 51 | MaterialPageRoute(builder: (context) => OrganizationsPage.withBloc()), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/home/widget/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/generated/locale_keys.g.dart'; 2 | import 'package:bunny_search/home/bloc/home_bloc.dart'; 3 | import 'package:bunny_search/home/widget/home_content_screen.dart'; 4 | import 'package:bunny_search/home/widget/home_splash_screen.dart'; 5 | import 'package:bunny_search/home/widget/support_dialog.dart'; 6 | import 'package:bunny_search/theme/bunny_snack_bar.dart'; 7 | import 'package:domain/brands/model/brand.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_bloc/flutter_bloc.dart'; 10 | import 'package:easy_localization/easy_localization.dart'; 11 | 12 | class HomePage extends StatefulWidget { 13 | const HomePage({Key? key}) : super(key: key); 14 | 15 | @override 16 | State createState() => _HomePageState(); 17 | 18 | static Widget withBloc() => BlocProvider( 19 | create: (context) => HomeBloc( 20 | brandsRepository: context.read(), 21 | keyValueStorage: context.read(), 22 | )..add(LoadEvent()), 23 | child: const HomePage(), 24 | ); 25 | } 26 | 27 | class _HomePageState extends State { 28 | String searchTerm = ''; 29 | late HomeBloc _bloc; 30 | late Widget _widget; 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | _bloc = context.read(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return BlocConsumer( 41 | listener: (context, state) { 42 | final isError = state.organizationsResult.isError == true || 43 | state.searchResult.orNull?.isError == true; 44 | if (isError) { 45 | _handleError(); 46 | } else if (state.showSupportDialog) { 47 | _showSupportDialog(); 48 | _bloc.add(SetSupportDialogShownEvent()); 49 | } 50 | }, 51 | builder: (context, state) { 52 | final isLoadingData = state.organizationsResult.isInProgress == true || 53 | state.organizationsResult.isError == true; 54 | final showSearchResults = 55 | state.searchResult.orNull?.isSuccessful == true; 56 | final showProgress = state.searchResult.orNull?.isInProgress == true; 57 | final showPopularBrands = !showSearchResults && !showProgress; 58 | final showOrganizations = 59 | state.organizationsResult.isSuccessful == true && 60 | !showSearchResults && 61 | !showProgress; 62 | final brands = state.searchResult.orNull?.result?.value ?? []; 63 | final organizations = state.organizationsResult.value ?? []; 64 | if (isLoadingData) { 65 | _widget = const HomeSplashScreen(); 66 | } else { 67 | _widget = HomeContentScreen( 68 | showProgress: showProgress, 69 | showOrganizations: showOrganizations, 70 | showPopularBrands: showPopularBrands, 71 | showSearchResults: showSearchResults, 72 | searchBrands: brands, 73 | popularBrands: state.popularBrands, 74 | organizations: organizations, 75 | onSearchTermChanged: (searchTerm) => 76 | _bloc.add(SearchEvent(searchTerm: searchTerm)), 77 | ); 78 | } 79 | return AnimatedSwitcher( 80 | switchInCurve: Curves.easeIn, 81 | switchOutCurve: Curves.easeOut, 82 | duration: const Duration(milliseconds: 750), 83 | child: _widget, 84 | ); 85 | }, 86 | ); 87 | } 88 | 89 | void _handleError() { 90 | ScaffoldMessenger.of(context).showSnackBar( 91 | BunnyDefaultSnackBar( 92 | text: LocaleKeys.general_error.tr(), 93 | onRetry: () => _bloc.add(LoadEvent()), 94 | ), 95 | ); 96 | } 97 | 98 | void _showSupportDialog() { 99 | showDialog(context: context, builder: (context) => const SupportDialog()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/home/widget/home_popular_brands_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/brand/widget/popular_brands_page.dart'; 2 | import 'package:bunny_search/generated/locale_keys.g.dart'; 3 | import 'package:bunny_search/home/widget/show_all_button.dart'; 4 | import 'package:bunny_search/theme/app_typography.dart'; 5 | import 'package:easy_localization/easy_localization.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class HomePopularBrandsSection extends StatelessWidget { 9 | const HomePopularBrandsSection({Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Padding( 14 | padding: const EdgeInsets.only(left: 16, right: 16, bottom: 12), 15 | child: Row( 16 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 17 | children: [ 18 | Text( 19 | LocaleKeys.home_popular_brands.tr(), 20 | style: AppTypography.headerMedium, 21 | ), 22 | ShowAllButton(onShowAll: () => _showAllPopularBrands(context)) 23 | ], 24 | ), 25 | ); 26 | } 27 | 28 | void _showAllPopularBrands(BuildContext context) { 29 | Navigator.of(context).push( 30 | MaterialPageRoute(builder: (context) => PopularBrandsPage.withBloc()), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/home/widget/home_splash_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/generated/locale_keys.g.dart'; 2 | import 'package:bunny_search/theme/app_colors.dart'; 3 | import 'package:bunny_search/theme/app_typography.dart'; 4 | import 'package:bunny_search/theme/images_provider.dart'; 5 | import 'package:easy_localization/easy_localization.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class HomeSplashScreen extends StatelessWidget { 9 | const HomeSplashScreen({Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Container( 14 | width: MediaQuery.of(context).size.width, 15 | decoration: const BoxDecoration( 16 | gradient: LinearGradient( 17 | begin: Alignment.topLeft, 18 | end: Alignment.bottomRight, 19 | colors: [Color(0xFFD8CCFF), Color(0xFFA3E3FF)], 20 | tileMode: TileMode.clamp, 21 | ), 22 | ), 23 | child: Column( 24 | mainAxisSize: MainAxisSize.max, 25 | mainAxisAlignment: MainAxisAlignment.center, 26 | children: [ 27 | Image.asset( 28 | ImagesProvider.SEARCH_BUNNY, 29 | width: MediaQuery.of(context).size.width / 3, 30 | ), 31 | const SizedBox( 32 | height: 20, 33 | ), 34 | Text( 35 | LocaleKeys.splash_loading_brands.tr(), 36 | style: AppTypography.regular.copyWith( 37 | color: AppColors.white, 38 | fontWeight: FontWeight.w400, 39 | letterSpacing: 0.5, 40 | fontSize: 18, 41 | decorationColor: AppColors.white.withOpacity(0.01), 42 | ), 43 | ) 44 | ], 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/home/widget/no_overscroll_behaviour.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NoOverscrollBehavior extends MaterialScrollBehavior { 4 | @override 5 | Widget buildOverscrollIndicator( 6 | BuildContext context, 7 | Widget child, 8 | ScrollableDetails details, 9 | ) { 10 | return child; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/home/widget/organizations_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/organization/model/organization_details.dart'; 2 | import 'package:bunny_search/organization/widget/organization_list_card.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class OrganizationsList extends StatelessWidget { 6 | final List organizations; 7 | 8 | const OrganizationsList({Key? key, required this.organizations}) 9 | : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return SizedBox( 14 | height: 176, 15 | child: ListView.separated( 16 | physics: const ClampingScrollPhysics(), 17 | separatorBuilder: (context, pos) => const SizedBox( 18 | width: 16, 19 | ), 20 | padding: const EdgeInsets.symmetric(horizontal: 16), 21 | itemBuilder: (context, pos) { 22 | return OrganizationListCard(details: organizations[pos]); 23 | }, 24 | itemCount: organizations.length, 25 | scrollDirection: Axis.horizontal, 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/home/widget/search_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/generated/locale_keys.g.dart'; 2 | import 'package:bunny_search/theme/app_colors.dart'; 3 | import 'package:bunny_search/theme/app_typography.dart'; 4 | import 'package:easy_localization/easy_localization.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | typedef OnSearchTermChanged = Function(String searchTerm); 8 | 9 | class SearchBar extends StatefulWidget { 10 | final OnSearchTermChanged onSearchTermChanged; 11 | 12 | const SearchBar({Key? key, required this.onSearchTermChanged}) 13 | : super(key: key); 14 | 15 | @override 16 | State createState() => _SearchBarState(); 17 | } 18 | 19 | class _SearchBarState extends State { 20 | final TextEditingController _searchController = TextEditingController(); 21 | 22 | @override 23 | void dispose() { 24 | _searchController.dispose(); 25 | super.dispose(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return Container( 31 | margin: const EdgeInsets.only(left: 16, top: 0), 32 | child: Container( 33 | decoration: BoxDecoration( 34 | borderRadius: BorderRadius.circular(12), 35 | color: Colors.white, 36 | border: Border.all(color: const Color(0xFFF2F2F7), width: 0.5), 37 | boxShadow: [ 38 | BoxShadow( 39 | color: const Color(0xFFDFE9F5).withOpacity(0.25), 40 | blurRadius: 15, 41 | offset: const Offset(0, 2), 42 | ) 43 | ], 44 | ), 45 | width: MediaQuery.of(context).size.width - 32, 46 | // TODO: icon color on focus / not empty controller 47 | child: TextFormField( 48 | controller: _searchController, 49 | cursorColor: AppColors.rose, 50 | style: AppTypography.regular, 51 | onChanged: widget.onSearchTermChanged, 52 | decoration: InputDecoration( 53 | hintText: LocaleKeys.home_search_hint.tr(), 54 | hintStyle: 55 | AppTypography.regular.copyWith(color: AppColors.inactive), 56 | contentPadding: const EdgeInsets.only(top: 20, bottom: 20), 57 | focusColor: AppColors.rose, 58 | prefixIcon: const Icon( 59 | Icons.search, 60 | color: AppColors.inactive, 61 | ), 62 | focusedBorder: OutlineInputBorder( 63 | borderSide: const BorderSide(width: 0.5, color: AppColors.rose), 64 | borderRadius: BorderRadius.circular(12), 65 | ), 66 | focusedErrorBorder: InputBorder.none, 67 | enabledBorder: InputBorder.none, 68 | disabledBorder: InputBorder.none, 69 | errorBorder: InputBorder.none, 70 | ), 71 | ), 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/home/widget/search_bunny_icon_clipper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SearchBunnyIconClipper extends CustomClipper { 4 | @override 5 | Path getClip(Size size) { 6 | var path = Path(); 7 | 8 | path.lineTo(0.0, size.height - 25); 9 | path.lineTo(size.width, size.height - 25); 10 | path.lineTo(size.width, 0.0); 11 | 12 | return path; 13 | } 14 | 15 | @override 16 | bool shouldReclip(covariant CustomClipper oldClipper) => true; 17 | } 18 | -------------------------------------------------------------------------------- /lib/home/widget/show_all_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/generated/locale_keys.g.dart'; 2 | import 'package:bunny_search/theme/app_typography.dart'; 3 | import 'package:easy_localization/easy_localization.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class ShowAllButton extends StatelessWidget { 7 | final VoidCallback onShowAll; 8 | 9 | const ShowAllButton({Key? key, required this.onShowAll}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return TextButton( 14 | style: TextButton.styleFrom( 15 | minimumSize: Size.zero, 16 | tapTargetSize: MaterialTapTargetSize.shrinkWrap, 17 | ), 18 | onPressed: onShowAll, 19 | child: Text( 20 | LocaleKeys.home_show_all.tr(), 21 | style: AppTypography.label, 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/home/widget/sliver_search_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/generated/locale_keys.g.dart'; 2 | import 'package:bunny_search/home/widget/background_wave_clipper.dart'; 3 | import 'package:bunny_search/home/widget/search_bar.dart' as sc; 4 | import 'package:bunny_search/home/widget/search_bunny_icon_clipper.dart'; 5 | import 'package:bunny_search/theme/app_typography.dart'; 6 | import 'package:bunny_search/theme/images_provider.dart'; 7 | import 'package:easy_localization/easy_localization.dart'; 8 | import 'package:flutter/material.dart'; 9 | 10 | class SliverSearchAppBar extends SliverPersistentHeaderDelegate { 11 | final sc.OnSearchTermChanged onSearchTermChanged; 12 | 13 | SliverSearchAppBar({required this.onSearchTermChanged}); 14 | 15 | @override 16 | Widget build( 17 | BuildContext context, double shrinkOffset, bool overlapsContent) { 18 | final adjustedShrink = shrinkOffset * 2; 19 | final snap = adjustedShrink > 60; 20 | double searchExtraOffset = ((280 - adjustedShrink) * 0.36).abs().toDouble(); 21 | return Stack( 22 | clipBehavior: Clip.none, 23 | children: [ 24 | Container( 25 | height: 280, 26 | child: ClipPath( 27 | clipper: BackgroundWaveClipper(), 28 | child: AnimatedContainer( 29 | curve: Curves.fastOutSlowIn, 30 | duration: Duration(milliseconds: 200), 31 | width: MediaQuery.of(context).size.width, 32 | height: 280, 33 | decoration: BoxDecoration( 34 | gradient: LinearGradient( 35 | begin: Alignment.topCenter, 36 | end: Alignment.bottomRight, 37 | colors: [Color(0xFFC3EDFF), Color(0xFFEFEAFF)], 38 | stops: [0.0, 0.51], 39 | tileMode: TileMode.clamp, 40 | )), 41 | ), 42 | ), 43 | ), 44 | AnimatedOpacity( 45 | opacity: snap ? 0 : 1, 46 | duration: Duration(milliseconds: 200), 47 | curve: Curves.fastOutSlowIn, 48 | child: Container( 49 | margin: EdgeInsets.only(left: 16, top: 80), 50 | child: Text( 51 | LocaleKeys.home_search_prompt.tr(), 52 | style: AppTypography.header, 53 | ), 54 | ), 55 | ), 56 | Positioned( 57 | right: 48, 58 | child: AnimatedOpacity( 59 | opacity: snap ? 0 : 1, 60 | curve: Curves.fastOutSlowIn, 61 | duration: Duration(milliseconds: 200), 62 | child: ClipPath( 63 | clipper: SearchBunnyIconClipper(), 64 | child: Container( 65 | margin: EdgeInsets.only(top: 48), 66 | child: Image.asset( 67 | ImagesProvider.SEARCH_BUNNY, 68 | width: 102, 69 | ), 70 | ), 71 | ), 72 | )), 73 | Positioned( 74 | top: 64 + searchExtraOffset, 75 | child: sc.SearchBar( 76 | onSearchTermChanged: onSearchTermChanged, 77 | ), 78 | ) 79 | ], 80 | ); 81 | } 82 | 83 | @override 84 | double get maxExtent => 280; 85 | 86 | @override 87 | double get minExtent => 140; 88 | 89 | @override 90 | bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => 91 | oldDelegate != this; 92 | } 93 | -------------------------------------------------------------------------------- /lib/home/widget/support_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/generated/locale_keys.g.dart'; 2 | import 'package:bunny_search/theme/app_colors.dart'; 3 | import 'package:bunny_search/theme/app_typography.dart'; 4 | import 'package:easy_localization/easy_localization.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class SupportDialog extends StatelessWidget { 8 | const SupportDialog({Key? key}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return AlertDialog( 13 | title: Text( 14 | LocaleKeys.home_support_dialog_title.tr(), 15 | style: AppTypography.header.copyWith(color: AppColors.accentBlack), 16 | ), 17 | content: SingleChildScrollView( 18 | child: ListBody( 19 | children: [ 20 | Text( 21 | LocaleKeys.home_support_dialog_content.tr(), 22 | style: AppTypography.description.copyWith( 23 | color: AppColors.accentBlack, 24 | fontWeight: FontWeight.normal, 25 | letterSpacing: 0.2, 26 | fontSize: 16, 27 | ), 28 | ), 29 | const SizedBox( 30 | height: 16, 31 | ), 32 | Text( 33 | LocaleKeys.home_support_dialog_team.tr(), 34 | style: AppTypography.description.copyWith( 35 | color: AppColors.accentBlack, 36 | fontWeight: FontWeight.bold, 37 | letterSpacing: 0.2, 38 | fontSize: 16, 39 | ), 40 | ), 41 | ], 42 | ), 43 | ), 44 | actions: [ 45 | TextButton( 46 | child: Text(LocaleKeys.home_support_dialog_close_button.tr()), 47 | onPressed: () => Navigator.of(context).pop(), 48 | ), 49 | ], 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:bunny_search/analytics/bloc_error_delegate.dart'; 5 | import 'package:bunny_search/app.dart'; 6 | import 'package:cached_network_image/cached_network_image.dart'; 7 | import 'package:data/brands/persisted_brands_repository.dart'; 8 | import 'package:data/brands/search_service.dart'; 9 | import 'package:data/organizations/repository/assets_organizations_repository.dart'; 10 | import 'package:data/storage/shared_preferences_key_value_storage.dart'; 11 | import 'package:domain/brands/repository/brands_repository.dart'; 12 | import 'package:domain/organizations/repository/organizations_repository.dart'; 13 | import 'package:domain/storage/key_value_storage.dart'; 14 | import 'package:easy_localization/easy_localization.dart'; 15 | import 'package:equatable/equatable.dart'; 16 | import 'package:fimber/fimber.dart'; 17 | import 'package:flutter/foundation.dart'; 18 | import 'package:flutter/material.dart'; 19 | import 'package:flutter/services.dart'; 20 | import 'package:flutter_bloc/flutter_bloc.dart'; 21 | import 'package:shared_preferences/shared_preferences.dart'; 22 | import 'package:data/database/database.dart'; 23 | import 'package:bunny_search/generated/codegen_loader.g.dart'; 24 | 25 | void main() { 26 | runZonedGuarded(() async { 27 | WidgetsFlutterBinding.ensureInitialized(); 28 | await EasyLocalization.ensureInitialized(); 29 | 30 | CachedNetworkImage.logLevel = CacheManagerLogLevel.warning; 31 | 32 | Bloc.observer = BlocErrorObserver(); 33 | 34 | EquatableConfig.stringify = true; 35 | 36 | /** Used in real app **/ 37 | //await Firebase.initializeApp(); 38 | 39 | await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); 40 | 41 | /** Used in real app: FirebaseOrganizationsRepository **/ 42 | final orgRepo = AssetsOrganizationsRepository( 43 | databaseJson: json.decode(await rootBundle 44 | .loadString('resources/database/bunny-search-database.json'))); 45 | final sharedPrefs = await SharedPreferences.getInstance(); 46 | 47 | final database = await initDatabase(); 48 | 49 | await _initCrashlytics(); 50 | 51 | _initFimber(); 52 | 53 | final keyValueStorage = SharedPreferencesKeyValueStorage(sharedPrefs); 54 | 55 | runApp( 56 | MultiRepositoryProvider( 57 | providers: [ 58 | RepositoryProvider( 59 | create: (context) => SearchService(), 60 | ), 61 | RepositoryProvider( 62 | create: (context) => orgRepo, 63 | ), 64 | RepositoryProvider( 65 | create: (context) => keyValueStorage, 66 | ), 67 | RepositoryProvider( 68 | create: (context) => PersistedBrandsRepository( 69 | organizationsRepository: orgRepo, 70 | storage: keyValueStorage, 71 | dao: database.brandsDao, 72 | searchService: context.read(), 73 | ), 74 | ) 75 | ], 76 | child: EasyLocalization( 77 | fallbackLocale: const Locale('ru'), 78 | useOnlyLangCode: true, 79 | path: 'resources/langs', 80 | supportedLocales: const [Locale('ru'), Locale('en')], 81 | assetLoader: const CodegenLoader(), 82 | child: const App(), 83 | ), 84 | ), 85 | ); 86 | }, (error, stackTrace) { 87 | /** Used in real app **/ 88 | //FirebaseCrashlytics.instance.recordError(error, stackTrace); 89 | }); 90 | } 91 | 92 | Future _initCrashlytics() async { 93 | /** Used in real app **/ 94 | /* await FirebaseCrashlytics.instance 95 | .setCrashlyticsCollectionEnabled(kReleaseMode); 96 | 97 | final Function? originalOnError = FlutterError.onError; 98 | FlutterError.onError = (FlutterErrorDetails errorDetails) async { 99 | await FirebaseCrashlytics.instance.recordFlutterError(errorDetails); 100 | // Forward to original handler. 101 | if (originalOnError != null) { 102 | originalOnError(errorDetails); 103 | } 104 | };*/ 105 | } 106 | 107 | void _initFimber() { 108 | if (kReleaseMode) { 109 | /** Used in real app **/ 110 | //Fimber.plantTree(CrashlyticsFimberTree()); 111 | } else { 112 | Fimber.plantTree(DebugTree()); 113 | } 114 | } 115 | 116 | Future initDatabase() async { 117 | final database = 118 | await $FloorBunnySearchDatabase.databaseBuilder('database.db').build(); 119 | return database; 120 | } 121 | -------------------------------------------------------------------------------- /lib/organization/bloc/organization_brands_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/brands/model/brand.dart'; 2 | import 'package:domain/brands/repository/brands_repository.dart'; 3 | import 'package:domain/organizations/model/organization_type.dart'; 4 | import 'package:domain/result/delayed_result.dart'; 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:fimber/fimber.dart'; 7 | import 'package:flutter_bloc/flutter_bloc.dart'; 8 | 9 | abstract class OrganizationBrandsBlocEvent extends Equatable { 10 | const OrganizationBrandsBlocEvent(); 11 | 12 | @override 13 | List get props => []; 14 | } 15 | 16 | class LoadBrandsEvent extends OrganizationBrandsBlocEvent { 17 | final OrganizationType organizationType; 18 | 19 | const LoadBrandsEvent({required this.organizationType}); 20 | 21 | @override 22 | List get props => [organizationType]; 23 | } 24 | 25 | class OrganizationBrandsState extends Equatable { 26 | final OrganizationType? organizationType; 27 | final DelayedResult> brandsResult; 28 | 29 | const OrganizationBrandsState({ 30 | required this.organizationType, 31 | required this.brandsResult, 32 | }); 33 | 34 | OrganizationBrandsState copyWith({ 35 | OrganizationType? organizationType, 36 | DelayedResult>? brandsResult, 37 | }) => 38 | OrganizationBrandsState( 39 | organizationType: organizationType ?? this.organizationType, 40 | brandsResult: brandsResult ?? this.brandsResult, 41 | ); 42 | 43 | @override 44 | List get props => [organizationType, brandsResult]; 45 | } 46 | 47 | class OrganizationBrandBloc 48 | extends Bloc { 49 | final BrandsRepository brandsRepository; 50 | 51 | OrganizationBrandBloc({required this.brandsRepository}) 52 | : super( 53 | const OrganizationBrandsState( 54 | organizationType: null, 55 | brandsResult: DelayedResult.inProgress(), 56 | ), 57 | ); 58 | 59 | @override 60 | Stream mapEventToState( 61 | OrganizationBrandsBlocEvent event, 62 | ) async* { 63 | if (event is LoadBrandsEvent) { 64 | yield* _mapLoadBrandsEventToState(event); 65 | } 66 | } 67 | 68 | Stream _mapLoadBrandsEventToState( 69 | LoadBrandsEvent event, 70 | ) async* { 71 | yield state.copyWith(brandsResult: const DelayedResult.inProgress()); 72 | 73 | try { 74 | final brands = await brandsRepository 75 | .getBrandsByOrganizationType(event.organizationType); 76 | yield state.copyWith( 77 | organizationType: event.organizationType, 78 | brandsResult: DelayedResult.success(brands), 79 | ); 80 | } on Exception catch (ex, st) { 81 | Fimber.e('Failed to load org brands', ex: ex, stacktrace: st); 82 | yield state.copyWith(brandsResult: DelayedResult.error(ex)); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/organization/bloc/organizations_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/organization/model/organization_details.dart'; 2 | import 'package:bunny_search/organization/model/organizations_mapper.dart'; 3 | import 'package:domain/brands/repository/brands_repository.dart'; 4 | import 'package:domain/result/delayed_result.dart'; 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:fimber/fimber.dart'; 7 | import 'package:flutter_bloc/flutter_bloc.dart'; 8 | 9 | abstract class OrganizationsBlocEvent extends Equatable { 10 | const OrganizationsBlocEvent(); 11 | 12 | @override 13 | List get props => []; 14 | } 15 | 16 | class LoadEvent extends OrganizationsBlocEvent {} 17 | 18 | class OrganizationsState extends Equatable { 19 | final DelayedResult> orgResult; 20 | 21 | const OrganizationsState({required this.orgResult}); 22 | 23 | OrganizationsState copyWith({ 24 | DelayedResult>? orgResult, 25 | }) => 26 | OrganizationsState(orgResult: orgResult ?? this.orgResult); 27 | 28 | @override 29 | List get props => [orgResult]; 30 | } 31 | 32 | class OrganizationsBloc 33 | extends Bloc { 34 | final BrandsRepository brandsRepository; 35 | 36 | OrganizationsBloc({required this.brandsRepository}) 37 | : super(const OrganizationsState(orgResult: DelayedResult.inProgress())); 38 | 39 | @override 40 | Stream mapEventToState( 41 | OrganizationsBlocEvent event, 42 | ) async* { 43 | if (event is LoadEvent) { 44 | yield* _mapLoadEventToState(); 45 | } 46 | } 47 | 48 | Stream _mapLoadEventToState() async* { 49 | yield state.copyWith(orgResult: const DelayedResult.inProgress()); 50 | 51 | try { 52 | final organizations = await brandsRepository.getAllOrganizations(); 53 | final mapped = organizations 54 | .map((o) => OrganizationsMapper.toOrganizationDetails(o)) 55 | .toList(); 56 | yield state.copyWith(orgResult: DelayedResult.success(mapped)); 57 | } on Exception catch (ex, st) { 58 | Fimber.e('Failed to load all orgz', ex: ex, stacktrace: st); 59 | yield state.copyWith(orgResult: DelayedResult.error(ex)); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/organization/model/organization_brand_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/organizations/model/organization_brand.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | class OrganizationBrandDetails extends Equatable { 5 | final OrganizationBrand info; 6 | final String logoSrc; 7 | 8 | const OrganizationBrandDetails({ 9 | required this.info, 10 | required this.logoSrc, 11 | }); 12 | 13 | @override 14 | List get props => [info, logoSrc]; 15 | } 16 | -------------------------------------------------------------------------------- /lib/organization/model/organization_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:domain/organizations/model/organization_type.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | class OrganizationDetails extends Equatable { 5 | final String id; 6 | final String title; 7 | final String logoSrc; 8 | final int brandsCount; 9 | final OrganizationType type; 10 | 11 | const OrganizationDetails({ 12 | required this.id, 13 | required this.title, 14 | required this.logoSrc, 15 | required this.brandsCount, 16 | required this.type, 17 | }); 18 | 19 | @override 20 | List get props => [id, title, logoSrc, brandsCount, type]; 21 | } 22 | -------------------------------------------------------------------------------- /lib/organization/model/organizations_mapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/organization/model/organization_brand_details.dart'; 2 | import 'package:bunny_search/organization/model/organization_details.dart'; 3 | import 'package:bunny_search/theme/images_provider.dart'; 4 | import 'package:domain/organizations/model/organization.dart'; 5 | import 'package:domain/organizations/model/organization_brand.dart'; 6 | import 'package:domain/organizations/model/organization_type.dart'; 7 | import 'package:easy_localization/easy_localization.dart'; 8 | import 'package:bunny_search/generated/locale_keys.g.dart'; 9 | 10 | class OrganizationsMapper { 11 | static OrganizationDetails toOrganizationDetails(Organization organization) { 12 | var logoSrc = ''; 13 | var title = ''; 14 | switch (organization.type) { 15 | case OrganizationType.petaWhite: 16 | logoSrc = ImagesProvider.ORG_PETA; 17 | title = LocaleKeys.organization_peta_dont_test.tr(); 18 | break; 19 | case OrganizationType.petaBlack: 20 | logoSrc = ImagesProvider.ORG_PETA; 21 | title = LocaleKeys.organization_peta_do_test.tr(); 22 | break; 23 | case OrganizationType.bunnySearch: 24 | logoSrc = ImagesProvider.ORG_BUNNY; 25 | title = LocaleKeys.organization_bunny_search.tr(); 26 | break; 27 | } 28 | 29 | return OrganizationDetails( 30 | id: organization.id, 31 | title: title, 32 | logoSrc: logoSrc, 33 | brandsCount: organization.brandsCount, 34 | type: organization.type, 35 | ); 36 | } 37 | 38 | static OrganizationBrandDetails toOrganizationBrandDetails( 39 | OrganizationBrand brand, 40 | ) { 41 | var logoSrc = ''; 42 | switch (brand.organizationType) { 43 | case OrganizationType.petaWhite: 44 | logoSrc = ImagesProvider.ORG_PETA; 45 | break; 46 | case OrganizationType.petaBlack: 47 | logoSrc = ImagesProvider.ORG_PETA; 48 | break; 49 | case OrganizationType.bunnySearch: 50 | logoSrc = ImagesProvider.ORG_BUNNY; 51 | break; 52 | } 53 | 54 | return OrganizationBrandDetails(info: brand, logoSrc: logoSrc); 55 | } 56 | 57 | static String organizationsToString(List organizations) { 58 | var result = organizationTypeToString(organizations[0].type); 59 | organizations.skip(1).forEach((e) { 60 | result += ' • ${organizationTypeToString(organizations[1].type)}'; 61 | }); 62 | return result; 63 | } 64 | 65 | static String organizationTypeToString(OrganizationType type) { 66 | switch (type) { 67 | case OrganizationType.petaWhite: 68 | return LocaleKeys.organization_peta_dont_test.tr(); 69 | case OrganizationType.petaBlack: 70 | return LocaleKeys.organization_peta_do_test.tr(); 71 | case OrganizationType.bunnySearch: 72 | return LocaleKeys.organization_bunny_search.tr(); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/organization/widget/organization_brands_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/brand/widget/brand_details_page.dart'; 2 | import 'package:bunny_search/brand/widget/brand_list_item.dart'; 3 | import 'package:bunny_search/organization/bloc/organization_brands_bloc.dart'; 4 | import 'package:bunny_search/organization/model/organization_details.dart'; 5 | import 'package:bunny_search/organization/model/organizations_mapper.dart'; 6 | import 'package:bunny_search/theme/app_colors.dart'; 7 | import 'package:bunny_search/theme/app_typography.dart'; 8 | import 'package:bunny_search/theme/bunny_appbar_back_button.dart'; 9 | import 'package:bunny_search/theme/bunny_snack_bar.dart'; 10 | import 'package:domain/brands/model/brand.dart'; 11 | import 'package:domain/organizations/model/organization.dart'; 12 | import 'package:flutter/material.dart'; 13 | import 'package:flutter_bloc/flutter_bloc.dart'; 14 | import 'package:easy_localization/easy_localization.dart'; 15 | import 'package:bunny_search/generated/locale_keys.g.dart'; 16 | 17 | class OrganizationBrandsPage extends StatefulWidget { 18 | final OrganizationDetails organization; 19 | 20 | const OrganizationBrandsPage({Key? key, required this.organization}) 21 | : super(key: key); 22 | 23 | @override 24 | State createState() => _OrganizationBrandsPageState(); 25 | 26 | static Widget withBloc(OrganizationDetails organization) => BlocProvider( 27 | create: (context) => 28 | OrganizationBrandBloc(brandsRepository: context.read()) 29 | ..add(LoadBrandsEvent(organizationType: organization.type)), 30 | child: OrganizationBrandsPage( 31 | organization: organization, 32 | ), 33 | ); 34 | } 35 | 36 | class _OrganizationBrandsPageState extends State { 37 | late OrganizationBrandBloc _bloc; 38 | 39 | @override 40 | void initState() { 41 | super.initState(); 42 | _bloc = context.read(); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return BlocConsumer( 48 | listener: (context, state) { 49 | final isError = state.brandsResult.isError == true; 50 | if (isError) { 51 | _handleError(); 52 | } 53 | }, 54 | builder: (context, state) { 55 | final progress = state.brandsResult.isInProgress; 56 | final brands = state.brandsResult.isSuccessful 57 | ? state.brandsResult.value ?? [] 58 | : []; 59 | return Scaffold( 60 | backgroundColor: AppColors.white, 61 | appBar: AppBar( 62 | centerTitle: true, 63 | backgroundColor: AppColors.white, 64 | elevation: 0, 65 | leading: const BunnyAppBarBackButton(), 66 | title: Column( 67 | children: [ 68 | Text( 69 | widget.organization.title, 70 | style: AppTypography.h4, 71 | ), 72 | const SizedBox( 73 | height: 8, 74 | ), 75 | Text( 76 | LocaleKeys.organization_brands_count.tr( 77 | namedArgs: {'count': '${widget.organization.brandsCount}'}, 78 | ), 79 | style: AppTypography.caption, 80 | ) 81 | ], 82 | ), 83 | ), 84 | body: progress 85 | ? const Center( 86 | child: CircularProgressIndicator( 87 | color: AppColors.rose, 88 | ), 89 | ) 90 | : SafeArea( 91 | child: ListView.builder( 92 | padding: const EdgeInsets.only(bottom: 24), 93 | itemBuilder: (context, pos) { 94 | Brand brand = brands[pos]; 95 | return TextButton( 96 | style: ButtonStyle( 97 | overlayColor: MaterialStateProperty.all( 98 | AppColors.rose.withOpacity(0.05), 99 | ), 100 | ), 101 | onPressed: () { 102 | Navigator.of(context).push( 103 | MaterialPageRoute( 104 | builder: (context) => 105 | BrandDetailsPage(brand: brand), 106 | ), 107 | ); 108 | }, 109 | child: BrandListItem( 110 | title: brand.title, 111 | filters: _buildFiltersString( 112 | brand.organizations.values.toList(), 113 | ), 114 | logoUrl: brand.logoUrl ?? '', 115 | ), 116 | ); 117 | }, 118 | itemCount: brands.length, 119 | ), 120 | ), 121 | ); 122 | }, 123 | ); 124 | } 125 | 126 | String _buildFiltersString(List organizations) { 127 | return organizations.map((o) => OrganizationsMapper.organizationTypeToString(o.type)).join(' • '); 128 | } 129 | 130 | void _handleError() { 131 | ScaffoldMessenger.of(context).showSnackBar( 132 | BunnyDefaultSnackBar( 133 | text: LocaleKeys.general_error.tr(), 134 | onRetry: () => _bloc.add( 135 | LoadBrandsEvent(organizationType: widget.organization.type), 136 | ), 137 | ), 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/organization/widget/organization_list_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/organization/model/organization_details.dart'; 2 | import 'package:bunny_search/organization/widget/organization_brands_page.dart'; 3 | import 'package:bunny_search/theme/app_colors.dart'; 4 | import 'package:bunny_search/theme/app_typography.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:easy_localization/easy_localization.dart'; 7 | import 'package:bunny_search/generated/locale_keys.g.dart'; 8 | 9 | class OrganizationListCard extends StatelessWidget { 10 | final OrganizationDetails details; 11 | 12 | const OrganizationListCard({Key? key, required this.details}) 13 | : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return GestureDetector( 18 | onTap: () { 19 | Navigator.of(context).push( 20 | MaterialPageRoute( 21 | builder: (context) => OrganizationBrandsPage.withBloc(details), 22 | ), 23 | ); 24 | }, 25 | child: Container( 26 | height: 176, 27 | width: 148, 28 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), 29 | decoration: BoxDecoration( 30 | border: Border.all(color: AppColors.border), 31 | borderRadius: BorderRadius.circular(24), 32 | ), 33 | child: Column( 34 | mainAxisAlignment: MainAxisAlignment.start, 35 | crossAxisAlignment: CrossAxisAlignment.start, 36 | children: [ 37 | Text( 38 | details.title, 39 | style: AppTypography.labelDark, 40 | ), 41 | const SizedBox( 42 | height: 8, 43 | ), 44 | Text( 45 | LocaleKeys.organization_brands_count.tr( 46 | namedArgs: {'count': '${details.brandsCount}'}, 47 | ), 48 | style: AppTypography.caption, 49 | ), 50 | const Spacer(), 51 | Align( 52 | alignment: Alignment.bottomRight, 53 | child: Image.asset( 54 | details.logoSrc, 55 | width: 64, 56 | height: 48, 57 | ), 58 | ) 59 | ], 60 | ), 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/organization/widget/organizations_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/organization/bloc/organizations_bloc.dart'; 2 | import 'package:bunny_search/organization/widget/organization_list_card.dart'; 3 | import 'package:bunny_search/theme/app_colors.dart'; 4 | import 'package:bunny_search/theme/app_typography.dart'; 5 | import 'package:bunny_search/theme/bunny_appbar_back_button.dart'; 6 | import 'package:bunny_search/theme/bunny_snack_bar.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_bloc/flutter_bloc.dart'; 9 | import 'package:easy_localization/easy_localization.dart'; 10 | import 'package:bunny_search/generated/locale_keys.g.dart'; 11 | 12 | class OrganizationsPage extends StatefulWidget { 13 | const OrganizationsPage({Key? key}) : super(key: key); 14 | 15 | @override 16 | State createState() => _OrganizationsPageState(); 17 | 18 | static Widget withBloc() => BlocProvider( 19 | create: (context) => OrganizationsBloc(brandsRepository: context.read()) 20 | ..add(LoadEvent()), 21 | child: const OrganizationsPage(), 22 | ); 23 | } 24 | 25 | class _OrganizationsPageState extends State { 26 | late OrganizationsBloc _bloc; 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | _bloc = context.read(); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return BlocConsumer( 37 | listener: (context, state) { 38 | final isError = state.orgResult.isError == true; 39 | if (isError) { 40 | _handleError(); 41 | } 42 | }, 43 | builder: (context, state) { 44 | final progress = state.orgResult.isInProgress; 45 | final orgz = 46 | state.orgResult.isSuccessful ? state.orgResult.value ?? [] : []; 47 | return Scaffold( 48 | backgroundColor: AppColors.white, 49 | appBar: AppBar( 50 | centerTitle: true, 51 | backgroundColor: AppColors.white, 52 | elevation: 0, 53 | leading: const BunnyAppBarBackButton(), 54 | title: Column( 55 | children: [ 56 | Text( 57 | LocaleKeys.organizations_title.tr(), 58 | style: AppTypography.h4, 59 | ), 60 | ], 61 | ), 62 | ), 63 | body: progress 64 | ? const Center( 65 | child: CircularProgressIndicator( 66 | color: AppColors.rose, 67 | ), 68 | ) 69 | : SafeArea( 70 | child: GridView.builder( 71 | padding: const EdgeInsets.symmetric( 72 | horizontal: 16, 73 | vertical: 20, 74 | ), 75 | gridDelegate: 76 | const SliverGridDelegateWithFixedCrossAxisCount( 77 | mainAxisSpacing: 12, 78 | crossAxisSpacing: 12, 79 | crossAxisCount: 2, 80 | ), 81 | itemBuilder: (context, pos) { 82 | return OrganizationListCard(details: orgz[pos]); 83 | }, 84 | itemCount: orgz.length, 85 | ), 86 | ), 87 | ); 88 | }, 89 | ); 90 | } 91 | 92 | void _handleError() { 93 | ScaffoldMessenger.of(context).showSnackBar( 94 | BunnyDefaultSnackBar( 95 | text: LocaleKeys.general_error.tr(), 96 | onRetry: () => _bloc.add(LoadEvent()), 97 | ), 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/theme/app_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppColors { 4 | static const Color main = Color(0xFF617171); 5 | 6 | static const Color positive = Color(0xFF4CDDB0); 7 | static const Color negative = Color(0xFFDC6565); 8 | 9 | static const Color delimiter = Color(0xFFC4C4C4); 10 | 11 | static const Color textBlue = Color(0xFF1F2547); 12 | static const Color inactive = Color(0xFF9699A9); 13 | static const Color accentBlack = Color(0xFF202021); 14 | 15 | static const Color rose = Color(0xFFDD859F); 16 | static const Color border = Color(0xFFF2F2F7); 17 | 18 | static const Color background = Color(0xFFF9F9FB); 19 | 20 | static const Color transparent = Colors.transparent; 21 | static const Color white = Color(0xFFFFFFFF); 22 | } -------------------------------------------------------------------------------- /lib/theme/app_typography.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/theme/app_colors.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class AppTypography { 5 | static const regularBold = TextStyle( 6 | fontSize: 14, 7 | color: AppColors.main, 8 | fontWeight: FontWeight.bold, 9 | fontFamily: 'Domine', 10 | ); 11 | 12 | static const regular = TextStyle( 13 | fontSize: 16, 14 | color: AppColors.textBlue, 15 | fontFamily: 'Inter', 16 | letterSpacing: -0.37, 17 | ); 18 | 19 | static const medium = TextStyle( 20 | fontSize: 16, 21 | color: AppColors.textBlue, 22 | fontWeight: FontWeight.w500, 23 | fontFamily: 'Inter', 24 | letterSpacing: -0.17, 25 | ); 26 | 27 | static const header = TextStyle( 28 | fontSize: 22, 29 | color: AppColors.textBlue, 30 | fontFamily: 'Inter', 31 | fontWeight: FontWeight.w500, 32 | height: 1.3, 33 | letterSpacing: -0.97, 34 | ); 35 | 36 | static const headerMedium = TextStyle( 37 | fontSize: 18, 38 | color: AppColors.textBlue, 39 | fontFamily: 'Inter', 40 | fontWeight: FontWeight.w500, 41 | letterSpacing: -0.17, 42 | ); 43 | 44 | static const h4 = TextStyle( 45 | fontSize: 16, 46 | color: AppColors.accentBlack, 47 | fontFamily: 'Inter', 48 | fontWeight: FontWeight.w500, 49 | letterSpacing: -0.3, 50 | ); 51 | 52 | static const label = TextStyle( 53 | fontSize: 14, 54 | color: AppColors.rose, 55 | fontFamily: 'Inter', 56 | fontWeight: FontWeight.w500, 57 | letterSpacing: -0.47, 58 | ); 59 | 60 | static const labelDark = TextStyle( 61 | fontSize: 14, 62 | color: AppColors.textBlue, 63 | fontFamily: 'Inter', 64 | fontWeight: FontWeight.w500, 65 | letterSpacing: -0.37, 66 | ); 67 | 68 | static const labelInactive = TextStyle( 69 | fontSize: 14, 70 | color: AppColors.inactive, 71 | fontFamily: 'Inter', 72 | fontWeight: FontWeight.w500, 73 | letterSpacing: -0.37, 74 | ); 75 | 76 | static const caption = TextStyle( 77 | fontSize: 12, 78 | color: AppColors.inactive, 79 | fontWeight: FontWeight.w400, 80 | fontFamily: 'Inter', 81 | letterSpacing: 0.17, 82 | ); 83 | 84 | static const title = TextStyle( 85 | fontSize: 26, 86 | color: AppColors.textBlue, 87 | fontFamily: 'Inter', 88 | fontWeight: FontWeight.w500, 89 | letterSpacing: -0.47, 90 | height: 1.26, 91 | ); 92 | 93 | static const description = TextStyle( 94 | fontSize: 14, 95 | color: AppColors.textBlue, 96 | fontFamily: 'Inter', 97 | fontWeight: FontWeight.w400, 98 | letterSpacing: -0.17, 99 | height: 1.42, 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /lib/theme/bunny_appbar_back_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/theme/app_colors.dart'; 2 | import 'package:bunny_search/theme/images_provider.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_svg/svg.dart'; 5 | 6 | class BunnyAppBarBackButton extends StatelessWidget { 7 | const BunnyAppBarBackButton({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Padding( 12 | padding: const EdgeInsets.only(left: 4), 13 | child: GestureDetector( 14 | behavior: HitTestBehavior.translucent, 15 | onTap: () { 16 | Navigator.of(context).pop(); 17 | }, 18 | child: Material( 19 | color: AppColors.transparent, 20 | child: InkWell( 21 | customBorder: const CircleBorder(side: BorderSide()), 22 | onTap: () { 23 | Navigator.of(context).pop(); 24 | }, 25 | highlightColor: AppColors.textBlue.withOpacity(0.1), 26 | child: Padding( 27 | padding: const EdgeInsets.only(bottom: 20, top: 20, right: 4), 28 | child: SvgPicture.asset( 29 | ImagesProvider.APP_BAR_BACK, 30 | color: AppColors.accentBlack, 31 | ), 32 | ), 33 | ), 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/theme/bunny_back_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/theme/app_colors.dart'; 2 | import 'package:bunny_search/theme/images_provider.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_svg/svg.dart'; 5 | 6 | class BunnyBackButton extends StatelessWidget { 7 | const BunnyBackButton({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Row( 12 | mainAxisAlignment: MainAxisAlignment.start, 13 | children: [ 14 | GestureDetector( 15 | behavior: HitTestBehavior.translucent, 16 | onTap: () { 17 | Navigator.of(context).pop(); 18 | }, 19 | child: Container( 20 | width: 40, 21 | height: 40, 22 | decoration: BoxDecoration( 23 | color: AppColors.white, 24 | shape: BoxShape.circle, 25 | border: Border.all(color: AppColors.white), 26 | ), 27 | child: Material( 28 | color: AppColors.transparent, 29 | child: InkWell( 30 | customBorder: const CircleBorder(side: BorderSide()), 31 | onTap: () { 32 | Navigator.of(context).pop(); 33 | }, 34 | highlightColor: AppColors.textBlue.withOpacity(0.1), 35 | child: Padding( 36 | padding: const EdgeInsets.all(8), 37 | child: SvgPicture.asset( 38 | ImagesProvider.BACK, 39 | color: AppColors.textBlue, 40 | width: 24, 41 | height: 24, 42 | ), 43 | ), 44 | ), 45 | ), 46 | ), 47 | ) 48 | ], 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/theme/bunny_cached_logo_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/theme/app_typography.dart'; 2 | import 'package:bunny_search/utils/cache/image_cache_manager.dart'; 3 | import 'package:cached_network_image/cached_network_image.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | 6 | class BunnyCachedLogoImage extends StatelessWidget { 7 | final String logoUrl; 8 | final String title; 9 | 10 | const BunnyCachedLogoImage({ 11 | Key? key, 12 | required this.logoUrl, 13 | required this.title, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return CachedNetworkImage( 19 | fadeOutDuration: const Duration(milliseconds: 250), 20 | imageUrl: logoUrl, 21 | placeholder: (context, url) => Text( 22 | title.substring(0, 1), 23 | style: AppTypography.header.copyWith(fontWeight: FontWeight.bold), 24 | textAlign: TextAlign.center, 25 | ), 26 | errorWidget: (context, error, stacktrace) { 27 | return Text( 28 | title.substring(0, 1), 29 | style: AppTypography.header.copyWith(fontWeight: FontWeight.bold), 30 | textAlign: TextAlign.center, 31 | ); 32 | }, 33 | cacheManager: OneYearImageCacheManager.instance, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/theme/bunny_snack_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:bunny_search/theme/app_colors.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | const Duration _snackBarDisplayDuration = Duration(milliseconds: 4000); 5 | 6 | class BunnyDefaultSnackBar extends SnackBar { 7 | BunnyDefaultSnackBar({ 8 | Key? key, 9 | required String text, 10 | SnackBarBehavior behavior = SnackBarBehavior.floating, 11 | bool isDismissible = true, 12 | VoidCallback? onRetry, 13 | SnackBarAction? action, 14 | }) : super( 15 | key: key, 16 | content: Padding( 17 | padding: const EdgeInsets.symmetric(vertical: 4.0), 18 | child: Text(text), 19 | ), 20 | backgroundColor: AppColors.textBlue, 21 | behavior: behavior, 22 | elevation: 0.0, 23 | action: onRetry != null 24 | ? SnackBarAction(label: 'Retry', onPressed: onRetry) 25 | : action, 26 | dismissDirection: DismissDirection.endToStart, 27 | duration: isDismissible && onRetry == null 28 | ? _snackBarDisplayDuration 29 | : const Duration(days: 365), 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /lib/theme/images_provider.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | // TODO: Refactor to codegen eventually 3 | class ImagesProvider { 4 | static const BASE_PATH = 'resources/images'; 5 | static const BACKGROUND = '$BASE_PATH/background.png'; 6 | 7 | static const BASE_ICON_PATH = 'resources/icons'; 8 | static const CHECK = '$BASE_ICON_PATH/ic_check.svg'; 9 | static const QUESTION = '$BASE_ICON_PATH/ic_question.svg'; 10 | static const CROSS = '$BASE_ICON_PATH/ic_cross.svg'; 11 | 12 | static const SEARCH_BUNNY = '$BASE_ICON_PATH/ic_bunny.png'; 13 | static const LEAPING_BUNNY = '$BASE_ICON_PATH/ic_leaping_bunny.png'; 14 | 15 | static const BACK = '$BASE_ICON_PATH/ic_chevron_right.svg'; 16 | 17 | static const APP_BAR_BACK = '$BASE_ICON_PATH/ic_back.svg'; 18 | 19 | static const ORG_PETA = '$BASE_PATH/ic_peta.png'; 20 | static const ORG_BUNNY = '$BASE_PATH/ic_bunny_search.png'; 21 | 22 | static const STOP = '$BASE_ICON_PATH/ic_stop.svg'; 23 | } 24 | -------------------------------------------------------------------------------- /lib/utils/cache/image_cache_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 2 | // TODO refactor 3 | // ignore: implementation_imports 4 | import 'package:flutter_cache_manager/src/storage/file_system/file_system_io.dart'; 5 | 6 | class OneYearImageCacheManager { 7 | static const _key = 'bunny_image_cache_key'; 8 | static CacheManager instance = CacheManager( 9 | Config( 10 | _key, 11 | stalePeriod: const Duration(days: 365), 12 | maxNrOfCacheObjects: 5000, 13 | repo: CacheObjectProvider(databaseName: _key), 14 | fileSystem: IOFileSystem(_key), 15 | fileService: HttpFileService(), 16 | ), 17 | ); 18 | } -------------------------------------------------------------------------------- /lib/utils/widget/focus_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | class FocusUtils { 4 | static void unfocus(BuildContext context) { 5 | final currentScope = FocusScope.of(context); 6 | if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) { 7 | FocusManager.instance.primaryFocus?.unfocus(); 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bunny_search 2 | description: Bunny Search App 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.1+1 19 | 20 | environment: 21 | sdk: ">=2.12.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | domain: 27 | path: ./domain 28 | data: 29 | path: ./data 30 | flutter_svg: ^0.22.0 31 | equatable: ^2.0.3 32 | firebase_core: ^1.6.0 33 | firebase_database: ^7.2.1 34 | sliding_up_panel: ^2.0.0+1 35 | flutter_bloc: ^7.2.0 36 | flutter_secure_storage: ^4.2.1 37 | shared_preferences: ^2.0.8 38 | fimber: ^0.6.1 39 | quiver: ^3.0.1 40 | rxdart: ^0.27.2 41 | floor: ^1.2.0 42 | cached_network_image: ^3.3.1 43 | firebase_crashlytics: ^2.2.4 44 | easy_localization: ^3.0.0 45 | cupertino_icons: ^1.0.2 46 | file: ^6.1.4 47 | 48 | dependency_overrides: 49 | platform: ^3.1.0 50 | 51 | dev_dependencies: 52 | flutter_test: 53 | sdk: flutter 54 | flutter_launcher_icons: ^0.9.2 55 | build_runner: ^2.0.5 56 | flutter_native_splash: ^1.2.4 57 | 58 | 59 | flutter_icons: 60 | android: true 61 | adaptive_icon_background: "resources/icons/background.png" 62 | adaptive_icon_foreground: "resources/icons/foreground.png" 63 | ios: true 64 | image_path: "resources/icons/launcher.png" 65 | remove_alpha_ios: true 66 | 67 | flutter_native_splash: 68 | # This package generates native code to customize Flutter's default white native splash screen 69 | # with background color and splash image. 70 | # Customize the parameters below, and run the following command in the terminal: 71 | # flutter pub run flutter_native_splash:create 72 | # To restore Flutter's default white splash screen, run the following command in the terminal: 73 | # flutter pub run flutter_native_splash:remove 74 | background_image: "resources/splash/splash_bg.png" 75 | android: true 76 | ios: true 77 | fullscreen: true 78 | android_gravity: center 79 | ios_content_mode: center 80 | 81 | # For information on the generic Dart part of this file, see the 82 | # following page: https://dart.dev/tools/pub/pubspec 83 | 84 | # The following section is specific to Flutter. 85 | flutter: 86 | 87 | # The following line ensures that the Material Icons font is 88 | # included with your application, so that you can use the icons in 89 | # the material Icons class. 90 | uses-material-design: true 91 | 92 | # To add assets to your application, add an assets section, like this: 93 | # assets: 94 | # - images/a_dot_burr.jpeg 95 | # - images/a_dot_ham.jpeg 96 | assets: 97 | - resources/images/ 98 | - resources/icons/ 99 | - resources/langs/ 100 | - resources/database/ 101 | 102 | # An image asset can refer to one or more resolution-specific "variants", see 103 | # https://flutter.dev/assets-and-images/#resolution-aware. 104 | 105 | # For details regarding adding assets from package dependencies, see 106 | # https://flutter.dev/assets-and-images/#from-packages 107 | 108 | fonts: 109 | - family: Inter 110 | fonts: 111 | - asset: resources/fonts/Inter-Regular.ttf 112 | - asset: resources/fonts/Inter-Medium.ttf 113 | weight: 500 114 | # For details regarding fonts from package dependencies, 115 | # see https://flutter.dev/custom-fonts/#from-packages 116 | -------------------------------------------------------------------------------- /resources/fonts/Inter-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/fonts/Inter-Black.ttf -------------------------------------------------------------------------------- /resources/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /resources/fonts/Inter-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/fonts/Inter-ExtraBold.ttf -------------------------------------------------------------------------------- /resources/fonts/Inter-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/fonts/Inter-ExtraLight.ttf -------------------------------------------------------------------------------- /resources/fonts/Inter-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/fonts/Inter-Light.ttf -------------------------------------------------------------------------------- /resources/fonts/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/fonts/Inter-Medium.ttf -------------------------------------------------------------------------------- /resources/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /resources/fonts/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/fonts/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /resources/fonts/Inter-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/fonts/Inter-Thin.ttf -------------------------------------------------------------------------------- /resources/icons/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/icons/background.png -------------------------------------------------------------------------------- /resources/icons/foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/icons/foreground.png -------------------------------------------------------------------------------- /resources/icons/ic_back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/ic_bunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/icons/ic_bunny.png -------------------------------------------------------------------------------- /resources/icons/ic_check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/icons/ic_chevron_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/ic_cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /resources/icons/ic_leaping_bunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/icons/ic_leaping_bunny.png -------------------------------------------------------------------------------- /resources/icons/ic_question.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/icons/ic_stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/icons/launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/icons/launcher.png -------------------------------------------------------------------------------- /resources/images/2.0x/ic_bunny_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/images/2.0x/ic_bunny_search.png -------------------------------------------------------------------------------- /resources/images/2.0x/ic_peta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/images/2.0x/ic_peta.png -------------------------------------------------------------------------------- /resources/images/3.0x/ic_bunny_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/images/3.0x/ic_bunny_search.png -------------------------------------------------------------------------------- /resources/images/3.0x/ic_peta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/images/3.0x/ic_peta.png -------------------------------------------------------------------------------- /resources/images/ic_bunny_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/images/ic_bunny_search.png -------------------------------------------------------------------------------- /resources/images/ic_peta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/images/ic_peta.png -------------------------------------------------------------------------------- /resources/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_title": "Bunny Search", 3 | "splash_loading_brands": "Loading beauty brands...", 4 | "home_organizations": "Organizations", 5 | "home_show_all": "Show All", 6 | "home_popular_brands": "Popular brands", 7 | "home_results": "Results", 8 | "home_support_dialog_title": "Welcome!", 9 | "home_support_dialog_content": "Thanks for using the app and making the effort to choose cruelty-free! \uD83D\uDC96️\n️\nWe are very happy to help make that choice easier for you, but there is one little thing we ask you to keep in mind.\n\nWe are a very small team of 3: a data manager, a designer, and an app developer. \n\nFurthermore, we are working on this app only in our free time, and we’re trying our best to keep the information up to date, communicate with the brands, get the official permissions from the organizations to show their data, and fix bugs! \n\nThe app is and will be completely free and doesn't contain any ads or affiliate links.\n\nWe are sorry if the brand you are looking for is missing from our lists, or you encounter any problems while using the app. \n\nPlease feel free to reach out to us about any concerns via bunnysearchmobileapp@gmail.com, and we will be happy to help!\n\nStay tuned for the updates, and thank you for choosing cruelty-free \uD83D\uDC30", 10 | "home_support_dialog_team": "Your Bunny Search Team \uD83D\uDC07\uD83D\uDD0D", 11 | "home_support_dialog_close_button": "Got it!", 12 | "home_search_prompt": "Let's find\ncruelty free brands", 13 | "home_search_hint": "Start brand search", 14 | "organization_peta_do_test": "PETA Do Test", 15 | "organization_peta_dont_test": "PETA Don't Test", 16 | "organization_bunny_search": "Bunny Search", 17 | "brand_details_cf_markers": "Cruelty-free markers", 18 | "brand_details_peta_dont_test_marker": "Is in PETA Don't Test list", 19 | "brand_details_peta_do_test_marker": "Is in PETA Do Test list", 20 | "brand_details_bunny_search_marker": "Approved by the Bunny Search team", 21 | "brand_details_other_markers": "Other markers", 22 | "brand_details_vegan_marker": "Brand has vegan products \uD83C\uDF31", 23 | "brand_details_based_on": "Based on: {source}", 24 | "organization_brands_count": "{count} brands", 25 | "organizations_title": "Organizations", 26 | "popular_brands_title": "Popular brands", 27 | "general_error": "Something went wrong :(" 28 | } -------------------------------------------------------------------------------- /resources/langs/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_title": "Bunny Search", 3 | "splash_loading_brands": "Загружаем бренды...", 4 | "home_organizations": "Организации", 5 | "home_show_all": "Все", 6 | "home_popular_brands": "Популярные бренды", 7 | "home_results": "Результаты", 8 | "home_support_dialog_title": "Привет!", 9 | "home_support_dialog_content": "Thanks for using the app and making the effort to choose cruelty-free! \uD83D\uDC96️\n️\nWe are very happy to help make that choice easier for you, but there is one little thing we ask you to keep in mind.\n\nWe are a very small team of 3: a data manager, a designer, and an app developer. \n\nFurthermore, we are working on this app only in our free time, and we’re trying our best to keep the information up to date, communicate with the brands, get the official permissions from the organizations to show their data, and fix bugs! \n\nThe app is and will be completely free and doesn't contain any ads or affiliate links.\n\nWe are sorry if the brand you are looking for is missing from our lists, or you encounter any problems while using the app. \n\nPlease feel free to reach out to us about any concerns via bunnysearchmobileapp@gmail.com, and we will be happy to help!\n\nStay tuned for the updates, and thank you for choosing cruelty-free \uD83D\uDC30", 10 | "home_support_dialog_team": "Твоя команда Bunny Search \uD83D\uDC07\uD83D\uDD0D", 11 | "home_support_dialog_close_button": "Понятно!", 12 | "home_search_prompt": "Давай найдем\nэтичные бренды", 13 | "home_search_hint": "Начать поиск...", 14 | "organization_peta_do_test": "PETA Неэтичный", 15 | "organization_peta_dont_test": "PETA Этичный", 16 | "organization_bunny_search": "Bunny Search", 17 | "brand_details_cf_markers": "Маркеры этичности", 18 | "brand_details_peta_dont_test_marker": "В этичном списке PETA", 19 | "brand_details_peta_do_test_marker": "В неэтичном списке PETA", 20 | "brand_details_bunny_search_marker": "Проверен командой Bunny Search", 21 | "brand_details_other_markers": "Другие маркеры", 22 | "brand_details_vegan_marker": "У бренда есть vegan продукты \uD83C\uDF31", 23 | "brand_details_based_on": "Источник: {source}", 24 | "organization_brands_count": "{count} брендов", 25 | "organizations_title": "Организации", 26 | "popular_brands_title": "Популярные бренды", 27 | "general_error": "Что-то пошло не так :(" 28 | } -------------------------------------------------------------------------------- /resources/scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ( 4 | cd domain || exit 5 | flutter clean 6 | flutter pub get 7 | ) 8 | ( 9 | cd data || exit 10 | flutter clean 11 | flutter pub get 12 | ) 13 | # clean lib 14 | flutter clean 15 | flutter pub get 16 | -------------------------------------------------------------------------------- /resources/scripts/generate_icons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # navigate to repo root from /resources/scripts 4 | cd ../.. 5 | sh ./resources/scripts/clean.sh 6 | flutter pub run flutter_launcher_icons:main 7 | -------------------------------------------------------------------------------- /resources/scripts/generate_json_models.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | flutter pub get 4 | flutter pub run build_runner build --delete-conflicting-outputs 5 | -------------------------------------------------------------------------------- /resources/scripts/generate_splash.sh: -------------------------------------------------------------------------------- 1 | flutter clean 2 | flutter pub get 3 | flutter pub run flutter_native_splash:create -------------------------------------------------------------------------------- /resources/scripts/run_clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # navigate to repo root from /resources/scripts 4 | cd ../.. 5 | sh ./resources/scripts/clean.sh -------------------------------------------------------------------------------- /resources/scripts/run_generate_json_models.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ../.. 4 | cd data || exit 5 | sh ../resources/scripts/generate_json_models.sh 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/scripts/update_translations.sh: -------------------------------------------------------------------------------- 1 | flutter pub run easy_localization:generate 2 | flutter pub run easy_localization:generate -f keys -o locale_keys.g.dart -------------------------------------------------------------------------------- /resources/splash/splash_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darjaorlova/bunny-search-source-code/137ef5ff61fba39768097b829a551a0c167176be/resources/splash/splash_bg.png --------------------------------------------------------------------------------