├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 180.png │ │ │ ├── 29.png │ │ │ ├── 40.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ ├── 1024.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── .gitignore ├── Podfile └── Podfile.lock ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── manifest.json └── index.html ├── samples ├── screen_1.png └── screen_2.png ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── drawable │ │ │ │ │ ├── app_icon.png │ │ │ │ │ └── launch_background.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 │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── food_rating_app │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── .metadata ├── lib ├── common │ ├── navigation.dart │ └── styles.dart ├── data │ ├── api │ │ ├── urls.dart │ │ └── api_service.dart │ ├── database │ │ └── database_helper.dart │ └── models │ │ └── restaurant.dart ├── utilities │ ├── date_time_helper.dart │ ├── background_service.dart │ └── notification_helper.dart ├── modules │ ├── screens │ │ ├── preferences │ │ │ ├── preferences_helper.dart │ │ │ └── settings_screen.dart │ │ ├── restaurant │ │ │ ├── widgets │ │ │ │ ├── animation_placeholder.dart │ │ │ │ └── restaurant_card.dart │ │ │ ├── favorite_screen.dart │ │ │ ├── list_screen.dart │ │ │ ├── review_screen.dart │ │ │ ├── search_screen.dart │ │ │ └── detail_screen.dart │ │ └── home_screen.dart │ └── providers │ │ ├── scheduling_provider.dart │ │ ├── preferences_provider.dart │ │ ├── database_provider.dart │ │ └── restaurant_provider.dart ├── routes.dart └── main.dart ├── .gitignore ├── pubspec.yaml ├── LICENSE ├── .github └── workflows │ └── dart.yml ├── analysis_options.yaml ├── README.md ├── test └── provider │ ├── restaurant_provider_test.mocks.dart │ └── restaurant_provider_test.dart └── assets ├── empty.json ├── done.json ├── not-found.json └── no-internet.json /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/web/favicon.png -------------------------------------------------------------------------------- /samples/screen_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/samples/screen_1.png -------------------------------------------------------------------------------- /samples/screen_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/samples/screen_2.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/web/icons/Icon-512.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/android/app/src/main/res/drawable/app_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronaut/flutter-food-review-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/food_rating_app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.food_rating_app 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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-6.7-all.zip 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.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/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.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: 18116933e77adc82f80866c928266a5b4f1ed645 8 | channel: stable 9 | 10 | project_type: app 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 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/common/navigation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | final GlobalKey navigatorKey = GlobalKey(); 4 | 5 | class Navigation { 6 | static intentWithData(String routeName, {Object? arguments}) { 7 | navigatorKey.currentState?.pushNamed(routeName, arguments: arguments); 8 | } 9 | 10 | static back() => navigatorKey.currentState?.pop(); 11 | } 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /lib/data/api/urls.dart: -------------------------------------------------------------------------------- 1 | class Urls { 2 | static const String baseUrl = 'https://restaurant-api.dicoding.dev'; 3 | static const String restaurantList = '/list'; 4 | static String restaurantDetail(String id) => '/detail/$id'; 5 | static const String searchRestaurant = '/search'; 6 | static const String restraurantReview = '/review'; 7 | static String restaurantImage(String pictureId) => 8 | 'https://restaurant-api.dicoding.dev/images/small/$pictureId'; 9 | static String largeRestaurantImage(String pictureId) => 10 | 'https://restaurant-api.dicoding.dev/images/large/$pictureId'; 11 | } 12 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /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 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /lib/utilities/date_time_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | class DateTimeHelper { 4 | static DateTime format() { 5 | /// Date and time format 6 | final now = DateTime.now(); 7 | final dateFormat = DateFormat('y/M/d'); 8 | const timeSpecific = '11:00:00'; 9 | final completeFormat = DateFormat('y/M/d H:m:s'); 10 | 11 | /// Today format 12 | final todayDate = dateFormat.format(now); 13 | final todayDateAndTime = '$todayDate $timeSpecific'; 14 | var resultToday = completeFormat.parseStrict(todayDateAndTime); 15 | 16 | /// Tomorrow format 17 | var formatted = resultToday.add(const Duration(days: 1)); 18 | final tomorrowDate = dateFormat.format(formatted); 19 | final tomorrowDateAndTime = '$tomorrowDate $timeSpecific'; 20 | var resultTomorrow = completeFormat.parseStrict(tomorrowDateAndTime); 21 | 22 | return now.isAfter(resultToday) ? resultTomorrow : resultToday; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: food_rating_app 2 | description: A new Flutter project. 3 | 4 | publish_to: "none" 5 | 6 | version: 1.0.0+1 7 | 8 | environment: 9 | sdk: ">=2.12.0 <3.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | android_alarm_manager: ^2.0.2 16 | animate_do: ^2.0.0 17 | cached_network_image: ^3.1.0+1 18 | cupertino_icons: ^1.0.2 19 | dio: ^4.0.4 20 | flutter_local_notifications: ^9.1.4 21 | flutter_spinkit: ^5.1.0 22 | provider: ^5.0.0 23 | google_fonts: ^2.1.0 24 | intl: ^0.17.0 25 | lottie: ^1.2.1 26 | persistent_bottom_nav_bar: ^4.0.2 27 | readmore: ^2.1.0 28 | rxdart: ^0.27.3 29 | shared_preferences: ^2.0.9 30 | styled_text: ^3.0.4+1 31 | sqflite: ^2.0.0+4 32 | 33 | dev_dependencies: 34 | flutter_test: 35 | sdk: flutter 36 | 37 | mockito: ^5.0.16 38 | http_mock_adapter: ^0.3.2 39 | flutter_lints: ^1.0.0 40 | build_runner: ^2.1.2 41 | 42 | flutter: 43 | uses-material-design: true 44 | 45 | assets: 46 | - assets/ 47 | -------------------------------------------------------------------------------- /lib/modules/screens/preferences/preferences_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class PreferencesHelper { 4 | final Future sharedPreferences; 5 | PreferencesHelper({required this.sharedPreferences}); 6 | 7 | static const darkTheme = 'DARK_THEME'; 8 | 9 | Future get isDarkTheme async { 10 | final prefs = await sharedPreferences; 11 | return prefs.getBool(darkTheme) ?? false; 12 | } 13 | 14 | void setDarkTheme(bool value) async { 15 | final prefs = await sharedPreferences; 16 | prefs.setBool(darkTheme, value); 17 | } 18 | 19 | static const dailyRestaurant = 'DAILY_RESTAURANT'; 20 | 21 | Future get isDailyRestaurantActive async { 22 | final prefs = await sharedPreferences; 23 | return prefs.getBool(dailyRestaurant) ?? false; 24 | } 25 | 26 | void setDailyRestaurantActive(bool value) async { 27 | final prefs = await sharedPreferences; 28 | prefs.setBool(dailyRestaurant, value); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/modules/providers/scheduling_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:android_alarm_manager/android_alarm_manager.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:food_rating_app/utilities/background_service.dart'; 4 | import 'package:food_rating_app/utilities/date_time_helper.dart'; 5 | 6 | class SchedulingProvider extends ChangeNotifier { 7 | bool _isScheduled = false; 8 | bool get isScheduled => _isScheduled; 9 | 10 | Future scheduledDailyRestaurant(bool value) async { 11 | _isScheduled = value; 12 | if (_isScheduled) { 13 | print('Scheduling acitvated'); 14 | notifyListeners(); 15 | return await AndroidAlarmManager.periodic( 16 | const Duration(hours: 24), 17 | 1, 18 | BackgroundService.callback, 19 | startAt: DateTimeHelper.format(), 20 | exact: true, 21 | wakeup: true, 22 | ); 23 | } else { 24 | print('Scheduling canceled'); 25 | notifyListeners(); 26 | return await AndroidAlarmManager.cancel(1); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:food_rating_app/modules/screens/home_screen.dart'; 3 | import 'package:food_rating_app/modules/screens/restaurant/detail_screen.dart'; 4 | import 'package:food_rating_app/modules/screens/restaurant/list_screen.dart'; 5 | import 'package:food_rating_app/modules/screens/restaurant/review_screen.dart'; 6 | import 'package:food_rating_app/modules/screens/restaurant/search_screen.dart'; 7 | 8 | Map allRoute(BuildContext context) { 9 | return { 10 | HomeScreen.routeName: (context) => const HomeScreen(), 11 | ListScreen.routeName: (context) => const ListScreen(), 12 | DetailScreen.routeName: (context) => DetailScreen( 13 | id: ModalRoute.of(context)!.settings.arguments as String, 14 | ), 15 | ReviewScreen.routeName: (context) => ReviewScreen( 16 | id: ModalRoute.of(context)!.settings.arguments as String, 17 | ), 18 | SearchScreen.routeName: (context) => const SearchScreen() 19 | 20 | /// TODO: Add more later 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "food_rating_app", 3 | "short_name": "food_rating_app", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Aditya Rohman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/utilities/background_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:isolate'; 2 | 3 | import 'dart:ui'; 4 | import 'package:food_rating_app/main.dart'; 5 | import 'package:food_rating_app/data/api/api_service.dart'; 6 | import 'package:food_rating_app/utilities/notification_helper.dart'; 7 | 8 | final ReceivePort port = ReceivePort(); 9 | 10 | class BackgroundService { 11 | static BackgroundService? _instance; 12 | static const String _isolateName = 'isolate'; 13 | static SendPort? _uiSendPort; 14 | 15 | BackgroundService._internal() { 16 | _instance = this; 17 | } 18 | 19 | factory BackgroundService() => _instance ?? BackgroundService._internal(); 20 | 21 | void initializeIsolate() { 22 | IsolateNameServer.registerPortWithName(port.sendPort, _isolateName); 23 | } 24 | 25 | static Future callback() async { 26 | final NotificationHelper _notificationHelper = NotificationHelper(); 27 | var result = await ApiService().getRestaurantList(); 28 | await _notificationHelper.showNotification( 29 | flutterLocalNotificationsPlugin, 30 | result, 31 | ); 32 | 33 | _uiSendPort ??= IsolateNameServer.lookupPortByName(_isolateName); 34 | _uiSendPort?.send(null); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/modules/providers/preferences_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:food_rating_app/modules/screens/preferences/preferences_helper.dart'; 3 | import 'package:food_rating_app/common/styles.dart'; 4 | 5 | class PreferencesProvider extends ChangeNotifier { 6 | final PreferencesHelper preferencesHelper; 7 | 8 | bool _isDarkTheme = false; 9 | bool get isDarkTheme => _isDarkTheme; 10 | 11 | bool _isDailyRestaurantActive = false; 12 | bool get isDailyRestaurantActive => _isDailyRestaurantActive; 13 | 14 | PreferencesProvider({required this.preferencesHelper}) { 15 | _getTheme(); 16 | _getDailyRestaurant(); 17 | } 18 | 19 | void _getTheme() async { 20 | _isDarkTheme = await preferencesHelper.isDarkTheme; 21 | notifyListeners(); 22 | } 23 | 24 | void _getDailyRestaurant() async { 25 | _isDailyRestaurantActive = await preferencesHelper.isDailyRestaurantActive; 26 | notifyListeners(); 27 | } 28 | 29 | void enableDarkTheme(bool value) async { 30 | preferencesHelper.setDarkTheme(value); 31 | _getTheme(); 32 | } 33 | 34 | void enableDailyRestaurant(bool value) async { 35 | preferencesHelper.setDailyRestaurantActive(value); 36 | _getDailyRestaurant(); 37 | } 38 | 39 | ThemeData get themeData => _isDarkTheme ? darkTheme : lightTheme; 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Dart 7 | 8 | on: 9 | push: 10 | branches: [ main ] 11 | pull_request: 12 | branches: [ main ] 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | # Note: This workflow uses the latest stable version of the Dart SDK. 22 | # You can specify other versions if desired, see documentation here: 23 | # https://github.com/dart-lang/setup-dart/blob/main/README.md 24 | # - uses: dart-lang/setup-dart@v1 25 | - uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603 26 | 27 | - name: Install dependencies 28 | run: dart pub get 29 | 30 | # Uncomment this step to verify the use of 'dart format' on each commit. 31 | # - name: Verify formatting 32 | # run: dart format --output=none --set-exit-if-changed . 33 | 34 | # Consider passing '--fatal-infos' for slightly stricter analysis. 35 | - name: Analyze project source 36 | run: dart analyze 37 | 38 | # Your project will need to have tests in test/ and a dependency on 39 | # package:test for this step to succeed. Note that Flutter projects will 40 | # want to change this to 'flutter test'. 41 | - name: Run tests 42 | run: dart test 43 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.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 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]} -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - flutter_local_notifications (0.0.1): 4 | - Flutter 5 | - FMDB (2.7.5): 6 | - FMDB/standard (= 2.7.5) 7 | - FMDB/standard (2.7.5) 8 | - path_provider_ios (0.0.1): 9 | - Flutter 10 | - shared_preferences_ios (0.0.1): 11 | - Flutter 12 | - sqflite (0.0.2): 13 | - Flutter 14 | - FMDB (>= 2.7.5) 15 | 16 | DEPENDENCIES: 17 | - Flutter (from `Flutter`) 18 | - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) 19 | - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) 20 | - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) 21 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 22 | 23 | SPEC REPOS: 24 | trunk: 25 | - FMDB 26 | 27 | EXTERNAL SOURCES: 28 | Flutter: 29 | :path: Flutter 30 | flutter_local_notifications: 31 | :path: ".symlinks/plugins/flutter_local_notifications/ios" 32 | path_provider_ios: 33 | :path: ".symlinks/plugins/path_provider_ios/ios" 34 | shared_preferences_ios: 35 | :path: ".symlinks/plugins/shared_preferences_ios/ios" 36 | sqflite: 37 | :path: ".symlinks/plugins/sqflite/ios" 38 | 39 | SPEC CHECKSUMS: 40 | Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a 41 | flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 42 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 43 | path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 44 | shared_preferences_ios: aef470a42dc4675a1cdd50e3158b42e3d1232b32 45 | sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 46 | 47 | PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c 48 | 49 | COCOAPODS: 1.11.2 50 | -------------------------------------------------------------------------------- /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 | CFBundleName 14 | Food Review 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/data/api/api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:food_rating_app/data/models/restaurant.dart'; 3 | import 'package:food_rating_app/data/api/urls.dart'; 4 | 5 | class ApiService { 6 | final Dio _client = Dio( 7 | BaseOptions( 8 | baseUrl: Urls.baseUrl, 9 | connectTimeout: 5000, 10 | receiveTimeout: 3000, 11 | contentType: Headers.formUrlEncodedContentType, 12 | ), 13 | ); 14 | 15 | Future getRestaurantList() async { 16 | final response = await _client.get(Urls.restaurantList); 17 | 18 | if (response.statusCode == 200) { 19 | return Restaurants.fromMap(response.data); 20 | } else { 21 | throw Exception('Failed to load restaurant list!'); 22 | } 23 | } 24 | 25 | Future searchRestaurant(String query) async { 26 | final response = await _client.get( 27 | Urls.searchRestaurant, 28 | queryParameters: {'q': query}, 29 | ); 30 | 31 | if (response.statusCode == 200) { 32 | return Restaurants.fromSearchMap(response.data); 33 | } else { 34 | throw Exception('Failed to load search result!'); 35 | } 36 | } 37 | 38 | Future getRestaurantDetailById(String id) async { 39 | final response = await _client.get(Urls.restaurantDetail(id)); 40 | 41 | if (response.statusCode == 200) { 42 | return Restaurant.fromMap(response.data); 43 | } else { 44 | throw Exception('Failed to load restaurant detail!'); 45 | } 46 | } 47 | 48 | Future postReviewById({ 49 | required String id, 50 | required String name, 51 | required String review, 52 | }) async { 53 | _client.interceptors.add(LogInterceptor(requestBody: true)); 54 | final request = await _client.post( 55 | Urls.restraurantReview, 56 | data: {'id': id, 'name': name, 'review': review}, 57 | ); 58 | 59 | if (request.statusCode == 201) { 60 | return true; 61 | } else { 62 | throw Exception('Failed to post review!'); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /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 30 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 46 | applicationId "co.rainn.food_rating_app" 47 | minSdkVersion 16 48 | targetSdkVersion 30 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | } 52 | 53 | buildTypes { 54 | release { 55 | // TODO: Add your own signing config for the release build. 56 | // Signing with the debug keys for now, so `flutter run --release` works. 57 | signingConfig signingConfigs.debug 58 | } 59 | } 60 | } 61 | 62 | flutter { 63 | source '../..' 64 | } 65 | 66 | dependencies { 67 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Food Rating App 2 | 3 | This app is called FoodReview. The main feature is we can discover some restaurants, get details, foods, drinks, and post a review for that restaurant. 4 | 5 | Sample 1 6 | 7 | Sample 2 8 | 9 | ### Languages and Frameworks: 10 | 11 | [Flutter][flutter] 12 | [Dart][dart] 13 | 14 | ### Here are the features in detail: 15 | 16 | ⚡ List of restaurant 17 | 18 | ⚡ Detail restaurant 19 | 20 | ⚡ Write a review 21 | 22 | ⚡ Search a restaurant 23 | 24 | ⚡ Favorite restaurant list 25 | 26 | ⚡ Add favorite restaurant 27 | 28 | ⚡ Delete favorite restaurant 29 | 30 | ⚡ Dark mode 31 | 32 | ⚡ Daily reminder 33 | 34 | ⚡ Testing 35 | 36 | ### Plugin used: 37 | 38 | - android_alarm_manager: ^2.0.2 39 | - animate_do: ^2.0.0 40 | - cached_network_image: ^3.1.0+1 41 | - cupertino_icons: ^1.0.2 42 | - dio: ^4.0.4 43 | - flutter_local_notifications: ^9.1.4 44 | - flutter_spinkit: ^5.1.0 45 | - provider: ^5.0.0 46 | - google_fonts: ^2.1.0 47 | - intl: ^0.17.0 48 | - lottie: ^1.2.1 49 | - persistent_bottom_nav_bar: ^4.0.2 50 | - readmore: ^2.1.0 51 | - rxdart: ^0.27.3 52 | - shared_preferences: ^2.0.9 53 | - styled_text: ^3.0.4+1 54 | - sqflite: ^2.0.0+4 55 | 56 | ## Getting Started 57 | 58 | This project is a starting point for a Flutter application. 59 | 60 | A few resources to get you started if this is your first Flutter project: 61 | 62 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 63 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 64 | 65 | For help getting started with Flutter, view our 66 | [online documentation](https://flutter.dev/docs), which offers tutorials, 67 | samples, guidance on mobile development, and a full API reference. 68 | 69 | [flutter]: https://flutter.dev 70 | [dart]: https://dart.dev 71 | -------------------------------------------------------------------------------- /lib/modules/providers/database_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:food_rating_app/data/database/database_helper.dart'; 3 | import 'package:food_rating_app/data/models/restaurant.dart'; 4 | 5 | enum DatabaseResultState { loading, noData, hasData, error } 6 | 7 | class DatabaseProvider extends ChangeNotifier { 8 | final DatabaseHelper databaseHelper; 9 | 10 | List _favorites = []; 11 | List get favorites => _favorites; 12 | 13 | String _message = ''; 14 | String get message => _message; 15 | 16 | DatabaseResultState? _databaseResultState; 17 | DatabaseResultState? get databaseResultState => _databaseResultState; 18 | 19 | DatabaseProvider({required this.databaseHelper}) { 20 | _getFavorites(); 21 | } 22 | 23 | void _getFavorites() async { 24 | _favorites = await databaseHelper.getFavorites(); 25 | 26 | if (_favorites.isNotEmpty) { 27 | _databaseResultState = DatabaseResultState.hasData; 28 | } else { 29 | _databaseResultState = DatabaseResultState.noData; 30 | _message = 'Database is empty'; 31 | } 32 | 33 | notifyListeners(); 34 | } 35 | 36 | Future addFavorite(Item restaurant) async { 37 | try { 38 | await databaseHelper.insertFavorite(restaurant); 39 | _getFavorites(); 40 | } catch (e) { 41 | _databaseResultState = DatabaseResultState.error; 42 | _message = 'Error: $e'; 43 | notifyListeners(); 44 | } 45 | } 46 | 47 | Future isFavorite(String id) async { 48 | final favorite = await databaseHelper.getFavoriteById(id); 49 | return favorite.isNotEmpty; 50 | } 51 | 52 | void deleteFavoriteById(String id) async { 53 | try { 54 | await databaseHelper.deleteFavorite(id); 55 | _getFavorites(); 56 | } catch (e) { 57 | _databaseResultState = DatabaseResultState.error; 58 | _message = 'Error: $e'; 59 | notifyListeners(); 60 | } 61 | } 62 | 63 | void deleteAllFavorite() async { 64 | try { 65 | await databaseHelper.deleteAllFavorite(); 66 | _getFavorites(); 67 | } catch (e) { 68 | _databaseResultState = DatabaseResultState.error; 69 | _message = 'Error: $e'; 70 | notifyListeners(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/modules/screens/restaurant/widgets/animation_placeholder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:food_rating_app/common/styles.dart'; 3 | import 'package:lottie/lottie.dart'; 4 | 5 | class AnimationPlaceholder extends StatelessWidget { 6 | final String animation; 7 | final String text; 8 | final bool hasButton; 9 | final String? buttonText; 10 | final Function()? onButtonTap; 11 | const AnimationPlaceholder({ 12 | Key? key, 13 | required this.animation, 14 | required this.text, 15 | this.hasButton = false, 16 | this.buttonText, 17 | this.onButtonTap, 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Center( 23 | child: Column( 24 | mainAxisAlignment: MainAxisAlignment.center, 25 | crossAxisAlignment: CrossAxisAlignment.center, 26 | children: [ 27 | Lottie.asset( 28 | animation, 29 | width: 180.0, 30 | height: 180.0, 31 | ), 32 | const SizedBox(height: 24.0), 33 | SizedBox( 34 | width: 280.0, 35 | child: Text( 36 | text, 37 | textAlign: TextAlign.center, 38 | style: TextStyles.kRegularBody.copyWith( 39 | fontSize: 18.0, 40 | ), 41 | ), 42 | ), 43 | const SizedBox(height: 18.0), 44 | hasButton 45 | ? ElevatedButton( 46 | child: Text( 47 | buttonText ?? '', 48 | style: TextStyles.kRegularTitle.copyWith( 49 | color: Colors.white, 50 | ), 51 | ), 52 | style: ElevatedButton.styleFrom( 53 | primary: Colors.black, 54 | padding: const EdgeInsets.symmetric( 55 | horizontal: 24.0, 56 | vertical: 14.0, 57 | ), 58 | shape: RoundedRectangleBorder( 59 | borderRadius: BorderRadius.circular(10.0), 60 | ), 61 | ), 62 | onPressed: onButtonTap, 63 | ) 64 | : const SizedBox(), 65 | ], 66 | ), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/data/database/database_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:food_rating_app/data/models/restaurant.dart'; 2 | import 'package:sqflite/sqflite.dart'; 3 | import 'package:path/path.dart'; 4 | 5 | class DatabaseHelper { 6 | static DatabaseHelper? _instance; 7 | static Database? _database; 8 | 9 | DatabaseHelper._internal() { 10 | _instance = this; 11 | } 12 | 13 | factory DatabaseHelper() => _instance ?? DatabaseHelper._internal(); 14 | 15 | static const String _tableName = 'favorite'; 16 | 17 | Future _initializeDb() async { 18 | final path = await getDatabasesPath(); 19 | final db = openDatabase( 20 | join(path, 'favorite_db.db'), 21 | onCreate: (db, version) async { 22 | await db.execute('''CREATE TABLE $_tableName ( 23 | id TEXT PRIMARY KEY, 24 | name TEXT, 25 | description TEXT, 26 | pictureId TEXT, 27 | city TEXT, 28 | rating DOUBLE 29 | )'''); 30 | }, 31 | version: 1, 32 | ); 33 | 34 | return db; 35 | } 36 | 37 | Future get database async { 38 | _database ??= await _initializeDb(); 39 | return _database; 40 | } 41 | 42 | /// Insert favorite 43 | Future insertFavorite(Item restaurant) async { 44 | final Database? db = await database; 45 | await db!.insert(_tableName, restaurant.toMap()); 46 | } 47 | 48 | /// Get all favorite 49 | Future> getFavorites() async { 50 | final Database? db = await database; 51 | List> results = await db!.query(_tableName); 52 | 53 | return results.map((res) => Item.fromMapMinimal(res)).toList(); 54 | } 55 | 56 | /// Get favorite by id 57 | Future getFavoriteById(String id) async { 58 | final Database? db = await database; 59 | List> results = await db!.query( 60 | _tableName, 61 | where: 'id = ?', 62 | whereArgs: [id], 63 | ); 64 | 65 | if (results.isNotEmpty) { 66 | return results.first; 67 | } else { 68 | return {}; 69 | } 70 | } 71 | 72 | /// Delete all favorite 73 | Future deleteAllFavorite() async { 74 | final Database? db = await database; 75 | await db!.delete(_tableName); 76 | } 77 | 78 | /// Delete favorite 79 | Future deleteFavorite(String id) async { 80 | final Database? db = await database; 81 | await db!.delete( 82 | _tableName, 83 | where: 'id = ?', 84 | whereArgs: [id], 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/provider/restaurant_provider_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.16 from annotations 2 | // in food_rating_app/test/provider/restaurant_provider_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i4; 6 | 7 | import 'package:food_rating_app/data/api/api_service.dart' as _i3; 8 | import 'package:food_rating_app/data/models/restaurant.dart' as _i2; 9 | import 'package:mockito/mockito.dart' as _i1; 10 | 11 | // ignore_for_file: avoid_redundant_argument_values 12 | // ignore_for_file: avoid_setters_without_getters 13 | // ignore_for_file: comment_references 14 | // ignore_for_file: implementation_imports 15 | // ignore_for_file: invalid_use_of_visible_for_testing_member 16 | // ignore_for_file: prefer_const_constructors 17 | // ignore_for_file: unnecessary_parenthesis 18 | // ignore_for_file: camel_case_types 19 | 20 | class _FakeRestaurants_0 extends _i1.Fake implements _i2.Restaurants {} 21 | 22 | class _FakeRestaurant_1 extends _i1.Fake implements _i2.Restaurant {} 23 | 24 | /// A class which mocks [ApiService]. 25 | /// 26 | /// See the documentation for Mockito's code generation for more information. 27 | class MockApiService extends _i1.Mock implements _i3.ApiService { 28 | MockApiService() { 29 | _i1.throwOnMissingStub(this); 30 | } 31 | 32 | @override 33 | _i4.Future<_i2.Restaurants> getRestaurantList() => 34 | (super.noSuchMethod(Invocation.method(#getRestaurantList, []), 35 | returnValue: Future<_i2.Restaurants>.value(_FakeRestaurants_0())) 36 | as _i4.Future<_i2.Restaurants>); 37 | @override 38 | _i4.Future<_i2.Restaurants> searchRestaurant(String? query) => 39 | (super.noSuchMethod(Invocation.method(#searchRestaurant, [query]), 40 | returnValue: Future<_i2.Restaurants>.value(_FakeRestaurants_0())) 41 | as _i4.Future<_i2.Restaurants>); 42 | @override 43 | _i4.Future<_i2.Restaurant> getRestaurantDetailById(String? id) => 44 | (super.noSuchMethod(Invocation.method(#getRestaurantDetailById, [id]), 45 | returnValue: Future<_i2.Restaurant>.value(_FakeRestaurant_1())) 46 | as _i4.Future<_i2.Restaurant>); 47 | @override 48 | _i4.Future postReviewById({String? id, String? name, String? review}) => 49 | (super.noSuchMethod( 50 | Invocation.method( 51 | #postReviewById, [], {#id: id, #name: name, #review: review}), 52 | returnValue: Future.value(false)) as _i4.Future); 53 | @override 54 | String toString() => super.toString(); 55 | } 56 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 8 | 15 | 19 | 23 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | 45 | 48 | 49 | 50 | 51 | 52 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /lib/utilities/notification_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:math'; 3 | 4 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 5 | import 'package:food_rating_app/data/models/restaurant.dart'; 6 | import 'package:food_rating_app/common/navigation.dart'; 7 | import 'package:rxdart/subjects.dart'; 8 | 9 | final selectNotificationSubject = BehaviorSubject(); 10 | 11 | class NotificationHelper { 12 | int index = Random().nextInt(20); 13 | static NotificationHelper? _instance; 14 | 15 | NotificationHelper._internal() { 16 | _instance = this; 17 | } 18 | 19 | factory NotificationHelper() => _instance ?? NotificationHelper._internal(); 20 | 21 | Future initNotifications( 22 | FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin, 23 | ) async { 24 | var initializationSettingsAndroid = 25 | const AndroidInitializationSettings('app_icon'); 26 | 27 | var initializationSettingsIOS = const IOSInitializationSettings( 28 | requestAlertPermission: false, 29 | requestBadgePermission: false, 30 | requestSoundPermission: false, 31 | ); 32 | 33 | var initializationSettings = InitializationSettings( 34 | android: initializationSettingsAndroid, 35 | iOS: initializationSettingsIOS, 36 | ); 37 | 38 | await flutterLocalNotificationsPlugin.initialize(initializationSettings, 39 | onSelectNotification: (String? payload) async { 40 | if (payload != null) { 41 | print('notification payload: ' + payload); 42 | } 43 | 44 | selectNotificationSubject.add(payload ?? 'empty payload'); 45 | }); 46 | } 47 | 48 | Future showNotification( 49 | FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin, 50 | Restaurants restaurants, 51 | ) async { 52 | var _channelId = '1'; 53 | var _channelName = 'channel_01'; 54 | var _channelDescription = 'daily restaurant channel'; 55 | 56 | var androidPlatformChannelSpecifics = AndroidNotificationDetails( 57 | _channelId, 58 | _channelName, 59 | channelDescription: _channelDescription, 60 | importance: Importance.max, 61 | priority: Priority.high, 62 | ticker: 'ticker', 63 | styleInformation: const DefaultStyleInformation(true, true), 64 | ); 65 | 66 | var iOSPlatformChannelSpecifics = const IOSNotificationDetails(); 67 | 68 | var platformChannelSpecifics = NotificationDetails( 69 | android: androidPlatformChannelSpecifics, 70 | iOS: iOSPlatformChannelSpecifics, 71 | ); 72 | 73 | var notificationTitle = 'Rekomendasi Restoran Hari Ini'; 74 | var restaurantName = restaurants.items![index].name; 75 | 76 | await flutterLocalNotificationsPlugin.show( 77 | 0, 78 | notificationTitle, 79 | restaurantName, 80 | platformChannelSpecifics, 81 | payload: json.encode( 82 | restaurants.toMap(), 83 | ), 84 | ); 85 | } 86 | 87 | void configureSelectNotificationSubject(String route) { 88 | selectNotificationSubject.stream.listen((String payload) { 89 | var data = Restaurants.fromMap(json.decode(payload)); 90 | var restaurant = data.items![index]; 91 | Navigation.intentWithData(route, arguments: restaurant.id); 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/modules/screens/home_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:food_rating_app/modules/screens/restaurant/detail_screen.dart'; 3 | import 'package:food_rating_app/modules/screens/restaurant/favorite_screen.dart'; 4 | import 'package:food_rating_app/modules/screens/restaurant/list_screen.dart'; 5 | import 'package:food_rating_app/modules/screens/preferences/settings_screen.dart'; 6 | import 'package:food_rating_app/common/styles.dart'; 7 | import 'package:food_rating_app/utilities/notification_helper.dart'; 8 | import 'package:persistent_bottom_nav_bar/persistent-tab-view.dart'; 9 | 10 | class HomeScreen extends StatefulWidget { 11 | static const routeName = '/'; 12 | const HomeScreen({Key? key}) : super(key: key); 13 | 14 | @override 15 | State createState() => _HomeScreenState(); 16 | } 17 | 18 | class _HomeScreenState extends State { 19 | final NotificationHelper _notificationHelper = NotificationHelper(); 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | _notificationHelper.configureSelectNotificationSubject( 25 | DetailScreen.routeName, 26 | ); 27 | } 28 | 29 | @override 30 | void dispose() { 31 | selectNotificationSubject.close(); 32 | super.dispose(); 33 | } 34 | 35 | List _buildScreens() { 36 | return [ 37 | const ListScreen(), 38 | const FavoriteScreen(), 39 | const SettingsScreen(), 40 | ]; 41 | } 42 | 43 | List _navBarItems(BuildContext context) { 44 | return [ 45 | PersistentBottomNavBarItem( 46 | icon: const Icon(Icons.view_agenda), 47 | title: 'Beranda', 48 | textStyle: TextStyles.kRegularTitle.copyWith( 49 | fontWeight: FontWeight.w400, 50 | ), 51 | activeColorPrimary: 52 | Theme.of(context).bottomNavigationBarTheme.selectedItemColor!, 53 | inactiveColorPrimary: 54 | Theme.of(context).bottomNavigationBarTheme.unselectedItemColor, 55 | ), 56 | PersistentBottomNavBarItem( 57 | icon: const Icon(Icons.favorite), 58 | title: 'Favorit', 59 | textStyle: TextStyles.kRegularTitle.copyWith( 60 | fontWeight: FontWeight.w400, 61 | ), 62 | activeColorPrimary: 63 | Theme.of(context).bottomNavigationBarTheme.selectedItemColor!, 64 | inactiveColorPrimary: 65 | Theme.of(context).bottomNavigationBarTheme.unselectedItemColor, 66 | ), 67 | PersistentBottomNavBarItem( 68 | icon: const Icon(Icons.settings), 69 | title: 'Setelan', 70 | textStyle: TextStyles.kRegularTitle.copyWith( 71 | fontWeight: FontWeight.w400, 72 | ), 73 | activeColorPrimary: 74 | Theme.of(context).bottomNavigationBarTheme.selectedItemColor!, 75 | inactiveColorPrimary: 76 | Theme.of(context).bottomNavigationBarTheme.unselectedItemColor, 77 | ), 78 | ]; 79 | } 80 | 81 | @override 82 | Widget build(BuildContext context) { 83 | return PersistentTabView( 84 | context, 85 | screens: _buildScreens(), 86 | items: _navBarItems(context), 87 | confineInSafeArea: true, 88 | backgroundColor: 89 | Theme.of(context).bottomNavigationBarTheme.backgroundColor!, 90 | navBarStyle: NavBarStyle.style9, 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:android_alarm_manager/android_alarm_manager.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 6 | import 'package:food_rating_app/common/navigation.dart'; 7 | import 'package:food_rating_app/data/api/api_service.dart'; 8 | import 'package:food_rating_app/data/database/database_helper.dart'; 9 | import 'package:food_rating_app/modules/providers/database_provider.dart'; 10 | import 'package:food_rating_app/modules/providers/restaurant_provider.dart'; 11 | import 'package:food_rating_app/modules/providers/scheduling_provider.dart'; 12 | import 'package:food_rating_app/modules/screens/home_screen.dart'; 13 | import 'package:food_rating_app/modules/screens/preferences/preferences_helper.dart'; 14 | import 'package:food_rating_app/modules/providers/preferences_provider.dart'; 15 | import 'package:food_rating_app/routes.dart'; 16 | import 'package:food_rating_app/utilities/background_service.dart'; 17 | import 'package:food_rating_app/utilities/notification_helper.dart'; 18 | import 'package:provider/provider.dart'; 19 | import 'package:shared_preferences/shared_preferences.dart'; 20 | 21 | final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = 22 | FlutterLocalNotificationsPlugin(); 23 | 24 | void main() async { 25 | WidgetsFlutterBinding.ensureInitialized(); 26 | final NotificationHelper _notificationHelper = NotificationHelper(); 27 | final BackgroundService _service = BackgroundService(); 28 | 29 | _service.initializeIsolate(); 30 | 31 | if (Platform.isAndroid) { 32 | await AndroidAlarmManager.initialize(); 33 | } 34 | 35 | await _notificationHelper.initNotifications(flutterLocalNotificationsPlugin); 36 | runApp( 37 | MultiProvider( 38 | providers: [ 39 | ChangeNotifierProvider( 40 | create: (_) => ListProvider( 41 | apiService: ApiService(), 42 | ), 43 | ), 44 | ChangeNotifierProvider( 45 | create: (_) => DetailProvider( 46 | apiService: ApiService(), 47 | ), 48 | ), 49 | ChangeNotifierProvider( 50 | create: (_) => SearchProvider( 51 | apiService: ApiService(), 52 | ), 53 | ), 54 | ChangeNotifierProvider( 55 | create: (_) => ReviewProvider( 56 | apiService: ApiService(), 57 | ), 58 | ), 59 | ChangeNotifierProvider( 60 | create: (_) => DatabaseProvider( 61 | databaseHelper: DatabaseHelper(), 62 | ), 63 | ), 64 | ChangeNotifierProvider( 65 | create: (_) => PreferencesProvider( 66 | preferencesHelper: PreferencesHelper( 67 | sharedPreferences: SharedPreferences.getInstance(), 68 | ), 69 | ), 70 | ), 71 | ChangeNotifierProvider( 72 | create: (_) => SchedulingProvider(), 73 | ), 74 | ], 75 | child: const FoodRatingApp(), 76 | ), 77 | ); 78 | } 79 | 80 | class FoodRatingApp extends StatelessWidget { 81 | const FoodRatingApp({Key? key}) : super(key: key); 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | return Consumer( 86 | builder: (context, provider, _) { 87 | return MaterialApp( 88 | title: 'Food Rating App', 89 | debugShowCheckedModeBanner: false, 90 | theme: provider.themeData, 91 | initialRoute: HomeScreen.routeName, 92 | navigatorKey: navigatorKey, 93 | routes: allRoute(context), 94 | ); 95 | }, 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | food_rating_app 30 | 31 | 32 | 33 | 36 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /test/provider/restaurant_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:food_rating_app/data/api/api_service.dart'; 3 | import 'package:food_rating_app/data/models/restaurant.dart'; 4 | import 'package:food_rating_app/modules/providers/restaurant_provider.dart'; 5 | import 'package:mockito/annotations.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | 8 | import 'restaurant_provider_test.mocks.dart'; 9 | 10 | @GenerateMocks([ApiService]) 11 | void main() { 12 | final MockApiService mockApiService = MockApiService(); 13 | 14 | group('Verify JSON Parsing: Restaurant Provider', () { 15 | /// Testing ListProvider - Part of restaurant_provider 16 | test('list restaurants fetched successfully', () async { 17 | var restaurantsJson = { 18 | "error": false, 19 | "message": "success", 20 | "count": 20, 21 | "restaurants": [ 22 | { 23 | "id": "rqdv5juczeskfw1e867", 24 | "name": "Melting Pot", 25 | "description": 26 | "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. ...", 27 | "pictureId": "14", 28 | "city": "Medan", 29 | "rating": 4.2 30 | }, 31 | { 32 | "id": "s1knt6za9kkfw1e867", 33 | "name": "Kafe Kita", 34 | "description": 35 | "Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. ...", 36 | "pictureId": "25", 37 | "city": "Gorontalo", 38 | "rating": 4 39 | } 40 | ] 41 | }; 42 | 43 | /// stub 44 | when(mockApiService.getRestaurantList()).thenAnswer( 45 | (_) async => Future.value(Restaurants.fromMap(restaurantsJson))); 46 | 47 | /// arrange 48 | ListProvider listProvider = ListProvider(apiService: mockApiService); 49 | await listProvider.fetchRestaurantList(); 50 | 51 | /// act 52 | var testFromApiResultId = listProvider.restaurants!.items![0].id == 53 | Restaurants.fromMap(restaurantsJson).items![0].id; 54 | var testFromApiResultName = listProvider.restaurants!.items![0].name == 55 | Restaurants.fromMap(restaurantsJson).items![0].name; 56 | 57 | /// assert 58 | expect(testFromApiResultId, true); 59 | expect(testFromApiResultName, true); 60 | }); 61 | 62 | /// Testing DetailProvider - Part of restaurant_provider 63 | test('detail restaurant fetched successfully', () async { 64 | var restaurantDetailJson = { 65 | "error": false, 66 | "message": "success", 67 | "restaurant": { 68 | "id": "rqdv5juczeskfw1e867", 69 | "name": "Melting Pot", 70 | "description": 71 | "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. ...", 72 | "city": "Medan", 73 | "address": "Jln. Pandeglang no 19", 74 | "pictureId": "14", 75 | "categories": [ 76 | {"name": "Italia"}, 77 | {"name": "Modern"} 78 | ], 79 | "menus": { 80 | "foods": [ 81 | {"name": "Paket rosemary"}, 82 | {"name": "Toastie salmon"} 83 | ], 84 | "drinks": [ 85 | {"name": "Es krim"}, 86 | {"name": "Sirup"} 87 | ] 88 | }, 89 | "rating": 4.2, 90 | "customerReviews": [ 91 | { 92 | "name": "Ahmad", 93 | "review": "Tidak rekomendasi untuk pelajar!", 94 | "date": "13 November 2019" 95 | } 96 | ] 97 | } 98 | }; 99 | 100 | /// stub 101 | when(mockApiService.getRestaurantDetailById('rqdv5juczeskfw1e867')) 102 | .thenAnswer( 103 | (_) => Future.value(Restaurant.fromMap(restaurantDetailJson))); 104 | 105 | /// arrange 106 | DetailProvider detailProvider = 107 | DetailProvider(apiService: mockApiService); 108 | await detailProvider.fetchRestaurantDetail('rqdv5juczeskfw1e867'); 109 | 110 | /// act 111 | var testFromApiResultId = detailProvider.restaurant!.item!.id == 112 | Restaurant.fromMap(restaurantDetailJson).item!.id; 113 | var testFromApiResultName = detailProvider.restaurant!.item!.name == 114 | Restaurant.fromMap(restaurantDetailJson).item!.name; 115 | 116 | /// assert 117 | expect(testFromApiResultId, true); 118 | expect(testFromApiResultName, true); 119 | }); 120 | }); 121 | } 122 | -------------------------------------------------------------------------------- /lib/modules/screens/preferences/settings_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:food_rating_app/common/navigation.dart'; 7 | import 'package:food_rating_app/modules/providers/preferences_provider.dart'; 8 | import 'package:food_rating_app/modules/providers/scheduling_provider.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | class SettingsScreen extends StatelessWidget { 12 | const SettingsScreen({Key? key}) : super(key: key); 13 | 14 | customDialog(BuildContext context) { 15 | if (Platform.isIOS) { 16 | showCupertinoDialog( 17 | context: context, 18 | builder: (context) { 19 | return CupertinoAlertDialog( 20 | title: const Text('Segera!'), 21 | content: const Text('Fitur ini belum tersedia'), 22 | actions: [ 23 | CupertinoDialogAction( 24 | child: const Text('Okay'), 25 | onPressed: () { 26 | Navigation.back(); 27 | }, 28 | ), 29 | ], 30 | ); 31 | }, 32 | ); 33 | } else { 34 | showDialog( 35 | context: context, 36 | builder: (context) { 37 | return AlertDialog( 38 | title: const Text('Segera!'), 39 | content: const Text('Fitur ini belum tersedia'), 40 | actions: [ 41 | TextButton( 42 | onPressed: () { 43 | Navigation.back(); 44 | }, 45 | child: const Text('Okay'), 46 | ), 47 | ], 48 | ); 49 | }, 50 | ); 51 | } 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return Scaffold( 57 | appBar: PreferredSize( 58 | preferredSize: const Size.fromHeight(96.0), 59 | child: AppBar( 60 | elevation: 0.0, 61 | titleSpacing: 24.0, 62 | centerTitle: false, 63 | automaticallyImplyLeading: false, 64 | toolbarHeight: 96.0, 65 | backgroundColor: Colors.transparent, 66 | systemOverlayStyle: SystemUiOverlayStyle( 67 | statusBarBrightness: Theme.of(context).brightness, 68 | ), 69 | title: Row( 70 | mainAxisAlignment: MainAxisAlignment.start, 71 | children: [ 72 | Text( 73 | 'Setelan', 74 | style: Theme.of(context).appBarTheme.toolbarTextStyle, 75 | ), 76 | ], 77 | ), 78 | ), 79 | ), 80 | body: ListView( 81 | padding: const EdgeInsets.symmetric(horizontal: 24.0), 82 | children: [ 83 | Material( 84 | color: Colors.transparent, 85 | child: ListTile( 86 | contentPadding: EdgeInsets.zero, 87 | title: const Text('Dark Theme'), 88 | trailing: Consumer( 89 | builder: (context, provider, _) { 90 | return Switch.adaptive( 91 | value: provider.isDarkTheme, 92 | onChanged: (value) { 93 | provider.enableDarkTheme(value); 94 | }, 95 | inactiveThumbColor: 96 | Theme.of(context).colorScheme.primaryVariant, 97 | activeColor: Theme.of(context).colorScheme.secondary, 98 | ); 99 | }, 100 | ), 101 | ), 102 | ), 103 | Material( 104 | color: Colors.transparent, 105 | child: ListTile( 106 | contentPadding: EdgeInsets.zero, 107 | title: const Text('Daily Restaurant'), 108 | trailing: Consumer( 109 | builder: (context, provider, _) { 110 | return Switch.adaptive( 111 | value: provider.isScheduled, 112 | onChanged: (value) async { 113 | if (Platform.isIOS) { 114 | customDialog(context); 115 | } else { 116 | provider.scheduledDailyRestaurant(value); 117 | } 118 | }, 119 | inactiveThumbColor: 120 | Theme.of(context).colorScheme.primaryVariant, 121 | activeColor: Theme.of(context).colorScheme.secondary, 122 | ); 123 | }, 124 | ), 125 | ), 126 | ), 127 | ], 128 | ), 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/data/models/restaurant.dart: -------------------------------------------------------------------------------- 1 | class Restaurants { 2 | bool? error; 3 | String? message; 4 | int? founded; 5 | int? count; 6 | List? items; 7 | 8 | Restaurants({ 9 | this.error, 10 | this.message, 11 | this.founded, 12 | this.count, 13 | this.items, 14 | }); 15 | 16 | factory Restaurants.fromMap(Map map) { 17 | return Restaurants( 18 | error: map["error"], 19 | message: map["message"], 20 | count: map["count"], 21 | items: List.from( 22 | map["restaurants"].map( 23 | (x) => Item.fromMapMinimal(x), 24 | ), 25 | ), 26 | ); 27 | } 28 | 29 | factory Restaurants.fromSearchMap(Map map) { 30 | return Restaurants( 31 | error: map["error"], 32 | founded: map["founded"], 33 | count: map["count"], 34 | items: List.from( 35 | map["restaurants"].map( 36 | (x) => Item.fromMapMinimal(x), 37 | ), 38 | ), 39 | ); 40 | } 41 | 42 | Map toMap() => { 43 | 'error': error, 44 | 'message': message, 45 | 'count': count, 46 | 'restaurants': List.from(items!.map((x) => x.toMap())), 47 | }; 48 | } 49 | 50 | class Restaurant { 51 | bool? error; 52 | String? message; 53 | Item? item; 54 | 55 | Restaurant({this.error, this.message, this.item}); 56 | 57 | factory Restaurant.fromMap(Map map) { 58 | return Restaurant( 59 | error: map["error"], 60 | message: map["message"], 61 | item: Item.fromMapDetail(map["restaurant"]), 62 | ); 63 | } 64 | 65 | Map toMap() => { 66 | 'error': error, 67 | 'message': message, 68 | 'item': item!.toMap(), 69 | }; 70 | } 71 | 72 | class Item { 73 | String? id; 74 | String? name; 75 | String? description; 76 | String? city; 77 | String? address; 78 | String? pictureId; 79 | List? categories; 80 | Menus? menus; 81 | double? rating; 82 | List? customerReviews; 83 | 84 | Item({ 85 | this.id, 86 | this.name, 87 | this.description, 88 | this.city, 89 | this.address, 90 | this.pictureId, 91 | this.categories, 92 | this.menus, 93 | this.rating, 94 | this.customerReviews, 95 | }); 96 | 97 | factory Item.fromMapMinimal(Map map) { 98 | return Item( 99 | id: map["id"], 100 | name: map["name"], 101 | description: map["description"], 102 | city: map["city"], 103 | pictureId: map["pictureId"], 104 | rating: map["rating"].toDouble(), 105 | ); 106 | } 107 | 108 | factory Item.fromMapDetail(Map map) { 109 | return Item( 110 | id: map["id"], 111 | name: map["name"], 112 | description: map["description"], 113 | city: map["city"], 114 | address: map["address"], 115 | pictureId: map["pictureId"], 116 | categories: List.from( 117 | map["categories"].map( 118 | (x) => Category.fromMap(x), 119 | ), 120 | ), 121 | menus: Menus.fromMap(map["menus"]), 122 | rating: map["rating"].toDouble(), 123 | customerReviews: List.from( 124 | map["customerReviews"].map( 125 | (x) => CustomerReview.fromMap(x), 126 | ), 127 | ), 128 | ); 129 | } 130 | 131 | Map toMap() { 132 | return { 133 | 'id': id, 134 | 'name': name, 135 | 'description': description, 136 | 'city': city, 137 | 'pictureId': pictureId, 138 | 'rating': rating, 139 | }; 140 | } 141 | } 142 | 143 | class Category { 144 | String? name; 145 | 146 | Category({this.name}); 147 | 148 | factory Category.fromMap(Map map) { 149 | return Category( 150 | name: map['name'], 151 | ); 152 | } 153 | } 154 | 155 | class Menus { 156 | List? foods; 157 | List? drinks; 158 | Menus({this.foods, this.drinks}); 159 | 160 | factory Menus.fromMap(Map map) { 161 | return Menus( 162 | foods: List.from( 163 | map['foods'].map( 164 | (x) => Category.fromMap(x), 165 | ), 166 | ), 167 | drinks: List.from( 168 | map['drinks'].map( 169 | (x) => Category.fromMap(x), 170 | ), 171 | ), 172 | ); 173 | } 174 | } 175 | 176 | class CustomerReview { 177 | String? id; 178 | String? name; 179 | String? review; 180 | String? date; 181 | CustomerReview({this.id, this.name, this.review, this.date}); 182 | 183 | factory CustomerReview.fromMap(Map map) { 184 | return CustomerReview( 185 | name: map['name'], 186 | review: map['review'], 187 | date: map['date'], 188 | ); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /lib/common/styles.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | 4 | const Color white = Color(0xFFFFFFFF); 5 | const Color whiteShadow = Color(0xFFE6EAEE); 6 | const Color whiteBackground = Color(0xFFF5F5F7); 7 | const Color black = Color(0xFF181A20); 8 | const Color grey = Color(0xFF373737); 9 | const Color darkGrey = Color(0xFF252525); 10 | const Color blackDarker = Color(0xFF0D0D0D); 11 | const Color customBlue = Color(0xFF497BEA); 12 | const Color customYellow = Color(0xFFFBBC05); 13 | 14 | final TextTheme textTheme = TextTheme( 15 | headline1: GoogleFonts.poppins( 16 | fontSize: 92, 17 | fontWeight: FontWeight.w300, 18 | letterSpacing: -1.5, 19 | ), 20 | headline2: GoogleFonts.poppins( 21 | fontSize: 57, 22 | fontWeight: FontWeight.w300, 23 | letterSpacing: -0.5, 24 | ), 25 | headline3: GoogleFonts.poppins( 26 | fontSize: 46, 27 | fontWeight: FontWeight.w400, 28 | ), 29 | headline4: GoogleFonts.poppins( 30 | fontSize: 32, 31 | fontWeight: FontWeight.w400, 32 | letterSpacing: 0.25, 33 | ), 34 | headline5: GoogleFonts.poppins( 35 | fontSize: 23, 36 | fontWeight: FontWeight.w400, 37 | ), 38 | headline6: GoogleFonts.poppins( 39 | fontSize: 19, 40 | fontWeight: FontWeight.w500, 41 | letterSpacing: 0.15, 42 | ), 43 | subtitle1: GoogleFonts.poppins( 44 | fontSize: 15, 45 | fontWeight: FontWeight.w400, 46 | letterSpacing: 0.15, 47 | ), 48 | subtitle2: GoogleFonts.poppins( 49 | fontSize: 13, 50 | fontWeight: FontWeight.w500, 51 | letterSpacing: 0.1, 52 | ), 53 | bodyText1: GoogleFonts.poppins( 54 | fontSize: 16, 55 | fontWeight: FontWeight.w400, 56 | letterSpacing: 0.5, 57 | ), 58 | bodyText2: GoogleFonts.poppins( 59 | fontSize: 14, 60 | fontWeight: FontWeight.w400, 61 | letterSpacing: 0.25, 62 | ), 63 | button: GoogleFonts.poppins( 64 | fontSize: 14, 65 | fontWeight: FontWeight.w500, 66 | letterSpacing: 1.25, 67 | ), 68 | caption: GoogleFonts.poppins( 69 | fontSize: 12, 70 | fontWeight: FontWeight.w400, 71 | letterSpacing: 0.4, 72 | ), 73 | overline: GoogleFonts.poppins( 74 | fontSize: 10, 75 | fontWeight: FontWeight.w400, 76 | letterSpacing: 1.5, 77 | ), 78 | ); 79 | 80 | class TextStyles { 81 | static var kHeading1 = GoogleFonts.poppins( 82 | fontSize: 22.0, 83 | fontWeight: FontWeight.w700, 84 | ); 85 | 86 | static var kHeading2 = GoogleFonts.poppins( 87 | fontSize: 18.0, 88 | fontWeight: FontWeight.w700, 89 | ); 90 | 91 | static var kRegularTitle = GoogleFonts.poppins( 92 | fontSize: 14.0, 93 | fontWeight: FontWeight.w600, 94 | ); 95 | 96 | static var kMediumTitle = GoogleFonts.poppins( 97 | fontSize: 16.0, 98 | fontWeight: FontWeight.w600, 99 | ); 100 | 101 | static var kRegularBody = GoogleFonts.poppins( 102 | fontSize: 12.0, 103 | fontWeight: FontWeight.w400, 104 | ); 105 | 106 | static var kSmallBody = GoogleFonts.poppins( 107 | fontSize: 10.0, 108 | fontWeight: FontWeight.w400, 109 | ); 110 | } 111 | 112 | ThemeData lightTheme = ThemeData( 113 | primaryColor: white, 114 | brightness: Brightness.light, 115 | scaffoldBackgroundColor: whiteBackground, 116 | visualDensity: VisualDensity.adaptivePlatformDensity, 117 | textTheme: textTheme.apply(bodyColor: black, displayColor: black), 118 | colorScheme: const ColorScheme.light().copyWith( 119 | primary: white, 120 | primaryVariant: Colors.grey[300], 121 | secondary: customBlue, 122 | secondaryVariant: customYellow, 123 | ), 124 | shadowColor: whiteShadow, 125 | iconTheme: const IconThemeData(color: black), 126 | appBarTheme: AppBarTheme( 127 | backgroundColor: white, 128 | shadowColor: Colors.transparent, 129 | toolbarTextStyle: textTheme.headline6!.copyWith(color: black), 130 | elevation: 0.0, 131 | ), 132 | bottomNavigationBarTheme: BottomNavigationBarThemeData( 133 | backgroundColor: white, 134 | selectedItemColor: customBlue, 135 | unselectedItemColor: Colors.grey[200], 136 | ), 137 | ); 138 | 139 | ThemeData darkTheme = ThemeData( 140 | primaryColor: darkGrey, 141 | brightness: Brightness.dark, 142 | scaffoldBackgroundColor: blackDarker, 143 | visualDensity: VisualDensity.adaptivePlatformDensity, 144 | textTheme: textTheme.apply(bodyColor: white, displayColor: white), 145 | colorScheme: const ColorScheme.dark().copyWith( 146 | primary: darkGrey, 147 | primaryVariant: grey, 148 | secondary: customBlue, 149 | secondaryVariant: customYellow, 150 | ), 151 | shadowColor: Colors.black, 152 | iconTheme: const IconThemeData(color: Colors.white), 153 | appBarTheme: AppBarTheme( 154 | backgroundColor: darkGrey, 155 | toolbarTextStyle: textTheme.headline6!.copyWith(color: Colors.white), 156 | elevation: 0.0, 157 | ), 158 | bottomNavigationBarTheme: const BottomNavigationBarThemeData( 159 | backgroundColor: darkGrey, 160 | selectedItemColor: customBlue, 161 | unselectedItemColor: grey, 162 | ), 163 | ); 164 | -------------------------------------------------------------------------------- /lib/modules/screens/restaurant/favorite_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:animate_do/animate_do.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:food_rating_app/data/api/urls.dart'; 4 | import 'package:food_rating_app/modules/providers/database_provider.dart'; 5 | import 'package:food_rating_app/modules/screens/restaurant/detail_screen.dart'; 6 | import 'package:food_rating_app/modules/screens/restaurant/widgets/animation_placeholder.dart'; 7 | import 'package:food_rating_app/modules/screens/restaurant/widgets/restaurant_card.dart'; 8 | import 'package:food_rating_app/common/styles.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | class FavoriteScreen extends StatefulWidget { 12 | static const routeName = '/favorite'; 13 | const FavoriteScreen({Key? key}) : super(key: key); 14 | 15 | @override 16 | State createState() => _FavoriteScreenState(); 17 | } 18 | 19 | class _FavoriteScreenState extends State 20 | with TickerProviderStateMixin { 21 | late AnimationController _colorAnimationController; 22 | late Animation _colorTween; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _colorAnimationController = AnimationController( 28 | vsync: this, 29 | duration: const Duration(seconds: 0), 30 | ); 31 | } 32 | 33 | @override 34 | void dispose() { 35 | _colorAnimationController.dispose(); 36 | super.dispose(); 37 | } 38 | 39 | bool _scrollListener(ScrollNotification scrollInfo) { 40 | if (scrollInfo.metrics.axis == Axis.vertical) { 41 | _colorAnimationController.animateTo(scrollInfo.metrics.pixels / 350); 42 | return true; 43 | } 44 | return false; 45 | } 46 | 47 | Widget _buildListFavorite(BuildContext context) { 48 | return Consumer( 49 | builder: (context, provider, _) { 50 | final favoriteRestaurants = provider.favorites; 51 | 52 | switch (provider.databaseResultState) { 53 | case DatabaseResultState.noData: 54 | return const Center( 55 | child: AnimationPlaceholder( 56 | animation: 'assets/empty.json', 57 | text: 'Tidak ada favorit untuk saat ini', 58 | ), 59 | ); 60 | case DatabaseResultState.hasData: 61 | return FadeInUp( 62 | from: 20.0, 63 | duration: const Duration(milliseconds: 500), 64 | child: ListView.builder( 65 | itemCount: favoriteRestaurants.length, 66 | itemBuilder: (context, index) { 67 | final favoriteRestaurant = favoriteRestaurants[index]; 68 | return RestaurantCard(item: favoriteRestaurant); 69 | }, 70 | ), 71 | ); 72 | case DatabaseResultState.error: 73 | return const Center( 74 | child: AnimationPlaceholder( 75 | animation: 'assets/not-found.json', 76 | text: 'Maaf, sepertinya ada masalah', 77 | ), 78 | ); 79 | default: 80 | return const SizedBox(); 81 | } 82 | }, 83 | ); 84 | } 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | _colorTween = ColorTween( 89 | begin: Theme.of(context).scaffoldBackgroundColor, 90 | end: Theme.of(context).appBarTheme.backgroundColor, 91 | ).animate(_colorAnimationController); 92 | 93 | return Scaffold( 94 | appBar: PreferredSize( 95 | preferredSize: const Size.fromHeight(96.0), 96 | child: AnimatedBuilder( 97 | animation: _colorAnimationController, 98 | builder: (context, child) { 99 | return AppBar( 100 | elevation: 0.0, 101 | titleSpacing: 24.0, 102 | centerTitle: false, 103 | automaticallyImplyLeading: false, 104 | backgroundColor: _colorTween.value, 105 | toolbarHeight: 96.0, 106 | title: Row( 107 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 108 | children: [ 109 | Text( 110 | 'Favorite', 111 | style: Theme.of(context).appBarTheme.toolbarTextStyle, 112 | ), 113 | IconButton( 114 | splashRadius: 24.0, 115 | splashColor: Colors.redAccent[100], 116 | padding: EdgeInsets.zero, 117 | icon: const Icon( 118 | Icons.delete_forever, 119 | color: Colors.redAccent, 120 | ), 121 | color: black, 122 | onPressed: () { 123 | Provider.of( 124 | context, 125 | listen: false, 126 | ).deleteAllFavorite(); 127 | }, 128 | ), 129 | ], 130 | ), 131 | ); 132 | }, 133 | ), 134 | ), 135 | body: NotificationListener( 136 | onNotification: _scrollListener, 137 | child: _buildListFavorite(context), 138 | ), 139 | ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lib/modules/screens/restaurant/widgets/restaurant_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:food_rating_app/common/navigation.dart'; 4 | import 'package:food_rating_app/data/api/urls.dart'; 5 | import 'package:food_rating_app/data/models/restaurant.dart'; 6 | import 'package:food_rating_app/modules/screens/restaurant/detail_screen.dart'; 7 | 8 | class RestaurantCard extends StatelessWidget { 9 | final Item item; 10 | const RestaurantCard({Key? key, required this.item}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return GestureDetector( 15 | onTap: () => Navigation.intentWithData( 16 | DetailScreen.routeName, 17 | arguments: item.id!, 18 | ), 19 | child: Container( 20 | margin: const EdgeInsets.symmetric( 21 | horizontal: 22.0, 22 | vertical: 12.0, 23 | ), 24 | decoration: BoxDecoration( 25 | color: Theme.of(context).primaryColor, 26 | borderRadius: BorderRadius.circular(15.0), 27 | boxShadow: [ 28 | BoxShadow( 29 | color: Theme.of(context).shadowColor.withOpacity(0.6), 30 | spreadRadius: 1.0, 31 | blurRadius: 30.0, 32 | offset: const Offset(0, 3.0), 33 | ), 34 | ], 35 | ), 36 | child: Column( 37 | children: [ 38 | Container( 39 | margin: const EdgeInsets.symmetric( 40 | horizontal: 22.0, 41 | vertical: 16.0, 42 | ), 43 | child: Row( 44 | children: [ 45 | Expanded( 46 | flex: 1, 47 | child: ClipRRect( 48 | borderRadius: BorderRadius.circular(20.0), 49 | child: FittedBox( 50 | child: CachedNetworkImage( 51 | imageUrl: Urls.restaurantImage(item.pictureId!), 52 | height: 80.0, 53 | width: 80.0, 54 | fit: BoxFit.cover, 55 | progressIndicatorBuilder: (context, url, download) { 56 | return Padding( 57 | padding: const EdgeInsets.all(24.0), 58 | child: CircularProgressIndicator( 59 | value: download.progress, 60 | strokeWidth: 1.5, 61 | ), 62 | ); 63 | }, 64 | errorWidget: (context, url, error) { 65 | return const Padding( 66 | padding: EdgeInsets.all(24.0), 67 | child: Icon(Icons.image), 68 | ); 69 | }, 70 | ), 71 | ), 72 | ), 73 | ), 74 | const SizedBox(width: 16.0), 75 | Expanded( 76 | flex: 2, 77 | child: Column( 78 | crossAxisAlignment: CrossAxisAlignment.start, 79 | children: [ 80 | Padding( 81 | padding: const EdgeInsets.only(bottom: 4.0), 82 | child: Text( 83 | item.name!, 84 | style: Theme.of(context) 85 | .textTheme 86 | .headline6! 87 | .copyWith(fontWeight: FontWeight.w600), 88 | ), 89 | ), 90 | Text( 91 | item.description!, 92 | maxLines: 2, 93 | overflow: TextOverflow.ellipsis, 94 | style: Theme.of(context).textTheme.bodyText2, 95 | ), 96 | ], 97 | ), 98 | ), 99 | ], 100 | ), 101 | ), 102 | Divider( 103 | color: Theme.of(context).colorScheme.primaryVariant, 104 | thickness: 0.8, 105 | ), 106 | Container( 107 | margin: const EdgeInsets.symmetric( 108 | horizontal: 22.0, 109 | vertical: 16.0, 110 | ), 111 | child: Row( 112 | children: [ 113 | Row( 114 | children: [ 115 | Container( 116 | padding: const EdgeInsets.all(6.0), 117 | decoration: BoxDecoration( 118 | color: Theme.of(context).colorScheme.secondaryVariant, 119 | borderRadius: BorderRadius.circular(10.0), 120 | ), 121 | child: const Icon( 122 | Icons.star, 123 | color: Colors.white, 124 | size: 20.0, 125 | ), 126 | ), 127 | const SizedBox(width: 16.0), 128 | Text( 129 | '${item.rating}', 130 | style: Theme.of(context).textTheme.bodyText1, 131 | ), 132 | ], 133 | ), 134 | const SizedBox(width: 32.0), 135 | Row( 136 | children: [ 137 | Container( 138 | padding: const EdgeInsets.all(6.0), 139 | decoration: BoxDecoration( 140 | color: Theme.of(context).colorScheme.secondary, 141 | borderRadius: BorderRadius.circular(10.0), 142 | ), 143 | child: const Icon( 144 | Icons.location_on, 145 | color: Colors.white, 146 | size: 20.0, 147 | ), 148 | ), 149 | const SizedBox(width: 16.0), 150 | Text( 151 | item.city!, 152 | style: Theme.of(context).textTheme.bodyText1, 153 | ), 154 | ], 155 | ), 156 | ], 157 | ), 158 | ), 159 | ], 160 | ), 161 | ), 162 | ); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lib/modules/providers/restaurant_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:food_rating_app/data/api/api_service.dart'; 5 | import 'package:food_rating_app/data/models/restaurant.dart'; 6 | 7 | enum FetchResultState { loading, noData, hasData, failure } 8 | 9 | class ListProvider extends ChangeNotifier { 10 | final ApiService apiService; 11 | 12 | /// Restaurant List 13 | Restaurants? _restaurants; 14 | Restaurants? get restaurants => _restaurants; 15 | 16 | String _message = ''; 17 | String get message => _message; 18 | 19 | FetchResultState? _fetchListState; 20 | FetchResultState? get fetchListState => _fetchListState; 21 | 22 | ListProvider({required this.apiService}) { 23 | fetchRestaurantList(); 24 | } 25 | 26 | void setFetchListState(FetchResultState newState) { 27 | _fetchListState = newState; 28 | notifyListeners(); 29 | } 30 | 31 | Future fetchRestaurantList() async { 32 | try { 33 | _fetchListState = FetchResultState.loading; 34 | 35 | final restaurantsData = await apiService.getRestaurantList(); 36 | 37 | if (restaurantsData.count == 0) { 38 | _fetchListState = FetchResultState.noData; 39 | notifyListeners(); 40 | return _message = restaurantsData.message!; 41 | } else { 42 | _fetchListState = FetchResultState.hasData; 43 | notifyListeners(); 44 | return _restaurants = restaurantsData; 45 | } 46 | } on TimeoutException catch (e) { 47 | _fetchListState = FetchResultState.failure; 48 | notifyListeners(); 49 | return _message = 'TIMEOUT: $e'; 50 | } on SocketException catch (e) { 51 | _fetchListState = FetchResultState.failure; 52 | notifyListeners(); 53 | return _message = 'NO CONNECTION: $e'; 54 | } on Error catch (e) { 55 | _fetchListState = FetchResultState.failure; 56 | notifyListeners(); 57 | return _message = 'ERROR: $e'; 58 | } 59 | } 60 | } 61 | 62 | class DetailProvider extends ChangeNotifier { 63 | final ApiService apiService; 64 | 65 | /// Restaurant Detail 66 | Restaurant? _restaurant; 67 | Restaurant? get restaurant => _restaurant; 68 | 69 | String _message = ''; 70 | String get message => _message; 71 | 72 | FetchResultState? _fetchDetailState; 73 | FetchResultState? get fetchDetailState => _fetchDetailState; 74 | 75 | DetailProvider({required this.apiService}); 76 | 77 | void setFetchDetailState(FetchResultState newState) { 78 | _fetchDetailState = newState; 79 | notifyListeners(); 80 | } 81 | 82 | Future fetchRestaurantDetail(String id) async { 83 | try { 84 | _fetchDetailState = FetchResultState.loading; 85 | 86 | final restaurantData = await apiService.getRestaurantDetailById(id); 87 | 88 | if (restaurantData.item == null) { 89 | _fetchDetailState = FetchResultState.noData; 90 | notifyListeners(); 91 | return _message = restaurantData.message!; 92 | } else { 93 | _fetchDetailState = FetchResultState.hasData; 94 | notifyListeners(); 95 | return _restaurant = restaurantData; 96 | } 97 | } on TimeoutException catch (e) { 98 | _fetchDetailState = FetchResultState.failure; 99 | notifyListeners(); 100 | return _message = 'TIMEOUT: $e'; 101 | } on SocketException catch (e) { 102 | _fetchDetailState = FetchResultState.failure; 103 | notifyListeners(); 104 | return _message = 'NO CONNECTION: $e'; 105 | } on Error catch (e) { 106 | _fetchDetailState = FetchResultState.failure; 107 | notifyListeners(); 108 | return _message = 'ERROR: $e'; 109 | } 110 | } 111 | } 112 | 113 | enum SearchResultState { searching, hasData, noData, failure } 114 | 115 | class SearchProvider extends ChangeNotifier { 116 | final ApiService apiService; 117 | 118 | /// Restaurant List 119 | Restaurants? _restaurants; 120 | Restaurants? get restaurants => _restaurants; 121 | 122 | String _message = ''; 123 | String get message => _message; 124 | 125 | SearchResultState? _searchState; 126 | SearchResultState? get searchState => _searchState; 127 | 128 | SearchProvider({required this.apiService}); 129 | 130 | void setSearchState(SearchResultState newState) { 131 | _searchState = newState; 132 | notifyListeners(); 133 | } 134 | 135 | Future fetchRestaurantSearchResult({String query = ''}) async { 136 | try { 137 | _searchState = SearchResultState.searching; 138 | 139 | final restaurantsData = await apiService.searchRestaurant(query); 140 | 141 | if (restaurantsData.founded! > 0) { 142 | _searchState = SearchResultState.hasData; 143 | notifyListeners(); 144 | return _restaurants = restaurantsData; 145 | } else { 146 | _searchState = SearchResultState.noData; 147 | notifyListeners(); 148 | return _message = 'No Data'; 149 | } 150 | } on TimeoutException catch (e) { 151 | _searchState = SearchResultState.failure; 152 | notifyListeners(); 153 | return _message = 'TIMEOUT: $e'; 154 | } on SocketException catch (e) { 155 | _searchState = SearchResultState.failure; 156 | notifyListeners(); 157 | return _message = 'NO CONNECTION: $e'; 158 | } on Error catch (e) { 159 | _searchState = SearchResultState.failure; 160 | notifyListeners(); 161 | return _message = 'ERROR: $e'; 162 | } 163 | } 164 | } 165 | 166 | enum PostResultState { idle, loading, success, failure } 167 | 168 | class ReviewProvider extends ChangeNotifier { 169 | final ApiService apiService; 170 | 171 | /// Restaurant Review 172 | CustomerReview? _review; 173 | CustomerReview? get review => _review; 174 | 175 | String _message = ''; 176 | String get message => _message; 177 | 178 | PostResultState? _postState = PostResultState.idle; 179 | PostResultState? get postState => _postState; 180 | 181 | ReviewProvider({required this.apiService}); 182 | 183 | void setPostState(PostResultState newState) { 184 | _postState = newState; 185 | notifyListeners(); 186 | } 187 | 188 | Future postReviewById({ 189 | required String id, 190 | required String name, 191 | required String review, 192 | }) async { 193 | try { 194 | _postState = PostResultState.loading; 195 | notifyListeners(); 196 | 197 | final restoReview = await apiService.postReviewById( 198 | id: id, 199 | name: name, 200 | review: review, 201 | ); 202 | 203 | if (restoReview) { 204 | _postState = PostResultState.success; 205 | notifyListeners(); 206 | return _message = 'Success'; 207 | } 208 | } on TimeoutException catch (e) { 209 | _postState = PostResultState.failure; 210 | notifyListeners(); 211 | return _message = 'TIMEOUT: $e'; 212 | } on SocketException catch (e) { 213 | _postState = PostResultState.failure; 214 | notifyListeners(); 215 | return _message = 'NO CONNECTION: $e'; 216 | } on Error catch (e) { 217 | _postState = PostResultState.failure; 218 | notifyListeners(); 219 | return _message = 'ERROR: $e'; 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /lib/modules/screens/restaurant/list_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:animate_do/animate_do.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_spinkit/flutter_spinkit.dart'; 4 | import 'package:food_rating_app/common/navigation.dart'; 5 | import 'package:food_rating_app/data/api/urls.dart'; 6 | import 'package:food_rating_app/modules/providers/restaurant_provider.dart'; 7 | import 'package:food_rating_app/modules/screens/restaurant/detail_screen.dart'; 8 | import 'package:food_rating_app/modules/screens/restaurant/search_screen.dart'; 9 | import 'package:food_rating_app/modules/screens/restaurant/widgets/animation_placeholder.dart'; 10 | import 'package:food_rating_app/modules/screens/restaurant/widgets/restaurant_card.dart'; 11 | import 'package:food_rating_app/common/styles.dart'; 12 | import 'package:provider/provider.dart'; 13 | 14 | class ListScreen extends StatefulWidget { 15 | static const routeName = '/list'; 16 | const ListScreen({Key? key}) : super(key: key); 17 | 18 | @override 19 | State createState() => _ListScreenState(); 20 | } 21 | 22 | class _ListScreenState extends State with TickerProviderStateMixin { 23 | late AnimationController _colorAnimationController; 24 | late Animation _colorTween; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | _colorAnimationController = AnimationController( 30 | vsync: this, 31 | duration: const Duration(seconds: 0), 32 | ); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | _colorAnimationController.dispose(); 38 | super.dispose(); 39 | } 40 | 41 | bool _scrollListener(ScrollNotification scrollInfo) { 42 | if (scrollInfo.metrics.axis == Axis.vertical) { 43 | _colorAnimationController.animateTo(scrollInfo.metrics.pixels / 350); 44 | return true; 45 | } 46 | return false; 47 | } 48 | 49 | Widget _buildGreetings(BuildContext context) { 50 | int hour = DateTime.now().hour; 51 | 52 | return FadeInUp( 53 | from: 20.0, 54 | duration: const Duration(milliseconds: 500), 55 | child: Container( 56 | margin: const EdgeInsets.all(24.0), 57 | child: Column( 58 | crossAxisAlignment: CrossAxisAlignment.start, 59 | children: [ 60 | Text( 61 | (hour >= 0 && hour < 12) 62 | ? 'Selamat Pagi 🧇' 63 | : (hour >= 12 && hour < 15) 64 | ? 'Selamat Siang 🍨' 65 | : (hour >= 15 && hour < 18) 66 | ? 'Selamat Sore ☕' 67 | : 'Selamat Malam 🍗', 68 | style: Theme.of(context).textTheme.headline6, 69 | ), 70 | const SizedBox(height: 14.0), 71 | Text( 72 | 'Telusuri Restoran Favoritmu', 73 | // style: TextStyles.kHeading1.copyWith( 74 | // color: customBlack, 75 | // fontSize: 32.0, 76 | // height: 1.2, 77 | // ), 78 | style: Theme.of(context).textTheme.headline4!.copyWith( 79 | fontWeight: FontWeight.w700, 80 | height: 1.2, 81 | ), 82 | ), 83 | ], 84 | ), 85 | ), 86 | ); 87 | } 88 | 89 | Widget _buildList(BuildContext context) { 90 | return Consumer( 91 | builder: (context, provider, _) { 92 | switch (provider.fetchListState) { 93 | case FetchResultState.loading: 94 | return const Padding( 95 | padding: EdgeInsets.only(top: 100.0), 96 | child: SpinKitFadingCircle(color: customBlue), 97 | ); 98 | case FetchResultState.noData: 99 | return Padding( 100 | padding: const EdgeInsets.only(top: 100.0), 101 | child: AnimationPlaceholder( 102 | animation: 'assets/not-found.json', 103 | text: 'Ops! Sepertinya restoran tidak tersedia', 104 | hasButton: true, 105 | buttonText: 'Refresh', 106 | // onButtonTap: () => provider.fetchRestaurantList(), 107 | onButtonTap: () {}, 108 | ), 109 | ); 110 | case FetchResultState.hasData: 111 | return FadeInUp( 112 | from: 20.0, 113 | duration: const Duration(milliseconds: 500), 114 | child: ListView.builder( 115 | shrinkWrap: true, 116 | physics: const NeverScrollableScrollPhysics(), 117 | itemCount: provider.restaurants!.items!.length, 118 | itemBuilder: (context, index) { 119 | return RestaurantCard( 120 | item: provider.restaurants!.items!.reversed.toList()[index], 121 | ); 122 | }, 123 | ), 124 | ); 125 | case FetchResultState.failure: 126 | return Padding( 127 | padding: const EdgeInsets.only(top: 100.0), 128 | child: AnimationPlaceholder( 129 | animation: 'assets/no-internet.json', 130 | text: 'Ops! Sepertinya koneksi internetmu dalam masalah', 131 | hasButton: true, 132 | buttonText: 'Refresh', 133 | // onButtonTap: () => provider.fetchRestaurantList(), 134 | onButtonTap: () {}, 135 | ), 136 | ); 137 | default: 138 | return const SizedBox(); 139 | } 140 | }, 141 | ); 142 | } 143 | 144 | @override 145 | Widget build(BuildContext context) { 146 | _colorTween = ColorTween( 147 | begin: Theme.of(context).scaffoldBackgroundColor, 148 | end: Theme.of(context).appBarTheme.backgroundColor, 149 | ).animate(_colorAnimationController); 150 | 151 | return Scaffold( 152 | appBar: PreferredSize( 153 | preferredSize: const Size.fromHeight(96.0), 154 | child: AnimatedBuilder( 155 | animation: _colorAnimationController, 156 | builder: (context, child) { 157 | return AppBar( 158 | elevation: 0.0, 159 | titleSpacing: 24.0, 160 | centerTitle: false, 161 | backgroundColor: _colorTween.value, 162 | toolbarHeight: 96.0, 163 | title: Row( 164 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 165 | children: [ 166 | Text( 167 | 'FoodReview', 168 | style: Theme.of(context).appBarTheme.toolbarTextStyle, 169 | ), 170 | CircleAvatar( 171 | backgroundColor: Theme.of(context) 172 | .colorScheme 173 | .primaryVariant 174 | .withOpacity(0.6), 175 | radius: 24, 176 | child: IconButton( 177 | splashRadius: 4.0, 178 | padding: EdgeInsets.zero, 179 | icon: const Icon(Icons.search), 180 | color: Theme.of(context).primaryIconTheme.color, 181 | onPressed: () => 182 | Navigation.intentWithData(SearchScreen.routeName), 183 | ), 184 | ), 185 | ], 186 | ), 187 | ); 188 | }, 189 | ), 190 | ), 191 | body: NotificationListener( 192 | onNotification: _scrollListener, 193 | child: ListView( 194 | children: [ 195 | _buildGreetings(context), 196 | _buildList(context), 197 | ], 198 | ), 199 | ), 200 | ); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /lib/modules/screens/restaurant/review_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_spinkit/flutter_spinkit.dart'; 3 | import 'package:food_rating_app/modules/providers/restaurant_provider.dart'; 4 | import 'package:food_rating_app/modules/screens/restaurant/widgets/animation_placeholder.dart'; 5 | import 'package:food_rating_app/common/styles.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class ReviewScreen extends StatelessWidget { 9 | static const routeName = '/review'; 10 | 11 | final String id; 12 | const ReviewScreen({Key? key, required this.id}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final _formKey = GlobalKey(); 17 | final TextEditingController _nameController = TextEditingController(); 18 | final TextEditingController _reviewController = TextEditingController(); 19 | 20 | Widget _buildForm() { 21 | return Container( 22 | margin: const EdgeInsets.all(24.0), 23 | child: Column(children: [ 24 | Form( 25 | key: _formKey, 26 | child: Column( 27 | children: [ 28 | TextFormField( 29 | controller: _nameController, 30 | textAlignVertical: TextAlignVertical.center, 31 | showCursor: true, 32 | cursorColor: Theme.of(context).iconTheme.color, 33 | decoration: InputDecoration( 34 | hintText: 'Nama saya', 35 | isCollapsed: true, 36 | contentPadding: const EdgeInsets.all(16.0), 37 | filled: true, 38 | fillColor: Theme.of(context) 39 | .colorScheme 40 | .primaryVariant 41 | .withOpacity(0.8), 42 | focusedBorder: OutlineInputBorder( 43 | borderSide: const BorderSide( 44 | color: customBlue, 45 | ), 46 | borderRadius: BorderRadius.circular(10.0), 47 | ), 48 | border: OutlineInputBorder( 49 | borderSide: BorderSide.none, 50 | borderRadius: BorderRadius.circular(10.0), 51 | ), 52 | ), 53 | validator: (value) { 54 | if (value!.isEmpty) { 55 | return 'Nama tidak boleh kosong'; 56 | } 57 | }, 58 | ), 59 | const SizedBox(height: 16.0), 60 | TextFormField( 61 | controller: _reviewController, 62 | textAlignVertical: TextAlignVertical.center, 63 | keyboardType: TextInputType.multiline, 64 | maxLines: 5, 65 | textAlign: TextAlign.justify, 66 | showCursor: true, 67 | cursorColor: Theme.of(context).iconTheme.color, 68 | decoration: InputDecoration( 69 | hintText: 'Pendapat saya tentang restoran ini ...', 70 | isCollapsed: true, 71 | contentPadding: const EdgeInsets.all(16.0), 72 | filled: true, 73 | fillColor: Theme.of(context) 74 | .colorScheme 75 | .primaryVariant 76 | .withOpacity(0.8), 77 | focusedBorder: OutlineInputBorder( 78 | borderSide: const BorderSide( 79 | color: customBlue, 80 | ), 81 | borderRadius: BorderRadius.circular(10.0), 82 | ), 83 | border: OutlineInputBorder( 84 | borderSide: BorderSide.none, 85 | borderRadius: BorderRadius.circular(10.0), 86 | ), 87 | ), 88 | validator: (value) { 89 | if (value!.isEmpty) { 90 | return 'Review tidak boleh kosong'; 91 | } 92 | }, 93 | ), 94 | ], 95 | ), 96 | ) 97 | ]), 98 | ); 99 | } 100 | 101 | Widget _buildContent(BuildContext context) { 102 | return Consumer( 103 | builder: (context, provider, _) { 104 | switch (provider.postState) { 105 | case PostResultState.idle: 106 | return _buildForm(); 107 | case PostResultState.loading: 108 | return const Center( 109 | child: SpinKitFadingCircle( 110 | color: customBlue, 111 | ), 112 | ); 113 | 114 | case PostResultState.success: 115 | return const Center( 116 | child: AnimationPlaceholder( 117 | animation: 'assets/done.json', 118 | text: 119 | 'Terimakasih sudah memberikan review! Semoga harimu menyenangkan', 120 | ), 121 | ); 122 | case PostResultState.failure: 123 | return Center( 124 | child: AnimationPlaceholder( 125 | animation: 'assets/no-internet.json', 126 | text: 'Ops! Sepertinya koneksi internetmu dalam masalah', 127 | hasButton: true, 128 | buttonText: 'Refresh', 129 | onButtonTap: () { 130 | provider.setPostState(PostResultState.idle); 131 | }, 132 | ), 133 | ); 134 | default: 135 | return const SizedBox(); 136 | } 137 | }, 138 | ); 139 | } 140 | 141 | return Scaffold( 142 | resizeToAvoidBottomInset: false, 143 | appBar: PreferredSize( 144 | preferredSize: const Size.fromHeight(96.0), 145 | child: AppBar( 146 | elevation: 0.0, 147 | titleSpacing: 24.0, 148 | centerTitle: false, 149 | automaticallyImplyLeading: false, 150 | backgroundColor: Colors.transparent, 151 | toolbarHeight: 96.0, 152 | title: Row( 153 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 154 | children: [ 155 | CircleAvatar( 156 | radius: 24.0, 157 | backgroundColor: Theme.of(context) 158 | .colorScheme 159 | .primaryVariant 160 | .withOpacity(0.6), 161 | child: IconButton( 162 | splashRadius: 4.0, 163 | padding: EdgeInsets.zero, 164 | icon: const Icon(Icons.arrow_back), 165 | color: Theme.of(context).primaryIconTheme.color, 166 | onPressed: () { 167 | Provider.of( 168 | context, 169 | listen: false, 170 | ).setPostState(PostResultState.idle); 171 | Navigator.pop(context); 172 | }, 173 | ), 174 | ), 175 | Text( 176 | 'Review', 177 | style: Theme.of(context).appBarTheme.toolbarTextStyle, 178 | ), 179 | IconButton( 180 | splashRadius: 24.0, 181 | splashColor: Colors.grey[200], 182 | padding: EdgeInsets.zero, 183 | icon: const Icon(Icons.send), 184 | color: Theme.of(context).iconTheme.color, 185 | onPressed: () { 186 | if (_formKey.currentState!.validate()) { 187 | Provider.of( 188 | context, 189 | listen: false, 190 | ).postReviewById( 191 | id: id, 192 | name: _nameController.text, 193 | review: _reviewController.text, 194 | ); 195 | } 196 | }, 197 | ), 198 | ], 199 | ), 200 | ), 201 | ), 202 | body: SafeArea(child: _buildContent(context)), 203 | ); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /assets/empty.json: -------------------------------------------------------------------------------- 1 | {"v":"4.7.0","fr":25,"ip":0,"op":50,"w":120,"h":120,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"ruoi","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.967]},"o":{"x":[0.167],"y":[0.033]},"n":["0p833_0p967_0p167_0p033"],"t":35,"s":[100],"e":[0]},{"t":49}]},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0,"y":0},"n":"0p833_0p833_0_0","t":0,"s":[57.361,61.016,0],"e":[57.699,41.796,0],"to":[-4.67500305175781,-4.12800598144531,0],"ti":[-13.9099960327148,5.27300262451172,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10.219,"s":[57.699,41.796,0],"e":[79.084,33.982,0],"to":[12.8159942626953,-4.85800170898438,0],"ti":[-4.54498291015625,3.73400115966797,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.445,"s":[79.084,33.982,0],"e":[59.691,9.121,0],"to":[6.61601257324219,-5.43799591064453,0],"ti":[20.0290069580078,1.20700073242188,0]},{"t":35}]},"a":{"a":0,"k":[60.531,10.945,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.994,0],[0,-0.994],[0.995,0],[0,0.994]],"o":[[0.995,0],[0,0.994],[-0.994,0],[0,-0.994]],"v":[[-0.001,-1.801],[1.801,-0.001],[-0.001,1.801],[-1.801,-0.001]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.529,0.529,0.529,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[62.4,13.144],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.422,0],[0,-1.422],[1.421,0],[0,1.422]],"o":[[1.421,0],[0,1.422],[-1.422,0],[0,-1.422]],"v":[[0.001,-2.574],[2.574,0],[0.001,2.574],[-2.574,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.529,0.529,0.529,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0.7},"lc":1,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[64.145,9.606],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.996,0],[0,-1.996],[1.996,0],[0,1.996]],"o":[[1.996,0],[0,1.996],[-1.996,0],[0,-1.996]],"v":[[0,-3.614],[3.614,0],[0,3.614],[-3.614,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.529,0.529,0.529,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0.7},"lc":1,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[57.957,10.552],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"ix":3,"mn":"ADBE Vector Group"},{"ty":"tr","p":{"a":0,"k":[60.531,10.941],"ix":2},"a":{"a":0,"k":[60.531,10.941],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"ruoi","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":50,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.967]},"o":{"x":[0.167],"y":[0.033]},"n":["0p833_0p967_0p167_0p033"],"t":35,"s":[100],"e":[0]},{"t":49}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[-0.75,-0.75,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-13.91,5.273],[-4.545,3.734],[20.029,1.207]],"o":[[-4.675,-4.128],[12.816,-4.858],[6.616,-5.438],[0,0]],"v":[[-7.383,24.76],[-7.046,5.54],[14.34,-2.273],[-3.178,-24.76]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.627,0.627,0.627,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1},"lc":2,"lj":2,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":2.028}},{"n":"g","nm":"gap","v":{"a":0,"k":2.028}},{"n":"o","nm":"offset","v":{"a":0,"k":0}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[67.87,37.631],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.953]},"o":{"x":[0.167],"y":[0.033]},"n":["0p833_0p953_0p167_0p033"],"t":0,"s":[0],"e":[100]},{"t":35}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim"}],"ip":0,"op":50,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"im_emptyBox Outlines","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[60,60,0]},"a":{"a":0,"k":[60,60,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-0.001,-16.607],[-32.143,-0.002],[-0.001,16.607],[32.144,-0.002]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.8,0.82,0.851,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[60,55.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[12.856,-23.249],[0,-16.605],[-12.857,-23.249],[-45,-6.641],[-32.144,0.001],[-45,6.645],[-12.857,23.249],[0,16.609],[12.856,23.249],[45,6.645],[32.143,0.001],[45,-6.641]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.957,0.957,0.957,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[60,55.748],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-16.072,24.171],[16.072,11.312],[16.072,-24.171],[-16.072,-24.171]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.902,0.914,0.929,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[76.072,83.33],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-32.143,-24.171],[-32.143,11.311],[-0.001,24.171],[32.144,11.311],[32.144,-24.171]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.8,0.82,0.851,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[60,83.33],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group"},{"ty":"tr","p":{"a":0,"k":[60,60.186],"ix":2},"a":{"a":0,"k":[60,60.186],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"box","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":50,"st":0,"bm":0,"sr":1}]} -------------------------------------------------------------------------------- /lib/modules/screens/restaurant/search_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:animate_do/animate_do.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:food_rating_app/common/navigation.dart'; 4 | import 'package:food_rating_app/data/api/urls.dart'; 5 | import 'package:food_rating_app/modules/providers/restaurant_provider.dart'; 6 | import 'package:food_rating_app/modules/screens/restaurant/detail_screen.dart'; 7 | import 'package:food_rating_app/modules/screens/restaurant/widgets/animation_placeholder.dart'; 8 | import 'package:food_rating_app/modules/screens/restaurant/widgets/restaurant_card.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | class SearchScreen extends StatefulWidget { 12 | static const routeName = '/search'; 13 | const SearchScreen({Key? key}) : super(key: key); 14 | 15 | @override 16 | State createState() => _SearchScreenState(); 17 | } 18 | 19 | class _SearchScreenState extends State 20 | with TickerProviderStateMixin { 21 | final TextEditingController _searchController = TextEditingController(); 22 | late AnimationController _colorAnimationController; 23 | late Animation _colorTween; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | _colorAnimationController = AnimationController( 29 | vsync: this, 30 | duration: const Duration(seconds: 0), 31 | ); 32 | } 33 | 34 | @override 35 | void dispose() { 36 | _colorAnimationController.dispose(); 37 | super.dispose(); 38 | } 39 | 40 | bool _scrollListener(ScrollNotification scrollInfo) { 41 | if (scrollInfo.metrics.axis == Axis.vertical) { 42 | _colorAnimationController.animateTo(scrollInfo.metrics.pixels / 350); 43 | return true; 44 | } 45 | return false; 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | _colorTween = ColorTween( 51 | begin: Theme.of(context).scaffoldBackgroundColor, 52 | end: Theme.of(context).appBarTheme.backgroundColor, 53 | ).animate(_colorAnimationController); 54 | 55 | return Scaffold( 56 | appBar: PreferredSize( 57 | preferredSize: const Size.fromHeight(96.0), 58 | child: AnimatedBuilder( 59 | animation: _colorAnimationController, 60 | builder: (context, child) { 61 | return AppBar( 62 | elevation: 0.0, 63 | titleSpacing: 24.0, 64 | centerTitle: false, 65 | automaticallyImplyLeading: false, 66 | backgroundColor: _colorTween.value, 67 | toolbarHeight: 96.0, 68 | title: Row( 69 | children: [ 70 | Expanded( 71 | flex: 1, 72 | child: CircleAvatar( 73 | backgroundColor: Theme.of(context) 74 | .colorScheme 75 | .primaryVariant 76 | .withOpacity(0.6), 77 | radius: 24.0, 78 | child: IconButton( 79 | splashRadius: 4.0, 80 | padding: EdgeInsets.zero, 81 | icon: const Icon(Icons.arrow_back), 82 | color: Theme.of(context).primaryIconTheme.color, 83 | onPressed: () { 84 | Provider.of( 85 | context, 86 | listen: false, 87 | ).setSearchState(SearchResultState.searching); 88 | Navigation.back(); 89 | }, 90 | ), 91 | ), 92 | ), 93 | const SizedBox(width: 16.0), 94 | Expanded( 95 | flex: 6, 96 | child: TextField( 97 | controller: _searchController, 98 | textAlignVertical: TextAlignVertical.center, 99 | showCursor: true, 100 | cursorColor: Theme.of(context).iconTheme.color, 101 | decoration: InputDecoration( 102 | hintText: 'Kafe Cemara', 103 | isCollapsed: true, 104 | filled: true, 105 | fillColor: Theme.of(context) 106 | .colorScheme 107 | .primaryVariant 108 | .withOpacity(0.8), 109 | contentPadding: const EdgeInsets.all(10.0), 110 | focusedBorder: OutlineInputBorder( 111 | borderSide: BorderSide.none, 112 | borderRadius: BorderRadius.circular(15.0), 113 | ), 114 | border: OutlineInputBorder( 115 | borderSide: BorderSide.none, 116 | borderRadius: BorderRadius.circular(15.0), 117 | ), 118 | prefixIcon: Icon( 119 | Icons.search, 120 | color: Colors.grey[500]!, 121 | ), 122 | suffixIcon: _searchController.text.isNotEmpty 123 | ? GestureDetector( 124 | onTap: () { 125 | _searchController.clear(); 126 | Provider.of( 127 | context, 128 | listen: false, 129 | ).fetchRestaurantSearchResult(query: ''); 130 | }, 131 | child: const Icon(Icons.close), 132 | ) 133 | : null, 134 | ), 135 | onChanged: (query) { 136 | if (query.isNotEmpty) { 137 | Provider.of( 138 | context, 139 | listen: false, 140 | ).fetchRestaurantSearchResult(query: query); 141 | } else { 142 | Provider.of( 143 | context, 144 | listen: false, 145 | ).setSearchState(SearchResultState.searching); 146 | } 147 | }, 148 | ), 149 | ), 150 | ], 151 | ), 152 | ); 153 | }), 154 | ), 155 | body: NotificationListener( 156 | onNotification: _scrollListener, 157 | child: SafeArea( 158 | child: Consumer( 159 | builder: (context, provider, _) { 160 | switch (provider.searchState) { 161 | case SearchResultState.searching: 162 | return const AnimationPlaceholder( 163 | animation: 'assets/loading.json', 164 | text: 'Cari restoran', 165 | ); 166 | case SearchResultState.hasData: 167 | return FadeInUp( 168 | from: 20.0, 169 | duration: const Duration(milliseconds: 500), 170 | child: ListView.builder( 171 | itemCount: provider.restaurants!.items!.length, 172 | itemBuilder: (context, index) { 173 | return RestaurantCard( 174 | item: provider.restaurants!.items![index], 175 | ); 176 | }, 177 | ), 178 | ); 179 | case SearchResultState.noData: 180 | return const AnimationPlaceholder( 181 | animation: 'assets/not-found.json', 182 | text: 'Ops! Sepertinya restoran tidak tersedia', 183 | ); 184 | case SearchResultState.failure: 185 | return AnimationPlaceholder( 186 | animation: 'assets/no-internet.json', 187 | text: 'Ops! Sepertinya koneksi internetmu dalam masalah', 188 | hasButton: true, 189 | buttonText: 'Refresh', 190 | onButtonTap: () => provider.fetchRestaurantSearchResult(), 191 | ); 192 | default: 193 | return const SizedBox(); 194 | } 195 | }, 196 | ), 197 | ), 198 | ), 199 | ); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /assets/done.json: -------------------------------------------------------------------------------- 1 | {"v":"5.4.3","fr":30,"ip":0,"op":120,"w":128,"h":128,"nm":"preview","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"confirm Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":54,"s":[180],"e":[368]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":62,"s":[368],"e":[356]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":67,"s":[356],"e":[362]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"n":["0p667_1_0p167_0"],"t":71,"s":[362],"e":[359]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"n":["0p667_1_0p167_0"],"t":74,"s":[359],"e":[360]},{"t":77}],"ix":10},"p":{"a":0,"k":[128,128,0],"ix":2},"a":{"a":0,"k":[24,24,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":54,"s":[100,100,100],"e":[200,200,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"n":["0p667_1_0p167_0","0p667_1_0p167_0","0p667_1_0p167_0"],"t":62,"s":[200,200,100],"e":[200,200,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"n":["0p833_1_0p167_0","0p833_1_0p167_0","0p833_1_0p167_0"],"t":95,"s":[200,200,100],"e":[100,100,100]},{"t":99}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":90,"s":[{"i":[[-0.408,-0.413],[0,0],[-0.407,0.413],[0,0],[0.407,0.413],[0,0],[0.407,-0.412],[0,0],[0,0],[0.407,-0.413],[0,0]],"o":[[0,0],[0.407,0.413],[0,0],[0.407,-0.412],[0,0],[-0.408,-0.412],[0,0],[0,0],[-0.407,-0.413],[0,0],[-0.408,0.413]],"v":[[-15.694,3.946],[-7.057,12.69],[-5.582,12.69],[15.695,-8.852],[15.695,-10.346],[13.379,-12.691],[11.903,-12.691],[-6.319,5.758],[-11.902,0.107],[-13.377,0.107],[-15.694,2.452]],"c":true}],"e":[{"i":[[-0.368,-0.019],[0,0],[-0.293,0.091],[0,0],[-0.07,0.143],[0,0],[0.035,-0.012],[0,0],[0,0],[-0.058,-0.029],[0,0]],"o":[[0,0],[0.057,0.028],[0,0],[-0.007,-0.023],[0,0],[-0.004,-0.075],[0,0],[0,0],[-0.16,0.025],[0,0],[0.007,0.126]],"v":[[-20.366,2.956],[-6.057,1.737],[-6.02,1.737],[9.133,0.57],[9.445,0.638],[9.441,-0.019],[9.09,-0.144],[-6.069,-1.633],[-20.324,-2.9],[-20.488,-2.784],[-20.491,2.811]],"c":true}]},{"t":95}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[24,24],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":54,"op":99,"st":34,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"send Outlines 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":35,"s":[128,128,0],"e":[188,128,0],"to":[10,0,0],"ti":[-26.6666660308838,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":50,"s":[188,128,0],"e":[288,128,0],"to":[26.6666660308838,0,0],"ti":[-16.6666660308838,0,0]},{"t":57}],"ix":2},"a":{"a":0,"k":[12,12,0],"ix":1},"s":{"a":0,"k":[400,400,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.624],[0,0],[-0.207,-0.016],[0,0],[0,0],[-0.001,-0.208],[0,0],[-0.558,0.28],[0,0],[0.619,0.31],[0,0]],"o":[[0,0],[-0.001,0.208],[0,0],[0,0],[-0.207,0.016],[0,0],[0,0.624],[0,0],[0.619,-0.309],[0,0],[-0.558,-0.279]],"v":[[-10.068,-9.161],[-10.076,-1.538],[-9.71,-1.141],[4.949,-0.001],[-9.71,1.139],[-10.076,1.536],[-10.068,9.16],[-8.854,9.91],[9.458,0.751],[9.458,-0.751],[-8.854,-9.911]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[12.077,12],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":28,"op":57,"st":25,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"send Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[128,128,0],"ix":2},"a":{"a":0,"k":[12,12,0],"ix":1},"s":{"a":0,"k":[400,400,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.624],[0,0],[-0.207,-0.016],[0,0],[0,0],[-0.001,-0.208],[0,0],[-0.558,0.28],[0,0],[0.619,0.31],[0,0]],"o":[[0,0],[-0.001,0.208],[0,0],[0,0],[-0.207,0.016],[0,0],[0,0.624],[0,0],[0.619,-0.309],[0,0],[-0.558,-0.279]],"v":[[-10.068,-9.161],[-10.076,-1.538],[-9.71,-1.141],[4.949,-0.001],[-9.71,1.139],[-10.076,1.536],[-10.068,9.16],[-8.854,9.91],[9.458,0.751],[9.458,-0.751],[-8.854,-9.911]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.1607843137254902,0.3843137254901961,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[12.077,12],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":28,"st":-5,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"send Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":90,"s":[128,128,0],"e":[138,128,0],"to":[1.66666662693024,0,0],"ti":[-1.66666662693024,0,0]},{"t":96}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":28,"s":[40,40,100],"e":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":35,"s":[110,110,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":39,"s":[100,100,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_1_0p333_0","0p833_1_0p333_0","0p833_1_0p333_0"],"t":90,"s":[100,100,100],"e":[17,17,100]},{"t":96}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":41,"s":[{"i":[[0,-9.941],[9.941,0],[0,9.941],[-9.941,0]],"o":[[0,9.941],[-9.941,0],[0,-9.941],[9.941,0]],"v":[[18,0],[0,18],[-18,0],[0,-18]],"c":true}],"e":[{"i":[[0,-5.75],[9.941,0],[0,9.941],[-9.941,0]],"o":[[0,5.125],[-9.941,0],[0,-9.941],[9.941,0]],"v":[[27,0],[0,16.75],[-18,0],[0,-16.375]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":47,"s":[{"i":[[0,-5.75],[9.941,0],[0,9.941],[-9.941,0]],"o":[[0,5.125],[-9.941,0],[0,-9.941],[9.941,0]],"v":[[27,0],[0,16.75],[-18,0],[0,-16.375]],"c":true}],"e":[{"i":[[0,-9.941],[9.941,0],[0,9.941],[-9.941,0]],"o":[[0,9.941],[-9.941,0],[0,-9.941],[9.941,0]],"v":[[16.125,0],[0,18],[-18,0],[0,-18]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":53,"s":[{"i":[[0,-9.941],[9.941,0],[0,9.941],[-9.941,0]],"o":[[0,9.941],[-9.941,0],[0,-9.941],[9.941,0]],"v":[[16.125,0],[0,18],[-18,0],[0,-18]],"c":true}],"e":[{"i":[[0,-9.941],[9.941,0],[0,9.941],[-9.941,0]],"o":[[0,9.941],[-9.941,0],[0,-9.941],[9.941,0]],"v":[[18.625,0.125],[0,18],[-18,0],[0,-18]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":57,"s":[{"i":[[0,-9.941],[9.941,0],[0,9.941],[-9.941,0]],"o":[[0,9.941],[-9.941,0],[0,-9.941],[9.941,0]],"v":[[18.625,0.125],[0,18],[-18,0],[0,-18]],"c":true}],"e":[{"i":[[0,-9.941],[9.941,0],[0,9.941],[-9.941,0]],"o":[[0,9.941],[-9.941,0],[0,-9.941],[9.941,0]],"v":[[18,0],[0,18],[-18,0],[0,-18]],"c":true}]},{"t":60}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":4,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.1607843137254902,0.3843137254901961,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[400,400],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":28,"op":96,"st":-5,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"send Outlines 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":90,"s":[148,128,0],"e":[128,128,0],"to":[-3.33333325386047,0,0],"ti":[3.33333325386047,0,0]},{"t":98}],"ix":2},"a":{"a":0,"k":[12,12,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":90,"s":[200,200,100],"e":[400,400,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":98,"s":[400,400,100],"e":[440,440,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":104,"s":[440,440,100],"e":[380,380,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":109,"s":[380,380,100],"e":[410,410,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":112,"s":[410,410,100],"e":[395,395,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":115,"s":[395,395,100],"e":[400,400,100]},{"t":118}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.624],[0,0],[-0.207,-0.016],[0,0],[0,0],[-0.001,-0.208],[0,0],[-0.558,0.28],[0,0],[0.619,0.31],[0,0]],"o":[[0,0],[-0.001,0.208],[0,0],[0,0],[-0.207,0.016],[0,0],[0,0.624],[0,0],[0.619,-0.309],[0,0],[-0.558,-0.279]],"v":[[-10.068,-9.161],[-10.076,-1.538],[-9.71,-1.141],[4.949,-0.001],[-9.71,1.139],[-10.076,1.536],[-10.068,9.16],[-8.854,9.91],[9.458,0.751],[9.458,-0.751],[-8.854,-9.911]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.1607843137254902,0.3843137254901961,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[12.077,12],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":120,"st":85,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[64,64,0],"ix":2},"a":{"a":0,"k":[128,128,0],"ix":1},"s":{"a":0,"k":[50,50,100],"ix":6}},"ao":0,"w":256,"h":256,"ip":0,"op":120,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /assets/not-found.json: -------------------------------------------------------------------------------- 1 | {"v":"5.7.3","fr":24,"ip":0,"op":120,"w":1000,"h":1000,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.47],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[-30.915]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":75,"s":[-30.915]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":79,"s":[8]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":85,"s":[-6.256]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":91,"s":[2.696]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[-1.78]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":101,"s":[1]},{"t":105,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":75,"s":[272,704,0],"to":[0,-11.333,0],"ti":[0,0.27,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":79,"s":[272,636,0],"to":[0,-0.27,0],"ti":[0,-7.924,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":85,"s":[272,702.38,0],"to":[0,4.285,0],"ti":[0,-0.124,0]},{"i":{"x":0.667,"y":0.646},"o":{"x":0.333,"y":0},"t":91,"s":[272,683.546,0],"to":[0,0.27,0],"ti":[0,6.313,0]},{"i":{"x":0.27,"y":1},"o":{"x":0.273,"y":1},"t":96,"s":[272,707.416,0],"to":[0,-5.361,0],"ti":[0,0,0]},{"i":{"x":0.27,"y":1},"o":{"x":0.167,"y":0},"t":101,"s":[272,700,0],"to":[0,0,0],"ti":[0,0,0]},{"t":105,"s":[272,704,0]}],"ix":2},"a":{"a":0,"k":[-270,124,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[266,-10]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.937254961799,0.941176530427,0.956862804936,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[74.509,-1.469],[0,0],[0,0],[-45.992,0.628]],"o":[[0,0],[0,0],[-74.509,1.469],[0,0],[0,0],[45.992,-0.628]],"v":[[72.997,-317.829],[95.264,-250.422],[1.509,-236.969],[-95.561,-249.52],[-73.293,-316.932],[0.508,-304.372]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[119.5,0.018],[0,0],[0,0],[-95.499,1.409]],"o":[[0,0],[0,0],[-118.504,-0.018],[0,0],[0,0],[95.499,-1.409]],"v":[[124.203,-162.817],[146.941,-93.981],[1,-73.518],[-146.917,-94.055],[-124.351,-162.368],[1.001,-142.091]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.937254965305,0.941176533699,0.956862807274,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-36,121.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 3","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-8.837],[0,0],[112.152,3.157],[0,0],[0,8.837],[0,0],[-8.837,0],[0,0]],"o":[[0,0],[0,8.837],[0,0],[-84.765,0.204],[0,0],[0,-8.837],[0,0],[8.837,0]],"v":[[48,-393.5],[176.5,-4.5],[-4.327,14.609],[-5.51,14.699],[-176.5,-4.5],[-48,-393.5],[-32,-409.5],[32,-409.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.674509823322,0.039215687662,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-36,121.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-131.444,0],[0,-24.439],[131.444,0],[0,24.439]],"o":[[131.444,0],[0,24.439],[-131.444,0],[0,-24.439]],"v":[[-3.5,-38.5],[234.5,5.75],[-3.5,50],[-241.5,5.75]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.968627512455,0.572549045086,0.105882361531,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-36,121.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 5","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[500,517,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-4.903],[125.312,0],[0,22.091],[0,0]],"o":[[0,0],[0,22.091],[-125.311,0],[0,-4.574],[0,0]],"v":[[213.405,285.559],[226.897,0],[0,40],[-226.897,0],[-214.087,290.437]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.584,0.584,0.584,0.5,0.292,0.292,0.292,1,0,0,0],"ix":9}},"s":{"a":0,"k":[0,-39.5],"ix":5},"e":{"a":0,"k":[0,57],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,196.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 3","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.47,"y":0},"t":0,"s":[546.5,781.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":0},"o":{"x":0.333,"y":0.333},"t":20,"s":[546.5,651.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.14,"y":1},"o":{"x":0.333,"y":0},"t":74,"s":[546.5,651.5,0],"to":[0,0,0],"ti":[0,21.667,0]},{"t":82,"s":[546.5,781.5,0]}],"ix":2},"a":{"a":0,"k":[56,141.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[15,15],"ix":2},"p":{"a":0,"k":[92,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"d":1,"ty":"el","s":{"a":0,"k":[15,15],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 2","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.180392161012,0.180392161012,0.235294133425,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-36,146.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":5,"s":[100,52]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":11,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":73,"s":[100,100]},{"t":78,"s":[100,52]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[51,51],"ix":2},"p":{"a":0,"k":[92,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"d":1,"ty":"el","s":{"a":0,"k":[51,51],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 2","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.925490260124,0.937254965305,0.933333396912,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-36,141.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":5,"s":[100,21]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":11,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":73,"s":[100,100]},{"t":78,"s":[100,21]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.53,"y":0},"t":29,"s":[0,0],"to":[-1.47,0],"ti":[-1.294,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":36,"s":[-8.817,0],"to":[1.294,0],"ti":[0.53,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45,"s":[7.762,0],"to":[-0.53,0],"ti":[0.373,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":53,"s":[-12,0],"to":[-0.373,0],"ti":[-2,0]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":61,"s":[5.522,0],"to":[2,0],"ti":[0.92,0]},{"t":69,"s":[0,0]}],"ix":2},"a":{"a":0,"k":[0,-9],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[209,364],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":219,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.164705887437,0.180392161012,0.223529428244,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[5.5,176],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[500,519.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[453.793,80],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.498039215803,0.494133025408,0.494133025408,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,196.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[500,519.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[453.793,80],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 2","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.584,0.584,0.584,0.5,0.292,0.292,0.292,1,0,0,0],"ix":9}},"s":{"a":0,"k":[0,-39.5],"ix":5},"e":{"a":0,"k":[0,57],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,196.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /lib/modules/screens/restaurant/detail_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:animate_do/animate_do.dart'; 2 | import 'package:cached_network_image/cached_network_image.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_spinkit/flutter_spinkit.dart'; 5 | import 'package:food_rating_app/common/navigation.dart'; 6 | import 'package:food_rating_app/data/api/urls.dart'; 7 | import 'package:food_rating_app/modules/providers/database_provider.dart'; 8 | import 'package:food_rating_app/data/models/restaurant.dart'; 9 | import 'package:food_rating_app/modules/providers/restaurant_provider.dart'; 10 | import 'package:food_rating_app/modules/screens/restaurant/review_screen.dart'; 11 | import 'package:food_rating_app/modules/screens/restaurant/widgets/animation_placeholder.dart'; 12 | import 'package:food_rating_app/common/styles.dart'; 13 | import 'package:provider/provider.dart'; 14 | import 'package:readmore/readmore.dart'; 15 | 16 | class DetailScreen extends StatefulWidget { 17 | static const routeName = '/detail'; 18 | 19 | final String id; 20 | const DetailScreen({ 21 | Key? key, 22 | required this.id, 23 | }) : super(key: key); 24 | 25 | @override 26 | State createState() => _DetailScreenState(); 27 | } 28 | 29 | class _DetailScreenState extends State 30 | with TickerProviderStateMixin { 31 | late AnimationController _colorAnimationController; 32 | late Animation _colorTween; 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | _colorAnimationController = AnimationController( 38 | vsync: this, 39 | duration: const Duration(seconds: 0), 40 | ); 41 | } 42 | 43 | @override 44 | void dispose() { 45 | _colorAnimationController.dispose(); 46 | super.dispose(); 47 | } 48 | 49 | bool _scrollListener(ScrollNotification scrollInfo) { 50 | if (scrollInfo.metrics.axis == Axis.vertical) { 51 | _colorAnimationController.animateTo(scrollInfo.metrics.pixels / 350); 52 | return true; 53 | } 54 | return false; 55 | } 56 | 57 | Widget _buildInfo(Item restaurant) { 58 | return Container( 59 | margin: const EdgeInsets.symmetric(horizontal: 24.0), 60 | child: Column( 61 | crossAxisAlignment: CrossAxisAlignment.start, 62 | children: [ 63 | /// Categories 64 | Container( 65 | margin: const EdgeInsets.only(bottom: 8.0), 66 | child: Row( 67 | children: [ 68 | for (final category in restaurant.categories!) 69 | Container( 70 | padding: const EdgeInsets.symmetric( 71 | horizontal: 12.0, 72 | vertical: 4.0, 73 | ), 74 | margin: const EdgeInsets.only(right: 8.0), 75 | decoration: BoxDecoration( 76 | color: Theme.of(context) 77 | .colorScheme 78 | .primaryVariant 79 | .withOpacity(0.8), 80 | borderRadius: BorderRadius.circular(8.0), 81 | ), 82 | child: Text( 83 | category.name!, 84 | style: Theme.of(context).textTheme.caption, 85 | ), 86 | ), 87 | ], 88 | ), 89 | ), 90 | 91 | /// Name 92 | Container( 93 | margin: const EdgeInsets.only(bottom: 8.0), 94 | child: Text( 95 | restaurant.name!, 96 | style: Theme.of(context) 97 | .textTheme 98 | .headline5! 99 | .copyWith(fontWeight: FontWeight.w700), 100 | ), 101 | ), 102 | 103 | /// Rating, Location 104 | Container( 105 | margin: const EdgeInsets.only(bottom: 8.0), 106 | child: Row( 107 | children: [ 108 | Row( 109 | children: [ 110 | Container( 111 | padding: const EdgeInsets.all(6.0), 112 | decoration: BoxDecoration( 113 | color: customYellow, 114 | borderRadius: BorderRadius.circular(10.0), 115 | ), 116 | child: const Icon( 117 | Icons.star, 118 | color: Colors.white, 119 | size: 20.0, 120 | ), 121 | ), 122 | const SizedBox(width: 16.0), 123 | Text( 124 | '${restaurant.rating}', 125 | style: Theme.of(context).textTheme.bodyText1, 126 | ), 127 | ], 128 | ), 129 | const SizedBox(width: 32.0), 130 | Row( 131 | children: [ 132 | Container( 133 | padding: const EdgeInsets.all(6.0), 134 | decoration: BoxDecoration( 135 | color: customBlue, 136 | borderRadius: BorderRadius.circular(10.0), 137 | ), 138 | child: const Icon( 139 | Icons.location_on, 140 | color: Colors.white, 141 | size: 20.0, 142 | ), 143 | ), 144 | const SizedBox(width: 16.0), 145 | Text( 146 | restaurant.city!, 147 | style: Theme.of(context).textTheme.bodyText1, 148 | ), 149 | ], 150 | ), 151 | ], 152 | ), 153 | ), 154 | 155 | /// Description 156 | Container( 157 | margin: const EdgeInsets.only(top: 8.0, bottom: 8.0), 158 | child: ReadMoreText( 159 | restaurant.description!, 160 | trimLines: 5, 161 | trimMode: TrimMode.Line, 162 | trimCollapsedText: 'Tampilkan lebih banyak', 163 | trimExpandedText: 'Tampilkan lebih sedikit', 164 | textAlign: TextAlign.justify, 165 | colorClickableText: Theme.of(context).colorScheme.secondary, 166 | style: Theme.of(context) 167 | .textTheme 168 | .bodyText1! 169 | .copyWith(letterSpacing: 0.2), 170 | moreStyle: Theme.of(context) 171 | .textTheme 172 | .bodyText1! 173 | .copyWith(fontWeight: FontWeight.w600), 174 | ), 175 | ), 176 | ], 177 | ), 178 | ); 179 | } 180 | 181 | Widget _buildMenus({required String label, required List menu}) { 182 | return Container( 183 | margin: const EdgeInsets.symmetric(vertical: 12.0), 184 | child: Column( 185 | crossAxisAlignment: CrossAxisAlignment.start, 186 | children: [ 187 | Container( 188 | margin: const EdgeInsets.fromLTRB(24.0, 0.0, 24.0, 14.0), 189 | child: Text( 190 | label, 191 | style: Theme.of(context) 192 | .textTheme 193 | .headline6! 194 | .copyWith(fontSize: 18.0, fontWeight: FontWeight.w600), 195 | ), 196 | ), 197 | SizedBox( 198 | height: 64.0, 199 | child: ListView.builder( 200 | shrinkWrap: false, 201 | scrollDirection: Axis.horizontal, 202 | padding: const EdgeInsets.symmetric(horizontal: 24.0), 203 | itemCount: menu.length, 204 | itemBuilder: (context, index) { 205 | return Container( 206 | padding: const EdgeInsets.all(20.0), 207 | margin: const EdgeInsets.only(right: 12.0), 208 | decoration: BoxDecoration( 209 | color: Theme.of(context).primaryColor, 210 | borderRadius: BorderRadius.circular(15.0), 211 | boxShadow: [ 212 | BoxShadow( 213 | color: Theme.of(context).shadowColor.withOpacity(0.6), 214 | spreadRadius: 1.0, 215 | blurRadius: 30.0, 216 | offset: const Offset(0, 3.0), 217 | ), 218 | ], 219 | ), 220 | child: Center( 221 | child: Text( 222 | menu[index].name!, 223 | style: Theme.of(context) 224 | .textTheme 225 | .bodyText1! 226 | .copyWith(letterSpacing: 0.0), 227 | ), 228 | ), 229 | ); 230 | }, 231 | ), 232 | ), 233 | ], 234 | ), 235 | ); 236 | } 237 | 238 | Widget _buildReview(List reviews) { 239 | return Container( 240 | margin: const EdgeInsets.symmetric(vertical: 14.0), 241 | child: Column( 242 | crossAxisAlignment: CrossAxisAlignment.start, 243 | children: [ 244 | Container( 245 | margin: const EdgeInsets.fromLTRB(24.0, 0.0, 24.0, 14.0), 246 | child: Text( 247 | 'Apa kata mereka (${reviews.length})', 248 | style: Theme.of(context) 249 | .textTheme 250 | .headline6! 251 | .copyWith(fontSize: 18.0, fontWeight: FontWeight.w600), 252 | ), 253 | ), 254 | ListView.builder( 255 | shrinkWrap: true, 256 | itemCount: reviews.length, 257 | physics: const NeverScrollableScrollPhysics(), 258 | itemBuilder: (context, index) { 259 | return Column( 260 | children: [ 261 | Container( 262 | padding: const EdgeInsets.symmetric( 263 | horizontal: 24.0, 264 | vertical: 14.0, 265 | ), 266 | child: Row( 267 | crossAxisAlignment: CrossAxisAlignment.start, 268 | children: [ 269 | Expanded( 270 | flex: 1, 271 | child: Container( 272 | height: 64.0, 273 | width: 56.0, 274 | decoration: BoxDecoration( 275 | color: Theme.of(context) 276 | .colorScheme 277 | .primaryVariant 278 | .withOpacity(0.6), 279 | borderRadius: BorderRadius.circular(10.0), 280 | ), 281 | child: Icon( 282 | Icons.person_outline, 283 | color: Colors.grey[700], 284 | ), 285 | ), 286 | ), 287 | const SizedBox(width: 16.0), 288 | Expanded( 289 | flex: 4, 290 | child: Column( 291 | crossAxisAlignment: CrossAxisAlignment.start, 292 | children: [ 293 | Text( 294 | reviews[index].name!, 295 | style: Theme.of(context) 296 | .textTheme 297 | .bodyText1! 298 | .copyWith(fontWeight: FontWeight.w600), 299 | ), 300 | Text( 301 | reviews[index].date!, 302 | style: Theme.of(context).textTheme.subtitle1, 303 | ), 304 | const SizedBox(width: 10.0), 305 | Text( 306 | reviews[index].review!, 307 | style: Theme.of(context).textTheme.bodyText2, 308 | ), 309 | ], 310 | ), 311 | ), 312 | ], 313 | ), 314 | ), 315 | Container( 316 | margin: const EdgeInsets.symmetric(horizontal: 24.0), 317 | child: Divider( 318 | color: Theme.of(context).colorScheme.primaryVariant, 319 | thickness: 0.8, 320 | ), 321 | ), 322 | ], 323 | ); 324 | }, 325 | ) 326 | ], 327 | ), 328 | ); 329 | } 330 | 331 | Widget _buildNewDetail({ 332 | required FetchResultState state, 333 | required DetailProvider provider, 334 | }) { 335 | switch (provider.fetchDetailState) { 336 | case FetchResultState.loading: 337 | return const Center( 338 | child: SpinKitFadingCircle( 339 | color: customBlue, 340 | ), 341 | ); 342 | case FetchResultState.noData: 343 | return Center( 344 | child: AnimationPlaceholder( 345 | animation: 'assets/not-found.json', 346 | text: 'Ops! Sepertinya restoran tidak tersedia', 347 | hasButton: true, 348 | buttonText: 'Refresh', 349 | onButtonTap: () => provider.fetchRestaurantDetail(widget.id), 350 | ), 351 | ); 352 | case FetchResultState.hasData: 353 | final restaurant = provider.restaurant!.item!; 354 | return ListView( 355 | children: [ 356 | Container( 357 | margin: const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 32.0), 358 | child: ClipRRect( 359 | borderRadius: BorderRadius.circular(15.0), 360 | child: CachedNetworkImage( 361 | imageUrl: Urls.largeRestaurantImage( 362 | restaurant.pictureId!, 363 | ), 364 | height: 200.0, 365 | width: MediaQuery.of(context).size.width, 366 | fit: BoxFit.cover, 367 | progressIndicatorBuilder: (context, url, download) { 368 | return Padding( 369 | padding: const EdgeInsets.all(50.0), 370 | child: FittedBox( 371 | child: CircularProgressIndicator( 372 | value: download.progress, 373 | strokeWidth: 1.5, 374 | ), 375 | ), 376 | ); 377 | }, 378 | errorWidget: (context, url, error) { 379 | return const Padding( 380 | padding: EdgeInsets.all(24.0), 381 | child: Icon(Icons.image), 382 | ); 383 | }, 384 | ), 385 | ), 386 | ), 387 | _buildInfo(restaurant), 388 | _buildMenus( 389 | label: 'Makanan Terpopuler', 390 | menu: restaurant.menus!.foods!, 391 | ), 392 | _buildMenus( 393 | label: 'Minuman Terpopuler', 394 | menu: restaurant.menus!.drinks!, 395 | ), 396 | _buildReview(restaurant.customerReviews!), 397 | 398 | /// Button Review 399 | Container( 400 | width: MediaQuery.of(context).size.width, 401 | margin: const EdgeInsets.symmetric( 402 | horizontal: 24.0, 403 | vertical: 14.0, 404 | ), 405 | child: ElevatedButton( 406 | child: Text( 407 | 'Tulis Review', 408 | style: TextStyles.kMediumTitle.copyWith( 409 | color: Colors.white, 410 | ), 411 | ), 412 | style: ElevatedButton.styleFrom( 413 | primary: customBlue, 414 | padding: const EdgeInsets.symmetric( 415 | horizontal: 16.0, 416 | vertical: 14.0, 417 | ), 418 | shape: RoundedRectangleBorder( 419 | borderRadius: BorderRadius.circular(10.0), 420 | ), 421 | ), 422 | onPressed: () => Navigation.intentWithData( 423 | ReviewScreen.routeName, 424 | arguments: restaurant.id!, 425 | ), 426 | ), 427 | ), 428 | ], 429 | ); 430 | case FetchResultState.failure: 431 | return Center( 432 | child: AnimationPlaceholder( 433 | animation: 'assets/no-internet.json', 434 | text: 'Ops! Sepertinya koneksi internetmu dalam masalah', 435 | hasButton: true, 436 | buttonText: 'Refresh', 437 | onButtonTap: () => provider.fetchRestaurantDetail(widget.id), 438 | ), 439 | ); 440 | default: 441 | return const SizedBox(); 442 | } 443 | } 444 | 445 | Widget _buildFavoriteButton({ 446 | required FetchResultState state, 447 | required DetailProvider provider, 448 | }) { 449 | if (state == FetchResultState.hasData) { 450 | final _restaurant = provider.restaurant!.item!; 451 | return Consumer(builder: (context, provider, _) { 452 | return Consumer( 453 | builder: (context, provider, _) { 454 | return FutureBuilder( 455 | future: provider.isFavorite(_restaurant.id!), 456 | builder: (context, snapshot) { 457 | var isFavorite = snapshot.data ?? false; 458 | return IconButton( 459 | splashRadius: 24.0, 460 | splashColor: Colors.grey[200], 461 | padding: EdgeInsets.zero, 462 | icon: const Icon(Icons.favorite), 463 | color: isFavorite ? Colors.redAccent : Colors.grey[300], 464 | onPressed: () => !isFavorite 465 | ? provider.addFavorite(_restaurant) 466 | : provider.deleteFavoriteById(_restaurant.id!), 467 | ); 468 | }, 469 | ); 470 | }, 471 | ); 472 | }); 473 | } 474 | return IconButton( 475 | splashRadius: 24.0, 476 | splashColor: Colors.grey[200], 477 | padding: EdgeInsets.zero, 478 | icon: const Icon(Icons.favorite), 479 | color: Colors.grey[300], 480 | onPressed: () {}, 481 | ); 482 | } 483 | 484 | @override 485 | Widget build(BuildContext context) { 486 | _colorTween = ColorTween( 487 | begin: Theme.of(context).scaffoldBackgroundColor, 488 | end: Theme.of(context).appBarTheme.backgroundColor, 489 | ).animate(_colorAnimationController); 490 | 491 | Provider.of( 492 | context, 493 | listen: false, 494 | ).fetchRestaurantDetail(widget.id); 495 | 496 | return Consumer( 497 | builder: (context, provider, _) { 498 | return Scaffold( 499 | appBar: PreferredSize( 500 | preferredSize: const Size.fromHeight(96.0), 501 | child: AnimatedBuilder( 502 | animation: _colorAnimationController, 503 | builder: (context, child) { 504 | return AppBar( 505 | elevation: 0.0, 506 | titleSpacing: 24.0, 507 | centerTitle: false, 508 | automaticallyImplyLeading: false, 509 | backgroundColor: _colorTween.value, 510 | toolbarHeight: 96.0, 511 | title: Row( 512 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 513 | children: [ 514 | CircleAvatar( 515 | backgroundColor: Theme.of(context) 516 | .colorScheme 517 | .primaryVariant 518 | .withOpacity(0.6), 519 | radius: 24.0, 520 | child: IconButton( 521 | splashRadius: 4.0, 522 | padding: EdgeInsets.zero, 523 | icon: const Icon(Icons.arrow_back), 524 | color: Theme.of(context).primaryIconTheme.color, 525 | onPressed: () => Navigation.back(), 526 | ), 527 | ), 528 | Text( 529 | 'Detail', 530 | style: Theme.of(context).appBarTheme.toolbarTextStyle, 531 | ), 532 | _buildFavoriteButton( 533 | state: provider.fetchDetailState!, 534 | provider: provider, 535 | ), 536 | ], 537 | ), 538 | ); 539 | }, 540 | ), 541 | ), 542 | body: SafeArea( 543 | child: NotificationListener( 544 | onNotification: _scrollListener, 545 | child: FadeInUp( 546 | from: 20.0, 547 | duration: const Duration(milliseconds: 500), 548 | child: _buildNewDetail( 549 | state: provider.fetchDetailState!, 550 | provider: provider, 551 | ), 552 | ), 553 | ), 554 | ), 555 | ); 556 | }, 557 | ); 558 | } 559 | } 560 | -------------------------------------------------------------------------------- /assets/no-internet.json: -------------------------------------------------------------------------------- 1 | {"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":150.000006109625,"w":500,"h":500,"nm":"edited","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"supsbot-3 Outlines","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[142.501,220.943,0],"to":[0.5,4,0],"ti":[-0.5,-4,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":40,"s":[145.501,244.943,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":109,"s":[145.501,244.943,0],"to":[-0.5,-4,0],"ti":[0.5,4,0]},{"t":129.000005254278,"s":[142.501,220.943,0]}],"ix":2},"a":{"a":0,"k":[3.507,1.913,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[120,120,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":40,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":109,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":119,"s":[120,120,100]},{"t":129.000005254278,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.304,-2.245],[0,0],[-2.153,0.291],[0.304,2.245],[0,0],[2.154,-0.291]],"o":[[0,0],[0.304,2.244],[2.154,-0.291],[0,0],[-0.304,-2.246],[-2.153,0.291]],"v":[[-4.954,-7.28],[-2.843,8.335],[1.605,11.872],[4.953,7.281],[2.842,-8.333],[-1.606,-11.872]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.999999820485,0.999999760646,0.999999820485,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[5.507,12.414],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":-2.00000008146167,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"supsbot-2 Outlines","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[110.045,195.882,0],"to":[-4.667,1.917,0],"ti":[4.667,-1.917,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":40,"s":[82.045,207.382,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":109,"s":[82.045,207.382,0],"to":[4.667,-1.917,0],"ti":[-4.667,1.917,0]},{"t":129.000005254278,"s":[110.045,195.882,0]}],"ix":2},"a":{"a":0,"k":[22.276,4.119,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[120,120,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":40,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":109,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":119,"s":[120,120,100]},{"t":129.000005254278,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.98,-1.103],[0,0],[-1.057,-1.899],[-1.979,1.101],[0,0],[1.057,1.9]],"o":[[0,0],[-1.98,1.102],[1.057,1.899],[0,0],[1.98,-1.102],[-1.057,-1.898]],"v":[[4.971,-7.267],[-8.797,0.394],[-10.469,5.826],[-4.972,7.269],[8.796,-0.393],[10.469,-5.826]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.999999820485,0.999999760646,0.999999820485,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[11.776,8.619],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"supsbot-1 Outlines","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[116.723,222.444,0],"to":[-1.75,2.583,0],"ti":[1.75,-2.583,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":40,"s":[106.223,237.944,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":109,"s":[106.223,237.944,0],"to":[1.75,-2.583,0],"ti":[-1.75,2.583,0]},{"t":129.000005254278,"s":[116.723,222.444,0]}],"ix":2},"a":{"a":0,"k":[21.324,3.698,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[120,120,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":40,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":109,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":119,"s":[120,120,100]},{"t":129.000005254278,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.04,-2.974],[0,0],[-1.791,-1.228],[-2.038,2.973],[0,0],[1.793,1.23]],"o":[[0,0],[-2.039,2.973],[1.793,1.23],[0,0],[2.04,-2.973],[-1.791,-1.229]],"v":[[3.846,-12.56],[-10.335,8.111],[-10.784,15.718],[-3.848,12.561],[10.333,-8.11],[10.782,-15.719]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.999999820485,0.999999760646,0.999999820485,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[12.824,17.198],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"supstop-2 Outlines","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[158.304,86.482,0],"to":[-0.792,-4.625,0],"ti":[0.792,4.625,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":40,"s":[153.554,58.732,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":109,"s":[153.554,58.732,0],"to":[0.792,4.625,0],"ti":[-0.792,-4.625,0]},{"t":129.000005254278,"s":[158.304,86.482,0]}],"ix":2},"a":{"a":0,"k":[7.507,25.414,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[120,120,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":40,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":109,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":119,"s":[120,120,100]},{"t":129.000005254278,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.304,2.246],[0,0],[2.153,-0.291],[-0.304,-2.245],[0,0],[-2.154,0.292]],"o":[[0,0],[-0.304,-2.245],[-2.154,0.291],[0,0],[0.304,2.246],[2.153,-0.291]],"v":[[4.953,7.279],[2.842,-8.334],[-1.605,-11.873],[-4.953,-7.28],[-2.842,8.333],[1.606,11.872]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.999999820485,0.999999760646,0.999999820485,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[5.507,12.414],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"supstop-3 Outlines","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[182.26,103.044,0],"to":[6.25,-2.208,0],"ti":[-6.25,2.208,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":40,"s":[219.76,89.794,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":109,"s":[219.76,89.794,0],"to":[-6.25,2.208,0],"ti":[6.25,-2.208,0]},{"t":129.000005254278,"s":[182.26,103.044,0]}],"ix":2},"a":{"a":0,"k":[3.025,13.119,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[120,120,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":40,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":109,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":119,"s":[120,120,100]},{"t":129.000005254278,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.98,1.102],[0,0],[1.056,1.898],[1.98,-1.101],[0,0],[-1.057,-1.9]],"o":[[0,0],[1.979,-1.101],[-1.057,-1.899],[0,0],[-1.981,1.102],[1.056,1.898]],"v":[[-4.97,7.267],[8.798,-0.394],[10.469,-5.825],[4.972,-7.268],[-8.795,0.393],[-10.468,5.826]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.999999820485,0.999999760646,0.999999820485,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[11.775,8.619],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"supstop-1 Outlines","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[173.082,90.482,0],"to":[2.958,-4.625,0],"ti":[-2.958,4.625,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":40,"s":[190.832,62.732,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":109,"s":[190.832,62.732,0],"to":[-2.958,4.625,0],"ti":[2.958,-4.625,0]},{"t":129.000005254278,"s":[173.082,90.482,0]}],"ix":2},"a":{"a":0,"k":[0.825,34.198,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[120,120,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":40,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":109,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":119,"s":[120,120,100]},{"t":129.000005254278,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.04,2.974],[0,0],[1.791,1.229],[2.039,-2.972],[0,0],[-1.792,-1.229]],"o":[[0,0],[2.038,-2.972],[-1.793,-1.229],[0,0],[-2.04,2.973],[1.792,1.229]],"v":[[-3.845,12.559],[10.335,-8.111],[10.784,-15.718],[3.846,-12.561],[-10.333,8.11],[-10.783,15.719]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.999999820485,0.999999760646,0.999999820485,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[12.824,17.197],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"cableout Outlines","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[-25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":119,"s":[-25]},{"t":134.000005457932,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[343.49,110.041,0],"to":[0.667,-12,0],"ti":[-0.667,12,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":30,"s":[347.49,38.041,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":119,"s":[347.49,38.041,0],"to":[-0.667,12,0],"ti":[0.667,-12,0]},{"t":134.000005457932,"s":[343.49,110.041,0]}],"ix":2},"a":{"a":0,"k":[216.825,0.549,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.953,-2.065],[-23.354,4.127],[-13.592,19.428],[4.128,23.349],[1.369,0.958],[1.774,-0.313],[-0.626,-3.549],[11.592,-16.569],[19.915,-3.521],[16.573,11.593],[2.067,-2.956]],"o":[[19.428,13.592],[23.349,-4.127],[13.589,-19.425],[-0.313,-1.776],[-1.368,-0.957],[-3.549,0.627],[3.519,19.918],[-11.594,16.572],[-19.918,3.522],[-2.953,-2.066],[-2.067,2.953]],"v":[[-69.376,37.494],[-3.039,52.172],[54.246,15.642],[68.922,-50.692],[66.236,-54.906],[61.359,-55.986],[56.066,-48.423],[43.551,8.159],[-5.311,39.316],[-61.895,26.798],[-70.984,28.406]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.700047571519,0.700047451842,0.700047571519,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[148.35,56.549],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.954,-2.066],[-2.067,2.953],[0,0],[0,0],[-1.706,0.301],[0,0],[-0.993,1.419],[0,0],[0.302,1.704],[0,0],[1.418,0.992],[0,0],[0,0],[2.955,2.067],[2.067,-2.953],[0,0]],"o":[[2.955,2.067],[0,0],[0,0],[1.418,0.992],[0,0],[1.704,-0.302],[0,0],[0.991,-1.417],[0,0],[-0.303,-1.704],[0,0],[0,0],[2.066,-2.953],[-2.954,-2.067],[0,0],[-2.067,2.954]],"v":[[-37.71,28.827],[-28.62,27.221],[-27.142,25.107],[-2.646,42.245],[2.233,43.323],[14.914,41.081],[19.126,38.394],[40.003,8.551],[41.081,3.675],[38.841,-9.007],[36.155,-13.22],[11.659,-30.355],[13.137,-32.469],[11.529,-41.557],[2.44,-39.952],[-39.315,19.736]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.700047571519,0.700047451842,0.700047571519,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.632,58.916],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"cablein-2 Outlines","parent":10,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[210.763,32.92,0],"ix":2},"a":{"a":0,"k":[19.601,10.671,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-3.487,-0.918],[0,0],[-0.918,3.485],[3.486,0.917],[0,0],[0.918,-3.488]],"o":[[0,0],[3.486,0.918],[0.918,-3.489],[0,0],[-3.487,-0.918],[-0.918,3.486]],"v":[[-13.781,3.121],[10.46,9.503],[18.433,4.854],[13.783,-3.12],[-10.458,-9.503],[-18.433,-4.852]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.700047571519,0.700047451842,0.700047571519,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[19.601,10.671],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"cablein-1 Outlines","parent":10,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[201.491,68.14,0],"ix":2},"a":{"a":0,"k":[19.601,10.671,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.486,0.917],[0,0],[0.918,-3.489],[-3.487,-0.918],[0,0],[-0.918,3.485]],"o":[[0,0],[-3.487,-0.918],[-0.918,3.485],[0,0],[3.485,0.917],[0.918,-3.489]],"v":[[13.783,-3.121],[-10.458,-9.503],[-18.433,-4.85],[-13.781,3.122],[10.46,9.504],[18.433,4.855]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.700047571519,0.700047451842,0.700047571519,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[19.601,10.671],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"cablein Outlines","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[18]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[-2]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":119,"s":[-2]},{"t":134.000005457932,"s":[18]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[-55.11,200.146,0],"to":[-1.5,7.167,0],"ti":[1.5,-7.167,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":30,"s":[-64.11,243.146,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":119,"s":[-64.11,243.146,0],"to":[1.5,-7.167,0],"ti":[-1.5,7.167,0]},{"t":134.000005457932,"s":[-55.11,200.146,0]}],"ix":2},"a":{"a":0,"k":[16.574,149.734,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.485,0.917],[20.487,-11.947],[6.036,-22.928],[-11.945,-20.481],[-1.617,-0.426],[-1.555,0.907],[1.815,3.114],[-5.148,19.556],[-17.469,10.19],[-19.558,-5.149],[-0.918,3.488]],"o":[[-22.929,-6.036],[-20.482,11.945],[-6.035,22.926],[0.908,1.559],[1.615,0.425],[3.114,-1.816],[-10.188,-17.473],[5.149,-19.558],[17.473,-10.191],[3.485,0.917],[0.918,-3.486]],"v":[[54.456,-63.989],[-12.867,-54.825],[-53.988,-0.743],[-44.826,66.573],[-40.848,69.599],[-35.9,68.927],[-33.548,60],[-41.365,2.579],[-6.29,-43.549],[51.132,-51.364],[59.105,-56.016]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.700047571519,0.700047451842,0.700047571519,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[60.274,85.193],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.487,0.918],[0.918,-3.486],[0,0],[0,0],[1.496,-0.871],[0,0],[0.441,-1.675],[0,0],[-0.872,-1.495],[0,0],[-1.674,-0.441],[0,0],[0,0],[-3.487,-0.918],[-0.918,3.486],[0,0]],"o":[[-3.487,-0.918],[0,0],[0,0],[-1.673,-0.441],[0,0],[-1.494,0.872],[0,0],[-0.44,1.672],[0,0],[0.874,1.495],[0,0],[0,0],[-0.918,3.487],[3.487,0.918],[0,0],[0.917,-3.486]],"v":[[30.392,-41.534],[22.417,-36.882],[21.762,-34.389],[-7.149,-42],[-12.1,-41.326],[-23.223,-34.837],[-26.246,-30.86],[-35.518,4.361],[-34.844,9.308],[-28.358,20.433],[-24.38,23.458],[4.53,31.068],[3.872,33.561],[8.523,41.534],[16.497,36.885],[35.042,-33.559]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.700047571519,0.700047451842,0.700047571519,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[156.939,42.702],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"roundbg Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[149.975,149.975,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":17,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":132,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":139,"s":[110,110,100]},{"t":149.000006068894,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-82.691],[82.691,0],[0,82.69],[-82.691,0]],"o":[[0,82.69],[-82.691,0],[0,-82.691],[82.691,0]],"v":[[149.725,0],[0,149.725],[-149.725,0],[0,-149.725]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.949442485735,0.949442366058,0.949442485735,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[149.975,149.975],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0}],"markers":[]} --------------------------------------------------------------------------------