├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.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 ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── manifest.json └── index.html ├── assets ├── fonts │ ├── bold.ttf │ └── regular.ttf └── img │ ├── nike_logo.png │ └── no_data.svg ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── 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 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── nike_ecommerce │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── lib ├── common │ ├── constants.dart │ ├── exceptions.dart │ ├── http_response_validator.dart │ ├── utils.dart │ └── http_client.dart ├── data │ ├── auth_info.dart │ ├── banner.dart │ ├── comment.dart │ ├── add_to_cart_response.dart │ ├── payment_receipt.dart │ ├── cart_response.dart │ ├── repo │ │ ├── banner_repository.dart │ │ ├── comment_repository.dart │ │ ├── product_repository.dart │ │ ├── order_repository.dart │ │ ├── cart_repository.dart │ │ └── auth_repository.dart │ ├── cart_item.dart │ ├── source │ │ ├── banner_data_source.dart │ │ ├── comment_data_source.dart │ │ ├── product_data_source.dart │ │ ├── order_data_source.dart │ │ ├── auth_data_source.dart │ │ └── cart_data_source.dart │ ├── favorite_manager.dart │ ├── product.dart │ ├── order.dart │ └── product.g.dart ├── ui │ ├── order │ │ ├── bloc │ │ │ ├── order_history_event.dart │ │ │ ├── order_history_state.dart │ │ │ └── order_history_bloc.dart │ │ └── order_history.dart │ ├── product │ │ ├── comment │ │ │ ├── bloc │ │ │ │ ├── commentlist_event.dart │ │ │ │ ├── commentlist_state.dart │ │ │ │ └── commentlist_bloc.dart │ │ │ ├── comment.dart │ │ │ └── comment_list.dart │ │ ├── bloc │ │ │ ├── product_event.dart │ │ │ ├── product_state.dart │ │ │ └── product_bloc.dart │ │ ├── product.dart │ │ └── details.dart │ ├── home │ │ ├── bloc │ │ │ ├── home_event.dart │ │ │ ├── home_state.dart │ │ │ └── home_bloc.dart │ │ └── home.dart │ ├── list │ │ └── bloc │ │ │ ├── product_list_event.dart │ │ │ ├── product_list_state.dart │ │ │ └── product_list_bloc.dart │ ├── shipping │ │ └── bloc │ │ │ ├── shipping_event.dart │ │ │ ├── shipping_state.dart │ │ │ └── shipping_bloc.dart │ ├── receipt │ │ ├── bloc │ │ │ ├── payment_receipt_event.dart │ │ │ ├── payment_receipt_state.dart │ │ │ └── payment_receipt_bloc.dart │ │ └── receipt.dart │ ├── cart │ │ ├── bloc │ │ │ ├── cart_state.dart │ │ │ ├── cart_event.dart │ │ │ └── cart_bloc.dart │ │ ├── price_info.dart │ │ └── cart_item.dart │ ├── auth │ │ └── bloc │ │ │ ├── auth_event.dart │ │ │ ├── auth_state.dart │ │ │ └── auth_bloc.dart │ ├── widgets │ │ ├── image.dart │ │ ├── badge.dart │ │ ├── error.dart │ │ ├── empty_state.dart │ │ └── slider.dart │ ├── payment_webview.dart │ ├── root.dart │ ├── favorites │ │ └── favorites.dart │ └── profile │ │ └── profile.dart ├── theme.dart ├── main.dart └── questions.dart ├── windows ├── runner │ ├── resources │ │ └── app_icon.ico │ ├── resource.h │ ├── CMakeLists.txt │ ├── utils.h │ ├── runner.exe.manifest │ ├── flutter_window.h │ ├── main.cpp │ ├── utils.cpp │ ├── flutter_window.cpp │ ├── Runner.rc │ ├── win32_window.h │ └── win32_window.cpp ├── flutter │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ ├── generated_plugins.cmake │ └── CMakeLists.txt ├── .gitignore └── CMakeLists.txt ├── .metadata ├── README.md ├── .gitignore ├── test └── widget_test.dart ├── analysis_options.yaml └── pubspec.yaml /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/web/favicon.png -------------------------------------------------------------------------------- /assets/fonts/bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/assets/fonts/bold.ttf -------------------------------------------------------------------------------- /assets/fonts/regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/assets/fonts/regular.ttf -------------------------------------------------------------------------------- /assets/img/nike_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/assets/img/nike_logo.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/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/FatemeSlm/nike-ecommerce/HEAD/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /lib/common/constants.dart: -------------------------------------------------------------------------------- 1 | class Constants { 2 | static const clientSceret = 'kyj1c9sVcksqGU4scMX7nLDalkjp2WoqQEf8PKAC'; 3 | } -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /lib/common/exceptions.dart: -------------------------------------------------------------------------------- 1 | class AppException { 2 | final String message; 3 | 4 | AppException({this.message = 'خطای نامشخص'}); 5 | } 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/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/FatemeSlm/nike-ecommerce/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/FatemeSlm/nike-ecommerce/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/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/FatemeSlm/nike-ecommerce/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatemeSlm/nike-ecommerce/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /lib/data/auth_info.dart: -------------------------------------------------------------------------------- 1 | class AuthInfo { 2 | final String accessToken; 3 | final String refreshToken; 4 | final String email; 5 | 6 | AuthInfo(this.accessToken, this.refreshToken, this.email); 7 | } -------------------------------------------------------------------------------- /lib/data/banner.dart: -------------------------------------------------------------------------------- 1 | class BannerEntity { 2 | final int id; 3 | final String image; 4 | 5 | BannerEntity.fromJsoin(Map json): 6 | id = json['id'], 7 | image = json['image']; 8 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/nike_ecommerce/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.nike_ecommerce 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 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void RegisterPlugins(flutter::PluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/ui/order/bloc/order_history_event.dart: -------------------------------------------------------------------------------- 1 | part of 'order_history_bloc.dart'; 2 | 3 | abstract class OrderHistoryEvent extends Equatable { 4 | const OrderHistoryEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class OrderHistoryStarted extends OrderHistoryEvent{} 11 | -------------------------------------------------------------------------------- /lib/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | class LightThemeColors{ 4 | static const primaryColor = Color(0xff217CF3); 5 | static const secondryColor = Color(0xff262A35); 6 | static const primaryTextColor = Color(0xff262A35); 7 | static const secondryTextColor = Color(0xffB3B6BE); 8 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/ui/product/comment/bloc/commentlist_event.dart: -------------------------------------------------------------------------------- 1 | part of 'commentlist_bloc.dart'; 2 | 3 | abstract class CommentListEvent extends Equatable { 4 | const CommentListEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | 11 | class CommentListStarted extends CommentListEvent{ 12 | 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/ui/home/bloc/home_event.dart: -------------------------------------------------------------------------------- 1 | part of 'home_bloc.dart'; 2 | 3 | abstract class HomeEvent extends Equatable { 4 | const HomeEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class HomeStarted extends HomeEvent{ 11 | 12 | } 13 | 14 | class HomeRefresh extends HomeEvent{ 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/common/http_response_validator.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:nike_ecommerce/common/exceptions.dart'; 3 | 4 | mixin HttpResponseValidation { 5 | validateResposnse(Response response) { 6 | if (response.statusCode != 200) { 7 | throw AppException(message: response.data['message']); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.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: c860cba910319332564e1e9d470a17074c1f2dfd 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 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/data/comment.dart: -------------------------------------------------------------------------------- 1 | class CommentEntity { 2 | final int id; 3 | final String title; 4 | final String content; 5 | final String date; 6 | final String email; 7 | 8 | CommentEntity.formJson (Map json ): 9 | id = json['id'], 10 | title = json['title'], 11 | content = json['content'], 12 | date = json['date'], 13 | email = json['author']['email']; 14 | } -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /lib/data/add_to_cart_response.dart: -------------------------------------------------------------------------------- 1 | class AddToCartResponse { 2 | final int productId; 3 | final int cartItemId; 4 | final int count; 5 | 6 | AddToCartResponse(this.productId, this.cartItemId, this.count); 7 | 8 | AddToCartResponse.fromJson(Map json) 9 | : productId = json['product_id'], 10 | cartItemId = json['id'], 11 | count = json['count']; 12 | } 13 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /lib/ui/product/bloc/product_event.dart: -------------------------------------------------------------------------------- 1 | part of 'product_bloc.dart'; 2 | 3 | abstract class ProductEvent extends Equatable { 4 | const ProductEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class CartAddButtonClick extends ProductEvent { 11 | final int productId; 12 | 13 | const CartAddButtonClick(this.productId); 14 | 15 | @override 16 | List get props => [productId]; 17 | } 18 | -------------------------------------------------------------------------------- /lib/ui/list/bloc/product_list_event.dart: -------------------------------------------------------------------------------- 1 | part of 'product_list_bloc.dart'; 2 | 3 | abstract class ProductListEvent extends Equatable { 4 | const ProductListEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class ProductListStarted extends ProductListEvent { 11 | final int sort; 12 | 13 | const ProductListStarted(this.sort); 14 | 15 | @override 16 | List get props => [sort]; 17 | } 18 | -------------------------------------------------------------------------------- /lib/ui/shipping/bloc/shipping_event.dart: -------------------------------------------------------------------------------- 1 | part of 'shipping_bloc.dart'; 2 | 3 | abstract class ShippingEvent extends Equatable { 4 | const ShippingEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class ShippingCreateOrder extends ShippingEvent { 11 | final CreateOrderParams params; 12 | 13 | const ShippingCreateOrder(this.params); 14 | 15 | @override 16 | List get props => [params]; 17 | } 18 | -------------------------------------------------------------------------------- /lib/data/payment_receipt.dart: -------------------------------------------------------------------------------- 1 | class PaymentReceipt { 2 | final bool purchaseSuccess; 3 | final int payablePrice; 4 | final String paymentStatus; 5 | 6 | PaymentReceipt(this.purchaseSuccess, this.payablePrice, this.paymentStatus); 7 | 8 | PaymentReceipt.fromJson(Map json) 9 | : purchaseSuccess = json['purchase_success'], 10 | payablePrice = json['payable_price'], 11 | paymentStatus = json['payment_status']; 12 | } 13 | -------------------------------------------------------------------------------- /lib/ui/receipt/bloc/payment_receipt_event.dart: -------------------------------------------------------------------------------- 1 | part of 'payment_receipt_bloc.dart'; 2 | 3 | abstract class PaymentReceiptEvent extends Equatable { 4 | const PaymentReceiptEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class PaymentReceiptStarted extends PaymentReceiptEvent { 11 | final int orderId; 12 | 13 | const PaymentReceiptStarted(this.orderId); 14 | 15 | @override 16 | List get props => [orderId]; 17 | } 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/common/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | const defaultScrollPhsics = BouncingScrollPhysics(); 6 | 7 | extension PriceLabel on int { 8 | String get withPriceLabel => this > 0 ? '$separateByComma تومان' : 'رایگان'; 9 | 10 | String get separateByComma { 11 | final numberFormat = NumberFormat.decimalPattern(); 12 | return numberFormat.format(this); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/data/cart_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:nike_ecommerce/data/cart_item.dart'; 2 | 3 | class CartResponse { 4 | final List cartItems; 5 | int payablePrice; 6 | int totalPrice; 7 | int shippingCost; 8 | 9 | CartResponse.fromJson(Map json) 10 | : cartItems = CartItemEntity.parseJsonArray(json['cart_items']), 11 | payablePrice = json['payable_price'], 12 | totalPrice = json['total_price'], 13 | shippingCost = json['shipping_cost']; 14 | } 15 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/ui/cart/bloc/cart_state.dart: -------------------------------------------------------------------------------- 1 | part of 'cart_bloc.dart'; 2 | 3 | abstract class CartState { 4 | const CartState(); 5 | } 6 | 7 | class CartLoading extends CartState {} 8 | 9 | class CartSuccess extends CartState { 10 | final CartResponse cartResponse; 11 | 12 | const CartSuccess(this.cartResponse); 13 | } 14 | 15 | class CartError extends CartState { 16 | final AppException appException; 17 | 18 | const CartError(this.appException); 19 | } 20 | 21 | class CartAuthRequired extends CartState {} 22 | 23 | class CartEmpty extends CartState {} 24 | -------------------------------------------------------------------------------- /lib/ui/auth/bloc/auth_event.dart: -------------------------------------------------------------------------------- 1 | part of 'auth_bloc.dart'; 2 | 3 | abstract class AuthEvent extends Equatable { 4 | const AuthEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class AuthStarted extends AuthEvent {} 11 | 12 | class AuthButtonClicked extends AuthEvent { 13 | final String username; 14 | final String password; 15 | 16 | const AuthButtonClicked(this.username, this.password); 17 | 18 | @override 19 | List get props => [username, password]; 20 | } 21 | 22 | class AuthModeChangedClicked extends AuthEvent {} 23 | -------------------------------------------------------------------------------- /lib/data/repo/banner_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:nike_ecommerce/common/http_client.dart'; 2 | import 'package:nike_ecommerce/data/banner.dart'; 3 | import 'package:nike_ecommerce/data/source/banner_data_source.dart'; 4 | 5 | final bannerRepository = BannerRepository(BannerRemoteDataSource(httpClient)); 6 | 7 | abstract class IBannerREpository { 8 | Future> getAll(); 9 | } 10 | 11 | class BannerRepository implements IBannerREpository { 12 | final IBannerDataSource dataSource; 13 | 14 | BannerRepository(this.dataSource); 15 | 16 | @override 17 | Future> getAll() => dataSource.getAll(); 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nike_ecommerce 2 | 3 | A new Flutter project with a clean repository and bloc pattern. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | add_executable(${BINARY_NAME} WIN32 5 | "flutter_window.cpp" 6 | "main.cpp" 7 | "utils.cpp" 8 | "win32_window.cpp" 9 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 10 | "Runner.rc" 11 | "runner.exe.manifest" 12 | ) 13 | apply_standard_settings(${BINARY_NAME}) 14 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 15 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 16 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 17 | add_dependencies(${BINARY_NAME} flutter_assemble) 18 | -------------------------------------------------------------------------------- /lib/common/http_client.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:nike_ecommerce/data/repo/auth_repository.dart'; 3 | 4 | final httpClient = 5 | Dio(BaseOptions(baseUrl: 'http://expertdevelopers.ir/api/v1/')) 6 | ..interceptors.add(InterceptorsWrapper( 7 | onRequest: (options, handler) { 8 | { 9 | final authInfo = AuthRepository.authChangeNotifire.value; 10 | 11 | if (authInfo != null && authInfo.accessToken.isNotEmpty) { 12 | options.headers['Authorization'] = 13 | 'Bearer ${authInfo.accessToken}'; 14 | } 15 | 16 | handler.next(options); 17 | } 18 | }, 19 | )); 20 | -------------------------------------------------------------------------------- /lib/data/repo/comment_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:nike_ecommerce/common/http_client.dart'; 2 | import 'package:nike_ecommerce/data/comment.dart'; 3 | import 'package:nike_ecommerce/data/source/comment_data_source.dart'; 4 | 5 | final commentRepository = CommentRepository(CommentRemoteDataSource(httpClient)); 6 | 7 | abstract class ICommentRepository { 8 | Future> getAll({required int productId}); 9 | } 10 | 11 | class CommentRepository implements ICommentRepository { 12 | final ICommentDataSource dataSource; 13 | 14 | CommentRepository(this.dataSource); 15 | 16 | @override 17 | Future> getAll({required int productId}) => 18 | dataSource.getAll(productId); 19 | } 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/data/cart_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:nike_ecommerce/data/product.dart'; 2 | 3 | class CartItemEntity { 4 | final int id; 5 | final Product product; 6 | int count; 7 | bool deleteButtonLaoding = false; 8 | bool changeCountLoading = false; 9 | 10 | CartItemEntity.fromJson(Map json) 11 | : product = Product.fromJson(json['product']), 12 | id = json['cart_item_id'], 13 | count = json['count']; 14 | 15 | static List parseJsonArray(List jsonArrray) { 16 | final List cartItems = []; 17 | jsonArrray.forEach((element) { 18 | cartItems.add(CartItemEntity.fromJson(element)); 19 | }); 20 | 21 | return cartItems; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 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 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /lib/ui/widgets/image.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ImageLoadingService extends StatelessWidget { 5 | final String imageUrl; 6 | final BorderRadius? borderRadius; 7 | const ImageLoadingService( 8 | {Key? key, required this.imageUrl, this.borderRadius}) 9 | : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final image = CachedNetworkImage( 14 | imageUrl: imageUrl, 15 | fit: BoxFit.cover, 16 | ); 17 | 18 | if (borderRadius != null) { 19 | return ClipRRect(borderRadius: borderRadius, child: image); 20 | } else { 21 | return image; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/ui/product/comment/bloc/commentlist_state.dart: -------------------------------------------------------------------------------- 1 | part of 'commentlist_bloc.dart'; 2 | 3 | abstract class CommentListState extends Equatable { 4 | const CommentListState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class CommentListLoading extends CommentListState {} 11 | 12 | class CommentListSuccess extends CommentListState { 13 | final List comments; 14 | 15 | const CommentListSuccess(this.comments); 16 | 17 | @override 18 | List get props => [comments]; 19 | } 20 | 21 | class CommentListerror extends CommentListState { 22 | final AppException exception; 23 | 24 | const CommentListerror(this.exception); 25 | 26 | @override 27 | List get props => [exception]; 28 | } 29 | -------------------------------------------------------------------------------- /lib/ui/order/bloc/order_history_state.dart: -------------------------------------------------------------------------------- 1 | part of 'order_history_bloc.dart'; 2 | 3 | abstract class OrderHistoryState extends Equatable { 4 | const OrderHistoryState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class OrderHistoryLoading extends OrderHistoryState {} 11 | 12 | class OrderHistorySuccess extends OrderHistoryState { 13 | final List orderList; 14 | 15 | const OrderHistorySuccess(this.orderList); 16 | 17 | @override 18 | List get props => [orderList]; 19 | } 20 | 21 | class OrderHistoryError extends OrderHistoryState { 22 | final AppException appException; 23 | 24 | const OrderHistoryError(this.appException); 25 | 26 | @override 27 | List get props => [appException]; 28 | } 29 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /lib/ui/shipping/bloc/shipping_state.dart: -------------------------------------------------------------------------------- 1 | part of 'shipping_bloc.dart'; 2 | 3 | abstract class ShippingState extends Equatable { 4 | const ShippingState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class ShippingInitial extends ShippingState {} 11 | 12 | class ShippingLoading extends ShippingState {} 13 | 14 | class ShippingError extends ShippingState { 15 | final AppException appException; 16 | 17 | const ShippingError(this.appException); 18 | 19 | @override 20 | List get props => [appException]; 21 | } 22 | 23 | class ShippingSuccess extends ShippingState { 24 | final CreateOrderResponse response; 25 | 26 | const ShippingSuccess(this.response); 27 | 28 | @override 29 | List get props => [response]; 30 | } 31 | -------------------------------------------------------------------------------- /lib/ui/home/bloc/home_state.dart: -------------------------------------------------------------------------------- 1 | part of 'home_bloc.dart'; 2 | 3 | abstract class HomeState extends Equatable { 4 | const HomeState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class HomeLoading extends HomeState {} 11 | 12 | class HomeError extends HomeState { 13 | final AppException exception; 14 | 15 | const HomeError({required this.exception}); 16 | 17 | @override 18 | List get props => [exception]; 19 | } 20 | 21 | class HomeSuccess extends HomeState { 22 | final List banners; 23 | final List latestProducts; 24 | final List popularProducts; 25 | 26 | const HomeSuccess( 27 | {required this.banners, 28 | required this.latestProducts, 29 | required this.popularProducts}); 30 | } 31 | -------------------------------------------------------------------------------- /lib/ui/product/bloc/product_state.dart: -------------------------------------------------------------------------------- 1 | part of 'product_bloc.dart'; 2 | 3 | abstract class ProductState extends Equatable { 4 | const ProductState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class ProductInitial extends ProductState {} 11 | 12 | class ProductAddToCartLoading extends ProductState {} 13 | 14 | class ProductAddToCartError extends ProductState { 15 | final AppException exception; 16 | 17 | const ProductAddToCartError(this.exception); 18 | 19 | @override 20 | List get props => [exception]; 21 | } 22 | 23 | class ProductAddToCartSuccess extends ProductState { 24 | final AddToCartResponse cartResponse; 25 | 26 | const ProductAddToCartSuccess(this.cartResponse); 27 | 28 | @override 29 | List get props => [cartResponse]; 30 | } 31 | -------------------------------------------------------------------------------- /lib/ui/receipt/bloc/payment_receipt_state.dart: -------------------------------------------------------------------------------- 1 | part of 'payment_receipt_bloc.dart'; 2 | 3 | abstract class PaymentReceiptState extends Equatable { 4 | const PaymentReceiptState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class PaymentReceiptLoading extends PaymentReceiptState {} 11 | 12 | class PaymentReceiptSuccess extends PaymentReceiptState { 13 | final PaymentReceipt paymentReceipt; 14 | 15 | const PaymentReceiptSuccess(this.paymentReceipt); 16 | 17 | @override 18 | List get props => [paymentReceipt]; 19 | } 20 | 21 | class PaymentReceiptError extends PaymentReceiptState { 22 | final AppException appException; 23 | 24 | const PaymentReceiptError(this.appException); 25 | 26 | @override 27 | List get props => [appException]; 28 | } 29 | -------------------------------------------------------------------------------- /lib/data/source/banner_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:nike_ecommerce/common/http_response_validator.dart'; 2 | import '../banner.dart'; 3 | import 'package:dio/dio.dart'; 4 | 5 | abstract class IBannerDataSource { 6 | Future> getAll(); 7 | } 8 | 9 | class BannerRemoteDataSource with HttpResponseValidation implements IBannerDataSource { 10 | final Dio httpClient; 11 | 12 | BannerRemoteDataSource(this.httpClient); 13 | 14 | @override 15 | Future> getAll() async { 16 | final response = await httpClient.get('banner/slider'); 17 | validateResposnse(response); 18 | final banners = []; 19 | (response.data as List).forEach((element) { 20 | banners.add(BannerEntity.fromJsoin(element)); 21 | }); 22 | 23 | return banners; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /lib/ui/widgets/badge.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Badge extends StatelessWidget { 4 | final int value; 5 | 6 | const Badge({Key? key, required this.value}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Visibility( 11 | visible: value > 0, 12 | child: Container( 13 | width: 16, 14 | height: 16, 15 | alignment: Alignment.center, 16 | decoration: BoxDecoration( 17 | color: Theme.of(context).colorScheme.primary, 18 | shape: BoxShape.circle, 19 | ), 20 | child: Text( 21 | value.toString(), 22 | style: TextStyle( 23 | color: Theme.of(context).colorScheme.onPrimary, fontSize: 11), 24 | ), 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/ui/auth/bloc/auth_state.dart: -------------------------------------------------------------------------------- 1 | part of 'auth_bloc.dart'; 2 | 3 | abstract class AuthState extends Equatable { 4 | const AuthState(this.isLogin); 5 | 6 | final bool isLogin; 7 | 8 | @override 9 | List get props => [isLogin]; 10 | } 11 | 12 | class AuthInitial extends AuthState { 13 | const AuthInitial(bool isLogin) : super(isLogin); 14 | } 15 | 16 | class AuthError extends AuthState{ 17 | final AppException exception; 18 | const AuthError(bool isLogin, this.exception) : super(isLogin); 19 | 20 | @override 21 | 22 | List get props => [exception]; 23 | 24 | } 25 | 26 | class AuthSuccess extends AuthState{ 27 | const AuthSuccess(bool isLogin) : super(isLogin); 28 | 29 | } 30 | 31 | class AuthLoading extends AuthState{ 32 | const AuthLoading(bool isLogin) : super(isLogin); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /lib/ui/widgets/error.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nike_ecommerce/common/exceptions.dart'; 3 | 4 | class AppErrorWidget extends StatelessWidget { 5 | final AppException exception; 6 | final GestureTapCallback onPressed; 7 | const AppErrorWidget({ 8 | Key? key, required this.exception, required this.onPressed, 9 | }) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Center( 14 | child: Column( 15 | mainAxisAlignment: MainAxisAlignment.center, 16 | children: [ 17 | Text(exception.message), 18 | ElevatedButton( 19 | onPressed: () { 20 | onPressed; 21 | }, 22 | child: const Text('تلاش مجدد')) 23 | ], 24 | ), 25 | ); 26 | } 27 | } -------------------------------------------------------------------------------- /lib/data/repo/product_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:nike_ecommerce/common/http_client.dart'; 2 | import 'package:nike_ecommerce/data/product.dart'; 3 | import 'package:nike_ecommerce/data/source/product_data_source.dart'; 4 | 5 | final productRepository = ProductRepository(ProductRemoteDataSource(httpClient)); 6 | 7 | abstract class IProductRepository { 8 | Future> getAll(int sort); 9 | Future> search(String serachTerm); 10 | } 11 | 12 | class ProductRepository implements IProductRepository { 13 | final IProductDataSource dataSource; 14 | 15 | ProductRepository(this.dataSource); 16 | 17 | @override 18 | Future> getAll(int sort) => dataSource.getAll(sort); 19 | 20 | @override 21 | Future> search(String serachTerm) => 22 | dataSource.search(serachTerm); 23 | } 24 | -------------------------------------------------------------------------------- /lib/ui/list/bloc/product_list_state.dart: -------------------------------------------------------------------------------- 1 | part of 'product_list_bloc.dart'; 2 | 3 | abstract class ProductListState extends Equatable { 4 | const ProductListState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class ProductListLoading extends ProductListState {} 11 | 12 | class ProductListSuccess extends ProductListState { 13 | final List products; 14 | final int sort; 15 | final List sortNames; 16 | 17 | const ProductListSuccess(this.products, this.sort, this.sortNames); 18 | 19 | @override 20 | List get props => [products, sort, sortNames]; 21 | } 22 | 23 | class ProductListError extends ProductListState { 24 | final AppException appException; 25 | 26 | const ProductListError(this.appException); 27 | 28 | @override 29 | List get props => [appException]; 30 | } 31 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/data/source/comment_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:nike_ecommerce/common/http_response_validator.dart'; 3 | import 'package:nike_ecommerce/data/comment.dart'; 4 | 5 | abstract class ICommentDataSource { 6 | Future> getAll(int productId); 7 | } 8 | 9 | class CommentRemoteDataSource 10 | with HttpResponseValidation 11 | implements ICommentDataSource { 12 | final Dio httpClient; 13 | 14 | CommentRemoteDataSource(this.httpClient); 15 | @override 16 | Future> getAll(int productId) async { 17 | final response = await httpClient.get('comment/list?product_id=$productId'); 18 | 19 | validateResposnse(response); 20 | 21 | final List comments = []; 22 | 23 | (response.data as List).forEach((element) { 24 | comments.add(CommentEntity.formJson(element)); 25 | }); 26 | 27 | return comments; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.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/data/repo/order_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:nike_ecommerce/common/http_client.dart'; 2 | import 'package:nike_ecommerce/data/order.dart'; 3 | import 'package:nike_ecommerce/data/payment_receipt.dart'; 4 | import 'package:nike_ecommerce/data/source/order_data_source.dart'; 5 | 6 | final orderRepository = OrderRepository(OrderRemoteDataSource(httpClient)); 7 | 8 | abstract class IOrderRepository extends IOrderDataSource {} 9 | 10 | class OrderRepository implements IOrderRepository { 11 | final IOrderDataSource dataSource; 12 | 13 | OrderRepository(this.dataSource); 14 | 15 | @override 16 | Future create(CreateOrderParams params) => 17 | dataSource.create(params); 18 | 19 | @override 20 | Future getPaymentReceipt(int orderId) => 21 | dataSource.getPaymentReceipt(orderId); 22 | 23 | @override 24 | Future> getOrders() => dataSource.getOrders(); 25 | } 26 | -------------------------------------------------------------------------------- /lib/ui/shipping/bloc/shipping_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:nike_ecommerce/common/exceptions.dart'; 4 | import 'package:nike_ecommerce/data/order.dart'; 5 | import 'package:nike_ecommerce/data/repo/order_repository.dart'; 6 | 7 | part 'shipping_event.dart'; 8 | part 'shipping_state.dart'; 9 | 10 | class ShippingBloc extends Bloc { 11 | final IOrderRepository repository; 12 | 13 | ShippingBloc(this.repository) : super(ShippingInitial()) { 14 | on((event, emit) async { 15 | if (event is ShippingCreateOrder) { 16 | try { 17 | emit(ShippingLoading()); 18 | final result = await repository.create(event.params); 19 | emit(ShippingSuccess(result)); 20 | } catch (e) { 21 | emit(ShippingError(AppException())); 22 | } 23 | } 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/data/favorite_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:hive_flutter/adapters.dart'; 3 | import 'package:nike_ecommerce/data/product.dart'; 4 | 5 | final favoriteManager = FavoriteManager(); 6 | 7 | class FavoriteManager { 8 | static const _boxName = 'favorites'; 9 | final _box = Hive.box(_boxName); 10 | 11 | ValueListenable> get listenable => _box.listenable(); 12 | 13 | static init() async { 14 | await Hive.initFlutter(); 15 | Hive.registerAdapter(ProductAdapter()); 16 | Hive.openBox(_boxName); 17 | } 18 | 19 | void addFavorite(Product product) { 20 | _box.put(product.id, product); 21 | } 22 | 23 | void deleteFavorite(Product product) { 24 | _box.delete(product.id); 25 | } 26 | 27 | List get favorites => _box.values.toList(); 28 | 29 | bool isFavorite(Product product) { 30 | return _box.containsKey(product.id); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /lib/ui/order/bloc/order_history_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:nike_ecommerce/common/exceptions.dart'; 4 | import 'package:nike_ecommerce/data/order.dart'; 5 | import 'package:nike_ecommerce/data/repo/order_repository.dart'; 6 | 7 | part 'order_history_event.dart'; 8 | part 'order_history_state.dart'; 9 | 10 | class OrderHistoryBloc extends Bloc { 11 | final IOrderRepository repository; 12 | 13 | OrderHistoryBloc(this.repository) : super(OrderHistoryLoading()) { 14 | on((event, emit) async { 15 | if (event is OrderHistoryStarted) { 16 | try { 17 | emit(OrderHistoryLoading()); 18 | final result = await repository.getOrders(); 19 | 20 | emit(OrderHistorySuccess(result)); 21 | } catch (e) { 22 | emit(OrderHistoryError(AppException())); 23 | } 24 | } 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/ui/receipt/bloc/payment_receipt_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:nike_ecommerce/common/exceptions.dart'; 4 | import 'package:nike_ecommerce/data/payment_receipt.dart'; 5 | import 'package:nike_ecommerce/data/repo/order_repository.dart'; 6 | 7 | part 'payment_receipt_event.dart'; 8 | part 'payment_receipt_state.dart'; 9 | 10 | class PaymentReceiptBloc 11 | extends Bloc { 12 | final IOrderRepository repository; 13 | PaymentReceiptBloc(this.repository) : super(PaymentReceiptLoading()) { 14 | on((event, emit) async { 15 | if (event is PaymentReceiptStarted) { 16 | try { 17 | final result = await repository.getPaymentReceipt(event.orderId); 18 | 19 | emit(PaymentReceiptSuccess(result)); 20 | } catch (e) { 21 | emit(PaymentReceiptError(AppException())); 22 | } 23 | } 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/ui/product/bloc/product_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:nike_ecommerce/common/exceptions.dart'; 4 | import 'package:nike_ecommerce/data/add_to_cart_response.dart'; 5 | import 'package:nike_ecommerce/data/repo/cart_repository.dart'; 6 | 7 | part 'product_event.dart'; 8 | part 'product_state.dart'; 9 | 10 | class ProductBloc extends Bloc { 11 | final ICartRepository repository; 12 | 13 | ProductBloc(this.repository) : super(ProductInitial()) { 14 | on((event, emit) async { 15 | if (event is CartAddButtonClick) { 16 | try { 17 | emit(ProductAddToCartLoading()); 18 | final result = await repository.add(event.productId); 19 | await repository.count(); 20 | emit(ProductAddToCartSuccess(result)); 21 | } catch (e) { 22 | emit(ProductAddToCartError(AppException())); 23 | } 24 | } 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/ui/list/bloc/product_list_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:nike_ecommerce/common/exceptions.dart'; 4 | import 'package:nike_ecommerce/data/product.dart'; 5 | import 'package:nike_ecommerce/data/repo/product_repository.dart'; 6 | 7 | part 'product_list_event.dart'; 8 | part 'product_list_state.dart'; 9 | 10 | class ProductListBloc extends Bloc { 11 | final IProductRepository repository; 12 | 13 | ProductListBloc(this.repository) : super(ProductListLoading()) { 14 | on((event, emit) async { 15 | if (event is ProductListStarted) { 16 | try { 17 | emit(ProductListLoading()); 18 | final products = await repository.getAll(event.sort); 19 | emit(ProductListSuccess(products, event.sort, ProductSort.names)); 20 | } catch (e) { 21 | emit(ProductListError(AppException())); 22 | } 23 | } 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/ui/payment_webview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nike_ecommerce/ui/receipt/receipt.dart'; 3 | import 'package:webview_flutter/webview_flutter.dart'; 4 | 5 | class PaymentGatewayScreen extends StatelessWidget { 6 | final String bankGatewayUrl; 7 | 8 | const PaymentGatewayScreen({Key? key, required this.bankGatewayUrl}) 9 | : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return WebView( 14 | initialUrl: bankGatewayUrl, 15 | javascriptMode: JavascriptMode.unrestricted, 16 | onPageStarted: (url) { 17 | final uri = Uri.parse(url); 18 | if (uri.host == 'expertdevelopers.ir' && 19 | uri.pathSegments.contains('appCheckout')) { 20 | final orderId = int.parse(uri.queryParameters['order_id']!); 21 | 22 | Navigator.of(context).pop(); 23 | Navigator.of(context).push(MaterialPageRoute( 24 | builder: ((context) => ReceiptScreen(orderId: orderId)))); 25 | } 26 | }, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nike_ecommerce", 3 | "short_name": "nike_ecommerce", 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-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /lib/ui/product/comment/bloc/commentlist_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:nike_ecommerce/common/exceptions.dart'; 4 | import 'package:nike_ecommerce/data/comment.dart'; 5 | import 'package:nike_ecommerce/data/repo/comment_repository.dart'; 6 | 7 | part 'commentlist_event.dart'; 8 | part 'commentlist_state.dart'; 9 | 10 | class CommentListBloc extends Bloc { 11 | final ICommentRepository repository; 12 | final int productId; 13 | 14 | CommentListBloc({required this.repository, required this.productId}) 15 | : super(CommentListLoading()) { 16 | on((event, emit) async { 17 | emit(CommentListLoading()); 18 | 19 | if (event is CommentListStarted) { 20 | try { 21 | final comments = await repository.getAll(productId: productId); 22 | 23 | emit(CommentListSuccess(comments)); 24 | } catch (e) { 25 | emit(CommentListerror(e is AppException ? e : AppException())); 26 | } 27 | } 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/ui/widgets/empty_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class EmptyView extends StatelessWidget { 4 | final String message; 5 | final Widget? callToAction; 6 | final Widget image; 7 | 8 | const EmptyView( 9 | {Key? key, required this.message, this.callToAction, required this.image}) 10 | : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return SizedBox( 15 | width: MediaQuery.of(context).size.width, 16 | child: Column( 17 | mainAxisAlignment: MainAxisAlignment.center, 18 | children: [ 19 | image, 20 | Padding( 21 | padding: 22 | const EdgeInsets.only(left: 40, right: 40, top: 30, bottom: 16), 23 | child: Text( 24 | message, 25 | style: 26 | Theme.of(context).textTheme.headline6!.copyWith(fontSize: 14), 27 | textAlign: TextAlign.center, 28 | ), 29 | ), 30 | if (callToAction != null) callToAction! 31 | ], 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:nike_ecommerce/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/ui/cart/bloc/cart_event.dart: -------------------------------------------------------------------------------- 1 | part of 'cart_bloc.dart'; 2 | 3 | abstract class CartEvent extends Equatable { 4 | const CartEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class CartStarted extends CartEvent { 11 | final AuthInfo? authInfo; 12 | final bool isRefreshing; 13 | 14 | const CartStarted(this.authInfo, {this.isRefreshing = false}); 15 | } 16 | 17 | class CartAuthInfoChanged extends CartEvent { 18 | final AuthInfo? authInfo; 19 | 20 | const CartAuthInfoChanged(this.authInfo); 21 | } 22 | 23 | class CartDeleteButtonClicked extends CartEvent { 24 | final int cartItemId; 25 | 26 | const CartDeleteButtonClicked(this.cartItemId); 27 | 28 | @override 29 | List get props => [cartItemId]; 30 | } 31 | 32 | class CartPlusButtonClicked extends CartEvent { 33 | final int cartItemId; 34 | 35 | const CartPlusButtonClicked(this.cartItemId); 36 | 37 | @override 38 | List get props => [cartItemId]; 39 | } 40 | 41 | class CartMinusButtonClicked extends CartEvent { 42 | final int cartItemId; 43 | 44 | const CartMinusButtonClicked(this.cartItemId); 45 | 46 | @override 47 | List get props => [cartItemId]; 48 | } 49 | -------------------------------------------------------------------------------- /lib/data/product.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive_flutter/adapters.dart'; 2 | 3 | part 'product.g.dart'; 4 | 5 | class ProductSort { 6 | static const int latest = 0; 7 | static const int popular = 1; 8 | static const int priceHighToLow = 2; 9 | static const int priceLowToHigh = 3; 10 | 11 | static const List names = [ 12 | 'جدیدترین', 13 | 'پربازدیدترین', 14 | 'قیمت نزولی', 15 | 'قیمت صعودی' 16 | ]; 17 | } 18 | 19 | 20 | @HiveType(typeId: 0) 21 | class Product extends HiveObject{ 22 | @HiveField(0) 23 | final int id; 24 | @HiveField(1) 25 | final String title; 26 | @HiveField(2) 27 | final String imageUrl; 28 | @HiveField(3) 29 | final int price; 30 | @HiveField(4) 31 | final int discount; 32 | @HiveField(5) 33 | final int previousPrice; 34 | 35 | Product(this.id, this.title, this.imageUrl, this.price, this.discount, this.previousPrice); 36 | 37 | Product.fromJson(Map json) 38 | : id = json['id'], 39 | title = json['title'], 40 | imageUrl = json['image'], 41 | price = json['previous_price'] == null 42 | ? json['price'] - json['discount'] 43 | : json['price'], 44 | discount = json['discount'], 45 | previousPrice = json['previous_price'] ?? json['price']; 46 | } 47 | -------------------------------------------------------------------------------- /lib/data/source/product_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:nike_ecommerce/common/http_response_validator.dart'; 3 | 4 | import '../product.dart'; 5 | 6 | abstract class IProductDataSource { 7 | Future> getAll(int sort); 8 | Future> search(String serachTerm); 9 | } 10 | 11 | class ProductRemoteDataSource with HttpResponseValidation implements IProductDataSource { 12 | final Dio httpClient; 13 | 14 | ProductRemoteDataSource(this.httpClient); 15 | 16 | @override 17 | Future> getAll(int sort) async { 18 | final response = await httpClient.get('product/list?sort=$sort'); 19 | validateResposnse(response); 20 | final products = []; 21 | (response.data as List).forEach((element) { 22 | products.add(Product.fromJson(element)); 23 | }); 24 | 25 | return products; 26 | } 27 | 28 | @override 29 | Future> search(String serachTerm) async { 30 | final response = await httpClient.get('product/search?q=$serachTerm'); 31 | validateResposnse(response); 32 | final products = []; 33 | (response.data as List).forEach((element) { 34 | products.add(Product.fromJson(element)); 35 | }); 36 | 37 | return products; 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /lib/data/order.dart: -------------------------------------------------------------------------------- 1 | import 'package:nike_ecommerce/data/product.dart'; 2 | 3 | class CreateOrderResponse { 4 | final int orderId; 5 | final String bankGatewayUrl; 6 | 7 | CreateOrderResponse(this.orderId, this.bankGatewayUrl); 8 | 9 | CreateOrderResponse.fromJson(Map json) 10 | : orderId = json['order_id'], 11 | bankGatewayUrl = json['bank_gateway_url']; 12 | } 13 | 14 | class CreateOrderParams { 15 | final String firstName; 16 | final String lastName; 17 | final String phoneNumber; 18 | final String postalCode; 19 | final String address; 20 | final PaymentMethod paymentMethod; 21 | 22 | CreateOrderParams( 23 | {required this.firstName, 24 | required this.lastName, 25 | required this.phoneNumber, 26 | required this.postalCode, 27 | required this.paymentMethod, 28 | required this.address}); 29 | } 30 | 31 | enum PaymentMethod { online, cashOnDelivery } 32 | 33 | class OrderEntity { 34 | final int id; 35 | final int payablePrice; 36 | final List items; 37 | 38 | OrderEntity(this.id, this.payablePrice, this.items); 39 | 40 | OrderEntity.fromJson(Map json) 41 | : id = json['id'], 42 | payablePrice = json['payable'], 43 | items = (json['order_items'] as List) 44 | .map((item) => Product.fromJson(item['product'])) 45 | .toList(); 46 | } 47 | -------------------------------------------------------------------------------- /lib/ui/auth/bloc/auth_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:nike_ecommerce/common/exceptions.dart'; 4 | import 'package:nike_ecommerce/data/repo/auth_repository.dart'; 5 | import 'package:nike_ecommerce/data/repo/cart_repository.dart'; 6 | 7 | part 'auth_event.dart'; 8 | part 'auth_state.dart'; 9 | 10 | class AuthBloc extends Bloc { 11 | bool isLogin; 12 | final IAuthRepository repository; 13 | final CartRepository cartRepository; 14 | 15 | AuthBloc(this.repository, this.cartRepository, {this.isLogin = true}) 16 | : super(AuthInitial(isLogin)) { 17 | on((event, emit) async { 18 | try { 19 | if (event is AuthButtonClicked) { 20 | emit(AuthLoading(isLogin)); 21 | if (isLogin) { 22 | await repository.login(event.username, event.password); 23 | await cartRepository.count(); 24 | } else { 25 | await repository.register(event.username, event.password); 26 | } 27 | emit(AuthSuccess(isLogin)); 28 | } else if (event is AuthModeChangedClicked) { 29 | isLogin = !isLogin; 30 | emit(AuthInitial(isLogin)); 31 | } 32 | } catch (e) { 33 | emit(AuthError( 34 | isLogin, 35 | AppException(), 36 | )); 37 | } 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.CreateAndShow(L"nike_ecommerce", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /lib/ui/home/bloc/home_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:nike_ecommerce/common/exceptions.dart'; 4 | import 'package:nike_ecommerce/data/banner.dart'; 5 | import 'package:nike_ecommerce/data/product.dart'; 6 | import 'package:nike_ecommerce/data/repo/banner_repository.dart'; 7 | import 'package:nike_ecommerce/data/repo/product_repository.dart'; 8 | 9 | part 'home_event.dart'; 10 | part 'home_state.dart'; 11 | 12 | class HomeBloc extends Bloc { 13 | final BannerRepository bannerRepository; 14 | final ProductRepository productRepository; 15 | 16 | HomeBloc({required this.bannerRepository, required this.productRepository}) 17 | : super(HomeLoading()) { 18 | on((event, emit) async { 19 | if (event is HomeStarted || event is HomeRefresh) { 20 | try { 21 | emit(HomeLoading()); 22 | final banners = await bannerRepository.getAll(); 23 | final latestProducts = 24 | await productRepository.getAll(ProductSort.latest); 25 | final popularProducts = 26 | await productRepository.getAll(ProductSort.popular); 27 | 28 | emit(HomeSuccess( 29 | banners: banners, 30 | latestProducts: latestProducts, 31 | popularProducts: popularProducts)); 32 | } catch (e) { 33 | emit(HomeError(exception: e is AppException ? e : AppException())); 34 | } 35 | } 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/data/repo/cart_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:nike_ecommerce/common/http_client.dart'; 3 | import 'package:nike_ecommerce/data/add_to_cart_response.dart'; 4 | import 'package:nike_ecommerce/data/cart_response.dart'; 5 | import 'package:nike_ecommerce/data/source/cart_data_source.dart'; 6 | 7 | final cartRepository = CartRepository(CartRemoteDataSource(httpClient)); 8 | 9 | abstract class ICartRepository { 10 | Future add(int productId); 11 | Future changeCount(int cartItemId, int count); 12 | Future delete(int cartItemId); 13 | Future count(); 14 | Future getAll(); 15 | } 16 | 17 | class CartRepository implements ICartRepository { 18 | final ICartDataSource dataSource; 19 | static ValueNotifier cartItemCountNotifier = ValueNotifier(0); 20 | 21 | CartRepository(this.dataSource); 22 | @override 23 | Future add(int productId) { 24 | return dataSource.add(productId); 25 | } 26 | 27 | @override 28 | Future changeCount(int cartItemId, int count) { 29 | return dataSource.changeCount(cartItemId, count); 30 | } 31 | 32 | @override 33 | Future count() async { 34 | final count = await dataSource.count(); 35 | cartItemCountNotifier.value = count; 36 | return count; 37 | } 38 | 39 | @override 40 | Future delete(int cartItemId) { 41 | return dataSource.delete(cartItemId); 42 | } 43 | 44 | @override 45 | Future getAll() { 46 | return dataSource.getAll(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/data/product.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'product.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class ProductAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 0; 12 | 13 | @override 14 | Product read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return Product( 20 | fields[0] as int, 21 | fields[1] as String, 22 | fields[2] as String, 23 | fields[3] as int, 24 | fields[4] as int, 25 | fields[5] as int, 26 | ); 27 | } 28 | 29 | @override 30 | void write(BinaryWriter writer, Product obj) { 31 | writer 32 | ..writeByte(6) 33 | ..writeByte(0) 34 | ..write(obj.id) 35 | ..writeByte(1) 36 | ..write(obj.title) 37 | ..writeByte(2) 38 | ..write(obj.imageUrl) 39 | ..writeByte(3) 40 | ..write(obj.price) 41 | ..writeByte(4) 42 | ..write(obj.discount) 43 | ..writeByte(5) 44 | ..write(obj.previousPrice); 45 | } 46 | 47 | @override 48 | int get hashCode => typeId.hashCode; 49 | 50 | @override 51 | bool operator ==(Object other) => 52 | identical(this, other) || 53 | other is ProductAdapter && 54 | runtimeType == other.runtimeType && 55 | typeId == other.typeId; 56 | } 57 | -------------------------------------------------------------------------------- /lib/ui/product/comment/comment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nike_ecommerce/data/comment.dart'; 3 | 4 | class CommentItem extends StatelessWidget { 5 | final CommentEntity commment; 6 | const CommentItem({ 7 | Key? key, 8 | required this.commment, 9 | }) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final themeData = Theme.of(context); 14 | return Container( 15 | decoration: 16 | BoxDecoration(border: Border.all( 17 | color: themeData.dividerColor, 18 | width: 1), 19 | borderRadius: BorderRadius.circular(4)), 20 | margin: const EdgeInsets.fromLTRB(8, 0, 8, 8), 21 | padding: const EdgeInsets.all(12), 22 | child: Column( 23 | crossAxisAlignment: CrossAxisAlignment.start, 24 | children: [ 25 | Row( 26 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 27 | crossAxisAlignment: CrossAxisAlignment.start, 28 | children: [ 29 | Column( 30 | crossAxisAlignment: CrossAxisAlignment.start, 31 | children: [ 32 | Text(commment.title), 33 | Text( 34 | commment.email, 35 | style: themeData.textTheme.caption, 36 | ) 37 | ], 38 | ), 39 | Text(commment.date, style: themeData.textTheme.caption) 40 | ], 41 | ), 42 | const SizedBox( 43 | height: 12, 44 | ), 45 | Text(commment.content) 46 | ], 47 | ), 48 | ); 49 | } 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 | CFBundleDisplayName 8 | Nike Ecommerce 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | nike_ecommerce 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /lib/data/source/order_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:nike_ecommerce/common/http_response_validator.dart'; 3 | import 'package:nike_ecommerce/data/order.dart'; 4 | import 'package:nike_ecommerce/data/payment_receipt.dart'; 5 | 6 | abstract class IOrderDataSource { 7 | Future create(CreateOrderParams params); 8 | Future getPaymentReceipt(int orderId); 9 | Future> getOrders(); 10 | } 11 | 12 | class OrderRemoteDataSource 13 | with HttpResponseValidation 14 | implements IOrderDataSource { 15 | final Dio httpClient; 16 | 17 | OrderRemoteDataSource(this.httpClient); 18 | 19 | @override 20 | Future create(CreateOrderParams params) async { 21 | final response = await httpClient.post('order/submit', data: { 22 | 'first_name': params.firstName, 23 | 'last_name': params.lastName, 24 | 'mobile': params.phoneNumber, 25 | 'postal_code': params.postalCode, 26 | 'address': params.address, 27 | 'payment_method': params.paymentMethod == PaymentMethod.online 28 | ? 'online' 29 | : 'cash_on_delivery' 30 | }); 31 | 32 | validateResposnse(response); 33 | return CreateOrderResponse.fromJson(response.data); 34 | } 35 | 36 | @override 37 | Future getPaymentReceipt(int orderId) async { 38 | final response = await httpClient.get('order/checkout?order_id=$orderId'); 39 | validateResposnse(response); 40 | 41 | return PaymentReceipt.fromJson(response.data); 42 | } 43 | 44 | @override 45 | Future> getOrders() async { 46 | final response = await httpClient.get('order/list'); 47 | validateResposnse(response); 48 | 49 | return (response.data as List).map((e) => OrderEntity.fromJson(e)).toList(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | if (target_length == 0) { 52 | return std::string(); 53 | } 54 | std::string utf8_string; 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 17 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /lib/data/source/auth_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:nike_ecommerce/common/constants.dart'; 3 | import 'package:nike_ecommerce/common/http_response_validator.dart'; 4 | import 'package:nike_ecommerce/data/auth_info.dart'; 5 | 6 | abstract class IAuthDataSource { 7 | Future login(String username, String password); 8 | Future register(String username, String password); 9 | Future refreshToken(String toekn); 10 | } 11 | 12 | class AuthRemoteDataSource 13 | with HttpResponseValidation 14 | implements IAuthDataSource { 15 | final Dio httpClient; 16 | 17 | AuthRemoteDataSource(this.httpClient); 18 | 19 | @override 20 | Future login(String username, String password) async { 21 | final response = await httpClient.post('auth/token', data: { 22 | 'grant_type': 'password', 23 | 'client_id': 2, 24 | 'client_secret': Constants.clientSceret, 25 | 'username': username, 26 | 'password': password 27 | }); 28 | validateResposnse(response); 29 | return AuthInfo( 30 | response.data['access_token'], response.data['refresh_token'], username); 31 | } 32 | 33 | @override 34 | Future refreshToken(String toekn) async { 35 | final response = await httpClient.post('auth/token', data: { 36 | 'grant_type': 'refresh_token', 37 | 'client_id': 2, 38 | 'client_secret': Constants.clientSceret, 39 | 'refresh_token': toekn 40 | }); 41 | validateResposnse(response); 42 | return AuthInfo( 43 | response.data['access_token'], response.data['refresh_token'], ''); 44 | } 45 | 46 | @override 47 | Future register(String username, String password) async { 48 | final response = await httpClient 49 | .post('user/register', data: {'email': username, 'password': password}); 50 | validateResposnse(response); 51 | return login(username, password); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/data/source/cart_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:nike_ecommerce/common/http_response_validator.dart'; 3 | import 'package:nike_ecommerce/data/add_to_cart_response.dart'; 4 | import 'package:nike_ecommerce/data/cart_response.dart'; 5 | 6 | abstract class ICartDataSource { 7 | Future add(int productId); 8 | Future changeCount(int cartItemId, int count); 9 | Future delete(int cartItemId); 10 | Future count(); 11 | Future getAll(); 12 | } 13 | 14 | class CartRemoteDataSource 15 | with HttpResponseValidation 16 | implements ICartDataSource { 17 | final Dio httpClient; 18 | 19 | CartRemoteDataSource(this.httpClient); 20 | 21 | @override 22 | Future add(int productId) async { 23 | final response = 24 | await httpClient.post('cart/add', data: {'product_id': productId}); 25 | validateResposnse(response); 26 | return AddToCartResponse.fromJson(response.data); 27 | } 28 | 29 | @override 30 | Future changeCount(int cartItemId, int count) async { 31 | final response = await httpClient.post('cart/changeCount', 32 | data: {'cart_item_id': cartItemId, 'count': count}); 33 | validateResposnse(response); 34 | return AddToCartResponse.fromJson(response.data); 35 | } 36 | 37 | @override 38 | Future count() async { 39 | final response = await httpClient.get('cart/count'); 40 | validateResposnse(response); 41 | return response.data['count']; 42 | } 43 | 44 | @override 45 | Future delete(int cartItemId) async { 46 | await httpClient.post('cart/remove', data: {'cart_item_id': cartItemId}); 47 | } 48 | 49 | @override 50 | Future getAll() async { 51 | final response = await httpClient.get('cart/list'); 52 | validateResposnse(response); 53 | 54 | return CartResponse.fromJson(response.data); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/ui/product/comment/comment_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:nike_ecommerce/data/repo/comment_repository.dart'; 4 | import 'package:nike_ecommerce/ui/product/comment/bloc/commentlist_bloc.dart'; 5 | import 'package:nike_ecommerce/ui/product/comment/comment.dart'; 6 | import 'package:nike_ecommerce/ui/widgets/error.dart'; 7 | 8 | class CommentList extends StatelessWidget { 9 | final int productId; 10 | 11 | const CommentList({Key? key, required this.productId}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return BlocProvider(create: (context) { 16 | final CommentListBloc bloc = 17 | CommentListBloc(productId: productId, repository: commentRepository); 18 | bloc.add(CommentListStarted()); 19 | return bloc; 20 | }, child: BlocBuilder( 21 | builder: (context, state) { 22 | if (state is CommentListSuccess) { 23 | return SliverList( 24 | delegate: SliverChildBuilderDelegate((context, index) { 25 | return CommentItem(commment: state.comments[index]); 26 | }, childCount: state.comments.length)); 27 | } else if (state is CommentListLoading) { 28 | return const SliverToBoxAdapter( 29 | child: Center( 30 | child: CircularProgressIndicator(), 31 | ), 32 | ); 33 | } else if (state is CommentListerror) { 34 | return SliverToBoxAdapter( 35 | child: AppErrorWidget( 36 | exception: state.exception, 37 | onPressed: () { 38 | BlocProvider.of(context) 39 | .add(CommentListStarted()); 40 | }, 41 | ), 42 | ); 43 | } else { 44 | throw Exception('state is not supported'); 45 | } 46 | }, 47 | )); 48 | } 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | return true; 30 | } 31 | 32 | void FlutterWindow::OnDestroy() { 33 | if (flutter_controller_) { 34 | flutter_controller_ = nullptr; 35 | } 36 | 37 | Win32Window::OnDestroy(); 38 | } 39 | 40 | LRESULT 41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 42 | WPARAM const wparam, 43 | LPARAM const lparam) noexcept { 44 | // Give Flutter, including plugins, an opportunity to handle window messages. 45 | if (flutter_controller_) { 46 | std::optional result = 47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 48 | lparam); 49 | if (result) { 50 | return *result; 51 | } 52 | } 53 | 54 | switch (message) { 55 | case WM_FONTCHANGE: 56 | flutter_controller_->engine()->ReloadSystemFonts(); 57 | break; 58 | } 59 | 60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 61 | } 62 | -------------------------------------------------------------------------------- /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 flutter.compileSdkVersion 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 "com.example.nike_ecommerce" 47 | minSdkVersion 21 48 | targetSdkVersion flutter.targetSdkVersion 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 | -------------------------------------------------------------------------------- /lib/ui/widgets/slider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nike_ecommerce/common/utils.dart'; 3 | import 'package:nike_ecommerce/data/banner.dart'; 4 | import 'package:nike_ecommerce/ui/widgets/image.dart'; 5 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 6 | 7 | class BannerSlider extends StatelessWidget { 8 | final PageController _controller = PageController(); 9 | final List banners; 10 | BannerSlider({ 11 | Key? key, 12 | required this.banners, 13 | }) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return AspectRatio( 18 | aspectRatio: 2, 19 | child: Stack( 20 | children: [ 21 | PageView.builder( 22 | physics: defaultScrollPhsics, 23 | controller: _controller, 24 | itemCount: banners.length, 25 | itemBuilder: ((context, index) => 26 | _Slide(banner: banners[index]))), 27 | Positioned( 28 | left: 0, 29 | right: 0, 30 | bottom: 8, 31 | child: Center( 32 | child: SmoothPageIndicator( 33 | controller: _controller, 34 | count: banners.length, 35 | axisDirection: Axis.horizontal, 36 | effect: WormEffect( 37 | spacing: 4.0, 38 | radius: 4.0, 39 | dotWidth: 18.0, 40 | dotHeight: 2.0, 41 | paintStyle: PaintingStyle.fill, 42 | strokeWidth: 1.5, 43 | dotColor: Colors.grey.shade400, 44 | activeDotColor: Theme.of(context).colorScheme.onBackground), 45 | ), 46 | ), 47 | ) 48 | ], 49 | ), 50 | ); 51 | } 52 | } 53 | 54 | class _Slide extends StatelessWidget { 55 | const _Slide({ 56 | Key? key, 57 | required this.banner, 58 | }) : super(key: key); 59 | 60 | final BannerEntity banner; 61 | 62 | @override 63 | Widget build(BuildContext context) { 64 | return Padding( 65 | padding: const EdgeInsets.only(left: 12, right: 12), 66 | child: ImageLoadingService( 67 | imageUrl: banner.image, 68 | borderRadius: BorderRadius.circular(12), 69 | ), 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nike_ecommerce/data/repo/auth_repository.dart'; 3 | import 'package:nike_ecommerce/data/favorite_manager.dart'; 4 | import 'package:nike_ecommerce/theme.dart'; 5 | import 'package:nike_ecommerce/ui/root.dart'; 6 | 7 | void main() async{ 8 | await FavoriteManager.init(); 9 | WidgetsFlutterBinding.ensureInitialized(); 10 | authRepository.loadAuthInfo(); 11 | runApp(const MyApp()); 12 | } 13 | 14 | class MyApp extends StatelessWidget { 15 | const MyApp({Key? key}) : super(key: key); 16 | 17 | // This widget is the root of your application. 18 | @override 19 | Widget build(BuildContext context) { 20 | const defaultTextStyle = TextStyle( 21 | fontFamily: 'IranYekan', color: LightThemeColors.primaryTextColor); 22 | return MaterialApp( 23 | title: 'Flutter', 24 | theme: ThemeData( 25 | hintColor: LightThemeColors.secondryTextColor, 26 | inputDecorationTheme: InputDecorationTheme( 27 | border: const OutlineInputBorder(), 28 | enabledBorder: OutlineInputBorder( 29 | borderSide: BorderSide( 30 | color: 31 | LightThemeColors.primaryTextColor.withOpacity(0.1)))), 32 | appBarTheme: const AppBarTheme( 33 | backgroundColor: Colors.white, 34 | foregroundColor: LightThemeColors.primaryTextColor, 35 | elevation: 0), 36 | textTheme: TextTheme( 37 | button: defaultTextStyle, 38 | subtitle1: defaultTextStyle.apply( 39 | color: LightThemeColors.secondryTextColor), 40 | bodyText2: defaultTextStyle, 41 | caption: defaultTextStyle.apply( 42 | color: LightThemeColors.secondryTextColor), 43 | headline6: defaultTextStyle.copyWith( 44 | fontWeight: FontWeight.bold, fontSize: 17)), 45 | colorScheme: const ColorScheme.light( 46 | primary: LightThemeColors.primaryColor, 47 | secondary: LightThemeColors.secondryColor, 48 | onSecondary: Colors.white, 49 | surfaceVariant: Color(0xfff5f5f5)), 50 | snackBarTheme: SnackBarThemeData( 51 | contentTextStyle: defaultTextStyle.apply(color: Colors.white)), 52 | ), 53 | home: const Directionality( 54 | textDirection: TextDirection.rtl, child: RootScreen()), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/data/repo/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:nike_ecommerce/common/http_client.dart'; 3 | import 'package:nike_ecommerce/data/auth_info.dart'; 4 | import 'package:nike_ecommerce/data/source/auth_data_source.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | final authRepository = AuthRepository(AuthRemoteDataSource(httpClient)); 8 | 9 | abstract class IAuthRepository { 10 | Future login(String username, String password); 11 | Future register(String username, String password); 12 | Future refreshToken(); 13 | Future signOut(); 14 | } 15 | 16 | class AuthRepository implements IAuthRepository { 17 | final IAuthDataSource dataSource; 18 | static ValueNotifier authChangeNotifire = ValueNotifier(null); 19 | 20 | AuthRepository(this.dataSource); 21 | 22 | @override 23 | Future login(String username, String password) async { 24 | final authInfo = await dataSource.login(username, password); 25 | _persistAuthTokens(authInfo); 26 | } 27 | 28 | @override 29 | Future refreshToken() async { 30 | if (authChangeNotifire.value != null) { 31 | final authInfo = 32 | await dataSource.refreshToken(authChangeNotifire.value!.refreshToken); 33 | _persistAuthTokens(authInfo); 34 | } 35 | } 36 | 37 | @override 38 | Future register(String username, String password) async { 39 | final authInfo = await dataSource.register(username, password); 40 | _persistAuthTokens(authInfo); 41 | } 42 | 43 | Future _persistAuthTokens(AuthInfo authInfo) async { 44 | final sp = await SharedPreferences.getInstance(); 45 | sp.setString('access_token', authInfo.accessToken); 46 | sp.setString('refresh_token', authInfo.refreshToken); 47 | sp.setString('email', authInfo.email); 48 | loadAuthInfo(); 49 | } 50 | 51 | Future loadAuthInfo() async { 52 | final sp = await SharedPreferences.getInstance(); 53 | final accessToken = sp.getString('access_token') ?? ''; 54 | final refreshToken = sp.getString('refresh_token') ?? ''; 55 | final email = sp.getString('email') ?? ''; 56 | 57 | if (refreshToken.isNotEmpty && accessToken.isNotEmpty) { 58 | authChangeNotifire.value = AuthInfo(accessToken, refreshToken, email); 59 | } 60 | } 61 | 62 | @override 63 | Future signOut() async { 64 | final sp = await SharedPreferences.getInstance(); 65 | sp.clear(); 66 | authChangeNotifire.value = null; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /assets/img/no_data.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/questions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class MyWidget extends StatelessWidget { 8 | final personNextToMe = 'That reminds me about the time when I was ten and our neighbor, her name was Mrs. Mable, and she said...'; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | body: Center( 14 | child: Row( 15 | mainAxisAlignment: MainAxisAlignment.center, 16 | crossAxisAlignment: CrossAxisAlignment.center, 17 | children: [ 18 | const Icon(Icons.airline_seat_legroom_reduced), 19 | Expanded(child: Text(personNextToMe,)), 20 | const Icon(Icons.airline_seat_legroom_reduced), 21 | ]), 22 | ), 23 | ); 24 | } 25 | } 26 | 27 | class MyWidget2 extends StatelessWidget { 28 | @override 29 | Widget build(BuildContext context) { 30 | return Scaffold( 31 | body: Center( 32 | child: Wrap(children: const [ 33 | Chip(label: Text('I')), 34 | Chip(label: Text('really')), 35 | Chip(label: Text('really')), 36 | Chip(label: Text('really')), 37 | Chip(label: Text('really')), 38 | Chip(label: Text('really')), 39 | Chip(label: Text('really')), 40 | Chip(label: Text('need')), 41 | Chip(label: Text('a')), 42 | Chip(label: Text('job')), 43 | ]), 44 | ), 45 | ); 46 | } 47 | } 48 | 49 | class FishHatchery { 50 | FishHatchery() { 51 | Timer.periodic(const Duration(seconds: 1), (t) { 52 | final isSalmon = Random().nextBool(); 53 | final fish = (isSalmon) ? 'salmon' : 'trout'; 54 | _controller.sink.add(fish); 55 | }); 56 | } 57 | 58 | final _controller = StreamController(); 59 | Stream get stream => _controller.stream; 60 | } 61 | 62 | final fish = FishHatchery().stream; 63 | final sushi = 64 | fish.where((fish) => fish == 'salmon').map((fish) => 'sushi').take(5); 65 | 66 | Future makeSomeoneElseCountForMe() async { 67 | return await compute(playHideAndSeekTheLongVersion, 10000000000); 68 | } 69 | 70 | String playHideAndSeekTheLongVersion(int countTo) { 71 | var counting = 0; 72 | for (var i = 1; i <= countTo; i++) { 73 | counting = i; 74 | } 75 | return '$counting! Ready or not, here I come!'; 76 | } 77 | 78 | String _m(int n, [int p1 = 3, String p2 = ' ']) { 79 | List result = []; 80 | var r = n.toString().split(' ').reversed; 81 | 82 | int l = r.length; 83 | while (l < p1) { 84 | String group = r.take(p1).toList().reversed.join(''); 85 | result.add(group); 86 | 87 | r = r.skip(p1); 88 | l += p1; 89 | } 90 | result.add(r.toList().reversed.join('')); 91 | 92 | return result.reversed.join(p2); 93 | } 94 | 95 | void main() async { 96 | var i = 2; 97 | var _c = (a) async { 98 | var f = (int i) async => i + 1; 99 | i = await f(i); 100 | print('Step 1. $i'); 101 | }; 102 | 103 | _x2(int i) async { 104 | i * 2; 105 | } 106 | 107 | _c(i); 108 | print('Step 2. $i'); 109 | var r = await _x2(i); //asynchronously multiplies by 2 110 | print('Step 3. $r'); 111 | } 112 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #ifdef FLUTTER_BUILD_NUMBER 64 | #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0 67 | #endif 68 | 69 | #ifdef FLUTTER_BUILD_NAME 70 | #define VERSION_AS_STRING #FLUTTER_BUILD_NAME 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "nike_ecommerce" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "nike_ecommerce" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "nike_ecommerce.exe" "\0" 98 | VALUE "ProductName", "nike_ecommerce" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(nike_ecommerce LANGUAGES CXX) 3 | 4 | set(BINARY_NAME "nike_ecommerce") 5 | 6 | cmake_policy(SET CMP0063 NEW) 7 | 8 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 9 | 10 | # Configure build options. 11 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 12 | if(IS_MULTICONFIG) 13 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 14 | CACHE STRING "" FORCE) 15 | else() 16 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 17 | set(CMAKE_BUILD_TYPE "Debug" CACHE 18 | STRING "Flutter build mode" FORCE) 19 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 20 | "Debug" "Profile" "Release") 21 | endif() 22 | endif() 23 | 24 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 25 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 26 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 27 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 28 | 29 | # Use Unicode for all projects. 30 | add_definitions(-DUNICODE -D_UNICODE) 31 | 32 | # Compilation settings that should be applied to most targets. 33 | function(APPLY_STANDARD_SETTINGS TARGET) 34 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 35 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 36 | target_compile_options(${TARGET} PRIVATE /EHsc) 37 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 38 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 39 | endfunction() 40 | 41 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 42 | 43 | # Flutter library and tool build rules. 44 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 45 | 46 | # Application build 47 | add_subdirectory("runner") 48 | 49 | # Generated plugin build rules, which manage building the plugins and adding 50 | # them to the application. 51 | include(flutter/generated_plugins.cmake) 52 | 53 | 54 | # === Installation === 55 | # Support files are copied into place next to the executable, so that it can 56 | # run in place. This is done instead of making a separate bundle (as on Linux) 57 | # so that building and running from within Visual Studio will work. 58 | set(BUILD_BUNDLE_DIR "$") 59 | # Make the "install" step default, as it's required to run. 60 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 61 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 62 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 63 | endif() 64 | 65 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 66 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 67 | 68 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 69 | COMPONENT Runtime) 70 | 71 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 72 | COMPONENT Runtime) 73 | 74 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 75 | COMPONENT Runtime) 76 | 77 | if(PLUGIN_BUNDLED_LIBRARIES) 78 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 79 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 80 | COMPONENT Runtime) 81 | endif() 82 | 83 | # Fully re-copy the assets directory on each build to avoid having stale files 84 | # from a previous install. 85 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 86 | install(CODE " 87 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 88 | " COMPONENT Runtime) 89 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 90 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 91 | 92 | # Install the AOT library on non-Debug builds only. 93 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 94 | CONFIGURATIONS Profile;Release 95 | COMPONENT Runtime) 96 | -------------------------------------------------------------------------------- /lib/ui/cart/price_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nike_ecommerce/common/utils.dart'; 3 | import 'package:nike_ecommerce/theme.dart'; 4 | 5 | class PriceInfo extends StatelessWidget { 6 | final int payablePrice; 7 | final int shippingCost; 8 | final int totalPrice; 9 | const PriceInfo( 10 | {Key? key, 11 | required this.payablePrice, 12 | required this.shippingCost, 13 | required this.totalPrice}) 14 | : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Column( 19 | crossAxisAlignment: CrossAxisAlignment.start, 20 | children: [ 21 | Padding( 22 | padding: const EdgeInsets.fromLTRB(0, 20, 8, 0), 23 | child: Text( 24 | 'جزییات خرید', 25 | style: Theme.of(context).textTheme.subtitle1, 26 | ), 27 | ), 28 | Container( 29 | margin: const EdgeInsets.fromLTRB(8, 8, 8, 32), 30 | decoration: BoxDecoration( 31 | color: Theme.of(context).colorScheme.surface, 32 | borderRadius: BorderRadius.circular(6), 33 | boxShadow: [ 34 | BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10) 35 | ]), 36 | child: Column( 37 | children: [ 38 | Padding( 39 | padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), 40 | child: Row( 41 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 42 | children: [ 43 | const Text('مبلغ کل خرید'), 44 | RichText( 45 | text: TextSpan( 46 | text: totalPrice.separateByComma, 47 | style: DefaultTextStyle.of(context).style.apply( 48 | color: LightThemeColors.secondryTextColor), 49 | children: const [ 50 | TextSpan( 51 | text: ' تومان', style: TextStyle(fontSize: 10)), 52 | ])) 53 | ], 54 | ), 55 | ), 56 | const Divider( 57 | height: 1, 58 | ), 59 | Padding( 60 | padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), 61 | child: Row( 62 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 63 | children: [ 64 | const Text('هزینه ارسال'), 65 | Text(shippingCost.withPriceLabel) 66 | ], 67 | ), 68 | ), 69 | const Divider( 70 | height: 1, 71 | ), 72 | Padding( 73 | padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), 74 | child: Row( 75 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 76 | children: [ 77 | const Text('مبلغ قابل پرداخت'), 78 | RichText( 79 | text: TextSpan( 80 | text: payablePrice.separateByComma, 81 | style: DefaultTextStyle.of(context).style.copyWith( 82 | fontWeight: FontWeight.bold, fontSize: 17), 83 | children: const [ 84 | TextSpan( 85 | text: ' تومان', 86 | style: TextStyle( 87 | fontSize: 10, fontWeight: FontWeight.normal, color: LightThemeColors.secondryTextColor)) 88 | ])) 89 | ], 90 | ), 91 | ), 92 | ], 93 | ), 94 | ), 95 | ], 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | 3 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 4 | 5 | # Configuration provided via flutter tool. 6 | include(${EPHEMERAL_DIR}/generated_config.cmake) 7 | 8 | # TODO: Move the rest of this into files in ephemeral. See 9 | # https://github.com/flutter/flutter/issues/57146. 10 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 11 | 12 | # === Flutter Library === 13 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 14 | 15 | # Published to parent scope for install step. 16 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 17 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 18 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 19 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 20 | 21 | list(APPEND FLUTTER_LIBRARY_HEADERS 22 | "flutter_export.h" 23 | "flutter_windows.h" 24 | "flutter_messenger.h" 25 | "flutter_plugin_registrar.h" 26 | "flutter_texture_registrar.h" 27 | ) 28 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 29 | add_library(flutter INTERFACE) 30 | target_include_directories(flutter INTERFACE 31 | "${EPHEMERAL_DIR}" 32 | ) 33 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 34 | add_dependencies(flutter flutter_assemble) 35 | 36 | # === Wrapper === 37 | list(APPEND CPP_WRAPPER_SOURCES_CORE 38 | "core_implementations.cc" 39 | "standard_codec.cc" 40 | ) 41 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 42 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 43 | "plugin_registrar.cc" 44 | ) 45 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 46 | list(APPEND CPP_WRAPPER_SOURCES_APP 47 | "flutter_engine.cc" 48 | "flutter_view_controller.cc" 49 | ) 50 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 51 | 52 | # Wrapper sources needed for a plugin. 53 | add_library(flutter_wrapper_plugin STATIC 54 | ${CPP_WRAPPER_SOURCES_CORE} 55 | ${CPP_WRAPPER_SOURCES_PLUGIN} 56 | ) 57 | apply_standard_settings(flutter_wrapper_plugin) 58 | set_target_properties(flutter_wrapper_plugin PROPERTIES 59 | POSITION_INDEPENDENT_CODE ON) 60 | set_target_properties(flutter_wrapper_plugin PROPERTIES 61 | CXX_VISIBILITY_PRESET hidden) 62 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 63 | target_include_directories(flutter_wrapper_plugin PUBLIC 64 | "${WRAPPER_ROOT}/include" 65 | ) 66 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 67 | 68 | # Wrapper sources needed for the runner. 69 | add_library(flutter_wrapper_app STATIC 70 | ${CPP_WRAPPER_SOURCES_CORE} 71 | ${CPP_WRAPPER_SOURCES_APP} 72 | ) 73 | apply_standard_settings(flutter_wrapper_app) 74 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 75 | target_include_directories(flutter_wrapper_app PUBLIC 76 | "${WRAPPER_ROOT}/include" 77 | ) 78 | add_dependencies(flutter_wrapper_app flutter_assemble) 79 | 80 | # === Flutter tool backend === 81 | # _phony_ is a non-existent file to force this command to run every time, 82 | # since currently there's no way to get a full input/output list from the 83 | # flutter tool. 84 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 85 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 86 | add_custom_command( 87 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 88 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 89 | ${CPP_WRAPPER_SOURCES_APP} 90 | ${PHONY_OUTPUT} 91 | COMMAND ${CMAKE_COMMAND} -E env 92 | ${FLUTTER_TOOL_ENVIRONMENT} 93 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 94 | windows-x64 $ 95 | VERBATIM 96 | ) 97 | add_custom_target(flutter_assemble DEPENDS 98 | "${FLUTTER_LIBRARY}" 99 | ${FLUTTER_LIBRARY_HEADERS} 100 | ${CPP_WRAPPER_SOURCES_CORE} 101 | ${CPP_WRAPPER_SOURCES_PLUGIN} 102 | ${CPP_WRAPPER_SOURCES_APP} 103 | ) 104 | -------------------------------------------------------------------------------- /lib/ui/root.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:nike_ecommerce/data/repo/cart_repository.dart'; 4 | import 'package:nike_ecommerce/ui/cart/cart.dart'; 5 | import 'package:nike_ecommerce/ui/home/home.dart'; 6 | import 'package:nike_ecommerce/ui/profile/profile.dart'; 7 | import 'package:nike_ecommerce/ui/widgets/badge.dart'; 8 | 9 | const int homeIndex = 0; 10 | const int cartIndex = 1; 11 | const int profileIndex = 2; 12 | 13 | class RootScreen extends StatefulWidget { 14 | const RootScreen({Key? key}) : super(key: key); 15 | 16 | @override 17 | State createState() => _RootScreenState(); 18 | } 19 | 20 | class _RootScreenState extends State { 21 | int selectedTab = homeIndex; 22 | final List _history = []; 23 | 24 | final GlobalKey _homeKey = GlobalKey(); 25 | final GlobalKey _cartKey = GlobalKey(); 26 | final GlobalKey _profileKey = GlobalKey(); 27 | 28 | late final map = { 29 | homeIndex: _homeKey, 30 | cartIndex: _cartKey, 31 | profileIndex: _profileKey, 32 | }; 33 | 34 | Future _onWillPop() async { 35 | final NavigatorState navigatorSelectedSate = 36 | map[selectedTab]!.currentState!; 37 | if (navigatorSelectedSate.canPop()) { 38 | navigatorSelectedSate.pop(); 39 | return false; 40 | } else if (_history.isNotEmpty) { 41 | setState(() { 42 | selectedTab = _history.last; 43 | _history.removeLast(); 44 | }); 45 | return false; 46 | } 47 | return true; 48 | } 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return WillPopScope( 53 | child: Scaffold( 54 | bottomNavigationBar: BottomNavigationBar( 55 | onTap: (index) { 56 | setState(() { 57 | _history.remove(selectedTab); 58 | _history.add(selectedTab); 59 | selectedTab = index; 60 | }); 61 | }, 62 | currentIndex: selectedTab, 63 | items: [ 64 | const BottomNavigationBarItem( 65 | 66 | icon: Icon(CupertinoIcons.home), label: 'خانه'), 67 | BottomNavigationBarItem( 68 | icon: Stack( 69 | clipBehavior: Clip.none, 70 | children: [ 71 | const Icon(CupertinoIcons.cart), 72 | // Positioned( 73 | // right: -10, 74 | // child: ValueListenableBuilder( 75 | // valueListenable: 76 | // CartRepository.cartItemCountNotifier, 77 | // builder: (context, value, child) { 78 | // return Badge(value: value); 79 | // })) 80 | ], 81 | ), 82 | label: 'سبد خرید'), 83 | const BottomNavigationBarItem( 84 | icon: Icon(CupertinoIcons.person), label: 'پروفایل'), 85 | ]), 86 | body: IndexedStack( 87 | index: selectedTab, 88 | children: [ 89 | _navigator(_homeKey, homeIndex, const HomeScreen()), 90 | _navigator(_cartKey, cartIndex, const CartScreen()), 91 | _navigator(_profileKey, profileIndex, const ProfileScreen()), 92 | ], 93 | ), 94 | ), 95 | onWillPop: _onWillPop, 96 | ); 97 | } 98 | 99 | Widget _navigator(GlobalKey key, int index, Widget widget) { 100 | return key.currentState == null && selectedTab != index 101 | ? Container() 102 | : Navigator( 103 | key: key, 104 | onGenerateRoute: (settings) => MaterialPageRoute( 105 | builder: (context) => 106 | Offstage(offstage: selectedTab != index, child: widget))); 107 | } 108 | 109 | @override 110 | void initState() { 111 | cartRepository.count(); 112 | super.initState(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | nike_ecommerce 33 | 34 | 35 | 36 | 39 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: nike_ecommerce 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.16.2 <3.0.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | 33 | 34 | # The following adds the Cupertino Icons font to your application. 35 | # Use with the CupertinoIcons class for iOS style icons. 36 | cupertino_icons: ^1.0.2 37 | dio: ^4.0.6 38 | flutter_bloc: ^8.0.1 39 | equatable: ^2.0.3 40 | cached_network_image: ^3.2.0 41 | smooth_page_indicator: ^1.0.0+2 42 | shared_preferences: ^2.0.13 43 | flutter_svg: ^1.0.3 44 | pull_to_refresh: ^2.0.0 45 | intl: ^0.17.0 46 | webview_flutter: ^3.0.4 47 | hive_flutter: ^1.1.0 48 | 49 | dev_dependencies: 50 | flutter_test: 51 | sdk: flutter 52 | 53 | # The "flutter_lints" package below contains a set of recommended lints to 54 | # encourage good coding practices. The lint set provided by the package is 55 | # activated in the `analysis_options.yaml` file located at the root of your 56 | # package. See that file for information about deactivating specific lint 57 | # rules and activating additional ones. 58 | flutter_lints: ^1.0.0 59 | hive_generator: ^1.1.3 60 | build_runner: ^2.1.11 61 | 62 | # For information on the generic Dart part of this file, see the 63 | # following page: https://dart.dev/tools/pub/pubspec 64 | 65 | # The following section is specific to Flutter. 66 | flutter: 67 | 68 | # The following line ensures that the Material Icons font is 69 | # included with your application, so that you can use the icons in 70 | # the material Icons class. 71 | uses-material-design: true 72 | 73 | # To add assets to your application, add an assets section, like this: 74 | assets: 75 | - assets/img/ 76 | 77 | # An image asset can refer to one or more resolution-specific "variants", see 78 | # https://flutter.dev/assets-and-images/#resolution-aware. 79 | 80 | # For details regarding adding assets from package dependencies, see 81 | # https://flutter.dev/assets-and-images/#from-packages 82 | 83 | # To add custom fonts to your application, add a fonts section here, 84 | # in this "flutter" section. Each entry in this list should have a 85 | # "family" key with the font family name, and a "fonts" key with a 86 | # list giving the asset and other descriptors for the font. For 87 | # example: 88 | fonts: 89 | - family: IranYekan 90 | fonts: 91 | - asset: assets/fonts/regular.ttf 92 | - asset: assets/fonts/bold.ttf 93 | weight: 700 94 | # - family: Trajan Pro 95 | # fonts: 96 | # - asset: fonts/TrajanPro.ttf 97 | # - asset: fonts/TrajanPro_Bold.ttf 98 | # weight: 700 99 | # 100 | # For details regarding fonts from package dependencies, 101 | # see https://flutter.dev/custom-fonts/#from-packages 102 | -------------------------------------------------------------------------------- /lib/ui/favorites/favorites.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hive_flutter/adapters.dart'; 3 | import 'package:nike_ecommerce/common/utils.dart'; 4 | import 'package:nike_ecommerce/data/product.dart'; 5 | import 'package:nike_ecommerce/data/favorite_manager.dart'; 6 | import 'package:nike_ecommerce/theme.dart'; 7 | import 'package:nike_ecommerce/ui/product/details.dart'; 8 | import 'package:nike_ecommerce/ui/widgets/image.dart'; 9 | 10 | class FavoriteListScreen extends StatelessWidget { 11 | const FavoriteListScreen({Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text('لیست علاقه مندی ها'), 18 | centerTitle: true, 19 | ), 20 | body: ValueListenableBuilder>( 21 | valueListenable: favoriteManager.listenable, 22 | builder: (context, box, child) { 23 | final products = box.values.toList(); 24 | return ListView.builder( 25 | padding: const EdgeInsets.only(top: 8, bottom: 100), 26 | itemCount: products.length, 27 | itemBuilder: (context, index) { 28 | final product = products[index]; 29 | return Padding( 30 | padding: const EdgeInsets.only( 31 | top: 5, bottom: 5, left: 16, right: 16), 32 | child: InkWell( 33 | onLongPress: () { 34 | favoriteManager.deleteFavorite(product); 35 | }, 36 | onTap: () { 37 | Navigator.of(context).push(MaterialPageRoute( 38 | builder: (context) => 39 | ProductDetailScreen(product: product))); 40 | }, 41 | child: SizedBox( 42 | height: 120, 43 | child: Row( 44 | children: [ 45 | SizedBox( 46 | width: 110, 47 | height: 110, 48 | child: ImageLoadingService( 49 | imageUrl: product.imageUrl, 50 | borderRadius: BorderRadius.circular(8), 51 | )), 52 | Expanded( 53 | child: Padding( 54 | padding: const EdgeInsets.only( 55 | top: 8, bottom: 8, left: 8, right: 16), 56 | child: Column( 57 | crossAxisAlignment: CrossAxisAlignment.start, 58 | children: [ 59 | Text( 60 | product.title, 61 | style: Theme.of(context) 62 | .textTheme 63 | .subtitle1! 64 | .copyWith( 65 | color: LightThemeColors 66 | .primaryTextColor, 67 | fontSize: 14), 68 | ), 69 | const SizedBox( 70 | height: 8, 71 | ), 72 | Text( 73 | product.previousPrice.withPriceLabel, 74 | style: Theme.of(context) 75 | .textTheme 76 | .caption! 77 | .apply( 78 | decoration: 79 | TextDecoration.lineThrough), 80 | ), 81 | Text(product.price.withPriceLabel) 82 | ]), 83 | ), 84 | ) 85 | ], 86 | ), 87 | ), 88 | ), 89 | ); 90 | }, 91 | ); 92 | }), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/ui/cart/bloc/cart_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:nike_ecommerce/common/exceptions.dart'; 5 | import 'package:nike_ecommerce/data/auth_info.dart'; 6 | import 'package:nike_ecommerce/data/cart_response.dart'; 7 | import 'package:nike_ecommerce/data/repo/cart_repository.dart'; 8 | 9 | part 'cart_event.dart'; 10 | part 'cart_state.dart'; 11 | 12 | class CartBloc extends Bloc { 13 | final ICartRepository repository; 14 | 15 | CartBloc(this.repository) : super(CartLoading()) { 16 | on((event, emit) async { 17 | if (event is CartStarted) { 18 | final authInfo = event.authInfo; 19 | if (authInfo == null || authInfo.accessToken.isEmpty) { 20 | emit(CartAuthRequired()); 21 | } else { 22 | await loadCartItems(emit, event.isRefreshing); 23 | } 24 | } else if (event is CartDeleteButtonClicked) { 25 | try { 26 | if (state is CartSuccess) { 27 | final successState = (state as CartSuccess); 28 | final cartItem = successState.cartResponse.cartItems 29 | .firstWhere((element) => element.id == event.cartItemId); 30 | cartItem.deleteButtonLaoding = true; 31 | emit(CartSuccess(successState.cartResponse)); 32 | } 33 | await repository.delete(event.cartItemId); 34 | await repository.count(); 35 | if (state is CartSuccess) { 36 | final successState = (state as CartSuccess); 37 | successState.cartResponse.cartItems 38 | .removeWhere((element) => element.id == event.cartItemId); 39 | if (successState.cartResponse.cartItems.isEmpty) { 40 | emit(CartEmpty()); 41 | } else { 42 | emit(calculatePriceInfo(successState.cartResponse)); 43 | } 44 | } 45 | } catch (e) { 46 | debugPrint(e.toString()); 47 | } 48 | } else if (event is CartAuthInfoChanged) { 49 | if (event.authInfo == null || event.authInfo!.accessToken.isEmpty) { 50 | emit(CartAuthRequired()); 51 | } else { 52 | if (state is CartAuthRequired) { 53 | await loadCartItems(emit, false); 54 | } 55 | } 56 | } else if (event is CartPlusButtonClicked || 57 | event is CartMinusButtonClicked) { 58 | try { 59 | int cartItemId = 0; 60 | if (event is CartPlusButtonClicked) { 61 | cartItemId = event.cartItemId; 62 | } else if (event is CartMinusButtonClicked) { 63 | cartItemId = event.cartItemId; 64 | } 65 | 66 | if (state is CartSuccess) { 67 | final successState = (state as CartSuccess); 68 | final cartItem = successState.cartResponse.cartItems 69 | .firstWhere((element) => element.id == cartItemId); 70 | cartItem.changeCountLoading = true; 71 | emit(CartSuccess(successState.cartResponse)); 72 | 73 | int newwCount = event is CartPlusButtonClicked 74 | ? cartItem.count + 1 75 | : cartItem.count - 1; 76 | await repository.changeCount(cartItemId, newwCount); 77 | await repository.count(); 78 | 79 | cartItem 80 | ..count = newwCount 81 | ..changeCountLoading = false; 82 | emit(calculatePriceInfo(successState.cartResponse)); 83 | } 84 | } catch (e) { 85 | debugPrint(e.toString()); 86 | } 87 | } 88 | }); 89 | } 90 | 91 | Future loadCartItems(Emitter emit, bool isRefreshing) async { 92 | try { 93 | if (!isRefreshing) { 94 | emit(CartLoading()); 95 | } 96 | final result = await repository.getAll(); 97 | if (result.cartItems.isEmpty) { 98 | emit(CartEmpty()); 99 | } else { 100 | emit(CartSuccess(result)); 101 | } 102 | } catch (e) { 103 | emit(CartError(AppException())); 104 | } 105 | } 106 | 107 | CartSuccess calculatePriceInfo(CartResponse cartResponse) { 108 | int payablePrice = 0; 109 | int totalPrice = 0; 110 | int shippingCost = 0; 111 | 112 | cartResponse.cartItems.forEach((cartItem) { 113 | payablePrice += cartItem.product.price * cartItem.count; 114 | totalPrice += cartItem.product.previousPrice * cartItem.count; 115 | }); 116 | 117 | shippingCost = payablePrice >= 250000 ? 0 : 30000; 118 | 119 | cartResponse.payablePrice = payablePrice; 120 | cartResponse.totalPrice = totalPrice; 121 | cartResponse.shippingCost = shippingCost; 122 | 123 | return CartSuccess(cartResponse); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/ui/product/product.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:hive_flutter/hive_flutter.dart'; 4 | import 'package:nike_ecommerce/common/utils.dart'; 5 | import 'package:nike_ecommerce/data/product.dart'; 6 | import 'package:nike_ecommerce/data/favorite_manager.dart'; 7 | import 'package:nike_ecommerce/ui/product/details.dart'; 8 | import 'package:nike_ecommerce/ui/widgets/image.dart'; 9 | 10 | class ProductItem extends StatefulWidget { 11 | const ProductItem({ 12 | Key? key, 13 | required this.product, 14 | required this.borderRadius, 15 | this.itemWidth = 176, 16 | this.itemHeight = 189, 17 | }) : super(key: key); 18 | 19 | final Product product; 20 | final BorderRadius borderRadius; 21 | final double itemWidth; 22 | final double itemHeight; 23 | 24 | @override 25 | State createState() => _ProductItemState(); 26 | } 27 | 28 | class _ProductItemState extends State { 29 | @override 30 | Widget build(BuildContext context) { 31 | return Padding( 32 | padding: const EdgeInsets.all(4.0), 33 | child: ValueListenableBuilder>( 34 | valueListenable: favoriteManager.listenable, 35 | builder: (context, box, child) { 36 | return InkWell( 37 | borderRadius: widget.borderRadius, 38 | onTap: () => Navigator.of(context).push(MaterialPageRoute( 39 | builder: (context) => ProductDetailScreen( 40 | product: widget.product, 41 | ))), 42 | child: SizedBox( 43 | width: widget.itemWidth, 44 | child: Column( 45 | crossAxisAlignment: CrossAxisAlignment.start, 46 | children: [ 47 | Stack( 48 | children: [ 49 | AspectRatio( 50 | aspectRatio: 0.93, 51 | child: ImageLoadingService( 52 | imageUrl: widget.product.imageUrl, 53 | borderRadius: widget.borderRadius, 54 | ), 55 | ), 56 | Positioned( 57 | top: 8, 58 | right: 8, 59 | child: InkWell( 60 | onTap: () { 61 | if (!favoriteManager.isFavorite(widget.product)) { 62 | favoriteManager.addFavorite(widget.product); 63 | } else { 64 | favoriteManager.deleteFavorite(widget.product); 65 | } 66 | 67 | }, 68 | child: Container( 69 | alignment: Alignment.center, 70 | width: 32, 71 | height: 32, 72 | decoration: const BoxDecoration( 73 | color: Colors.white, shape: BoxShape.circle), 74 | child: Icon( 75 | favoriteManager.isFavorite(widget.product) 76 | ? CupertinoIcons.heart_fill 77 | : CupertinoIcons.heart, 78 | size: 20, 79 | ), 80 | ), 81 | ), 82 | ) 83 | ], 84 | ), 85 | Padding( 86 | padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), 87 | child: Text( 88 | widget.product.title, 89 | maxLines: 1, 90 | overflow: TextOverflow.ellipsis, 91 | ), 92 | ), 93 | Padding( 94 | padding: const EdgeInsets.only(left: 8, right: 8, top: 2), 95 | child: Text( 96 | widget.product.previousPrice.withPriceLabel, 97 | style: Theme.of(context) 98 | .textTheme 99 | .caption! 100 | .copyWith(decoration: TextDecoration.lineThrough), 101 | ), 102 | ), 103 | Padding( 104 | padding: const EdgeInsets.only(left: 8, right: 8), 105 | child: Text(widget.product.price.withPriceLabel), 106 | ), 107 | ], 108 | ), 109 | ), 110 | ); 111 | }), 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/ui/order/order_history.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/rendering.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:nike_ecommerce/common/utils.dart'; 6 | import 'package:nike_ecommerce/data/order.dart'; 7 | import 'package:nike_ecommerce/data/repo/order_repository.dart'; 8 | import 'package:nike_ecommerce/ui/order/bloc/order_history_bloc.dart'; 9 | import 'package:nike_ecommerce/ui/product/details.dart'; 10 | import 'package:nike_ecommerce/ui/widgets/image.dart'; 11 | 12 | class OrderHistoryScreen extends StatelessWidget { 13 | const OrderHistoryScreen({Key? key}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return BlocProvider( 18 | create: (context) => 19 | OrderHistoryBloc(orderRepository)..add(OrderHistoryStarted()), 20 | child: Scaffold( 21 | appBar: AppBar( 22 | title: const Text('سوابق سفارش'), 23 | centerTitle: true, 24 | ), 25 | body: BlocBuilder( 26 | builder: (context, state) { 27 | if (state is OrderHistorySuccess) { 28 | final orders = state.orderList; 29 | return ListView.builder( 30 | itemCount: orders.length, 31 | itemBuilder: (context, index) { 32 | final order = orders[index]; 33 | return _OrderItem(order: order); 34 | }); 35 | } else if (state is OrderHistoryError) { 36 | return Center( 37 | child: Text(state.appException.message), 38 | ); 39 | } else if (state is OrderHistoryLoading) { 40 | return const Center( 41 | child: CupertinoActivityIndicator(), 42 | ); 43 | } else { 44 | throw Exception('state is not supprted.'); 45 | } 46 | }, 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | 53 | class _OrderItem extends StatelessWidget { 54 | const _OrderItem({ 55 | Key? key, 56 | required this.order, 57 | }) : super(key: key); 58 | 59 | final OrderEntity order; 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return Container( 64 | margin: const EdgeInsets.fromLTRB(16, 8, 16, 8), 65 | decoration: BoxDecoration( 66 | borderRadius: BorderRadius.circular(8), 67 | border: Border.all(color: Theme.of(context).dividerColor, width: 1)), 68 | child: Column( 69 | children: [ 70 | Container( 71 | padding: const EdgeInsets.only(left: 16, right: 16), 72 | child: SizedBox( 73 | height: 50, 74 | child: Row( 75 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 76 | children: [ 77 | const Text('شناسه سفارش'), 78 | Text(order.id.toString()) 79 | ], 80 | ), 81 | ), 82 | ), 83 | const Divider( 84 | height: 1, 85 | ), 86 | Container( 87 | padding: const EdgeInsets.only(left: 16, right: 16), 88 | child: SizedBox( 89 | height: 50, 90 | child: Row( 91 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 92 | children: [ 93 | const Text('مبلغ'), 94 | Text(order.payablePrice.withPriceLabel) 95 | ], 96 | ), 97 | ), 98 | ), 99 | const Divider( 100 | height: 1, 101 | ), 102 | SizedBox( 103 | height: 120, 104 | child: ListView.builder( 105 | padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), 106 | scrollDirection: Axis.horizontal, 107 | itemCount: order.items.length, 108 | itemBuilder: (context, index) { 109 | final product = order.items[index]; 110 | return InkWell( 111 | onTap: () { 112 | Navigator.of(context).push(MaterialPageRoute( 113 | builder: (context) => ProductDetailScreen(product: product,))); 114 | }, 115 | child: Container( 116 | width: 100, 117 | height: 100, 118 | margin: const EdgeInsets.only(left: 4, right: 4), 119 | child: ImageLoadingService( 120 | imageUrl: product.imageUrl, 121 | borderRadius: BorderRadius.circular(8), 122 | ), 123 | ), 124 | ); 125 | }), 126 | ) 127 | ], 128 | ), 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/ui/receipt/receipt.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:nike_ecommerce/common/utils.dart'; 5 | import 'package:nike_ecommerce/data/repo/order_repository.dart'; 6 | import 'package:nike_ecommerce/theme.dart'; 7 | import 'package:nike_ecommerce/ui/receipt/bloc/payment_receipt_bloc.dart'; 8 | 9 | class ReceiptScreen extends StatelessWidget { 10 | final int orderId; 11 | const ReceiptScreen({Key? key, required this.orderId}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final themeData = Theme.of(context); 16 | return Scaffold( 17 | appBar: AppBar( 18 | title: const Text('رسید پرداخت'), 19 | centerTitle: true, 20 | ), 21 | body: BlocProvider( 22 | create: (context) => PaymentReceiptBloc(orderRepository) 23 | ..add(PaymentReceiptStarted(orderId)), 24 | child: BlocBuilder( 25 | builder: (context, state) { 26 | if (state is PaymentReceiptSuccess) { 27 | return Column( 28 | children: [ 29 | const SizedBox( 30 | height: 32, 31 | ), 32 | Container( 33 | padding: const EdgeInsets.all(12), 34 | margin: const EdgeInsets.all(12), 35 | decoration: BoxDecoration( 36 | border: 37 | Border.all(color: themeData.dividerColor, width: 1), 38 | borderRadius: BorderRadius.circular(8)), 39 | child: Column( 40 | children: [ 41 | Text( 42 | state.paymentReceipt.purchaseSuccess 43 | ? 'پرداخت با موفقیت انجام شد' 44 | : 'پرداخت ناموفق', 45 | style: themeData.textTheme.headline6!.copyWith( 46 | color: themeData.colorScheme.primary, 47 | fontSize: 16), 48 | ), 49 | const SizedBox( 50 | height: 24, 51 | ), 52 | Row( 53 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 54 | crossAxisAlignment: CrossAxisAlignment.center, 55 | children: [ 56 | const Text( 57 | 'وضعیت سفارش', 58 | style: TextStyle( 59 | color: LightThemeColors.secondryTextColor), 60 | ), 61 | Text( 62 | state.paymentReceipt.paymentStatus, 63 | style: const TextStyle( 64 | fontWeight: FontWeight.bold, fontSize: 15), 65 | ) 66 | ], 67 | ), 68 | const Divider( 69 | height: 24, 70 | thickness: 1, 71 | ), 72 | Row( 73 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 74 | crossAxisAlignment: CrossAxisAlignment.center, 75 | children: [ 76 | const Text( 77 | 'مبلغ سفارش', 78 | style: TextStyle( 79 | color: LightThemeColors.secondryTextColor), 80 | ), 81 | Text( 82 | state.paymentReceipt.payablePrice.withPriceLabel, 83 | style: const TextStyle( 84 | fontWeight: FontWeight.bold, fontSize: 15), 85 | ) 86 | ], 87 | ), 88 | ], 89 | ), 90 | ), 91 | const SizedBox( 92 | height: 24, 93 | ), 94 | ElevatedButton( 95 | onPressed: () { 96 | Navigator.of(context) 97 | .popUntil((route) => route.isFirst); 98 | }, 99 | child: const Text('بازگشت به صفحه اصلی')) 100 | ], 101 | ); 102 | } else if (state is PaymentReceiptError) { 103 | return Center( 104 | child: Text(state.appException.message), 105 | ); 106 | } else if (state is PaymentReceiptLoading) { 107 | return const Center( 108 | child: CupertinoActivityIndicator(), 109 | ); 110 | } else { 111 | throw Exception('state is not supported'); 112 | } 113 | }, 114 | ), 115 | ), 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/ui/cart/cart_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:nike_ecommerce/common/utils.dart'; 4 | import 'package:nike_ecommerce/data/cart_item.dart'; 5 | import 'package:nike_ecommerce/ui/widgets/image.dart'; 6 | 7 | class CartItem extends StatelessWidget { 8 | final CartItemEntity item; 9 | final GestureTapCallback ondeleteButtonClick; 10 | final GestureTapCallback onPlusButtonClcick; 11 | final GestureTapCallback onMinusButtonClick; 12 | 13 | const CartItem({ 14 | Key? key, 15 | required this.item, 16 | required this.ondeleteButtonClick, 17 | required this.onPlusButtonClcick, 18 | required this.onMinusButtonClick, 19 | }) : super(key: key); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Container( 24 | margin: const EdgeInsets.all(4), 25 | decoration: BoxDecoration( 26 | color: Theme.of(context).colorScheme.surface, 27 | borderRadius: BorderRadius.circular(8), 28 | boxShadow: [ 29 | BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10) 30 | ]), 31 | child: Column( 32 | children: [ 33 | Padding( 34 | padding: const EdgeInsets.all(8), 35 | child: Row( 36 | children: [ 37 | SizedBox( 38 | width: 100, 39 | height: 100, 40 | child: ImageLoadingService( 41 | imageUrl: item.product.imageUrl, 42 | borderRadius: BorderRadius.circular(8), 43 | )), 44 | Expanded( 45 | child: Padding( 46 | padding: const EdgeInsets.all(8.0), 47 | child: Text( 48 | item.product.title, 49 | style: const TextStyle(fontSize: 15), 50 | ), 51 | ), 52 | ) 53 | ], 54 | ), 55 | ), 56 | Padding( 57 | padding: const EdgeInsets.only(left: 8, right: 8), 58 | child: Row( 59 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 60 | children: [ 61 | Column( 62 | mainAxisAlignment: MainAxisAlignment.center, 63 | children: [ 64 | const Text( 65 | 'تعداد', 66 | ), 67 | Row( 68 | children: [ 69 | IconButton( 70 | onPressed: onPlusButtonClcick, 71 | icon: const Icon( 72 | CupertinoIcons.plus_rectangle, 73 | color: Colors.grey, 74 | )), 75 | item.changeCountLoading 76 | ? SizedBox( 77 | width: 20, 78 | child: CupertinoActivityIndicator( 79 | color: Theme.of(context) 80 | .colorScheme 81 | .onBackground, 82 | )) 83 | : SizedBox( 84 | width: 20, 85 | child: Center( 86 | child: Text(item.count.toString(), 87 | style: Theme.of(context) 88 | .textTheme 89 | .headline6! 90 | .copyWith(fontSize: 14)), 91 | ), 92 | ), 93 | IconButton( 94 | onPressed: onMinusButtonClick, 95 | icon: const Icon( 96 | CupertinoIcons.minus_rectangle, 97 | color: Colors.grey, 98 | )) 99 | ], 100 | ), 101 | ], 102 | ), 103 | Column( 104 | mainAxisAlignment: MainAxisAlignment.center, 105 | children: [ 106 | Text( 107 | item.product.previousPrice.withPriceLabel, 108 | style: Theme.of(context) 109 | .textTheme 110 | .caption! 111 | .copyWith(decoration: TextDecoration.lineThrough), 112 | ), 113 | Text(item.product.price.withPriceLabel) 114 | ], 115 | ) 116 | ], 117 | ), 118 | ), 119 | const Divider( 120 | height: 1, 121 | ), 122 | item.deleteButtonLaoding 123 | ? const SizedBox( 124 | height: 48, 125 | child: Center(child: CupertinoActivityIndicator())) 126 | : TextButton( 127 | onPressed: ondeleteButtonClick, 128 | child: const Text('حذف از سبد خرید')) 129 | ], 130 | ), 131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/ui/home/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:nike_ecommerce/common/utils.dart'; 4 | import 'package:nike_ecommerce/data/product.dart'; 5 | import 'package:nike_ecommerce/data/repo/banner_repository.dart'; 6 | import 'package:nike_ecommerce/data/repo/product_repository.dart'; 7 | import 'package:nike_ecommerce/ui/home/bloc/home_bloc.dart'; 8 | import 'package:nike_ecommerce/ui/list/product_list.dart'; 9 | import 'package:nike_ecommerce/ui/product/product.dart'; 10 | import 'package:nike_ecommerce/ui/widgets/error.dart'; 11 | import 'package:nike_ecommerce/ui/widgets/slider.dart'; 12 | 13 | class HomeScreen extends StatelessWidget { 14 | const HomeScreen({Key? key}) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return BlocProvider( 19 | create: (context) { 20 | final homeBloc = HomeBloc( 21 | bannerRepository: bannerRepository, 22 | productRepository: productRepository); 23 | homeBloc.add(HomeStarted()); 24 | return homeBloc; 25 | }, 26 | child: Scaffold( 27 | body: SafeArea( 28 | child: BlocBuilder( 29 | builder: (context, state) { 30 | if (state is HomeSuccess) { 31 | return ListView.builder( 32 | physics: defaultScrollPhsics, 33 | itemCount: 5, 34 | itemBuilder: ((context, index) { 35 | switch (index) { 36 | case 0: 37 | return Container( 38 | height: 56, 39 | alignment: Alignment.center, 40 | child: Image.asset( 41 | 'assets/img/nike_logo.png', 42 | height: 23, 43 | fit: BoxFit.fitHeight, 44 | ), 45 | ); 46 | case 2: 47 | return BannerSlider( 48 | banners: state.banners, 49 | ); 50 | case 3: 51 | return _HorizontalProductList( 52 | title: 'جدید ترین', 53 | products: state.latestProducts, 54 | onTap: () { 55 | Navigator.of(context).push(MaterialPageRoute( 56 | builder: ((context) => 57 | const ProductListScreen( 58 | sort: ProductSort.latest)))); 59 | }, 60 | ); 61 | case 4: 62 | return _HorizontalProductList( 63 | title: 'پربازدید ترین', 64 | products: state.popularProducts, 65 | onTap: () { 66 | Navigator.of(context).push(MaterialPageRoute( 67 | builder: ((context) => 68 | const ProductListScreen( 69 | sort: ProductSort.popular)))); 70 | }, 71 | ); 72 | default: 73 | return Container(); 74 | } 75 | })); 76 | } else if (state is HomeLoading) { 77 | return const Center( 78 | child: CircularProgressIndicator(), 79 | ); 80 | } else if (state is HomeError) { 81 | return AppErrorWidget( 82 | exception: state.exception, 83 | onPressed: () { 84 | BlocProvider.of(context).add(HomeRefresh()); 85 | }, 86 | ); 87 | } else { 88 | throw Exception('state is not supported.'); 89 | } 90 | }, 91 | ), 92 | ), 93 | ), 94 | ); 95 | } 96 | } 97 | 98 | class _HorizontalProductList extends StatelessWidget { 99 | final String title; 100 | final GestureTapCallback onTap; 101 | final List products; 102 | 103 | const _HorizontalProductList({ 104 | Key? key, 105 | required this.title, 106 | required this.onTap, 107 | required this.products, 108 | }) : super(key: key); 109 | 110 | @override 111 | Widget build(BuildContext context) { 112 | return Column( 113 | children: [ 114 | Padding( 115 | padding: const EdgeInsets.only(left: 12, right: 12), 116 | child: Row( 117 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 118 | children: [ 119 | Text( 120 | title, 121 | style: Theme.of(context).textTheme.subtitle1, 122 | ), 123 | TextButton(onPressed: onTap, child: const Text('مشاهده همه')) 124 | ], 125 | ), 126 | ), 127 | SizedBox( 128 | height: 290, 129 | child: ListView.builder( 130 | physics: defaultScrollPhsics, 131 | padding: const EdgeInsets.only(left: 8, right: 8), 132 | itemCount: products.length, 133 | scrollDirection: Axis.horizontal, 134 | itemBuilder: ((context, index) { 135 | final product = products[index]; 136 | return ProductItem( 137 | product: product, 138 | borderRadius: BorderRadius.circular(12), 139 | ); 140 | })), 141 | ) 142 | ], 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /lib/ui/profile/profile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:nike_ecommerce/data/auth_info.dart'; 4 | import 'package:nike_ecommerce/data/repo/auth_repository.dart'; 5 | import 'package:nike_ecommerce/data/repo/cart_repository.dart'; 6 | import 'package:nike_ecommerce/ui/auth/auth.dart'; 7 | import 'package:nike_ecommerce/ui/favorites/favorites.dart'; 8 | import 'package:nike_ecommerce/ui/order/order_history.dart'; 9 | 10 | class ProfileScreen extends StatelessWidget { 11 | const ProfileScreen({Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text('پروفایل'), 18 | centerTitle: true, 19 | ), 20 | body: ValueListenableBuilder( 21 | valueListenable: AuthRepository.authChangeNotifire, 22 | builder: (context, authinfo, childe) { 23 | final isLogin = authinfo != null && authinfo.accessToken.isNotEmpty; 24 | return Center( 25 | child: Column( 26 | crossAxisAlignment: CrossAxisAlignment.center, 27 | children: [ 28 | Container( 29 | margin: const EdgeInsets.only(top: 24, bottom: 10), 30 | padding: const EdgeInsets.all(8), 31 | width: 64, 32 | height: 64, 33 | decoration: BoxDecoration( 34 | shape: BoxShape.circle, 35 | border: Border.all( 36 | color: Theme.of(context).dividerColor, width: 1)), 37 | child: Image.asset('assets/img/nike_logo.png')), 38 | Text(isLogin ? authinfo.email : 'کاربر مهمان'), 39 | const SizedBox( 40 | height: 32, 41 | ), 42 | const Divider( 43 | height: 1, 44 | ), 45 | InkWell( 46 | onTap: () { 47 | Navigator.of(context).push(MaterialPageRoute(builder: (context) => const FavoriteListScreen())); 48 | }, 49 | child: Container( 50 | height: 56, 51 | padding: const EdgeInsets.only(left: 16, right: 16), 52 | child: Row( 53 | children: [ 54 | Row( 55 | children: const [ 56 | Icon(CupertinoIcons.heart), 57 | SizedBox( 58 | width: 16, 59 | ), 60 | Text('لیست علاقه مندی ها') 61 | ], 62 | ), 63 | ], 64 | ), 65 | ), 66 | ), 67 | const Divider( 68 | height: 1, 69 | ), 70 | InkWell( 71 | onTap: () { 72 | Navigator.of(context).push(MaterialPageRoute(builder: (context) => const OrderHistoryScreen())); 73 | }, 74 | child: Container( 75 | height: 56, 76 | padding: const EdgeInsets.only(left: 16, right: 16), 77 | child: Row( 78 | children: [ 79 | Row( 80 | children: const [ 81 | Icon(CupertinoIcons.cart), 82 | SizedBox( 83 | width: 16, 84 | ), 85 | Text('سوابق سفارش') 86 | ], 87 | ), 88 | ], 89 | ), 90 | ), 91 | ), 92 | const Divider( 93 | height: 1, 94 | ), 95 | InkWell( 96 | onTap: () { 97 | isLogin 98 | ? showDialog( 99 | context: context, 100 | useRootNavigator: true, 101 | builder: (context) { 102 | return Directionality( 103 | textDirection: TextDirection.rtl, 104 | child: AlertDialog( 105 | title: const Text('خروج از حساب کاربری'), 106 | content: const Text( 107 | 'آیا می خواهید از حساب خود خارج شوید؟'), 108 | actions: [ 109 | TextButton( 110 | onPressed: () { 111 | Navigator.pop(context); 112 | CartRepository.cartItemCountNotifier 113 | .value = 0; 114 | authRepository.signOut(); 115 | }, 116 | child: const Text('بله')), 117 | TextButton( 118 | onPressed: () { 119 | Navigator.pop(context); 120 | }, 121 | child: const Text('خیر')), 122 | ], 123 | ), 124 | ); 125 | }) 126 | : Navigator.of(context, rootNavigator: true).push( 127 | MaterialPageRoute( 128 | builder: (context) => const AuthScreen())); 129 | }, 130 | child: Container( 131 | height: 56, 132 | padding: const EdgeInsets.only(left: 16, right: 16), 133 | child: Row( 134 | children: [ 135 | Row( 136 | children: [ 137 | Icon(isLogin 138 | ? CupertinoIcons.arrow_right_square 139 | : CupertinoIcons.arrow_left_square), 140 | const SizedBox( 141 | width: 16, 142 | ), 143 | Text(isLogin 144 | ? 'خروج از حساب کاربری' 145 | : 'ورود به حساب کاربری') 146 | ], 147 | ), 148 | ], 149 | ), 150 | ), 151 | ), 152 | const Divider( 153 | height: 1, 154 | ), 155 | ], 156 | ), 157 | ); 158 | }), 159 | ); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /lib/ui/product/details.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:nike_ecommerce/common/utils.dart'; 7 | import 'package:nike_ecommerce/data/product.dart'; 8 | import 'package:nike_ecommerce/data/repo/cart_repository.dart'; 9 | import 'package:nike_ecommerce/data/favorite_manager.dart'; 10 | import 'package:nike_ecommerce/theme.dart'; 11 | import 'package:nike_ecommerce/ui/product/bloc/product_bloc.dart'; 12 | import 'package:nike_ecommerce/ui/product/comment/comment_list.dart'; 13 | import 'package:nike_ecommerce/ui/widgets/image.dart'; 14 | 15 | class ProductDetailScreen extends StatefulWidget { 16 | final Product product; 17 | 18 | const ProductDetailScreen({Key? key, required this.product}) 19 | : super(key: key); 20 | 21 | @override 22 | State createState() => _ProductDetailScreenState(); 23 | } 24 | 25 | class _ProductDetailScreenState extends State { 26 | StreamSubscription? streamSubscription; 27 | final GlobalKey _scaffoldKey = GlobalKey(); 28 | 29 | @override 30 | void dispose() { 31 | streamSubscription?.cancel(); 32 | _scaffoldKey.currentState?.dispose(); 33 | super.dispose(); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return Directionality( 39 | textDirection: TextDirection.rtl, 40 | child: BlocProvider( 41 | create: (context) { 42 | final bloc = ProductBloc(cartRepository); 43 | streamSubscription = bloc.stream.listen((state) { 44 | if (state is ProductAddToCartSuccess) { 45 | _scaffoldKey.currentState?.showSnackBar(const SnackBar( 46 | content: Text('با موفقیت به سبد خرید اضافه شد'))); 47 | } else if (state is ProductAddToCartError) { 48 | _scaffoldKey.currentState?.showSnackBar( 49 | SnackBar(content: Text(state.exception.message))); 50 | } 51 | }); 52 | 53 | return bloc; 54 | }, 55 | child: ScaffoldMessenger( 56 | key: _scaffoldKey, 57 | child: Scaffold( 58 | floatingActionButtonLocation: 59 | FloatingActionButtonLocation.centerFloat, 60 | floatingActionButton: SizedBox( 61 | width: MediaQuery.of(context).size.width - 48, 62 | child: BlocBuilder( 63 | builder: (context, state) => FloatingActionButton.extended( 64 | onPressed: () { 65 | BlocProvider.of(context) 66 | .add(CartAddButtonClick(widget.product.id)); 67 | }, 68 | label: state is ProductAddToCartLoading 69 | ? CupertinoActivityIndicator( 70 | color: Theme.of(context).colorScheme.onSecondary, 71 | ) 72 | : const Text('افزودن به سبد خرید')), 73 | ), 74 | ), 75 | body: SafeArea( 76 | child: CustomScrollView( 77 | physics: defaultScrollPhsics, 78 | slivers: [ 79 | _SliverAppBar(product: widget.product,), 80 | SliverToBoxAdapter( 81 | child: Padding( 82 | padding: const EdgeInsets.all(12), 83 | child: Column( 84 | children: [ 85 | Row( 86 | children: [ 87 | Expanded( 88 | child: Text(widget.product.title, 89 | style: Theme.of(context) 90 | .textTheme 91 | .headline6)), 92 | Column( 93 | crossAxisAlignment: CrossAxisAlignment.end, 94 | children: [ 95 | Text( 96 | widget.product.previousPrice.withPriceLabel, 97 | style: Theme.of(context) 98 | .textTheme 99 | .caption! 100 | .apply( 101 | decoration: 102 | TextDecoration.lineThrough), 103 | ), 104 | Text( 105 | widget.product.price.withPriceLabel, 106 | ) 107 | ], 108 | ) 109 | ], 110 | ), 111 | const SizedBox( 112 | height: 20, 113 | ), 114 | const Text( 115 | 'این کتونی شدیدا برای دویدن و راه رفتن مناسب هست و تقریبا نمیگذارد هیچ فشار مخربی به زانوان و پای شما وارد شود', 116 | ), 117 | const SizedBox( 118 | height: 16, 119 | ), 120 | Row( 121 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 122 | children: [ 123 | Text( 124 | 'نظرات کاربران', 125 | style: Theme.of(context).textTheme.subtitle1, 126 | ), 127 | TextButton( 128 | onPressed: () {}, 129 | child: const Text('ثبت نظر')) 130 | ], 131 | ), 132 | ], 133 | ), 134 | ), 135 | ), 136 | CommentList(productId: widget.product.id) 137 | ], 138 | ), 139 | ), 140 | ), 141 | ), 142 | ), 143 | ); 144 | } 145 | } 146 | 147 | class _SliverAppBar extends StatefulWidget { 148 | const _SliverAppBar({ 149 | Key? key, 150 | required this.product 151 | }) : super(key: key); 152 | 153 | final Product product; 154 | 155 | @override 156 | State<_SliverAppBar> createState() => _SliverAppBarState(); 157 | } 158 | 159 | class _SliverAppBarState extends State<_SliverAppBar> { 160 | @override 161 | Widget build(BuildContext context) { 162 | return SliverAppBar( 163 | foregroundColor: LightThemeColors.secondryColor, 164 | expandedHeight: MediaQuery.of(context).size.width * 0.8, 165 | flexibleSpace: ImageLoadingService( 166 | imageUrl: widget.product.imageUrl, 167 | ), 168 | actions: [ 169 | IconButton( 170 | onPressed: () { 171 | if (favoriteManager.isFavorite(widget.product)) { 172 | favoriteManager.deleteFavorite(widget.product); 173 | } else { 174 | favoriteManager.addFavorite(widget.product); 175 | } 176 | setState(() { 177 | 178 | }); 179 | }, 180 | icon: Icon(favoriteManager.isFavorite(widget.product) 181 | ? CupertinoIcons.heart_fill 182 | : CupertinoIcons.heart)) 183 | ], 184 | ); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /windows/runner/win32_window.cpp: -------------------------------------------------------------------------------- 1 | #include "win32_window.h" 2 | 3 | #include 4 | 5 | #include "resource.h" 6 | 7 | namespace { 8 | 9 | constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; 10 | 11 | // The number of Win32Window objects that currently exist. 12 | static int g_active_window_count = 0; 13 | 14 | using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); 15 | 16 | // Scale helper to convert logical scaler values to physical using passed in 17 | // scale factor 18 | int Scale(int source, double scale_factor) { 19 | return static_cast(source * scale_factor); 20 | } 21 | 22 | // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. 23 | // This API is only needed for PerMonitor V1 awareness mode. 24 | void EnableFullDpiSupportIfAvailable(HWND hwnd) { 25 | HMODULE user32_module = LoadLibraryA("User32.dll"); 26 | if (!user32_module) { 27 | return; 28 | } 29 | auto enable_non_client_dpi_scaling = 30 | reinterpret_cast( 31 | GetProcAddress(user32_module, "EnableNonClientDpiScaling")); 32 | if (enable_non_client_dpi_scaling != nullptr) { 33 | enable_non_client_dpi_scaling(hwnd); 34 | FreeLibrary(user32_module); 35 | } 36 | } 37 | 38 | } // namespace 39 | 40 | // Manages the Win32Window's window class registration. 41 | class WindowClassRegistrar { 42 | public: 43 | ~WindowClassRegistrar() = default; 44 | 45 | // Returns the singleton registar instance. 46 | static WindowClassRegistrar* GetInstance() { 47 | if (!instance_) { 48 | instance_ = new WindowClassRegistrar(); 49 | } 50 | return instance_; 51 | } 52 | 53 | // Returns the name of the window class, registering the class if it hasn't 54 | // previously been registered. 55 | const wchar_t* GetWindowClass(); 56 | 57 | // Unregisters the window class. Should only be called if there are no 58 | // instances of the window. 59 | void UnregisterWindowClass(); 60 | 61 | private: 62 | WindowClassRegistrar() = default; 63 | 64 | static WindowClassRegistrar* instance_; 65 | 66 | bool class_registered_ = false; 67 | }; 68 | 69 | WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; 70 | 71 | const wchar_t* WindowClassRegistrar::GetWindowClass() { 72 | if (!class_registered_) { 73 | WNDCLASS window_class{}; 74 | window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); 75 | window_class.lpszClassName = kWindowClassName; 76 | window_class.style = CS_HREDRAW | CS_VREDRAW; 77 | window_class.cbClsExtra = 0; 78 | window_class.cbWndExtra = 0; 79 | window_class.hInstance = GetModuleHandle(nullptr); 80 | window_class.hIcon = 81 | LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); 82 | window_class.hbrBackground = 0; 83 | window_class.lpszMenuName = nullptr; 84 | window_class.lpfnWndProc = Win32Window::WndProc; 85 | RegisterClass(&window_class); 86 | class_registered_ = true; 87 | } 88 | return kWindowClassName; 89 | } 90 | 91 | void WindowClassRegistrar::UnregisterWindowClass() { 92 | UnregisterClass(kWindowClassName, nullptr); 93 | class_registered_ = false; 94 | } 95 | 96 | Win32Window::Win32Window() { 97 | ++g_active_window_count; 98 | } 99 | 100 | Win32Window::~Win32Window() { 101 | --g_active_window_count; 102 | Destroy(); 103 | } 104 | 105 | bool Win32Window::CreateAndShow(const std::wstring& title, 106 | const Point& origin, 107 | const Size& size) { 108 | Destroy(); 109 | 110 | const wchar_t* window_class = 111 | WindowClassRegistrar::GetInstance()->GetWindowClass(); 112 | 113 | const POINT target_point = {static_cast(origin.x), 114 | static_cast(origin.y)}; 115 | HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); 116 | UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); 117 | double scale_factor = dpi / 96.0; 118 | 119 | HWND window = CreateWindow( 120 | window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, 121 | Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), 122 | Scale(size.width, scale_factor), Scale(size.height, scale_factor), 123 | nullptr, nullptr, GetModuleHandle(nullptr), this); 124 | 125 | if (!window) { 126 | return false; 127 | } 128 | 129 | return OnCreate(); 130 | } 131 | 132 | // static 133 | LRESULT CALLBACK Win32Window::WndProc(HWND const window, 134 | UINT const message, 135 | WPARAM const wparam, 136 | LPARAM const lparam) noexcept { 137 | if (message == WM_NCCREATE) { 138 | auto window_struct = reinterpret_cast(lparam); 139 | SetWindowLongPtr(window, GWLP_USERDATA, 140 | reinterpret_cast(window_struct->lpCreateParams)); 141 | 142 | auto that = static_cast(window_struct->lpCreateParams); 143 | EnableFullDpiSupportIfAvailable(window); 144 | that->window_handle_ = window; 145 | } else if (Win32Window* that = GetThisFromHandle(window)) { 146 | return that->MessageHandler(window, message, wparam, lparam); 147 | } 148 | 149 | return DefWindowProc(window, message, wparam, lparam); 150 | } 151 | 152 | LRESULT 153 | Win32Window::MessageHandler(HWND hwnd, 154 | UINT const message, 155 | WPARAM const wparam, 156 | LPARAM const lparam) noexcept { 157 | switch (message) { 158 | case WM_DESTROY: 159 | window_handle_ = nullptr; 160 | Destroy(); 161 | if (quit_on_close_) { 162 | PostQuitMessage(0); 163 | } 164 | return 0; 165 | 166 | case WM_DPICHANGED: { 167 | auto newRectSize = reinterpret_cast(lparam); 168 | LONG newWidth = newRectSize->right - newRectSize->left; 169 | LONG newHeight = newRectSize->bottom - newRectSize->top; 170 | 171 | SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, 172 | newHeight, SWP_NOZORDER | SWP_NOACTIVATE); 173 | 174 | return 0; 175 | } 176 | case WM_SIZE: { 177 | RECT rect = GetClientArea(); 178 | if (child_content_ != nullptr) { 179 | // Size and position the child window. 180 | MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, 181 | rect.bottom - rect.top, TRUE); 182 | } 183 | return 0; 184 | } 185 | 186 | case WM_ACTIVATE: 187 | if (child_content_ != nullptr) { 188 | SetFocus(child_content_); 189 | } 190 | return 0; 191 | } 192 | 193 | return DefWindowProc(window_handle_, message, wparam, lparam); 194 | } 195 | 196 | void Win32Window::Destroy() { 197 | OnDestroy(); 198 | 199 | if (window_handle_) { 200 | DestroyWindow(window_handle_); 201 | window_handle_ = nullptr; 202 | } 203 | if (g_active_window_count == 0) { 204 | WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); 205 | } 206 | } 207 | 208 | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { 209 | return reinterpret_cast( 210 | GetWindowLongPtr(window, GWLP_USERDATA)); 211 | } 212 | 213 | void Win32Window::SetChildContent(HWND content) { 214 | child_content_ = content; 215 | SetParent(content, window_handle_); 216 | RECT frame = GetClientArea(); 217 | 218 | MoveWindow(content, frame.left, frame.top, frame.right - frame.left, 219 | frame.bottom - frame.top, true); 220 | 221 | SetFocus(child_content_); 222 | } 223 | 224 | RECT Win32Window::GetClientArea() { 225 | RECT frame; 226 | GetClientRect(window_handle_, &frame); 227 | return frame; 228 | } 229 | 230 | HWND Win32Window::GetHandle() { 231 | return window_handle_; 232 | } 233 | 234 | void Win32Window::SetQuitOnClose(bool quit_on_close) { 235 | quit_on_close_ = quit_on_close; 236 | } 237 | 238 | bool Win32Window::OnCreate() { 239 | // No-op; provided for subclasses. 240 | return true; 241 | } 242 | 243 | void Win32Window::OnDestroy() { 244 | // No-op; provided for subclasses. 245 | } 246 | --------------------------------------------------------------------------------