├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── movies_app │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets └── images │ └── icon.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── core │ ├── data │ │ ├── error │ │ │ ├── exceptions.dart │ │ │ └── failure.dart │ │ └── network │ │ │ ├── api_constants.dart │ │ │ └── error_message_model.dart │ ├── domain │ │ ├── entities │ │ │ ├── media.dart │ │ │ ├── media.g.dart │ │ │ └── media_details.dart │ │ └── usecase │ │ │ └── base_use_case.dart │ ├── presentation │ │ ├── components │ │ │ ├── circle_dot.dart │ │ │ ├── custom_app_bar.dart │ │ │ ├── custom_slider.dart │ │ │ ├── details_card.dart │ │ │ ├── error_screen.dart │ │ │ ├── error_text.dart │ │ │ ├── image_with_shimmer.dart │ │ │ ├── loading_indicator.dart │ │ │ ├── overview_section.dart │ │ │ ├── section_header.dart │ │ │ ├── section_listview.dart │ │ │ ├── section_listview_card.dart │ │ │ ├── section_title.dart │ │ │ ├── slider_card.dart │ │ │ ├── slider_card_image.dart │ │ │ ├── vertical_listview.dart │ │ │ └── vertical_listview_card.dart │ │ └── pages │ │ │ └── main_page.dart │ ├── resources │ │ ├── app_colors.dart │ │ ├── app_constants.dart │ │ ├── app_router.dart │ │ ├── app_routes.dart │ │ ├── app_strings.dart │ │ ├── app_theme.dart │ │ └── app_values.dart │ ├── services │ │ └── service_locator.dart │ └── utils │ │ ├── enums.dart │ │ └── functions.dart ├── main.dart ├── movies │ ├── data │ │ ├── datasource │ │ │ └── movies_remote_data_source.dart │ │ ├── models │ │ │ ├── cast_model.dart │ │ │ ├── movie_details_model.dart │ │ │ ├── movie_model.dart │ │ │ └── review_model.dart │ │ └── repository │ │ │ └── movies_repository_impl.dart │ ├── domain │ │ ├── entities │ │ │ ├── cast.dart │ │ │ └── review.dart │ │ ├── repository │ │ │ └── movies_repository.dart │ │ └── usecases │ │ │ ├── get_all_popular_movies_usecase.dart │ │ │ ├── get_all_top_rated_movies_usecase.dart │ │ │ ├── get_movie_details_usecase.dart │ │ │ └── get_movies_usecase.dart │ └── presentation │ │ ├── components │ │ ├── avatar.dart │ │ ├── cast_card.dart │ │ ├── movie_card_details.dart │ │ ├── review_card.dart │ │ └── review_content.dart │ │ ├── controllers │ │ ├── movie_details_bloc │ │ │ ├── movie_details_bloc.dart │ │ │ ├── movie_details_event.dart │ │ │ └── movie_details_state.dart │ │ ├── movies_bloc │ │ │ ├── movies_bloc.dart │ │ │ ├── movies_event.dart │ │ │ └── movies_state.dart │ │ ├── popular_movies_bloc │ │ │ ├── popular_movies_bloc.dart │ │ │ ├── popular_movies_event.dart │ │ │ └── popular_movies_state.dart │ │ └── top_rated_movies_bloc │ │ │ ├── top_rated_movies_bloc.dart │ │ │ ├── top_rated_movies_event.dart │ │ │ └── top_rated_movies_state.dart │ │ └── views │ │ ├── movie_details_view.dart │ │ ├── movies_view.dart │ │ ├── popular_movies_view.dart │ │ └── top_rated_movies_view.dart ├── search │ ├── data │ │ ├── datasource │ │ │ └── search_remote_data_source.dart │ │ ├── models │ │ │ └── search_result_item_model.dart │ │ └── repository │ │ │ └── search_repository_impl.dart │ ├── domain │ │ ├── entities │ │ │ └── search_result_item.dart │ │ ├── repository │ │ │ └── search_repository.dart │ │ └── usecases │ │ │ └── search_usecase.dart │ └── presentation │ │ ├── components │ │ ├── grid_view_card.dart │ │ ├── no_results.dart │ │ ├── search_field.dart │ │ ├── search_grid_view.dart │ │ └── search_text.dart │ │ ├── controllers │ │ └── search_bloc │ │ │ ├── search_bloc.dart │ │ │ ├── search_event.dart │ │ │ └── search_state.dart │ │ └── views │ │ └── search_view.dart ├── tv_shows │ ├── data │ │ ├── datasource │ │ │ └── tv_shows_remote_data_source.dart │ │ ├── models │ │ │ ├── episode_model.dart │ │ │ ├── season_details_model.dart │ │ │ ├── season_model.dart │ │ │ ├── tv_show_details_model.dart │ │ │ └── tv_show_model.dart │ │ └── repository │ │ │ └── tv_shows_repository_impl.dart │ ├── domain │ │ ├── entities │ │ │ ├── episode.dart │ │ │ ├── season.dart │ │ │ └── season_details.dart │ │ ├── repository │ │ │ └── tv_shows_repository.dart │ │ └── usecases │ │ │ ├── get_all_popular_tv_shows_usecase.dart │ │ │ ├── get_all_top_rated_tv_shows_usecase.dart │ │ │ ├── get_season_details_usecase.dart │ │ │ ├── get_tv_show_details_usecase.dart │ │ │ └── get_tv_shows_usecase.dart │ └── presentation │ │ ├── components │ │ ├── episode_card.dart │ │ ├── episodes_widget.dart │ │ ├── season_card.dart │ │ ├── seasons_section.dart │ │ └── tv_show_card_details.dart │ │ ├── controllers │ │ ├── popular_tv_shows_bloc │ │ │ ├── popular_tv_shows_bloc.dart │ │ │ ├── popular_tv_shows_event.dart │ │ │ └── popular_tv_shows_state.dart │ │ ├── top_rated_tv_shows_bloc │ │ │ ├── top_rated_tv_shows_bloc.dart │ │ │ ├── top_rated_tv_shows_event.dart │ │ │ └── top_rated_tv_shows_state.dart │ │ ├── tv_show_details_bloc │ │ │ ├── tv_show_details_bloc.dart │ │ │ ├── tv_show_details_event.dart │ │ │ └── tv_show_details_state.dart │ │ └── tv_shows_bloc │ │ │ ├── tv_shows_bloc.dart │ │ │ ├── tv_shows_event.dart │ │ │ └── tv_shows_state.dart │ │ └── views │ │ ├── popular_tv_shows_view.dart │ │ ├── top_rated_tv_shows_view.dart │ │ ├── tv_show_details_view.dart │ │ └── tv_shows_view.dart └── watchlist │ ├── data │ ├── datasource │ │ └── watchlist_local_data_source.dart │ ├── models │ │ └── watchlist_item_model.dart │ └── repository │ │ └── watchlist_repository_impl.dart │ ├── domain │ ├── repository │ │ └── watchlist_repository.dart │ └── usecases │ │ ├── add_watchlist_item_usecase.dart │ │ ├── check_if_item_added_usecase.dart │ │ ├── get_watchlist_items_usecase.dart │ │ └── remove_watchlist_item_usecase.dart │ └── presentation │ ├── components │ └── empty_watchlist_text.dart │ ├── controllers │ └── watchlist_bloc │ │ ├── watchlist_bloc.dart │ │ ├── watchlist_event.dart │ │ └── watchlist_state.dart │ └── views │ └── watchlist_view.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── main.cc ├── my_application.cc └── my_application.h ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── app_icon_1024.png │ │ ├── app_icon_128.png │ │ ├── app_icon_16.png │ │ ├── app_icon_256.png │ │ ├── app_icon_32.png │ │ ├── app_icon_512.png │ │ └── app_icon_64.png │ ├── Base.lproj │ └── MainMenu.xib │ ├── Configs │ ├── AppInfo.xcconfig │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements ├── pubspec.lock ├── pubspec.yaml ├── screenshots ├── 01.png ├── 02.png ├── 03.png ├── 04.png ├── 05.png ├── 06.png ├── 07.png └── 08.png ├── test └── widget_test.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── index.html └── manifest.json └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: d6260f127fe3f88c98231243b387b48448479bff 8 | channel: dev 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: d6260f127fe3f88c98231243b387b48448479bff 17 | base_revision: d6260f127fe3f88c98231243b387b48448479bff 18 | - platform: android 19 | create_revision: d6260f127fe3f88c98231243b387b48448479bff 20 | base_revision: d6260f127fe3f88c98231243b387b48448479bff 21 | - platform: ios 22 | create_revision: d6260f127fe3f88c98231243b387b48448479bff 23 | base_revision: d6260f127fe3f88c98231243b387b48448479bff 24 | - platform: linux 25 | create_revision: d6260f127fe3f88c98231243b387b48448479bff 26 | base_revision: d6260f127fe3f88c98231243b387b48448479bff 27 | - platform: macos 28 | create_revision: d6260f127fe3f88c98231243b387b48448479bff 29 | base_revision: d6260f127fe3f88c98231243b387b48448479bff 30 | - platform: web 31 | create_revision: d6260f127fe3f88c98231243b387b48448479bff 32 | base_revision: d6260f127fe3f88c98231243b387b48448479bff 33 | - platform: windows 34 | create_revision: d6260f127fe3f88c98231243b387b48448479bff 35 | base_revision: d6260f127fe3f88c98231243b387b48448479bff 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Movie App 3 | Movie app made in Flutter with clean architecture using BLoC, Hive and the movie db API. 4 | 5 | ## Features 6 | - Search feature 7 | - Watchlist feature 8 | - Now playing movies 9 | - Popular movies 10 | - Top rated movies 11 | - Movie details 12 | - Movie cast 13 | - Movie reviews 14 | - Similar movies 15 | - On air tv shows 16 | - Popular tv shows 17 | - Top rated tv shows 18 | - TV show details 19 | - Similar tv shows 20 | - TV show season details 21 | 22 | 23 | ## Screenshots 24 |

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |

34 | 35 | ## Installation 36 | ### 1. Clone the repo 37 | ```bash 38 | git clone https://github.com/mohamadayash22/flutter-movie-app.git 39 | cd flutter-movie-app 40 | ``` 41 | ### 2. Run pub get 42 | ```bash 43 | flutter pub get 44 | ``` 45 | ### 3. Add your API key into api_constants file 46 | ```bash 47 | apiKey = 'YOUR_API_KEY'; 48 | ``` 49 | ### 4. Run the app 50 | ```bash 51 | flutter run 52 | ``` 53 | 54 | ## Packages 55 | - [cached_network_image](https://pub.dev/packages/cached_network_image) 56 | - [carousel_slider](https://pub.dev/packages/carousel_slider) 57 | - [cupertino_icons](https://pub.dev/packages/cupertino_icons) 58 | - [dartz](https://pub.dev/packages/dartz) 59 | - [dio](https://pub.dev/packages/dio) 60 | - [equatable](https://pub.dev/packages/equatable) 61 | - [flutter_bloc](https://pub.dev/packages/flutter_bloc) 62 | - [flutter_rating_bar](https://pub.dev/packages/flutter_rating_bar) 63 | - [get_it](https://pub.dev/packages/get_it) 64 | - [go_router](https://pub.dev/packages/go_router) 65 | - [google_fonts](https://pub.dev/packages/google_fonts) 66 | - [hive](https://pub.dev/packages/hive) 67 | - [hive_flutter](https://pub.dev/packages/hive_flutter) 68 | - [path_provider](https://pub.dev/packages/path_provider) 69 | - [readmore](https://pub.dev/packages/readmore) 70 | - [shimmer](https://pub.dev/packages/shimmer) 71 | - [stream_transform](https://pub.dev/packages/stream_transform) 72 | - [url_launcher](https://pub.dev/packages/url_launcher) 73 | 74 | 75 | ## Acknowledgements 76 | This app is based on 77 | [Build a Movie App - FlutterFlow](https://www.youtube.com/watch?v=ZPkVRoa1AA8) YouTube video by [@abuanwar072](https://github.com/abuanwar072/), special thanks to Abu Anwar for his amazing videos and tutorials! 78 | -------------------------------------------------------------------------------- /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 | always_specify_types: false 28 | sort_constructors_first: false 29 | always_use_package_imports: true 30 | avoid_dynamic_calls: false 31 | avoid_empty_else: true 32 | avoid_relative_lib_imports: true 33 | avoid_type_to_string: true 34 | avoid_types_as_parameter_names: true 35 | unnecessary_statements: true 36 | 37 | 38 | # Additional information about this file can be found at 39 | # https://dart.dev/guides/language/analysis-options 40 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/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 33 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.example.movies_app" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 50 | minSdkVersion flutter.minSdkVersion 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | } 72 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/movies_app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.movies_app 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.2.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 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/assets/images/icon.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/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/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/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/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/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/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/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/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/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/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/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/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/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/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/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/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/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/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/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/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/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/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/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/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /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 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleDisplayName 12 | Movie App 13 | CFBundleExecutable 14 | $(EXECUTABLE_NAME) 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | movies_app 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | $(FLUTTER_BUILD_NAME) 25 | CFBundleSignature 26 | ???? 27 | CFBundleVersion 28 | $(FLUTTER_BUILD_NUMBER) 29 | LSRequiresIPhoneOS 30 | 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | LSApplicationQueriesSchemes 36 | 37 | youtube 38 | 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | UISupportedInterfaceOrientations~ipad 46 | 47 | UIInterfaceOrientationPortrait 48 | UIInterfaceOrientationPortraitUpsideDown 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | CADisableMinimumFrameDurationOnPhone 55 | 56 | UIApplicationSupportsIndirectInputEvents 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/core/data/error/exceptions.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/data/network/error_message_model.dart'; 2 | 3 | class ServerException implements Exception { 4 | final ErrorMessageModel errorMessageModel; 5 | 6 | const ServerException({required this.errorMessageModel}); 7 | } 8 | 9 | class DatabaseException implements Exception {} 10 | -------------------------------------------------------------------------------- /lib/core/data/error/failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class Failure extends Equatable { 4 | final String message; 5 | 6 | const Failure(this.message); 7 | @override 8 | List get props => [message]; 9 | } 10 | 11 | class ServerFailure extends Failure { 12 | const ServerFailure(super.message); 13 | } 14 | 15 | class DatabaseFailure extends Failure { 16 | const DatabaseFailure(super.message); 17 | } 18 | -------------------------------------------------------------------------------- /lib/core/data/network/error_message_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class ErrorMessageModel extends Equatable { 4 | final int statusCode; 5 | final String statusMessage; 6 | final bool success; 7 | 8 | const ErrorMessageModel({ 9 | required this.statusCode, 10 | required this.statusMessage, 11 | required this.success, 12 | }); 13 | 14 | factory ErrorMessageModel.fromJson(Map json) { 15 | return ErrorMessageModel( 16 | statusCode: json['status_code'], 17 | statusMessage: json['status_message'], 18 | success: json['success'], 19 | ); 20 | } 21 | 22 | @override 23 | List get props => [ 24 | statusCode, 25 | statusMessage, 26 | success, 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /lib/core/domain/entities/media.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:hive_flutter/hive_flutter.dart'; 3 | import 'package:movies_app/core/domain/entities/media_details.dart'; 4 | 5 | part 'media.g.dart'; 6 | 7 | @HiveType(typeId: 1) 8 | class Media extends Equatable { 9 | @HiveField(0) 10 | final int tmdbID; 11 | @HiveField(1) 12 | final String title; 13 | @HiveField(2) 14 | final String posterUrl; 15 | @HiveField(3) 16 | final String backdropUrl; 17 | @HiveField(4) 18 | final double voteAverage; 19 | @HiveField(5) 20 | final String releaseDate; 21 | @HiveField(6) 22 | final String overview; 23 | @HiveField(7) 24 | final bool isMovie; 25 | 26 | const Media({ 27 | required this.tmdbID, 28 | required this.title, 29 | required this.posterUrl, 30 | required this.backdropUrl, 31 | required this.voteAverage, 32 | required this.releaseDate, 33 | required this.overview, 34 | required this.isMovie, 35 | }); 36 | 37 | factory Media.fromMediaDetails(MediaDetails mediaDetails) { 38 | return Media( 39 | tmdbID: mediaDetails.tmdbID, 40 | title: mediaDetails.title, 41 | posterUrl: mediaDetails.posterUrl, 42 | backdropUrl: mediaDetails.backdropUrl, 43 | voteAverage: mediaDetails.voteAverage, 44 | releaseDate: mediaDetails.releaseDate, 45 | overview: mediaDetails.overview, 46 | isMovie: mediaDetails.lastEpisodeToAir == null, 47 | ); 48 | } 49 | 50 | @override 51 | List get props => [ 52 | tmdbID, 53 | title, 54 | posterUrl, 55 | backdropUrl, 56 | voteAverage, 57 | releaseDate, 58 | overview, 59 | isMovie, 60 | ]; 61 | } 62 | -------------------------------------------------------------------------------- /lib/core/domain/entities/media.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'media.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class MediaAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 1; 12 | 13 | @override 14 | Media 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 Media( 20 | tmdbID: fields[0] as int, 21 | title: fields[1] as String, 22 | posterUrl: fields[2] as String, 23 | backdropUrl: fields[3] as String, 24 | voteAverage: fields[4] as double, 25 | releaseDate: fields[5] as String, 26 | overview: fields[6] as String, 27 | isMovie: fields[7] as bool, 28 | ); 29 | } 30 | 31 | @override 32 | void write(BinaryWriter writer, Media obj) { 33 | writer 34 | ..writeByte(8) 35 | ..writeByte(0) 36 | ..write(obj.tmdbID) 37 | ..writeByte(1) 38 | ..write(obj.title) 39 | ..writeByte(2) 40 | ..write(obj.posterUrl) 41 | ..writeByte(3) 42 | ..write(obj.backdropUrl) 43 | ..writeByte(4) 44 | ..write(obj.voteAverage) 45 | ..writeByte(5) 46 | ..write(obj.releaseDate) 47 | ..writeByte(6) 48 | ..write(obj.overview) 49 | ..writeByte(7) 50 | ..write(obj.isMovie); 51 | } 52 | 53 | @override 54 | int get hashCode => typeId.hashCode; 55 | 56 | @override 57 | bool operator ==(Object other) => 58 | identical(this, other) || 59 | other is MediaAdapter && 60 | runtimeType == other.runtimeType && 61 | typeId == other.typeId; 62 | } 63 | -------------------------------------------------------------------------------- /lib/core/domain/entities/media_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:movies_app/core/domain/entities/media.dart'; 3 | import 'package:movies_app/movies/domain/entities/cast.dart'; 4 | import 'package:movies_app/movies/domain/entities/review.dart'; 5 | import 'package:movies_app/tv_shows/domain/entities/episode.dart'; 6 | import 'package:movies_app/tv_shows/domain/entities/season.dart'; 7 | 8 | // ignore: must_be_immutable 9 | class MediaDetails extends Equatable { 10 | int? id; 11 | final int tmdbID; 12 | final String title; 13 | final String posterUrl; 14 | final String backdropUrl; 15 | final String releaseDate; 16 | final Episode? lastEpisodeToAir; 17 | final String genres; 18 | final String? runtime; 19 | final int? numberOfSeasons; 20 | final String overview; 21 | final double voteAverage; 22 | final String voteCount; 23 | final String trailerUrl; 24 | final List? cast; 25 | final List? reviews; 26 | final List? seasons; 27 | final List similar; 28 | bool isAdded; 29 | 30 | MediaDetails({ 31 | this.id, 32 | required this.tmdbID, 33 | required this.title, 34 | required this.posterUrl, 35 | required this.backdropUrl, 36 | required this.releaseDate, 37 | this.lastEpisodeToAir, 38 | required this.genres, 39 | this.runtime, 40 | this.numberOfSeasons, 41 | required this.overview, 42 | required this.voteAverage, 43 | required this.voteCount, 44 | required this.trailerUrl, 45 | this.cast, 46 | this.reviews, 47 | this.seasons, 48 | required this.similar, 49 | this.isAdded = false, 50 | }); 51 | 52 | @override 53 | List get props => [ 54 | id, 55 | tmdbID, 56 | title, 57 | posterUrl, 58 | backdropUrl, 59 | releaseDate, 60 | genres, 61 | overview, 62 | voteAverage, 63 | voteCount, 64 | trailerUrl, 65 | similar, 66 | isAdded, 67 | ]; 68 | } 69 | -------------------------------------------------------------------------------- /lib/core/domain/usecase/base_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import 'package:movies_app/core/data/error/failure.dart'; 5 | 6 | abstract class BaseUseCase { 7 | Future> call(P p); 8 | } 9 | 10 | class NoParameters extends Equatable { 11 | const NoParameters(); 12 | @override 13 | List get props => []; 14 | } 15 | -------------------------------------------------------------------------------- /lib/core/presentation/components/circle_dot.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/resources/app_colors.dart'; 3 | import 'package:movies_app/core/resources/app_values.dart'; 4 | 5 | class CircleDot extends StatelessWidget { 6 | const CircleDot({ 7 | super.key, 8 | }); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Padding( 13 | padding: const EdgeInsets.symmetric(horizontal: AppPadding.p6), 14 | child: Container( 15 | width: AppSize.s6, 16 | height: AppSize.s6, 17 | decoration: const BoxDecoration( 18 | shape: BoxShape.circle, 19 | color: AppColors.circleDotColor, 20 | ), 21 | ), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/core/presentation/components/custom_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:movies_app/core/resources/app_values.dart'; 4 | 5 | class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { 6 | const CustomAppBar({ 7 | super.key, 8 | required this.title, 9 | }); 10 | 11 | final String title; 12 | 13 | @override 14 | Size get preferredSize => const Size.fromHeight(AppSize.s60); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return AppBar( 19 | title: Text(title), 20 | leading: context.canPop() 21 | ? IconButton( 22 | onPressed: () { 23 | context.pop(); 24 | }, 25 | icon: const Icon( 26 | Icons.arrow_back_ios_new_rounded, 27 | size: AppSize.s20, 28 | ), 29 | ) 30 | : null, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/core/presentation/components/custom_slider.dart: -------------------------------------------------------------------------------- 1 | import 'package:carousel_slider/carousel_slider.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'package:movies_app/core/resources/app_constants.dart'; 5 | 6 | class CustomSlider extends StatelessWidget { 7 | final Widget Function(BuildContext context, int itemIndex, int) itemBuilder; 8 | const CustomSlider({ 9 | required this.itemBuilder, 10 | super.key, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final size = MediaQuery.of(context).size; 16 | return CarouselSlider.builder( 17 | itemCount: AppConstants.carouselSliderItemsCount, 18 | options: CarouselOptions( 19 | viewportFraction: 1, 20 | height: size.height * 0.55, 21 | autoPlay: true, 22 | ), 23 | itemBuilder: itemBuilder, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/core/presentation/components/error_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/presentation/components/error_text.dart'; 3 | 4 | import 'package:movies_app/core/resources/app_colors.dart'; 5 | import 'package:movies_app/core/resources/app_strings.dart'; 6 | import 'package:movies_app/core/resources/app_values.dart'; 7 | 8 | class ErrorScreen extends StatelessWidget { 9 | const ErrorScreen({ 10 | super.key, 11 | required this.onTryAgainPressed, 12 | }); 13 | 14 | final Function() onTryAgainPressed; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final textTheme = Theme.of(context).textTheme; 19 | return SizedBox( 20 | width: double.infinity, 21 | child: Column( 22 | mainAxisAlignment: MainAxisAlignment.center, 23 | children: [ 24 | Padding( 25 | padding: const EdgeInsets.only(bottom: AppPadding.p18), 26 | child: Image.asset( 27 | 'assets/images/icon.png', 28 | width: double.infinity, 29 | height: AppSize.s150, 30 | ), 31 | ), 32 | const ErrorText(), 33 | const SizedBox(height: AppSize.s15), 34 | ElevatedButton( 35 | onPressed: onTryAgainPressed, 36 | style: ElevatedButton.styleFrom( 37 | backgroundColor: AppColors.primary, 38 | shape: RoundedRectangleBorder( 39 | borderRadius: BorderRadius.circular(AppSize.s30), 40 | ), 41 | ), 42 | child: Text( 43 | AppStrings.tryAgain, 44 | style: textTheme.bodyMedium, 45 | ), 46 | ), 47 | ], 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/core/presentation/components/error_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/resources/app_strings.dart'; 3 | 4 | class ErrorText extends StatelessWidget { 5 | const ErrorText({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | final textTheme = Theme.of(context).textTheme; 10 | return Column( 11 | mainAxisAlignment: MainAxisAlignment.center, 12 | children: [ 13 | Text( 14 | AppStrings.oops, 15 | style: textTheme.titleMedium, 16 | ), 17 | Text( 18 | AppStrings.errorMessage, 19 | style: textTheme.bodyLarge, 20 | ), 21 | Text( 22 | AppStrings.tryAgainLater, 23 | style: textTheme.bodyLarge, 24 | ), 25 | ], 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/core/presentation/components/image_with_shimmer.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:movies_app/core/resources/app_colors.dart'; 4 | import 'package:shimmer/shimmer.dart'; 5 | 6 | class ImageWithShimmer extends StatelessWidget { 7 | const ImageWithShimmer({ 8 | super.key, 9 | required this.imageUrl, 10 | required this.width, 11 | required this.height, 12 | }); 13 | 14 | final String imageUrl; 15 | final double height; 16 | final double width; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return CachedNetworkImage( 21 | imageUrl: imageUrl, 22 | height: height, 23 | width: width, 24 | fit: BoxFit.cover, 25 | placeholder: (_, __) => Shimmer.fromColors( 26 | baseColor: Colors.grey[850]!, 27 | highlightColor: Colors.grey[800]!, 28 | child: Container( 29 | height: height, 30 | color: AppColors.secondaryText, 31 | ), 32 | ), 33 | errorWidget: (_, __, ___) => const Icon( 34 | Icons.error, 35 | color: AppColors.error, 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/core/presentation/components/loading_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:movies_app/core/resources/app_colors.dart'; 4 | 5 | class LoadingIndicator extends StatelessWidget { 6 | const LoadingIndicator({ 7 | super.key, 8 | }); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return const Center( 13 | child: CircularProgressIndicator( 14 | color: AppColors.primary, 15 | ), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/core/presentation/components/overview_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:readmore/readmore.dart'; 3 | 4 | import 'package:movies_app/core/resources/app_strings.dart'; 5 | import 'package:movies_app/core/resources/app_values.dart'; 6 | 7 | class OverviewSection extends StatelessWidget { 8 | final String overview; 9 | 10 | const OverviewSection({ 11 | super.key, 12 | required this.overview, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Padding( 18 | padding: const EdgeInsets.symmetric(horizontal: AppPadding.p16), 19 | child: ReadMoreText( 20 | overview, 21 | trimLines: 5, 22 | trimMode: TrimMode.Line, 23 | trimCollapsedText: AppStrings.showMore, 24 | trimExpandedText: AppStrings.showLess, 25 | style: Theme.of(context).textTheme.bodyLarge, 26 | moreStyle: Theme.of(context) 27 | .textTheme 28 | .bodyLarge! 29 | .copyWith(fontWeight: FontWeight.w600), 30 | lessStyle: Theme.of(context) 31 | .textTheme 32 | .bodyLarge! 33 | .copyWith(fontWeight: FontWeight.w600), 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/core/presentation/components/section_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:movies_app/core/resources/app_colors.dart'; 4 | import 'package:movies_app/core/resources/app_strings.dart'; 5 | import 'package:movies_app/core/resources/app_values.dart'; 6 | 7 | class SectionHeader extends StatelessWidget { 8 | final String title; 9 | final Function() onSeeAllTap; 10 | 11 | const SectionHeader({ 12 | super.key, 13 | required this.title, 14 | required this.onSeeAllTap, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | final textTheme = Theme.of(context).textTheme; 20 | return Padding( 21 | padding: const EdgeInsets.symmetric( 22 | vertical: AppPadding.p4, 23 | horizontal: AppPadding.p16, 24 | ), 25 | child: Row( 26 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 27 | children: [ 28 | Text( 29 | title, 30 | style: textTheme.titleSmall, 31 | ), 32 | InkWell( 33 | onTap: onSeeAllTap, 34 | child: Row( 35 | children: [ 36 | Text( 37 | AppStrings.seeAll, 38 | style: textTheme.bodyLarge, 39 | ), 40 | const Icon( 41 | Icons.arrow_forward_ios_rounded, 42 | size: AppSize.s12, 43 | color: AppColors.primaryText, 44 | ), 45 | ], 46 | ), 47 | ) 48 | ], 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/core/presentation/components/section_listview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:movies_app/core/resources/app_values.dart'; 4 | 5 | class SectionListView extends StatelessWidget { 6 | final int itemCount; 7 | final double height; 8 | final Widget Function(BuildContext context, int index) itemBuilder; 9 | 10 | const SectionListView({ 11 | required this.height, 12 | required this.itemCount, 13 | required this.itemBuilder, 14 | super.key, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return SizedBox( 20 | height: height, 21 | child: ListView.separated( 22 | padding: const EdgeInsets.symmetric(horizontal: AppPadding.p16), 23 | itemCount: itemCount, 24 | scrollDirection: Axis.horizontal, 25 | itemBuilder: itemBuilder, 26 | separatorBuilder: (context, index) => 27 | const SizedBox(width: AppSize.s10), 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/core/presentation/components/section_listview_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/domain/entities/media.dart'; 3 | import 'package:movies_app/core/presentation/components/image_with_shimmer.dart'; 4 | 5 | import 'package:movies_app/core/resources/app_colors.dart'; 6 | import 'package:movies_app/core/resources/app_values.dart'; 7 | import 'package:movies_app/core/utils/functions.dart'; 8 | 9 | class SectionListViewCard extends StatelessWidget { 10 | final Media media; 11 | 12 | const SectionListViewCard({ 13 | required this.media, 14 | super.key, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | final textTheme = Theme.of(context).textTheme; 20 | return SizedBox( 21 | width: AppSize.s120, 22 | child: Column( 23 | crossAxisAlignment: CrossAxisAlignment.start, 24 | children: [ 25 | InkWell( 26 | onTap: () { 27 | navigateToDetailsView(context, media); 28 | }, 29 | child: ClipRRect( 30 | borderRadius: BorderRadius.circular(AppSize.s8), 31 | child: ImageWithShimmer( 32 | imageUrl: media.posterUrl, 33 | width: double.infinity, 34 | height: AppSize.s175, 35 | ), 36 | ), 37 | ), 38 | Column( 39 | crossAxisAlignment: CrossAxisAlignment.start, 40 | children: [ 41 | Text( 42 | media.title, 43 | maxLines: 2, 44 | overflow: TextOverflow.ellipsis, 45 | style: textTheme.bodyMedium, 46 | ), 47 | Row( 48 | children: [ 49 | const Icon( 50 | Icons.star_rate_rounded, 51 | color: AppColors.ratingIconColor, 52 | size: AppSize.s18, 53 | ), 54 | Text( 55 | '${media.voteAverage}/10', 56 | style: textTheme.bodySmall, 57 | ), 58 | ], 59 | ), 60 | ], 61 | ) 62 | ], 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/core/presentation/components/section_title.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:movies_app/core/resources/app_values.dart'; 4 | 5 | class SectionTitle extends StatelessWidget { 6 | const SectionTitle({ 7 | super.key, 8 | required this.title, 9 | }); 10 | 11 | final String title; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Padding( 16 | padding: const EdgeInsets.only( 17 | right: AppPadding.p16, 18 | left: AppPadding.p16, 19 | top: AppPadding.p8, 20 | bottom: AppPadding.p4, 21 | ), 22 | child: Text( 23 | title, 24 | style: Theme.of(context).textTheme.titleSmall, 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/core/presentation/components/slider_card_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/presentation/components/image_with_shimmer.dart'; 3 | import 'package:movies_app/core/resources/app_colors.dart'; 4 | 5 | class SliderCardImage extends StatelessWidget { 6 | const SliderCardImage({ 7 | super.key, 8 | required this.imageUrl, 9 | }); 10 | 11 | final String imageUrl; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final size = MediaQuery.of(context).size; 16 | return ShaderMask( 17 | blendMode: BlendMode.dstIn, 18 | shaderCallback: (rect) { 19 | return const LinearGradient( 20 | begin: Alignment.topCenter, 21 | end: Alignment.bottomCenter, 22 | colors: [ 23 | AppColors.black, 24 | AppColors.black, 25 | AppColors.transparent, 26 | ], 27 | stops: [0.3, 0.5, 1], 28 | ).createShader( 29 | Rect.fromLTRB(0, 0, rect.width, rect.height), 30 | ); 31 | }, 32 | child: ImageWithShimmer( 33 | height: size.height * 0.6, 34 | width: double.infinity, 35 | imageUrl: imageUrl, 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/core/presentation/components/vertical_listview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:movies_app/core/resources/app_values.dart'; 4 | 5 | class VerticalListView extends StatefulWidget { 6 | final int itemCount; 7 | final Widget Function(BuildContext context, int index) itemBuilder; 8 | final Function addEvent; 9 | 10 | const VerticalListView({ 11 | super.key, 12 | required this.itemCount, 13 | required this.itemBuilder, 14 | required this.addEvent, 15 | }); 16 | 17 | @override 18 | State createState() => _VerticalListViewState(); 19 | } 20 | 21 | class _VerticalListViewState extends State { 22 | final _scrollController = ScrollController(); 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _scrollController.addListener(_onScroll); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | _scrollController 33 | ..removeListener(_onScroll) 34 | ..dispose(); 35 | super.dispose(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return ListView.separated( 41 | padding: const EdgeInsets.all(AppPadding.p8), 42 | controller: _scrollController, 43 | physics: const BouncingScrollPhysics(), 44 | itemCount: widget.itemCount, 45 | itemBuilder: widget.itemBuilder, 46 | separatorBuilder: (context, index) { 47 | return const SizedBox(height: AppSize.s10); 48 | }, 49 | ); 50 | } 51 | 52 | void _onScroll() { 53 | if (_scrollController.position.atEdge) { 54 | if (_scrollController.position.pixels != 0) { 55 | widget.addEvent(); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/core/resources/app_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppColors { 4 | static const primary = Color(0xffef233c); 5 | static const secondary = Color(0xff272b30); 6 | static const primaryBackground = Color(0xff1a1d1f); 7 | static const secondaryBackground = Color(0xff272b30); 8 | static const primaryText = Color(0xffa9aaac); 9 | static const secondaryText = Colors.white; 10 | static const primaryBtnText = Colors.white; 11 | static const error = Colors.red; 12 | static const black = Colors.black; 13 | static const inactiveColor = Color(0x26ffffff); 14 | static const transparent = Colors.transparent; 15 | static const ratingIconColor = Color(0xffffbe21); 16 | static const circleDotColor = Color(0x33ffffff); 17 | static const iconContainerColor = Color(0xB2272830); 18 | } 19 | -------------------------------------------------------------------------------- /lib/core/resources/app_constants.dart: -------------------------------------------------------------------------------- 1 | class AppConstants { 2 | static const int carouselSliderItemsCount = 4; 3 | } 4 | -------------------------------------------------------------------------------- /lib/core/resources/app_routes.dart: -------------------------------------------------------------------------------- 1 | class AppRoutes { 2 | static const String moviesRoute = 'movies'; 3 | static const String movieDetailsRoute = 'movieDetails'; 4 | static const String popularMoviesRoute = 'popularMovies'; 5 | static const String topRatedMoviesRoute = 'topRatedMovies'; 6 | 7 | static const String tvShowsRoute = 'tvShows'; 8 | static const String tvShowDetailsRoute = 'tvShowDetails'; 9 | static const String popularTvShowsRoute = 'popularTvShowsRoute'; 10 | static const String topRatedTvShowsRoute = 'topRatedTvShowsRoute'; 11 | 12 | static const String searchRoute = 'search'; 13 | static const String watchlistRoute = 'watchlist'; 14 | } 15 | -------------------------------------------------------------------------------- /lib/core/resources/app_strings.dart: -------------------------------------------------------------------------------- 1 | class AppStrings { 2 | static const String appTitle = 'Movies App'; 3 | static const String seeAll = 'see all'; 4 | static const String popularMovies = 'Popular movies'; 5 | static const String topRatedMovies = 'Top rated movies'; 6 | static const String story = 'Story'; 7 | static const String videos = 'Videos'; 8 | static const String cast = 'Cast'; 9 | static const String reviews = 'Reviews'; 10 | static const String similar = 'Similar'; 11 | static const String showLess = 'Show less'; 12 | static const String showMore = 'Show more'; 13 | static const String movies = 'Movies'; 14 | static const String shows = 'Shows'; 15 | static const String search = 'Search'; 16 | static const String watchlist = 'Watchlist'; 17 | static const String popularShows = 'Popular shows'; 18 | static const String topRatedShows = 'Top rated shows'; 19 | static const String lastEpisodeOnAir = 'Last Episode on Air'; 20 | static const String seasons = 'Seasons'; 21 | static const String season = 'Season'; 22 | static const String episodes = 'Episodes'; 23 | static const String episode = 'Episode'; 24 | static const String airDate = 'Air date:'; 25 | static const String lastEpisode = 'Last Episode'; 26 | static const String searchText = 27 | 'By typing in search bar, Movia search in movies and series and then show you the best results.'; 28 | static const String searchHint = 'Search for Movies, Series...'; 29 | static const String watchlistIsEmpty = 'Watchlist is empty'; 30 | static const String watchlistText = 31 | 'After adding movies and series to watchlist, they will appear here.'; 32 | static const String oops = 'Ooops'; 33 | static const String tryAgainLater = 'Please try again later'; 34 | static const String errorMessage = 'Something went wrong'; 35 | static const String tryAgain = 'try again'; 36 | static const String noResults = 'No results'; 37 | } 38 | -------------------------------------------------------------------------------- /lib/core/resources/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:google_fonts/google_fonts.dart'; 4 | import 'package:movies_app/core/resources/app_colors.dart'; 5 | 6 | ThemeData getApplicationTheme() { 7 | return ThemeData( 8 | // main colors 9 | scaffoldBackgroundColor: AppColors.primaryBackground, 10 | 11 | // Bottom nav bar theme 12 | bottomNavigationBarTheme: const BottomNavigationBarThemeData( 13 | backgroundColor: AppColors.secondaryBackground, 14 | selectedItemColor: AppColors.primary, 15 | unselectedItemColor: AppColors.primaryText, 16 | type: BottomNavigationBarType.fixed, 17 | ), 18 | 19 | // app bar theme 20 | appBarTheme: AppBarTheme( 21 | backgroundColor: AppColors.primaryBackground, 22 | elevation: 0, 23 | centerTitle: true, 24 | systemOverlayStyle: SystemUiOverlayStyle.light, 25 | titleTextStyle: _getTextStyle( 26 | fontSize: 18, 27 | color: AppColors.secondaryText, 28 | ), 29 | ), 30 | 31 | // text theme 32 | textTheme: TextTheme( 33 | titleMedium: _getTextStyle( 34 | fontSize: 20, 35 | color: AppColors.secondaryText, 36 | ), 37 | titleSmall: _getTextStyle( 38 | fontSize: 18, 39 | color: AppColors.secondaryText, 40 | ), 41 | bodyLarge: _getTextStyle( 42 | fontSize: 14, 43 | fontWeight: FontWeight.w400, 44 | color: AppColors.primaryText, 45 | ), 46 | bodyMedium: _getTextStyle( 47 | fontSize: 14, 48 | color: AppColors.secondaryText, 49 | ), 50 | bodySmall: _getTextStyle( 51 | fontSize: 12, 52 | fontWeight: FontWeight.w400, 53 | color: AppColors.primaryText, 54 | ), 55 | ), 56 | ); 57 | } 58 | 59 | TextStyle _getTextStyle({ 60 | required double fontSize, 61 | FontWeight fontWeight = FontWeight.w600, 62 | required Color color, 63 | }) { 64 | return GoogleFonts.poppins( 65 | fontSize: fontSize, 66 | fontWeight: fontWeight, 67 | color: color, 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /lib/core/resources/app_values.dart: -------------------------------------------------------------------------------- 1 | class AppMargin { 2 | static const double m8 = 8.0; 3 | static const double m10 = 10.0; 4 | static const double m12 = 12.0; 5 | static const double m14 = 14.0; 6 | static const double m16 = 16.0; 7 | static const double m18 = 18.0; 8 | static const double m20 = 20.0; 9 | } 10 | 11 | class AppPadding { 12 | static const double p2 = 2.0; 13 | static const double p4 = 4.0; 14 | static const double p6 = 6.0; 15 | static const double p8 = 8.0; 16 | static const double p12 = 12.0; 17 | static const double p10 = 10.0; 18 | static const double p14 = 14.0; 19 | static const double p16 = 16.0; 20 | static const double p18 = 18.0; 21 | static const double p20 = 20.0; 22 | static const double p24 = 24.0; 23 | static const double p32 = 32.0; 24 | static const double p36 = 36.0; 25 | } 26 | 27 | class AppSize { 28 | static const double s0 = 0.0; 29 | static const double s1 = 1.0; 30 | static const double s4 = 4.0; 31 | static const double s6 = 6.0; 32 | static const double s8 = 8.0; 33 | static const double s10 = 10.0; 34 | static const double s12 = 12.0; 35 | static const double s14 = 14.0; 36 | static const double s15 = 15.0; 37 | static const double s16 = 16.0; 38 | static const double s18 = 18.0; 39 | static const double s20 = 20.0; 40 | static const double s22 = 22.0; 41 | static const double s24 = 24.0; 42 | static const double s30 = 30.0; 43 | static const double s36 = 36.0; 44 | static const double s40 = 40.0; 45 | static const double s45 = 45.0; 46 | static const double s60 = 60.0; 47 | static const double s84 = 84.0; 48 | static const double s100 = 100.0; 49 | static const double s110 = 110.0; 50 | static const double s120 = 120.0; 51 | static const double s130 = 130.0; 52 | static const double s140 = 140.0; 53 | static const double s150 = 150.0; 54 | static const double s160 = 160.0; 55 | static const double s175 = 175.0; 56 | static const double s190 = 190.0; 57 | static const double s200 = 200.0; 58 | static const double s220 = 220.0; 59 | static const double s240 = 240.0; 60 | static const double s400 = 400.0; 61 | static const double s800 = 800.0; 62 | } 63 | -------------------------------------------------------------------------------- /lib/core/utils/enums.dart: -------------------------------------------------------------------------------- 1 | enum RequestStatus { loading, loaded, error } 2 | 3 | enum GetAllRequestStatus { loading, loaded, error, fetchMoreError } 4 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:hive_flutter/hive_flutter.dart'; 4 | import 'package:movies_app/core/domain/entities/media.dart'; 5 | import 'package:movies_app/core/resources/app_router.dart'; 6 | import 'package:movies_app/core/services/service_locator.dart'; 7 | import 'package:movies_app/core/resources/app_strings.dart'; 8 | import 'package:movies_app/core/resources/app_theme.dart'; 9 | import 'package:movies_app/watchlist/presentation/controllers/watchlist_bloc/watchlist_bloc.dart'; 10 | 11 | void main() async { 12 | await Hive.initFlutter(); 13 | Hive.registerAdapter(MediaAdapter()); 14 | await Hive.openBox('items'); 15 | ServiceLocator.init(); 16 | 17 | runApp( 18 | BlocProvider( 19 | create: (context) => sl(), 20 | child: const MyApp(), 21 | ), 22 | ); 23 | } 24 | 25 | class MyApp extends StatelessWidget { 26 | const MyApp({super.key}); 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return MaterialApp.router( 31 | debugShowCheckedModeBanner: false, 32 | title: AppStrings.appTitle, 33 | theme: getApplicationTheme(), 34 | routerConfig: AppRouter().router, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/movies/data/models/cast_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/utils/functions.dart'; 2 | import 'package:movies_app/movies/domain/entities/cast.dart'; 3 | 4 | class CastModel extends Cast { 5 | const CastModel({ 6 | required super.name, 7 | required super.profileUrl, 8 | required super.gender, 9 | }); 10 | 11 | factory CastModel.fromJson(Map json) { 12 | return CastModel( 13 | name: json['name'], 14 | profileUrl: getProfileImageUrl(json), 15 | gender: json['gender'], 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/movies/data/models/movie_details_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/domain/entities/media_details.dart'; 2 | import 'package:movies_app/core/utils/functions.dart'; 3 | import 'package:movies_app/movies/data/models/movie_model.dart'; 4 | import 'package:movies_app/movies/data/models/review_model.dart'; 5 | import 'package:movies_app/movies/data/models/cast_model.dart'; 6 | 7 | // ignore: must_be_immutable 8 | class MovieDetailsModel extends MediaDetails { 9 | MovieDetailsModel({ 10 | required super.tmdbID, 11 | required super.title, 12 | required super.posterUrl, 13 | required super.backdropUrl, 14 | required super.releaseDate, 15 | required super.genres, 16 | required super.runtime, 17 | required super.overview, 18 | required super.voteAverage, 19 | required super.voteCount, 20 | required super.trailerUrl, 21 | required super.cast, 22 | required super.reviews, 23 | required super.similar, 24 | }); 25 | 26 | factory MovieDetailsModel.fromJson(Map json) { 27 | return MovieDetailsModel( 28 | tmdbID: json['id'], 29 | title: json['title'], 30 | posterUrl: getPosterUrl(json['poster_path']), 31 | backdropUrl: getBackdropUrl(json['backdrop_path']), 32 | releaseDate: getDate(json['release_date']), 33 | genres: getGenres(json['genres']), 34 | runtime: getLength(json['runtime']), 35 | overview: json['overview'] ?? '', 36 | voteAverage: 37 | double.parse((json['vote_average'] as double).toStringAsFixed(1)), 38 | voteCount: getVotesCount(json['vote_count']), 39 | trailerUrl: getTrailerUrl(json), 40 | cast: List.from( 41 | (json['credits']['cast'] as List).map((e) => CastModel.fromJson(e))), 42 | reviews: List.from((json['reviews']['results'] as List) 43 | .map((e) => ReviewModel.fromJson(e))), 44 | similar: List.from((json['similar']['results'] as List) 45 | .map((e) => MovieModel.fromJson(e))), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/movies/data/models/movie_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/domain/entities/media.dart'; 2 | import 'package:movies_app/core/utils/functions.dart'; 3 | 4 | class MovieModel extends Media { 5 | const MovieModel({ 6 | required super.tmdbID, 7 | required super.title, 8 | required super.posterUrl, 9 | required super.backdropUrl, 10 | required super.voteAverage, 11 | required super.releaseDate, 12 | required super.overview, 13 | required super.isMovie, 14 | }); 15 | 16 | factory MovieModel.fromJson(Map json) => MovieModel( 17 | tmdbID: json['id'], 18 | title: json['title'], 19 | posterUrl: getPosterUrl(json['poster_path']), 20 | backdropUrl: getBackdropUrl(json['backdrop_path']), 21 | voteAverage: double.parse((json['vote_average']).toStringAsFixed(1)), 22 | releaseDate: getDate(json['release_date']), 23 | overview: json['overview'] ?? '', 24 | isMovie: true, 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /lib/movies/data/models/review_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/utils/functions.dart'; 2 | import 'package:movies_app/movies/domain/entities/review.dart'; 3 | 4 | class ReviewModel extends Review { 5 | const ReviewModel({ 6 | required super.authorName, 7 | required super.authorUserName, 8 | required super.avatarUrl, 9 | required super.rating, 10 | required super.content, 11 | required super.elapsedTime, 12 | }); 13 | 14 | factory ReviewModel.fromJson(Map json) { 15 | return ReviewModel( 16 | authorName: json['author'], 17 | authorUserName: '@${json['author_details']['username']}', 18 | avatarUrl: getAvatarUrl(json['author_details']['avatar_path']), 19 | rating: json['author_details']['rating'] != null 20 | ? json['author_details']['rating'] / 2 21 | : -1, 22 | content: json['content'], 23 | elapsedTime: getElapsedTime(json['created_at']), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/movies/data/repository/movies_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:movies_app/core/data/error/exceptions.dart'; 4 | import 'package:movies_app/core/data/error/failure.dart'; 5 | import 'package:movies_app/core/domain/entities/media.dart'; 6 | import 'package:movies_app/core/domain/entities/media_details.dart'; 7 | import 'package:movies_app/movies/domain/repository/movies_repository.dart'; 8 | 9 | import 'package:movies_app/movies/data/datasource/movies_remote_data_source.dart'; 10 | 11 | class MoviesRepositoryImpl extends MoviesRespository { 12 | final MoviesRemoteDataSource _baseMoviesRemoteDataSource; 13 | 14 | MoviesRepositoryImpl(this._baseMoviesRemoteDataSource); 15 | 16 | @override 17 | Future> getMovieDetails(int movieId) async { 18 | try { 19 | final result = await _baseMoviesRemoteDataSource.getMovieDetails(movieId); 20 | return Right(result); 21 | } on ServerException catch (failure) { 22 | return Left(ServerFailure(failure.errorMessageModel.statusMessage)); 23 | } on DioError catch (failure) { 24 | return Left(ServerFailure(failure.message)); 25 | } 26 | } 27 | 28 | @override 29 | Future>>> getMovies() async { 30 | try { 31 | final result = await _baseMoviesRemoteDataSource.getMovies(); 32 | return Right(result); 33 | } on ServerException catch (failure) { 34 | return Left(ServerFailure(failure.errorMessageModel.statusMessage)); 35 | } on DioError catch (failure) { 36 | return Left(ServerFailure(failure.message)); 37 | } 38 | } 39 | 40 | @override 41 | Future>> getAllPopularMovies(int page) async { 42 | try { 43 | final result = 44 | await _baseMoviesRemoteDataSource.getAllPopularMovies(page); 45 | return Right(result); 46 | } on ServerException catch (failure) { 47 | return Left(ServerFailure(failure.errorMessageModel.statusMessage)); 48 | } on DioError catch (failure) { 49 | return Left(ServerFailure(failure.message)); 50 | } 51 | } 52 | 53 | @override 54 | Future>> getAllTopRatedMovies(int page) async { 55 | try { 56 | final result = 57 | await _baseMoviesRemoteDataSource.getAllTopRatedMovies(page); 58 | return Right(result); 59 | } on ServerException catch (failure) { 60 | return Left(ServerFailure(failure.errorMessageModel.statusMessage)); 61 | } on DioError catch (failure) { 62 | return Left(ServerFailure(failure.message)); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/movies/domain/entities/cast.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Cast extends Equatable { 4 | final String name; 5 | final String profileUrl; 6 | final int gender; 7 | 8 | const Cast({ 9 | required this.name, 10 | required this.profileUrl, 11 | required this.gender, 12 | }); 13 | 14 | @override 15 | List get props => [ 16 | name, 17 | profileUrl, 18 | gender, 19 | ]; 20 | } 21 | -------------------------------------------------------------------------------- /lib/movies/domain/entities/review.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Review extends Equatable { 4 | final String authorName; 5 | final String authorUserName; 6 | final String avatarUrl; 7 | final double rating; 8 | final String content; 9 | final String elapsedTime; 10 | 11 | const Review({ 12 | required this.authorName, 13 | required this.authorUserName, 14 | required this.avatarUrl, 15 | required this.rating, 16 | required this.content, 17 | required this.elapsedTime, 18 | }); 19 | 20 | @override 21 | List get props => [ 22 | authorName, 23 | authorUserName, 24 | avatarUrl, 25 | rating, 26 | content, 27 | elapsedTime, 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /lib/movies/domain/repository/movies_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:movies_app/core/domain/entities/media.dart'; 3 | 4 | import 'package:movies_app/core/data/error/failure.dart'; 5 | import 'package:movies_app/core/domain/entities/media_details.dart'; 6 | 7 | abstract class MoviesRespository { 8 | Future>>> getMovies(); 9 | Future> getMovieDetails(int movieId); 10 | Future>> getAllPopularMovies(int page); 11 | Future>> getAllTopRatedMovies(int page); 12 | } 13 | -------------------------------------------------------------------------------- /lib/movies/domain/usecases/get_all_popular_movies_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/data/error/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:movies_app/core/domain/entities/media.dart'; 4 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 5 | import 'package:movies_app/movies/domain/repository/movies_repository.dart'; 6 | 7 | class GetAllPopularMoviesUseCase extends BaseUseCase, int> { 8 | final MoviesRespository _baseMoviesRespository; 9 | 10 | GetAllPopularMoviesUseCase(this._baseMoviesRespository); 11 | 12 | @override 13 | Future>> call(int p) async { 14 | return await _baseMoviesRespository.getAllPopularMovies(p); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/movies/domain/usecases/get_all_top_rated_movies_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/data/error/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:movies_app/core/domain/entities/media.dart'; 4 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 5 | import 'package:movies_app/movies/domain/repository/movies_repository.dart'; 6 | 7 | class GetAllTopRatedMoviesUseCase extends BaseUseCase, int> { 8 | final MoviesRespository _baseMoviesRespository; 9 | 10 | GetAllTopRatedMoviesUseCase(this._baseMoviesRespository); 11 | 12 | @override 13 | Future>> call(int p) async { 14 | return await _baseMoviesRespository.getAllTopRatedMovies(p); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/movies/domain/usecases/get_movie_details_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/data/error/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:movies_app/core/domain/entities/media_details.dart'; 4 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 5 | import 'package:movies_app/movies/domain/repository/movies_repository.dart'; 6 | 7 | class GetMoviesDetailsUseCase extends BaseUseCase { 8 | final MoviesRespository _baseMoviesRespository; 9 | 10 | GetMoviesDetailsUseCase(this._baseMoviesRespository); 11 | 12 | @override 13 | Future> call(int p) async { 14 | return await _baseMoviesRespository.getMovieDetails(p); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/movies/domain/usecases/get_movies_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/data/error/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:movies_app/core/domain/entities/media.dart'; 4 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 5 | import 'package:movies_app/movies/domain/repository/movies_repository.dart'; 6 | 7 | class GetMoviesUseCase extends BaseUseCase>, NoParameters> { 8 | final MoviesRespository _baseMoviesRespository; 9 | 10 | GetMoviesUseCase(this._baseMoviesRespository); 11 | 12 | @override 13 | Future>>> call(NoParameters p) async { 14 | return await _baseMoviesRespository.getMovies(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/movies/presentation/components/avatar.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:movies_app/core/resources/app_colors.dart'; 4 | import 'package:movies_app/core/resources/app_values.dart'; 5 | import 'package:shimmer/shimmer.dart'; 6 | 7 | class Avatar extends StatelessWidget { 8 | const Avatar({ 9 | super.key, 10 | required this.avatarUrl, 11 | }); 12 | 13 | final String avatarUrl; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return CachedNetworkImage( 18 | imageUrl: avatarUrl, 19 | imageBuilder: (context, imageProvider) => CircleAvatar( 20 | radius: AppSize.s20, 21 | backgroundColor: AppColors.transparent, 22 | backgroundImage: imageProvider, 23 | ), 24 | placeholder: (context, _) => Shimmer.fromColors( 25 | baseColor: Colors.grey[850]!, 26 | highlightColor: Colors.grey[800]!, 27 | child: const CircleAvatar( 28 | radius: AppSize.s20, 29 | ), 30 | ), 31 | errorWidget: (_, __, ___) => const Icon( 32 | Icons.error, 33 | color: AppColors.error, 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/movies/presentation/components/cast_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/presentation/components/image_with_shimmer.dart'; 3 | 4 | import 'package:movies_app/movies/domain/entities/cast.dart'; 5 | import 'package:movies_app/core/resources/app_values.dart'; 6 | 7 | class CastCard extends StatelessWidget { 8 | final Cast cast; 9 | const CastCard({ 10 | required this.cast, 11 | super.key, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final textTheme = Theme.of(context).textTheme; 17 | return SizedBox( 18 | width: AppSize.s100, 19 | child: Column( 20 | children: [ 21 | ClipRRect( 22 | borderRadius: BorderRadius.circular(AppSize.s8), 23 | child: ImageWithShimmer( 24 | imageUrl: cast.profileUrl, 25 | width: double.infinity, 26 | height: AppSize.s130, 27 | ), 28 | ), 29 | Text( 30 | cast.name, 31 | style: textTheme.bodyLarge, 32 | maxLines: 2, 33 | textAlign: TextAlign.center, 34 | ) 35 | ], 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/movies/presentation/components/movie_card_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/domain/entities/media_details.dart'; 3 | import 'package:movies_app/core/presentation/components/circle_dot.dart'; 4 | 5 | class MovieCardDetails extends StatelessWidget { 6 | const MovieCardDetails({ 7 | super.key, 8 | required this.movieDetails, 9 | }); 10 | 11 | final MediaDetails movieDetails; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final textTheme = Theme.of(context).textTheme; 16 | if (movieDetails.releaseDate.isNotEmpty && 17 | movieDetails.genres.isNotEmpty && 18 | movieDetails.runtime!.isNotEmpty) { 19 | return Row( 20 | children: [ 21 | if (movieDetails.releaseDate.isNotEmpty) ...[ 22 | Text( 23 | movieDetails.releaseDate.split(',')[1], 24 | style: textTheme.bodyLarge, 25 | ), 26 | const CircleDot(), 27 | ], 28 | if (movieDetails.genres.isNotEmpty) ...[ 29 | Text( 30 | movieDetails.genres, 31 | style: textTheme.bodyLarge, 32 | ), 33 | const CircleDot(), 34 | ] else ...[ 35 | if (movieDetails.runtime!.isNotEmpty) ...[ 36 | const CircleDot(), 37 | ] 38 | ], 39 | Text( 40 | movieDetails.runtime!, 41 | style: textTheme.bodyLarge, 42 | ), 43 | ], 44 | ); 45 | } else { 46 | return const SizedBox(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/movies/presentation/components/review_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/resources/app_values.dart'; 3 | import 'package:movies_app/movies/domain/entities/review.dart'; 4 | import 'package:movies_app/movies/presentation/components/avatar.dart'; 5 | 6 | class ReviewContent extends StatelessWidget { 7 | const ReviewContent({ 8 | super.key, 9 | required this.review, 10 | }); 11 | 12 | final Review review; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final textTheme = Theme.of(context).textTheme; 17 | return SingleChildScrollView( 18 | physics: const BouncingScrollPhysics(), 19 | child: Padding( 20 | padding: const EdgeInsets.all(AppPadding.p16), 21 | child: Column( 22 | children: [ 23 | Row( 24 | children: [ 25 | Padding( 26 | padding: const EdgeInsets.only(right: AppPadding.p6), 27 | child: Avatar(avatarUrl: review.avatarUrl), 28 | ), 29 | Column( 30 | crossAxisAlignment: CrossAxisAlignment.start, 31 | children: [ 32 | Text( 33 | review.authorName, 34 | style: textTheme.bodyMedium, 35 | ), 36 | Text( 37 | review.authorUserName, 38 | style: textTheme.bodyLarge, 39 | ), 40 | ], 41 | ) 42 | ], 43 | ), 44 | Padding( 45 | padding: const EdgeInsets.only(top: AppPadding.p10), 46 | child: Text( 47 | review.content, 48 | style: textTheme.bodyLarge, 49 | ), 50 | ), 51 | ], 52 | ), 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/movies/presentation/controllers/movie_details_bloc/movie_details_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:equatable/equatable.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:movies_app/core/domain/entities/media_details.dart'; 6 | import 'package:movies_app/core/utils/enums.dart'; 7 | import 'package:movies_app/movies/domain/usecases/get_movie_details_usecase.dart'; 8 | 9 | part 'movie_details_event.dart'; 10 | part 'movie_details_state.dart'; 11 | 12 | class MovieDetailsBloc extends Bloc { 13 | final GetMoviesDetailsUseCase _getMoviesDetailsUseCase; 14 | 15 | MovieDetailsBloc(this._getMoviesDetailsUseCase) 16 | : super(const MovieDetailsState()) { 17 | on(_getMovieDetails); 18 | } 19 | 20 | Future _getMovieDetails( 21 | GetMovieDetailsEvent event, Emitter emit) async { 22 | emit( 23 | state.copyWith( 24 | status: RequestStatus.loading, 25 | ), 26 | ); 27 | final result = await _getMoviesDetailsUseCase(event.id); 28 | result.fold( 29 | (l) => emit( 30 | state.copyWith( 31 | status: RequestStatus.error, 32 | ), 33 | ), 34 | (r) => emit( 35 | state.copyWith( 36 | status: RequestStatus.loaded, 37 | movieDetails: r, 38 | ), 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/movies/presentation/controllers/movie_details_bloc/movie_details_event.dart: -------------------------------------------------------------------------------- 1 | part of 'movie_details_bloc.dart'; 2 | 3 | abstract class MovieDetailsEvent extends Equatable { 4 | const MovieDetailsEvent(); 5 | } 6 | 7 | class GetMovieDetailsEvent extends MovieDetailsEvent { 8 | final int id; 9 | const GetMovieDetailsEvent(this.id); 10 | 11 | @override 12 | List get props => [id]; 13 | } 14 | -------------------------------------------------------------------------------- /lib/movies/presentation/controllers/movie_details_bloc/movie_details_state.dart: -------------------------------------------------------------------------------- 1 | part of 'movie_details_bloc.dart'; 2 | 3 | class MovieDetailsState { 4 | final MediaDetails? movieDetails; 5 | final RequestStatus status; 6 | final String message; 7 | 8 | const MovieDetailsState({ 9 | this.movieDetails, 10 | this.status = RequestStatus.loading, 11 | this.message = '', 12 | }); 13 | 14 | MovieDetailsState copyWith({ 15 | MediaDetails? movieDetails, 16 | RequestStatus? status, 17 | String? message, 18 | }) { 19 | return MovieDetailsState( 20 | movieDetails: movieDetails ?? this.movieDetails, 21 | status: status ?? this.status, 22 | message: message ?? this.message, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/movies/presentation/controllers/movies_bloc/movies_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 5 | import 'package:movies_app/core/utils/enums.dart'; 6 | import 'package:movies_app/movies/domain/usecases/get_movies_usecase.dart'; 7 | 8 | import 'package:movies_app/movies/presentation/controllers/movies_bloc/movies_event.dart'; 9 | import 'package:movies_app/movies/presentation/controllers/movies_bloc/movies_state.dart'; 10 | 11 | class MoviesBloc extends Bloc { 12 | final GetMoviesUseCase _getMoviesUseCase; 13 | 14 | MoviesBloc( 15 | this._getMoviesUseCase, 16 | ) : super(const MoviesState()) { 17 | on(_getMovies); 18 | } 19 | 20 | Future _getMovies( 21 | GetMoviesEvent event, Emitter emit) async { 22 | emit( 23 | state.copyWith( 24 | status: RequestStatus.loading, 25 | ), 26 | ); 27 | final result = await _getMoviesUseCase(const NoParameters()); 28 | result.fold( 29 | (l) => emit( 30 | state.copyWith( 31 | status: RequestStatus.error, 32 | message: l.message, 33 | ), 34 | ), 35 | (r) => emit( 36 | state.copyWith( 37 | status: RequestStatus.loaded, 38 | movies: r, 39 | ), 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/movies/presentation/controllers/movies_bloc/movies_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class MoviesEvent extends Equatable { 4 | const MoviesEvent(); 5 | @override 6 | List get props => []; 7 | } 8 | 9 | class GetMoviesEvent extends MoviesEvent {} 10 | -------------------------------------------------------------------------------- /lib/movies/presentation/controllers/movies_bloc/movies_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:movies_app/core/domain/entities/media.dart'; 3 | import 'package:movies_app/core/utils/enums.dart'; 4 | 5 | class MoviesState extends Equatable { 6 | final List> movies; 7 | final RequestStatus status; 8 | final String message; 9 | 10 | const MoviesState({ 11 | this.movies = const [], 12 | this.status = RequestStatus.loading, 13 | this.message = '', 14 | }); 15 | 16 | MoviesState copyWith({ 17 | List>? movies, 18 | RequestStatus? status, 19 | String? message, 20 | }) { 21 | return MoviesState( 22 | movies: movies ?? this.movies, 23 | status: status ?? this.status, 24 | message: message ?? this.message, 25 | ); 26 | } 27 | 28 | @override 29 | List get props => [ 30 | movies, 31 | status, 32 | message, 33 | ]; 34 | } 35 | -------------------------------------------------------------------------------- /lib/movies/presentation/controllers/popular_movies_bloc/popular_movies_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:equatable/equatable.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:movies_app/core/domain/entities/media.dart'; 6 | import 'package:movies_app/movies/domain/usecases/get_all_popular_movies_usecase.dart'; 7 | 8 | import 'package:movies_app/core/utils/enums.dart'; 9 | 10 | part 'popular_movies_event.dart'; 11 | part 'popular_movies_state.dart'; 12 | 13 | class PopularMoviesBloc extends Bloc { 14 | final GetAllPopularMoviesUseCase _allPopularMoviesUseCase; 15 | 16 | PopularMoviesBloc(this._allPopularMoviesUseCase) 17 | : super(const PopularMoviesState()) { 18 | on(_getAllPopularMovies); 19 | on(_fetchMoreMovies); 20 | } 21 | 22 | int page = 1; 23 | 24 | Future _getAllPopularMovies( 25 | GetPopularMoviesEvent event, Emitter emit) async { 26 | if (state.status == GetAllRequestStatus.loading) { 27 | await _getMovies(emit); 28 | } else if (state.status == GetAllRequestStatus.loaded) { 29 | await _getMovies(emit); 30 | } else { 31 | emit( 32 | state.copyWith( 33 | status: GetAllRequestStatus.loading, 34 | ), 35 | ); 36 | await _getMovies(emit); 37 | } 38 | } 39 | 40 | Future _getMovies(Emitter emit) async { 41 | final result = await _allPopularMoviesUseCase(page); 42 | result.fold( 43 | (l) => emit( 44 | state.copyWith( 45 | status: GetAllRequestStatus.error, 46 | ), 47 | ), 48 | (r) { 49 | page++; 50 | emit( 51 | state.copyWith( 52 | status: GetAllRequestStatus.loaded, 53 | movies: state.movies + r, 54 | ), 55 | ); 56 | }, 57 | ); 58 | } 59 | 60 | Future _fetchMoreMovies(FetchMorePopularMoviesEvent event, 61 | Emitter emit) async { 62 | final result = await _allPopularMoviesUseCase(page); 63 | result.fold( 64 | (l) => emit( 65 | state.copyWith( 66 | status: GetAllRequestStatus.fetchMoreError, 67 | ), 68 | ), 69 | (r) { 70 | page++; 71 | return emit( 72 | state.copyWith( 73 | status: GetAllRequestStatus.loaded, 74 | movies: state.movies + r, 75 | ), 76 | ); 77 | }, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/movies/presentation/controllers/popular_movies_bloc/popular_movies_event.dart: -------------------------------------------------------------------------------- 1 | part of 'popular_movies_bloc.dart'; 2 | 3 | abstract class PopularMoviesEvent extends Equatable { 4 | const PopularMoviesEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class GetPopularMoviesEvent extends PopularMoviesEvent {} 11 | 12 | class FetchMorePopularMoviesEvent extends PopularMoviesEvent {} 13 | -------------------------------------------------------------------------------- /lib/movies/presentation/controllers/popular_movies_bloc/popular_movies_state.dart: -------------------------------------------------------------------------------- 1 | part of 'popular_movies_bloc.dart'; 2 | 3 | class PopularMoviesState extends Equatable { 4 | final List movies; 5 | final GetAllRequestStatus status; 6 | final String message; 7 | 8 | const PopularMoviesState({ 9 | this.movies = const [], 10 | this.status = GetAllRequestStatus.loading, 11 | this.message = '', 12 | }); 13 | 14 | PopularMoviesState copyWith({ 15 | List? movies, 16 | GetAllRequestStatus? status, 17 | String? message, 18 | }) { 19 | return PopularMoviesState( 20 | movies: movies ?? this.movies, 21 | status: status ?? this.status, 22 | message: message ?? this.message, 23 | ); 24 | } 25 | 26 | @override 27 | List get props => [ 28 | movies, 29 | status, 30 | message, 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /lib/movies/presentation/controllers/top_rated_movies_bloc/top_rated_movies_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:movies_app/core/domain/entities/media.dart'; 6 | import 'package:movies_app/core/utils/enums.dart'; 7 | import 'package:movies_app/movies/domain/usecases/get_all_top_rated_movies_usecase.dart'; 8 | 9 | part 'top_rated_movies_event.dart'; 10 | part 'top_rated_movies_state.dart'; 11 | 12 | class TopRatedMoviesBloc 13 | extends Bloc { 14 | TopRatedMoviesBloc(this._getAllTopRatedMoviesUseCase) 15 | : super(const TopRatedMoviesState()) { 16 | on(_getAllTopRatedMovies); 17 | on(_fetchMoreMovies); 18 | } 19 | 20 | final GetAllTopRatedMoviesUseCase _getAllTopRatedMoviesUseCase; 21 | int page = 1; 22 | 23 | Future _getAllTopRatedMovies( 24 | TopRatedMoviesEvent event, Emitter emit) async { 25 | if (state.status == GetAllRequestStatus.loading) { 26 | await _getMovies(emit); 27 | } else if (state.status == GetAllRequestStatus.loaded) { 28 | await _getMovies(emit); 29 | } else if (state.status == GetAllRequestStatus.error) { 30 | emit( 31 | state.copyWith( 32 | status: GetAllRequestStatus.loading, 33 | ), 34 | ); 35 | await _getMovies(emit); 36 | } 37 | } 38 | 39 | Future _getMovies(Emitter emit) async { 40 | final result = await _getAllTopRatedMoviesUseCase(page); 41 | result.fold( 42 | (l) => emit( 43 | state.copyWith( 44 | status: GetAllRequestStatus.error, 45 | ), 46 | ), 47 | (r) { 48 | page++; 49 | emit( 50 | state.copyWith( 51 | status: GetAllRequestStatus.loaded, 52 | movies: state.movies + r, 53 | ), 54 | ); 55 | }, 56 | ); 57 | } 58 | 59 | Future _fetchMoreMovies(FetchMoreTopRatedMoviesEvent event, 60 | Emitter emit) async { 61 | final result = await _getAllTopRatedMoviesUseCase(page); 62 | result.fold( 63 | (l) => emit( 64 | state.copyWith( 65 | status: GetAllRequestStatus.fetchMoreError, 66 | ), 67 | ), 68 | (r) { 69 | page++; 70 | return emit( 71 | state.copyWith( 72 | status: GetAllRequestStatus.loaded, 73 | movies: state.movies + r, 74 | ), 75 | ); 76 | }, 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/movies/presentation/controllers/top_rated_movies_bloc/top_rated_movies_event.dart: -------------------------------------------------------------------------------- 1 | part of 'top_rated_movies_bloc.dart'; 2 | 3 | abstract class TopRatedMoviesEvent extends Equatable { 4 | const TopRatedMoviesEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class GetTopRatedMoviesEvent extends TopRatedMoviesEvent {} 11 | 12 | class FetchMoreTopRatedMoviesEvent extends TopRatedMoviesEvent {} 13 | -------------------------------------------------------------------------------- /lib/movies/presentation/controllers/top_rated_movies_bloc/top_rated_movies_state.dart: -------------------------------------------------------------------------------- 1 | part of 'top_rated_movies_bloc.dart'; 2 | 3 | class TopRatedMoviesState extends Equatable { 4 | const TopRatedMoviesState({ 5 | this.movies = const [], 6 | this.status = GetAllRequestStatus.loading, 7 | this.message = '', 8 | }); 9 | 10 | final List movies; 11 | final GetAllRequestStatus status; 12 | final String message; 13 | 14 | TopRatedMoviesState copyWith({ 15 | List? movies, 16 | GetAllRequestStatus? status, 17 | String? message, 18 | }) { 19 | return TopRatedMoviesState( 20 | movies: movies ?? this.movies, 21 | status: status ?? this.status, 22 | message: message ?? this.message, 23 | ); 24 | } 25 | 26 | @override 27 | List get props => [ 28 | movies, 29 | status, 30 | message, 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /lib/search/data/datasource/search_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:movies_app/core/data/error/exceptions.dart'; 3 | import 'package:movies_app/core/data/network/api_constants.dart'; 4 | import 'package:movies_app/core/data/network/error_message_model.dart'; 5 | import 'package:movies_app/search/data/models/search_result_item_model.dart'; 6 | 7 | abstract class SearchRemoteDataSource { 8 | Future> search(String title); 9 | } 10 | 11 | class SearchRemoteDataSourceImpl extends SearchRemoteDataSource { 12 | @override 13 | Future> search(String title) async { 14 | final response = await Dio().get(ApiConstants.getSearchPath(title)); 15 | if (response.statusCode == 200) { 16 | return List.from((response.data['results'] as List) 17 | .where((e) => e['media_type'] != 'person') 18 | .map((e) => SearchResultItemModel.fromJson(e))); 19 | } else { 20 | throw ServerException( 21 | errorMessageModel: ErrorMessageModel.fromJson(response.data), 22 | ); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/search/data/models/search_result_item_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/utils/functions.dart'; 2 | import 'package:movies_app/search/domain/entities/search_result_item.dart'; 3 | 4 | class SearchResultItemModel extends SearchResultItem { 5 | const SearchResultItemModel({ 6 | required super.tmdbID, 7 | required super.posterUrl, 8 | required super.title, 9 | required super.isMovie, 10 | }); 11 | 12 | factory SearchResultItemModel.fromJson(Map json) { 13 | return SearchResultItemModel( 14 | tmdbID: json['id'], 15 | posterUrl: getPosterUrl(json['poster_path']), 16 | title: json['title'] ?? json['name'], 17 | isMovie: json['media_type'] == 'movie', 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/search/data/repository/search_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:movies_app/core/data/error/exceptions.dart'; 3 | import 'package:movies_app/search/data/datasource/search_remote_data_source.dart'; 4 | import 'package:movies_app/search/domain/entities/search_result_item.dart'; 5 | import 'package:movies_app/core/data/error/failure.dart'; 6 | import 'package:dartz/dartz.dart'; 7 | import 'package:movies_app/search/domain/repository/search_repository.dart'; 8 | 9 | class SearchRepositoryImpl extends SearchRepository { 10 | final SearchRemoteDataSource _baseSearchRemoteDataSource; 11 | 12 | SearchRepositoryImpl(this._baseSearchRemoteDataSource); 13 | 14 | @override 15 | Future>> search(String title) async { 16 | try { 17 | final result = await _baseSearchRemoteDataSource.search(title); 18 | return Right(result); 19 | } on ServerException catch (failure) { 20 | return Left(ServerFailure(failure.errorMessageModel.statusMessage)); 21 | } on DioError catch (failure) { 22 | return Left(ServerFailure(failure.message)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/search/domain/entities/search_result_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class SearchResultItem extends Equatable { 4 | final int tmdbID; 5 | final String posterUrl; 6 | final String title; 7 | final bool isMovie; 8 | 9 | const SearchResultItem({ 10 | required this.tmdbID, 11 | required this.posterUrl, 12 | required this.title, 13 | required this.isMovie, 14 | }); 15 | 16 | @override 17 | List get props => [ 18 | tmdbID, 19 | posterUrl, 20 | title, 21 | isMovie, 22 | ]; 23 | } 24 | -------------------------------------------------------------------------------- /lib/search/domain/repository/search_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:movies_app/core/data/error/failure.dart'; 3 | import 'package:movies_app/search/domain/entities/search_result_item.dart'; 4 | 5 | abstract class SearchRepository { 6 | Future>> search(String title); 7 | } 8 | -------------------------------------------------------------------------------- /lib/search/domain/usecases/search_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/data/error/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 4 | import 'package:movies_app/search/domain/entities/search_result_item.dart'; 5 | import 'package:movies_app/search/domain/repository/search_repository.dart'; 6 | 7 | class SearchUseCase extends BaseUseCase, String> { 8 | final SearchRepository _baseSearchRepository; 9 | 10 | SearchUseCase(this._baseSearchRepository); 11 | 12 | @override 13 | Future>> call(String p) async { 14 | return await _baseSearchRepository.search(p); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/search/presentation/components/grid_view_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:movies_app/core/presentation/components/image_with_shimmer.dart'; 4 | import 'package:movies_app/core/resources/app_routes.dart'; 5 | import 'package:movies_app/core/resources/app_values.dart'; 6 | import 'package:movies_app/search/domain/entities/search_result_item.dart'; 7 | 8 | class GridViewCard extends StatelessWidget { 9 | const GridViewCard({ 10 | super.key, 11 | required this.item, 12 | }); 13 | 14 | final SearchResultItem item; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final textTheme = Theme.of(context).textTheme; 19 | return Column( 20 | children: [ 21 | GestureDetector( 22 | onTap: () { 23 | item.isMovie 24 | ? context.pushNamed(AppRoutes.movieDetailsRoute, 25 | params: {'movieId': item.tmdbID.toString()}) 26 | : context.pushNamed(AppRoutes.tvShowDetailsRoute, 27 | params: {'tvShowId': item.tmdbID.toString()}); 28 | }, 29 | child: AspectRatio( 30 | aspectRatio: 2 / 3, 31 | child: ClipRRect( 32 | borderRadius: BorderRadius.circular(AppSize.s8), 33 | child: ImageWithShimmer( 34 | imageUrl: item.posterUrl, 35 | width: double.infinity, 36 | height: AppSize.s150, 37 | ), 38 | ), 39 | ), 40 | ), 41 | Expanded( 42 | child: Text( 43 | item.title, 44 | maxLines: 1, 45 | overflow: TextOverflow.ellipsis, 46 | style: textTheme.bodyMedium, 47 | ), 48 | ), 49 | ], 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/search/presentation/components/no_results.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/resources/app_strings.dart'; 3 | 4 | class NoResults extends StatelessWidget { 5 | const NoResults({ 6 | super.key, 7 | }); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final textTheme = Theme.of(context).textTheme; 12 | 13 | return Expanded( 14 | child: Center( 15 | child: Text( 16 | AppStrings.noResults, 17 | style: textTheme.bodyLarge, 18 | ), 19 | ), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/search/presentation/components/search_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:movies_app/core/resources/app_colors.dart'; 4 | import 'package:movies_app/core/resources/app_strings.dart'; 5 | import 'package:movies_app/core/resources/app_values.dart'; 6 | import 'package:movies_app/search/presentation/controllers/search_bloc/search_bloc.dart'; 7 | 8 | class SearchField extends StatefulWidget { 9 | const SearchField({ 10 | super.key, 11 | }); 12 | 13 | @override 14 | State createState() => _SearchFieldState(); 15 | } 16 | 17 | class _SearchFieldState extends State { 18 | final _textController = TextEditingController(); 19 | 20 | @override 21 | void dispose() { 22 | _textController.dispose(); 23 | super.dispose(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | final textTheme = Theme.of(context).textTheme; 29 | return Form( 30 | child: TextFormField( 31 | controller: _textController, 32 | cursorColor: AppColors.primaryText, 33 | cursorWidth: AppSize.s1, 34 | style: textTheme.bodyLarge, 35 | onChanged: (title) { 36 | context.read().add(GetSearchResultsEvent(title)); 37 | }, 38 | decoration: InputDecoration( 39 | focusedBorder: OutlineInputBorder( 40 | borderSide: const BorderSide( 41 | color: AppColors.primaryText, 42 | ), 43 | borderRadius: BorderRadius.circular(AppSize.s8), 44 | ), 45 | enabledBorder: OutlineInputBorder( 46 | borderSide: const BorderSide( 47 | color: AppColors.primaryText, 48 | ), 49 | borderRadius: BorderRadius.circular(AppSize.s8), 50 | ), 51 | prefixIcon: const Icon( 52 | Icons.search_rounded, 53 | color: AppColors.primaryText, 54 | ), 55 | suffixIcon: GestureDetector( 56 | onTap: () { 57 | _textController.text = ''; 58 | context.read().add(const GetSearchResultsEvent('')); 59 | }, 60 | child: const Icon( 61 | Icons.clear_rounded, 62 | color: AppColors.primaryText, 63 | ), 64 | ), 65 | hintText: AppStrings.searchHint, 66 | hintStyle: textTheme.bodyLarge, 67 | ), 68 | ), 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/search/presentation/components/search_grid_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/resources/app_values.dart'; 3 | import 'package:movies_app/search/domain/entities/search_result_item.dart'; 4 | import 'package:movies_app/search/presentation/components/grid_view_card.dart'; 5 | 6 | class SearchGridView extends StatelessWidget { 7 | const SearchGridView({ 8 | super.key, 9 | required this.results, 10 | }); 11 | 12 | final List results; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Expanded( 17 | child: GridView.builder( 18 | padding: const EdgeInsets.symmetric(vertical: AppPadding.p12), 19 | itemCount: results.length, 20 | physics: const BouncingScrollPhysics(), 21 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 22 | crossAxisCount: 3, 23 | crossAxisSpacing: 12, 24 | childAspectRatio: 0.55, 25 | ), 26 | itemBuilder: (context, index) { 27 | return GridViewCard(item: results[index]); 28 | }, 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/search/presentation/components/search_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/resources/app_strings.dart'; 3 | import 'package:movies_app/core/resources/app_values.dart'; 4 | 5 | class SearchText extends StatelessWidget { 6 | const SearchText({ 7 | super.key, 8 | }); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final textTheme = Theme.of(context).textTheme; 13 | return Expanded( 14 | child: Column( 15 | mainAxisAlignment: MainAxisAlignment.center, 16 | children: [ 17 | Text( 18 | AppStrings.search, 19 | style: textTheme.titleMedium, 20 | ), 21 | Padding( 22 | padding: const EdgeInsets.only(top: AppPadding.p6), 23 | child: Text( 24 | AppStrings.searchText, 25 | style: textTheme.bodyLarge, 26 | textAlign: TextAlign.center, 27 | ), 28 | ), 29 | ], 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/search/presentation/controllers/search_bloc/search_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:movies_app/search/domain/entities/search_result_item.dart'; 6 | import 'package:movies_app/search/domain/usecases/search_usecase.dart'; 7 | import 'package:stream_transform/stream_transform.dart'; 8 | 9 | part 'search_event.dart'; 10 | part 'search_state.dart'; 11 | 12 | const _duration = Duration(milliseconds: 400); 13 | 14 | EventTransformer debounce(Duration duration) { 15 | return (events, mapper) => events.debounce(duration).switchMap(mapper); 16 | } 17 | 18 | class SearchBloc extends Bloc { 19 | SearchBloc(this._searchUseCase) : super(const SearchState()) { 20 | on(_getSearchResults, 21 | transformer: debounce(_duration)); 22 | } 23 | 24 | final SearchUseCase _searchUseCase; 25 | 26 | Future _getSearchResults( 27 | GetSearchResultsEvent event, Emitter emit) async { 28 | if (event.title.trim().isEmpty) { 29 | return emit( 30 | state.copyWith( 31 | status: SearchRequestStatus.empty, 32 | ), 33 | ); 34 | } 35 | 36 | emit( 37 | state.copyWith( 38 | status: SearchRequestStatus.loading, 39 | ), 40 | ); 41 | 42 | final result = await _searchUseCase(event.title); 43 | result.fold( 44 | (l) => emit( 45 | state.copyWith( 46 | status: SearchRequestStatus.error, 47 | message: l.message, 48 | ), 49 | ), 50 | (r) { 51 | if (r.isEmpty) { 52 | emit( 53 | state.copyWith( 54 | status: SearchRequestStatus.noResults, 55 | ), 56 | ); 57 | } else { 58 | emit( 59 | state.copyWith( 60 | status: SearchRequestStatus.loaded, 61 | searchResults: r, 62 | ), 63 | ); 64 | } 65 | }, 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/search/presentation/controllers/search_bloc/search_event.dart: -------------------------------------------------------------------------------- 1 | part of 'search_bloc.dart'; 2 | 3 | abstract class SearchEvent extends Equatable { 4 | const SearchEvent(); 5 | } 6 | 7 | class GetSearchResultsEvent extends SearchEvent { 8 | final String title; 9 | 10 | const GetSearchResultsEvent(this.title); 11 | 12 | @override 13 | List get props => [title]; 14 | } 15 | -------------------------------------------------------------------------------- /lib/search/presentation/controllers/search_bloc/search_state.dart: -------------------------------------------------------------------------------- 1 | part of 'search_bloc.dart'; 2 | 3 | enum SearchRequestStatus { empty, loading, loaded, error, noResults } 4 | 5 | class SearchState extends Equatable { 6 | const SearchState({ 7 | this.searchResults = const [], 8 | this.status = SearchRequestStatus.empty, 9 | this.message = '', 10 | }); 11 | 12 | final List searchResults; 13 | final SearchRequestStatus status; 14 | final String message; 15 | 16 | SearchState copyWith({ 17 | List? searchResults, 18 | SearchRequestStatus? status, 19 | String? message, 20 | }) { 21 | return SearchState( 22 | searchResults: searchResults ?? this.searchResults, 23 | status: status ?? this.status, 24 | message: message ?? this.message, 25 | ); 26 | } 27 | 28 | @override 29 | List get props => [ 30 | searchResults, 31 | status, 32 | message, 33 | ]; 34 | } 35 | -------------------------------------------------------------------------------- /lib/search/presentation/views/search_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:movies_app/core/presentation/components/error_text.dart'; 4 | import 'package:movies_app/core/presentation/components/loading_indicator.dart'; 5 | import 'package:movies_app/core/resources/app_values.dart'; 6 | import 'package:movies_app/core/services/service_locator.dart'; 7 | import 'package:movies_app/search/presentation/components/no_results.dart'; 8 | import 'package:movies_app/search/presentation/components/search_grid_view.dart'; 9 | import 'package:movies_app/search/presentation/components/search_text.dart'; 10 | import 'package:movies_app/search/presentation/components/search_field.dart'; 11 | import 'package:movies_app/search/presentation/controllers/search_bloc/search_bloc.dart'; 12 | 13 | class SearchView extends StatelessWidget { 14 | const SearchView({super.key}); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return BlocProvider( 19 | create: (context) => sl(), 20 | child: const SearchWidget(), 21 | ); 22 | } 23 | } 24 | 25 | class SearchWidget extends StatelessWidget { 26 | const SearchWidget({super.key}); 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return SafeArea( 31 | child: Scaffold( 32 | body: Padding( 33 | padding: const EdgeInsets.only( 34 | top: AppPadding.p12, 35 | left: AppPadding.p16, 36 | right: AppPadding.p16, 37 | ), 38 | child: Column( 39 | children: [ 40 | const SearchField(), 41 | BlocBuilder( 42 | builder: (context, state) { 43 | switch (state.status) { 44 | case SearchRequestStatus.empty: 45 | return const SearchText(); 46 | case SearchRequestStatus.loading: 47 | return const Expanded(child: LoadingIndicator()); 48 | case SearchRequestStatus.loaded: 49 | return SearchGridView(results: state.searchResults); 50 | case SearchRequestStatus.error: 51 | return const Expanded(child: ErrorText()); 52 | case SearchRequestStatus.noResults: 53 | return const NoResults(); 54 | } 55 | }, 56 | ), 57 | ], 58 | ), 59 | ), 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/tv_shows/data/models/episode_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/utils/functions.dart'; 2 | import 'package:movies_app/tv_shows/domain/entities/episode.dart'; 3 | 4 | class EpisodeModel extends Episode { 5 | const EpisodeModel({ 6 | required super.number, 7 | required super.season, 8 | required super.name, 9 | required super.runtime, 10 | required super.stillPath, 11 | required super.airDate, 12 | }); 13 | 14 | factory EpisodeModel.fromJson(Map json) { 15 | return EpisodeModel( 16 | number: json['episode_number'], 17 | season: json['season_number'], 18 | name: json['name'], 19 | runtime: getLength(json['runtime']), 20 | stillPath: getStillUrl(json['still_path']), 21 | airDate: getDate(json['air_date']), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/tv_shows/data/models/season_details_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/tv_shows/data/models/episode_model.dart'; 2 | import 'package:movies_app/tv_shows/domain/entities/season_details.dart'; 3 | 4 | class SeasonDetailsModel extends SeasonDetails { 5 | const SeasonDetailsModel({ 6 | required super.episodes, 7 | }); 8 | 9 | factory SeasonDetailsModel.fromJson(Map json) { 10 | return SeasonDetailsModel( 11 | episodes: List.from( 12 | (json['episodes'] as List).map((e) => EpisodeModel.fromJson(e))), 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/tv_shows/data/models/season_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/utils/functions.dart'; 2 | import 'package:movies_app/tv_shows/domain/entities/season.dart'; 3 | 4 | class SeasonModel extends Season { 5 | const SeasonModel({ 6 | required super.tmdbID, 7 | required super.name, 8 | required super.episodeCount, 9 | required super.airDate, 10 | required super.overview, 11 | required super.posterUrl, 12 | required super.seasonNumber, 13 | }); 14 | 15 | factory SeasonModel.fromJson(Map json) { 16 | return SeasonModel( 17 | tmdbID: json['id'], 18 | name: json['name'], 19 | episodeCount: json['episode_count'], 20 | airDate: getDate(json['air_date']), 21 | overview: json['overview'], 22 | posterUrl: getPosterUrl(json['poster_path']), 23 | seasonNumber: json['season_number'], 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/tv_shows/data/models/tv_show_details_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/domain/entities/media_details.dart'; 2 | import 'package:movies_app/core/utils/functions.dart'; 3 | import 'package:movies_app/tv_shows/data/models/tv_show_model.dart'; 4 | 5 | import 'package:movies_app/tv_shows/data/models/episode_model.dart'; 6 | import 'package:movies_app/tv_shows/data/models/season_model.dart'; 7 | 8 | // ignore: must_be_immutable 9 | class TVShowDetailsModel extends MediaDetails { 10 | TVShowDetailsModel({ 11 | required super.tmdbID, 12 | required super.title, 13 | required super.posterUrl, 14 | required super.backdropUrl, 15 | required super.releaseDate, 16 | required super.lastEpisodeToAir, 17 | required super.genres, 18 | required super.overview, 19 | required super.voteAverage, 20 | required super.voteCount, 21 | required super.trailerUrl, 22 | required super.numberOfSeasons, 23 | required super.seasons, 24 | required super.similar, 25 | }); 26 | 27 | factory TVShowDetailsModel.fromJson(Map json) { 28 | return TVShowDetailsModel( 29 | tmdbID: json['id'], 30 | title: json['name'], 31 | posterUrl: getPosterUrl(json['poster_path']), 32 | backdropUrl: getBackdropUrl(json['backdrop_path']), 33 | releaseDate: getDate(json['first_air_date']), 34 | lastEpisodeToAir: EpisodeModel.fromJson(json['last_episode_to_air']), 35 | genres: getGenres(json['genres']), 36 | numberOfSeasons: json['number_of_seasons'], 37 | voteAverage: 38 | double.parse((json['vote_average'] as double).toStringAsFixed(1)), 39 | voteCount: getVotesCount(json['vote_count']), 40 | overview: json['overview'], 41 | trailerUrl: getTrailerUrl(json), 42 | seasons: List.from( 43 | ((json['seasons'] as List) 44 | .where((e) => e['name'] != 'Specials') 45 | .map((e) => SeasonModel.fromJson(e))), 46 | ), 47 | similar: List.from((json['similar']['results'] as List) 48 | .map((e) => TVShowModel.fromJson(e))), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/tv_shows/data/models/tv_show_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/domain/entities/media.dart'; 2 | import 'package:movies_app/core/utils/functions.dart'; 3 | 4 | class TVShowModel extends Media { 5 | const TVShowModel({ 6 | required super.tmdbID, 7 | required super.title, 8 | required super.posterUrl, 9 | required super.backdropUrl, 10 | required super.voteAverage, 11 | required super.releaseDate, 12 | required super.overview, 13 | required super.isMovie, 14 | }); 15 | 16 | factory TVShowModel.fromJson(Map json) { 17 | return TVShowModel( 18 | tmdbID: json['id'], 19 | title: json['name'], 20 | posterUrl: getPosterUrl(json['poster_path']), 21 | backdropUrl: getBackdropUrl(json['backdrop_path']), 22 | voteAverage: double.parse((json['vote_average']).toStringAsFixed(1)), 23 | releaseDate: getDate(json['first_air_date']), 24 | overview: json['overview'], 25 | isMovie: false, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/tv_shows/domain/entities/episode.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Episode extends Equatable { 4 | final int number; 5 | final int season; 6 | final String name; 7 | final String runtime; 8 | final String stillPath; 9 | final String airDate; 10 | 11 | const Episode({ 12 | required this.number, 13 | required this.season, 14 | required this.name, 15 | required this.runtime, 16 | required this.stillPath, 17 | required this.airDate, 18 | }); 19 | 20 | @override 21 | List get props => [ 22 | number, 23 | season, 24 | name, 25 | runtime, 26 | stillPath, 27 | airDate, 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /lib/tv_shows/domain/entities/season.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Season extends Equatable { 4 | final int tmdbID; 5 | final String name; 6 | final int episodeCount; 7 | final String airDate; 8 | final String overview; 9 | final String posterUrl; 10 | final int seasonNumber; 11 | 12 | const Season({ 13 | required this.tmdbID, 14 | required this.name, 15 | required this.episodeCount, 16 | required this.airDate, 17 | required this.overview, 18 | required this.posterUrl, 19 | required this.seasonNumber, 20 | }); 21 | 22 | @override 23 | List get props => [ 24 | tmdbID, 25 | name, 26 | episodeCount, 27 | airDate, 28 | overview, 29 | posterUrl, 30 | seasonNumber, 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /lib/tv_shows/domain/entities/season_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:movies_app/tv_shows/domain/entities/episode.dart'; 3 | 4 | class SeasonDetails extends Equatable { 5 | const SeasonDetails({ 6 | required this.episodes, 7 | }); 8 | 9 | final List episodes; 10 | 11 | @override 12 | List get props => [ 13 | episodes, 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /lib/tv_shows/domain/repository/tv_shows_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:movies_app/core/data/error/failure.dart'; 3 | import 'package:movies_app/core/domain/entities/media.dart'; 4 | import 'package:movies_app/core/domain/entities/media_details.dart'; 5 | import 'package:movies_app/tv_shows/domain/entities/season_details.dart'; 6 | import 'package:movies_app/tv_shows/domain/usecases/get_season_details_usecase.dart'; 7 | 8 | abstract class TVShowsRepository { 9 | Future>>> getTVShows(); 10 | Future> getTVShowDetails(int id); 11 | Future> getSeasonDetails( 12 | SeasonDetailsParams params); 13 | Future>> getAllPopularTVShows(int page); 14 | Future>> getAllTopRatedTVShows(int page); 15 | } 16 | -------------------------------------------------------------------------------- /lib/tv_shows/domain/usecases/get_all_popular_tv_shows_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/data/error/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:movies_app/core/domain/entities/media.dart'; 4 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 5 | import 'package:movies_app/tv_shows/domain/repository/tv_shows_repository.dart'; 6 | 7 | class GetAllPopularTVShowsUseCase extends BaseUseCase, int> { 8 | final TVShowsRepository _baseTVShowsRepository; 9 | 10 | GetAllPopularTVShowsUseCase(this._baseTVShowsRepository); 11 | 12 | @override 13 | Future>> call(int p) async { 14 | return await _baseTVShowsRepository.getAllPopularTVShows(p); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/tv_shows/domain/usecases/get_all_top_rated_tv_shows_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:movies_app/core/data/error/failure.dart'; 3 | import 'package:movies_app/core/domain/entities/media.dart'; 4 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 5 | import 'package:movies_app/tv_shows/domain/repository/tv_shows_repository.dart'; 6 | 7 | class GetAllTopRatedTVShowsUseCase extends BaseUseCase, int> { 8 | final TVShowsRepository _baseTVShowsRepository; 9 | 10 | GetAllTopRatedTVShowsUseCase(this._baseTVShowsRepository); 11 | 12 | @override 13 | Future>> call(int p) async { 14 | return await _baseTVShowsRepository.getAllTopRatedTVShows(p); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/tv_shows/domain/usecases/get_season_details_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:movies_app/core/data/error/failure.dart'; 3 | import 'package:dartz/dartz.dart'; 4 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 5 | import 'package:movies_app/tv_shows/domain/entities/season_details.dart'; 6 | import 'package:movies_app/tv_shows/domain/repository/tv_shows_repository.dart'; 7 | 8 | class GetSeasonDetailsUseCase 9 | extends BaseUseCase { 10 | final TVShowsRepository _baseTVShowsRepository; 11 | 12 | GetSeasonDetailsUseCase(this._baseTVShowsRepository); 13 | @override 14 | Future> call(SeasonDetailsParams p) async { 15 | return await _baseTVShowsRepository.getSeasonDetails(p); 16 | } 17 | } 18 | 19 | class SeasonDetailsParams extends Equatable { 20 | final int id; 21 | final int seasonNumber; 22 | 23 | const SeasonDetailsParams({ 24 | required this.id, 25 | required this.seasonNumber, 26 | }); 27 | 28 | @override 29 | List get props => [ 30 | id, 31 | seasonNumber, 32 | ]; 33 | } 34 | -------------------------------------------------------------------------------- /lib/tv_shows/domain/usecases/get_tv_show_details_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/data/error/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:movies_app/core/domain/entities/media_details.dart'; 4 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 5 | import 'package:movies_app/tv_shows/domain/repository/tv_shows_repository.dart'; 6 | 7 | class GetTVShowDetailsUseCase extends BaseUseCase { 8 | final TVShowsRepository _baseTVShowsRepository; 9 | 10 | GetTVShowDetailsUseCase(this._baseTVShowsRepository); 11 | @override 12 | Future> call(int p) async { 13 | return await _baseTVShowsRepository.getTVShowDetails(p); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/tv_shows/domain/usecases/get_tv_shows_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/data/error/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:movies_app/core/domain/entities/media.dart'; 4 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 5 | import 'package:movies_app/tv_shows/domain/repository/tv_shows_repository.dart'; 6 | 7 | class GetTVShowsUseCase extends BaseUseCase>, NoParameters> { 8 | final TVShowsRepository _baseTVShowsRepository; 9 | 10 | GetTVShowsUseCase(this._baseTVShowsRepository); 11 | 12 | @override 13 | Future>>> call(NoParameters p) async { 14 | return await _baseTVShowsRepository.getTVShows(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/components/episode_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/presentation/components/image_with_shimmer.dart'; 3 | import 'package:movies_app/core/resources/app_strings.dart'; 4 | import 'package:movies_app/core/resources/app_values.dart'; 5 | import 'package:movies_app/tv_shows/domain/entities/episode.dart'; 6 | 7 | class EpisodeCard extends StatelessWidget { 8 | const EpisodeCard({ 9 | super.key, 10 | required this.episode, 11 | }); 12 | 13 | final Episode episode; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final textTheme = Theme.of(context).textTheme; 18 | return Container( 19 | height: AppSize.s84, 20 | padding: const EdgeInsets.symmetric(horizontal: AppPadding.p16), 21 | child: Row( 22 | children: [ 23 | Padding( 24 | padding: const EdgeInsets.only(right: AppPadding.p8), 25 | child: ClipRRect( 26 | borderRadius: BorderRadius.circular(AppSize.s8), 27 | child: ImageWithShimmer( 28 | imageUrl: episode.stillPath, 29 | width: AppSize.s150, 30 | height: double.infinity, 31 | ), 32 | ), 33 | ), 34 | Expanded( 35 | child: Column( 36 | mainAxisAlignment: MainAxisAlignment.center, 37 | crossAxisAlignment: CrossAxisAlignment.start, 38 | children: [ 39 | Text( 40 | '${AppStrings.episode} ${episode.number}', 41 | style: textTheme.bodyMedium, 42 | ), 43 | Text( 44 | episode.name, 45 | maxLines: 1, 46 | overflow: TextOverflow.ellipsis, 47 | style: textTheme.bodyLarge, 48 | ), 49 | Text( 50 | episode.airDate, 51 | style: textTheme.bodyLarge, 52 | ), 53 | Text( 54 | episode.runtime, 55 | style: textTheme.bodyLarge, 56 | ), 57 | ], 58 | ), 59 | ) 60 | ], 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/components/episodes_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/resources/app_strings.dart'; 3 | import 'package:movies_app/core/resources/app_values.dart'; 4 | import 'package:movies_app/tv_shows/domain/entities/episode.dart'; 5 | import 'package:movies_app/tv_shows/presentation/components/episode_card.dart'; 6 | 7 | class EpisodesWidget extends StatelessWidget { 8 | const EpisodesWidget({ 9 | super.key, 10 | required this.episodes, 11 | }); 12 | 13 | final List episodes; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final textTheme = Theme.of(context).textTheme; 18 | return SizedBox( 19 | height: AppSize.s400, 20 | child: Column( 21 | children: [ 22 | Padding( 23 | padding: const EdgeInsets.only( 24 | bottom: AppPadding.p8, 25 | top: AppPadding.p6, 26 | ), 27 | child: Text( 28 | AppStrings.episodes, 29 | style: textTheme.titleMedium, 30 | ), 31 | ), 32 | Expanded( 33 | child: ListView.separated( 34 | padding: const EdgeInsets.only(bottom: AppPadding.p8), 35 | physics: const BouncingScrollPhysics(), 36 | itemCount: episodes.length, 37 | itemBuilder: (context, index) => 38 | EpisodeCard(episode: episodes[index]), 39 | separatorBuilder: (context, index) => 40 | const SizedBox(height: AppSize.s10), 41 | ), 42 | ), 43 | ], 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/components/seasons_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/resources/app_values.dart'; 3 | import 'package:movies_app/tv_shows/domain/entities/season.dart'; 4 | import 'package:movies_app/tv_shows/presentation/components/season_card.dart'; 5 | 6 | class SeasonsSection extends StatelessWidget { 7 | const SeasonsSection({ 8 | super.key, 9 | required this.seasons, 10 | required this.tmdbID, 11 | }); 12 | 13 | final List seasons; 14 | final int tmdbID; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return ConstrainedBox( 19 | constraints: const BoxConstraints(maxHeight: AppSize.s400), 20 | child: ListView.separated( 21 | shrinkWrap: true, 22 | padding: const EdgeInsets.symmetric(horizontal: AppPadding.p16), 23 | physics: const BouncingScrollPhysics(), 24 | itemCount: seasons.length, 25 | itemBuilder: (context, index) => SeasonCard( 26 | season: seasons[index], 27 | tvShowId: tmdbID, 28 | ), 29 | separatorBuilder: (context, index) => 30 | const SizedBox(height: AppSize.s10), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/components/tv_show_card_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/presentation/components/circle_dot.dart'; 3 | 4 | import 'package:movies_app/core/resources/app_strings.dart'; 5 | import 'package:movies_app/tv_shows/domain/entities/episode.dart'; 6 | import 'package:movies_app/tv_shows/domain/entities/season.dart'; 7 | 8 | class TVShowCardDetails extends StatelessWidget { 9 | const TVShowCardDetails({ 10 | super.key, 11 | required this.lastEpisode, 12 | required this.genres, 13 | required this.seasons, 14 | }); 15 | 16 | final Episode lastEpisode; 17 | final String genres; 18 | final List seasons; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final textTheme = Theme.of(context).textTheme; 23 | if (lastEpisode.number != 0 && 24 | lastEpisode.season != 0 && 25 | genres.isNotEmpty && 26 | seasons.isNotEmpty) { 27 | return Row( 28 | children: [ 29 | Text( 30 | 'S${lastEpisode.season}E${lastEpisode.number}', 31 | style: textTheme.bodyLarge, 32 | ), 33 | const CircleDot(), 34 | if (genres.isNotEmpty) ...[ 35 | Text( 36 | genres, 37 | style: textTheme.bodyLarge, 38 | ), 39 | const CircleDot(), 40 | ] else ...[ 41 | if (seasons.isNotEmpty) ...[ 42 | const CircleDot(), 43 | ] 44 | ], 45 | Text( 46 | _getNbOfSeasons(seasons.length), 47 | style: textTheme.bodyLarge, 48 | ), 49 | ], 50 | ); 51 | } else { 52 | return const SizedBox(); 53 | } 54 | } 55 | } 56 | 57 | String _getNbOfSeasons(int seasons) { 58 | return '$seasons ${seasons == 1 ? AppStrings.season : AppStrings.seasons}'; 59 | } 60 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/controllers/popular_tv_shows_bloc/popular_tv_shows_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:movies_app/core/domain/entities/media.dart'; 6 | import 'package:movies_app/core/utils/enums.dart'; 7 | import 'package:movies_app/tv_shows/domain/usecases/get_all_popular_tv_shows_usecase.dart'; 8 | 9 | part 'popular_tv_shows_event.dart'; 10 | part 'popular_tv_shows_state.dart'; 11 | 12 | class PopularTVShowsBloc 13 | extends Bloc { 14 | PopularTVShowsBloc(this._getAllPopularTvShowsUseCase) 15 | : super(const PopularTVShowsState()) { 16 | on(_getPopularTvShows); 17 | on(_fetchMoreTVShows); 18 | } 19 | 20 | final GetAllPopularTVShowsUseCase _getAllPopularTvShowsUseCase; 21 | int page = 1; 22 | 23 | Future _getPopularTvShows( 24 | GetPopularTVShowsEvent event, Emitter emit) async { 25 | if (state.status == GetAllRequestStatus.loading) { 26 | await _getTVShows(emit); 27 | } else if (state.status == GetAllRequestStatus.loaded) { 28 | await _getTVShows(emit); 29 | } else { 30 | emit( 31 | state.copyWith( 32 | status: GetAllRequestStatus.loading, 33 | ), 34 | ); 35 | await _getTVShows(emit); 36 | } 37 | } 38 | 39 | Future _getTVShows(Emitter emit) async { 40 | final result = await _getAllPopularTvShowsUseCase(page); 41 | result.fold( 42 | (l) => emit( 43 | state.copyWith( 44 | status: GetAllRequestStatus.error, 45 | message: l.message, 46 | ), 47 | ), 48 | (r) { 49 | page++; 50 | emit( 51 | state.copyWith( 52 | status: GetAllRequestStatus.loaded, 53 | tvShows: state.tvShows + r, 54 | ), 55 | ); 56 | }, 57 | ); 58 | } 59 | 60 | Future _fetchMoreTVShows(FetchMorePopularTVShowsEvent event, 61 | Emitter emit) async { 62 | final result = await _getAllPopularTvShowsUseCase(page); 63 | result.fold( 64 | (l) => emit( 65 | state.copyWith( 66 | status: GetAllRequestStatus.fetchMoreError, 67 | message: l.message, 68 | ), 69 | ), 70 | (r) { 71 | page++; 72 | emit( 73 | state.copyWith( 74 | status: GetAllRequestStatus.loaded, 75 | tvShows: state.tvShows + r, 76 | ), 77 | ); 78 | }, 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/controllers/popular_tv_shows_bloc/popular_tv_shows_event.dart: -------------------------------------------------------------------------------- 1 | part of 'popular_tv_shows_bloc.dart'; 2 | 3 | abstract class PopularTVShowsEvent extends Equatable { 4 | const PopularTVShowsEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class GetPopularTVShowsEvent extends PopularTVShowsEvent {} 11 | 12 | class FetchMorePopularTVShowsEvent extends PopularTVShowsEvent {} 13 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/controllers/popular_tv_shows_bloc/popular_tv_shows_state.dart: -------------------------------------------------------------------------------- 1 | part of 'popular_tv_shows_bloc.dart'; 2 | 3 | class PopularTVShowsState extends Equatable { 4 | const PopularTVShowsState({ 5 | this.tvShows = const [], 6 | this.status = GetAllRequestStatus.loading, 7 | this.message = '', 8 | }); 9 | 10 | final List tvShows; 11 | final GetAllRequestStatus status; 12 | final String message; 13 | 14 | PopularTVShowsState copyWith({ 15 | List? tvShows, 16 | GetAllRequestStatus? status, 17 | String? message, 18 | }) { 19 | return PopularTVShowsState( 20 | tvShows: tvShows ?? this.tvShows, 21 | status: status ?? this.status, 22 | message: message ?? this.message, 23 | ); 24 | } 25 | 26 | @override 27 | List get props => [ 28 | tvShows, 29 | status, 30 | message, 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/controllers/top_rated_tv_shows_bloc/top_rated_tv_shows_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:movies_app/core/domain/entities/media.dart'; 6 | import 'package:movies_app/core/utils/enums.dart'; 7 | import 'package:movies_app/tv_shows/domain/usecases/get_all_top_rated_tv_shows_usecase.dart'; 8 | 9 | part 'top_rated_tv_shows_event.dart'; 10 | part 'top_rated_tv_shows_state.dart'; 11 | 12 | class TopRatedTVShowsBloc 13 | extends Bloc { 14 | TopRatedTVShowsBloc(this._getAllTopRatedTvShowsUseCase) 15 | : super(const TopRatedTVShowsState()) { 16 | on(_getTopRatedTVShows); 17 | on(_fetchMoreTVShows); 18 | } 19 | 20 | final GetAllTopRatedTVShowsUseCase _getAllTopRatedTvShowsUseCase; 21 | int page = 1; 22 | 23 | Future _getTopRatedTVShows( 24 | GetTopRatedTVShowsEvent event, Emitter emit) async { 25 | if (state.status == GetAllRequestStatus.loading) { 26 | await _getTVShows(emit); 27 | } else if (state.status == GetAllRequestStatus.loaded) { 28 | await _getTVShows(emit); 29 | } else { 30 | emit( 31 | state.copyWith( 32 | status: GetAllRequestStatus.loading, 33 | ), 34 | ); 35 | await _getTVShows(emit); 36 | } 37 | } 38 | 39 | Future _getTVShows(Emitter emit) async { 40 | final result = await _getAllTopRatedTvShowsUseCase(page); 41 | result.fold( 42 | (l) => emit( 43 | state.copyWith( 44 | status: GetAllRequestStatus.error, 45 | message: l.message, 46 | ), 47 | ), 48 | (r) { 49 | page++; 50 | emit( 51 | state.copyWith( 52 | status: GetAllRequestStatus.loaded, 53 | tvShows: state.tvShows + r, 54 | ), 55 | ); 56 | }, 57 | ); 58 | } 59 | 60 | Future _fetchMoreTVShows(FetchMoreTopRatedTVShowsEvent event, 61 | Emitter emit) async { 62 | final result = await _getAllTopRatedTvShowsUseCase(page); 63 | result.fold( 64 | (l) => emit( 65 | state.copyWith( 66 | status: GetAllRequestStatus.fetchMoreError, 67 | message: l.message, 68 | ), 69 | ), 70 | (r) { 71 | page++; 72 | emit( 73 | state.copyWith( 74 | status: GetAllRequestStatus.loaded, 75 | tvShows: state.tvShows + r, 76 | ), 77 | ); 78 | }, 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/controllers/top_rated_tv_shows_bloc/top_rated_tv_shows_event.dart: -------------------------------------------------------------------------------- 1 | part of 'top_rated_tv_shows_bloc.dart'; 2 | 3 | abstract class TopRatedTVShowsEvent extends Equatable { 4 | const TopRatedTVShowsEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class GetTopRatedTVShowsEvent extends TopRatedTVShowsEvent {} 11 | 12 | class FetchMoreTopRatedTVShowsEvent extends TopRatedTVShowsEvent {} 13 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/controllers/top_rated_tv_shows_bloc/top_rated_tv_shows_state.dart: -------------------------------------------------------------------------------- 1 | part of 'top_rated_tv_shows_bloc.dart'; 2 | 3 | class TopRatedTVShowsState extends Equatable { 4 | const TopRatedTVShowsState({ 5 | this.tvShows = const [], 6 | this.status = GetAllRequestStatus.loading, 7 | this.message = '', 8 | }); 9 | 10 | final List tvShows; 11 | final GetAllRequestStatus status; 12 | final String message; 13 | 14 | TopRatedTVShowsState copyWith({ 15 | List? tvShows, 16 | GetAllRequestStatus? status, 17 | String? message, 18 | }) { 19 | return TopRatedTVShowsState( 20 | tvShows: tvShows ?? this.tvShows, 21 | status: status ?? this.status, 22 | message: message ?? this.message, 23 | ); 24 | } 25 | 26 | @override 27 | List get props => [ 28 | tvShows, 29 | status, 30 | message, 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/controllers/tv_show_details_bloc/tv_show_details_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:movies_app/core/domain/entities/media_details.dart'; 6 | import 'package:movies_app/core/utils/enums.dart'; 7 | import 'package:movies_app/tv_shows/domain/entities/season_details.dart'; 8 | import 'package:movies_app/tv_shows/domain/usecases/get_season_details_usecase.dart'; 9 | import 'package:movies_app/tv_shows/domain/usecases/get_tv_show_details_usecase.dart'; 10 | part 'tv_show_details_event.dart'; 11 | part 'tv_show_details_state.dart'; 12 | 13 | class TVShowDetailsBloc extends Bloc { 14 | TVShowDetailsBloc( 15 | this._getTvShowDetailsUseCase, 16 | this._getSeasonDetailsUseCase, 17 | ) : super(const TVShowDetailsState()) { 18 | on(_getTvShowDetails); 19 | on(_getSeasonDetails); 20 | } 21 | 22 | final GetTVShowDetailsUseCase _getTvShowDetailsUseCase; 23 | final GetSeasonDetailsUseCase _getSeasonDetailsUseCase; 24 | 25 | Future _getTvShowDetails( 26 | GetTVShowDetailsEvent event, Emitter emit) async { 27 | emit( 28 | state.copyWith( 29 | tvShowDetailsStatus: RequestStatus.loading, 30 | ), 31 | ); 32 | final result = await _getTvShowDetailsUseCase(event.id); 33 | result.fold( 34 | (l) => emit( 35 | TVShowDetailsState( 36 | tvShowDetailsStatus: RequestStatus.error, 37 | tvShowDetailsMessage: l.message, 38 | ), 39 | ), 40 | (r) => emit( 41 | TVShowDetailsState( 42 | tvShowDetailsStatus: RequestStatus.loaded, 43 | tvShowDetails: r, 44 | ), 45 | ), 46 | ); 47 | } 48 | 49 | Future _getSeasonDetails( 50 | GetSeasonDetailsEvent event, Emitter emit) async { 51 | emit( 52 | state.copyWith( 53 | seasonDetailsStatus: RequestStatus.loading, 54 | ), 55 | ); 56 | final result = await _getSeasonDetailsUseCase( 57 | SeasonDetailsParams( 58 | id: event.id, 59 | seasonNumber: event.seasonNumber, 60 | ), 61 | ); 62 | result.fold( 63 | (l) => emit( 64 | state.copyWith( 65 | seasonDetailsStatus: RequestStatus.error, 66 | seasonDetailsMessage: l.message, 67 | ), 68 | ), 69 | (r) => emit( 70 | state.copyWith( 71 | seasonDetailsStatus: RequestStatus.loaded, 72 | seasonDetails: r, 73 | ), 74 | ), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/controllers/tv_show_details_bloc/tv_show_details_event.dart: -------------------------------------------------------------------------------- 1 | part of 'tv_show_details_bloc.dart'; 2 | 3 | abstract class TVShowDetailsEvent extends Equatable { 4 | const TVShowDetailsEvent(); 5 | } 6 | 7 | class GetTVShowDetailsEvent extends TVShowDetailsEvent { 8 | final int id; 9 | const GetTVShowDetailsEvent(this.id); 10 | 11 | @override 12 | List get props => [id]; 13 | } 14 | 15 | class GetSeasonDetailsEvent extends TVShowDetailsEvent { 16 | final int id; 17 | final int seasonNumber; 18 | 19 | const GetSeasonDetailsEvent({ 20 | required this.id, 21 | required this.seasonNumber, 22 | }); 23 | 24 | @override 25 | List get props => [id, seasonNumber]; 26 | } 27 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/controllers/tv_show_details_bloc/tv_show_details_state.dart: -------------------------------------------------------------------------------- 1 | part of 'tv_show_details_bloc.dart'; 2 | 3 | class TVShowDetailsState extends Equatable { 4 | const TVShowDetailsState({ 5 | this.tvShowDetails, 6 | this.tvShowDetailsStatus = RequestStatus.loading, 7 | this.tvShowDetailsMessage = '', 8 | this.seasonDetails, 9 | this.seasonDetailsStatus = RequestStatus.loading, 10 | this.seasonDetailsMessage = '', 11 | }); 12 | 13 | final MediaDetails? tvShowDetails; 14 | final RequestStatus tvShowDetailsStatus; 15 | final String tvShowDetailsMessage; 16 | final SeasonDetails? seasonDetails; 17 | final RequestStatus seasonDetailsStatus; 18 | final String seasonDetailsMessage; 19 | 20 | TVShowDetailsState copyWith({ 21 | final MediaDetails? tvShowDetails, 22 | final RequestStatus? tvShowDetailsStatus, 23 | final String? tvShowDetailsMessage, 24 | final SeasonDetails? seasonDetails, 25 | final RequestStatus? seasonDetailsStatus, 26 | final String? seasonDetailsMessage, 27 | }) { 28 | return TVShowDetailsState( 29 | tvShowDetails: tvShowDetails ?? this.tvShowDetails, 30 | tvShowDetailsStatus: tvShowDetailsStatus ?? this.tvShowDetailsStatus, 31 | tvShowDetailsMessage: tvShowDetailsMessage ?? this.tvShowDetailsMessage, 32 | seasonDetails: seasonDetails ?? this.seasonDetails, 33 | seasonDetailsStatus: seasonDetailsStatus ?? this.seasonDetailsStatus, 34 | seasonDetailsMessage: seasonDetailsMessage ?? this.seasonDetailsMessage, 35 | ); 36 | } 37 | 38 | @override 39 | List get props => [ 40 | tvShowDetails, 41 | tvShowDetailsStatus, 42 | tvShowDetailsMessage, 43 | seasonDetails, 44 | seasonDetailsStatus, 45 | seasonDetailsMessage 46 | ]; 47 | } 48 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/controllers/tv_shows_bloc/tv_shows_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:movies_app/core/domain/entities/media.dart'; 6 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 7 | import 'package:movies_app/core/utils/enums.dart'; 8 | import 'package:movies_app/tv_shows/domain/usecases/get_tv_shows_usecase.dart'; 9 | 10 | part 'tv_shows_event.dart'; 11 | part 'tv_shows_state.dart'; 12 | 13 | class TVShowsBloc extends Bloc { 14 | TVShowsBloc(this._getTvShowsUseCase) : super(const TVShowsState()) { 15 | on(_getTvShows); 16 | } 17 | 18 | final GetTVShowsUseCase _getTvShowsUseCase; 19 | 20 | Future _getTvShows( 21 | TVShowsEvent event, Emitter emit) async { 22 | emit( 23 | const TVShowsState( 24 | status: RequestStatus.loading, 25 | ), 26 | ); 27 | final result = await _getTvShowsUseCase(const NoParameters()); 28 | result.fold( 29 | (l) => emit( 30 | const TVShowsState( 31 | status: RequestStatus.error, 32 | ), 33 | ), 34 | (r) => emit( 35 | TVShowsState( 36 | status: RequestStatus.loaded, 37 | tvShows: r, 38 | ), 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/controllers/tv_shows_bloc/tv_shows_event.dart: -------------------------------------------------------------------------------- 1 | part of 'tv_shows_bloc.dart'; 2 | 3 | abstract class TVShowsEvent extends Equatable { 4 | const TVShowsEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class GetTVShowsEvent extends TVShowsEvent {} 11 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/controllers/tv_shows_bloc/tv_shows_state.dart: -------------------------------------------------------------------------------- 1 | part of 'tv_shows_bloc.dart'; 2 | 3 | class TVShowsState extends Equatable { 4 | final List> tvShows; 5 | final RequestStatus status; 6 | final String message; 7 | 8 | const TVShowsState({ 9 | this.tvShows = const [], 10 | this.status = RequestStatus.loading, 11 | this.message = '', 12 | }); 13 | 14 | @override 15 | List get props => [ 16 | tvShows, 17 | status, 18 | message, 19 | ]; 20 | } 21 | -------------------------------------------------------------------------------- /lib/tv_shows/presentation/views/popular_tv_shows_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:movies_app/core/domain/entities/media.dart'; 4 | import 'package:movies_app/core/presentation/components/custom_app_bar.dart'; 5 | import 'package:movies_app/core/presentation/components/error_screen.dart'; 6 | import 'package:movies_app/core/presentation/components/loading_indicator.dart'; 7 | import 'package:movies_app/core/presentation/components/vertical_listview.dart'; 8 | import 'package:movies_app/core/presentation/components/vertical_listview_card.dart'; 9 | import 'package:movies_app/core/resources/app_strings.dart'; 10 | import 'package:movies_app/core/services/service_locator.dart'; 11 | import 'package:movies_app/core/utils/enums.dart'; 12 | import 'package:movies_app/tv_shows/presentation/controllers/popular_tv_shows_bloc/popular_tv_shows_bloc.dart'; 13 | 14 | class PopularTVShowsView extends StatelessWidget { 15 | const PopularTVShowsView({super.key}); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return BlocProvider( 20 | create: (context) => 21 | sl()..add(GetPopularTVShowsEvent()), 22 | child: Scaffold( 23 | appBar: const CustomAppBar( 24 | title: AppStrings.popularShows, 25 | ), 26 | body: BlocBuilder( 27 | builder: (context, state) { 28 | switch (state.status) { 29 | case GetAllRequestStatus.loading: 30 | return const LoadingIndicator(); 31 | case GetAllRequestStatus.loaded: 32 | return PopularTVShowsWidget(tvShows: state.tvShows); 33 | case GetAllRequestStatus.error: 34 | return ErrorScreen( 35 | onTryAgainPressed: () { 36 | context 37 | .read() 38 | .add(GetPopularTVShowsEvent()); 39 | }, 40 | ); 41 | case GetAllRequestStatus.fetchMoreError: 42 | return PopularTVShowsWidget(tvShows: state.tvShows); 43 | } 44 | }, 45 | ), 46 | ), 47 | ); 48 | } 49 | } 50 | 51 | class PopularTVShowsWidget extends StatelessWidget { 52 | const PopularTVShowsWidget({ 53 | super.key, 54 | required this.tvShows, 55 | }); 56 | 57 | final List tvShows; 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | return VerticalListView( 62 | itemCount: tvShows.length + 1, 63 | itemBuilder: (context, index) { 64 | if (index < tvShows.length) { 65 | return VerticalListViewCard(media: tvShows[index]); 66 | } else { 67 | return const LoadingIndicator(); 68 | } 69 | }, 70 | addEvent: () { 71 | context.read().add(FetchMorePopularTVShowsEvent()); 72 | }, 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/watchlist/data/datasource/watchlist_local_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive_flutter/hive_flutter.dart'; 2 | import 'package:movies_app/watchlist/data/models/watchlist_item_model.dart'; 3 | 4 | abstract class WatchlistLocalDataSource { 5 | Future> getWatchListItems(); 6 | Future addWatchListItem(WatchlistItemModel item); 7 | Future removeWatchListItem(int index); 8 | Future isItemAdded(int tmdbID); 9 | } 10 | 11 | class WatchlistLocalDataSourceImpl extends WatchlistLocalDataSource { 12 | final Box _box = Hive.box('items'); 13 | 14 | @override 15 | Future> getWatchListItems() async { 16 | return _box.values 17 | .map((e) => WatchlistItemModel.fromEntity(e)) 18 | .toList() 19 | .reversed 20 | .toList(); 21 | } 22 | 23 | @override 24 | Future addWatchListItem(WatchlistItemModel item) async { 25 | return await _box.add(item); 26 | } 27 | 28 | @override 29 | Future removeWatchListItem(int index) async { 30 | await _box.deleteAt(index); 31 | } 32 | 33 | @override 34 | Future isItemAdded(int tmdbID) async { 35 | return _box.values.toList().indexWhere((e) => e.tmdbID == tmdbID); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/watchlist/data/models/watchlist_item_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/domain/entities/media.dart'; 2 | 3 | class WatchlistItemModel extends Media { 4 | const WatchlistItemModel({ 5 | required super.tmdbID, 6 | required super.title, 7 | required super.releaseDate, 8 | required super.voteAverage, 9 | required super.posterUrl, 10 | required super.backdropUrl, 11 | required super.overview, 12 | required super.isMovie, 13 | }); 14 | 15 | factory WatchlistItemModel.fromEntity(Media media) { 16 | return WatchlistItemModel( 17 | tmdbID: media.tmdbID, 18 | title: media.title, 19 | releaseDate: media.releaseDate, 20 | voteAverage: media.voteAverage, 21 | posterUrl: media.posterUrl, 22 | backdropUrl: media.backdropUrl, 23 | overview: media.overview, 24 | isMovie: media.isMovie, 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/watchlist/data/repository/watchlist_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive_flutter/hive_flutter.dart'; 2 | import 'package:movies_app/core/domain/entities/media.dart'; 3 | import 'package:movies_app/watchlist/data/datasource/watchlist_local_data_source.dart'; 4 | import 'package:movies_app/watchlist/data/models/watchlist_item_model.dart'; 5 | import 'package:movies_app/core/data/error/failure.dart'; 6 | import 'package:dartz/dartz.dart'; 7 | import 'package:movies_app/watchlist/domain/repository/watchlist_repository.dart'; 8 | 9 | class WatchListRepositoryImpl extends WatchlistRepository { 10 | final WatchlistLocalDataSource _baseWatchlistLocalDataSource; 11 | 12 | WatchListRepositoryImpl(this._baseWatchlistLocalDataSource); 13 | 14 | @override 15 | Future>> getWatchListItems() async { 16 | final result = (await _baseWatchlistLocalDataSource.getWatchListItems()); 17 | try { 18 | return Right(result); 19 | } on HiveError catch (failure) { 20 | return Left(DatabaseFailure(failure.message)); 21 | } 22 | } 23 | 24 | @override 25 | Future> addWatchListItem(Media media) async { 26 | try { 27 | int id = await _baseWatchlistLocalDataSource.addWatchListItem( 28 | WatchlistItemModel.fromEntity(media), 29 | ); 30 | return Right(id); 31 | } on HiveError catch (failure) { 32 | return Left(DatabaseFailure(failure.message)); 33 | } 34 | } 35 | 36 | @override 37 | Future> removeWatchListItem(int index) async { 38 | try { 39 | await _baseWatchlistLocalDataSource.removeWatchListItem(index); 40 | return const Right(unit); 41 | } on HiveError catch (failure) { 42 | return Left(DatabaseFailure(failure.message)); 43 | } 44 | } 45 | 46 | @override 47 | Future> checkIfItemAdded(int tmdbId) async { 48 | try { 49 | final result = await _baseWatchlistLocalDataSource.isItemAdded(tmdbId); 50 | return Right(result); 51 | } on HiveError catch (failure) { 52 | return Left(DatabaseFailure(failure.message)); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/watchlist/domain/repository/watchlist_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:movies_app/core/data/error/failure.dart'; 3 | import 'package:movies_app/core/domain/entities/media.dart'; 4 | 5 | abstract class WatchlistRepository { 6 | Future>> getWatchListItems(); 7 | Future> addWatchListItem(Media media); 8 | Future> removeWatchListItem(int index); 9 | Future> checkIfItemAdded(int tmdbId); 10 | } 11 | -------------------------------------------------------------------------------- /lib/watchlist/domain/usecases/add_watchlist_item_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/data/error/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:movies_app/core/domain/entities/media.dart'; 4 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 5 | import 'package:movies_app/watchlist/domain/repository/watchlist_repository.dart'; 6 | 7 | class AddWatchlistItemUseCase extends BaseUseCase { 8 | final WatchlistRepository _baseWatchListRepository; 9 | 10 | AddWatchlistItemUseCase(this._baseWatchListRepository); 11 | 12 | @override 13 | Future> call(Media p) async { 14 | return await _baseWatchListRepository.addWatchListItem(p); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/watchlist/domain/usecases/check_if_item_added_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/data/error/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 4 | import 'package:movies_app/watchlist/domain/repository/watchlist_repository.dart'; 5 | 6 | class CheckIfItemAddedUseCase extends BaseUseCase { 7 | final WatchlistRepository _watchlistRepository; 8 | 9 | CheckIfItemAddedUseCase(this._watchlistRepository); 10 | @override 11 | Future> call(int p) async { 12 | return await _watchlistRepository.checkIfItemAdded(p); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/watchlist/domain/usecases/get_watchlist_items_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/data/error/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:movies_app/core/domain/entities/media.dart'; 4 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 5 | import 'package:movies_app/watchlist/domain/repository/watchlist_repository.dart'; 6 | 7 | class GetWatchlistItemsUseCase extends BaseUseCase, NoParameters> { 8 | final WatchlistRepository _baseWatchListRepository; 9 | 10 | GetWatchlistItemsUseCase(this._baseWatchListRepository); 11 | 12 | @override 13 | Future>> call(NoParameters p) async { 14 | return await _baseWatchListRepository.getWatchListItems(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/watchlist/domain/usecases/remove_watchlist_item_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:movies_app/core/data/error/failure.dart'; 3 | import 'package:movies_app/core/domain/usecase/base_use_case.dart'; 4 | import 'package:movies_app/watchlist/domain/repository/watchlist_repository.dart'; 5 | 6 | class RemoveWatchlistItemUseCase extends BaseUseCase { 7 | final WatchlistRepository _baseWatchListRepository; 8 | 9 | RemoveWatchlistItemUseCase(this._baseWatchListRepository); 10 | 11 | @override 12 | Future> call(int p) async { 13 | return await _baseWatchListRepository.removeWatchListItem(p); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/watchlist/presentation/components/empty_watchlist_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/resources/app_strings.dart'; 3 | import 'package:movies_app/core/resources/app_values.dart'; 4 | 5 | class EmptyWatchlistText extends StatelessWidget { 6 | const EmptyWatchlistText({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | final textTheme = Theme.of(context).textTheme; 11 | return Column( 12 | mainAxisAlignment: MainAxisAlignment.center, 13 | children: [ 14 | Text( 15 | AppStrings.watchlistIsEmpty, 16 | style: textTheme.titleMedium, 17 | ), 18 | Padding( 19 | padding: const EdgeInsets.only(top: AppPadding.p6), 20 | child: Text( 21 | AppStrings.watchlistText, 22 | style: textTheme.bodyLarge, 23 | textAlign: TextAlign.center, 24 | ), 25 | ), 26 | ], 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/watchlist/presentation/controllers/watchlist_bloc/watchlist_event.dart: -------------------------------------------------------------------------------- 1 | part of 'watchlist_bloc.dart'; 2 | 3 | abstract class WatchlistEvent extends Equatable { 4 | const WatchlistEvent(); 5 | } 6 | 7 | class GetWatchListItemsEvent extends WatchlistEvent { 8 | @override 9 | List get props => []; 10 | } 11 | 12 | class AddWatchListItemEvent extends WatchlistEvent { 13 | final Media media; 14 | 15 | const AddWatchListItemEvent({ 16 | required this.media, 17 | }); 18 | 19 | @override 20 | List get props => [media]; 21 | } 22 | 23 | class RemoveWatchListItemEvent extends WatchlistEvent { 24 | final int index; 25 | 26 | const RemoveWatchListItemEvent(this.index); 27 | 28 | @override 29 | List get props => [index]; 30 | } 31 | 32 | class CheckItemAddedEvent extends WatchlistEvent { 33 | final int tmdbId; 34 | 35 | const CheckItemAddedEvent({ 36 | required this.tmdbId, 37 | }); 38 | 39 | @override 40 | List get props => [tmdbId]; 41 | } 42 | -------------------------------------------------------------------------------- /lib/watchlist/presentation/controllers/watchlist_bloc/watchlist_state.dart: -------------------------------------------------------------------------------- 1 | part of 'watchlist_bloc.dart'; 2 | 3 | enum WatchlistRequestStatus { 4 | empty, 5 | loading, 6 | loaded, 7 | error, 8 | itemAdded, 9 | itemRemoved, 10 | isItemAdded, 11 | } 12 | 13 | class WatchlistState extends Equatable { 14 | const WatchlistState({ 15 | this.id, 16 | this.items = const [], 17 | this.status = WatchlistRequestStatus.loading, 18 | this.message = '', 19 | }); 20 | 21 | final int? id; 22 | final List items; 23 | final WatchlistRequestStatus status; 24 | final String message; 25 | 26 | WatchlistState copyWith({ 27 | int? id, 28 | List? items, 29 | WatchlistRequestStatus? status, 30 | String? message, 31 | }) { 32 | return WatchlistState( 33 | id: id ?? this.id, 34 | items: items ?? this.items, 35 | status: status ?? this.status, 36 | message: message ?? this.message, 37 | ); 38 | } 39 | 40 | @override 41 | List get props => [ 42 | id, 43 | items, 44 | status, 45 | message, 46 | ]; 47 | } 48 | -------------------------------------------------------------------------------- /lib/watchlist/presentation/views/watchlist_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:movies_app/core/domain/entities/media.dart'; 4 | import 'package:movies_app/core/presentation/components/custom_app_bar.dart'; 5 | import 'package:movies_app/core/presentation/components/error_screen.dart'; 6 | import 'package:movies_app/core/presentation/components/loading_indicator.dart'; 7 | import 'package:movies_app/core/presentation/components/vertical_listview_card.dart'; 8 | import 'package:movies_app/core/resources/app_strings.dart'; 9 | import 'package:movies_app/core/resources/app_values.dart'; 10 | import 'package:movies_app/core/services/service_locator.dart'; 11 | import 'package:movies_app/watchlist/presentation/components/empty_watchlist_text.dart'; 12 | import 'package:movies_app/watchlist/presentation/controllers/watchlist_bloc/watchlist_bloc.dart'; 13 | 14 | class WatchlistView extends StatelessWidget { 15 | const WatchlistView({super.key}); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return BlocProvider( 20 | create: (context) => sl()..add(GetWatchListItemsEvent()), 21 | child: Scaffold( 22 | appBar: const CustomAppBar( 23 | title: AppStrings.watchlist, 24 | ), 25 | body: BlocBuilder( 26 | builder: (context, state) { 27 | if (state.status == WatchlistRequestStatus.loading) { 28 | return const LoadingIndicator(); 29 | } else if (state.status == WatchlistRequestStatus.loaded) { 30 | return WatchlistWidget(items: state.items); 31 | } else if (state.status == WatchlistRequestStatus.empty) { 32 | return const EmptyWatchlistText(); 33 | } else { 34 | return ErrorScreen( 35 | onTryAgainPressed: () { 36 | context.read().add(GetWatchListItemsEvent()); 37 | }, 38 | ); 39 | } 40 | }, 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | 47 | class WatchlistWidget extends StatelessWidget { 48 | const WatchlistWidget({ 49 | super.key, 50 | required this.items, 51 | }); 52 | 53 | final List items; 54 | 55 | @override 56 | Widget build(BuildContext context) { 57 | return ListView.separated( 58 | itemCount: items.length, 59 | physics: const BouncingScrollPhysics(), 60 | padding: const EdgeInsets.symmetric( 61 | horizontal: AppPadding.p12, 62 | vertical: AppPadding.p6, 63 | ), 64 | itemBuilder: (context, index) { 65 | return VerticalListViewCard(media: items[index]); 66 | }, 67 | separatorBuilder: (context, index) => const SizedBox(height: AppSize.s10), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/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 | #include 10 | 11 | void fl_register_plugins(FlPluginRegistry* registry) { 12 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 13 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 14 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 15 | } 16 | -------------------------------------------------------------------------------- /linux/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 fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | url_launcher_linux 7 | ) 8 | 9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 10 | ) 11 | 12 | set(PLUGIN_BUNDLED_LIBRARIES) 13 | 14 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 19 | endforeach(plugin) 20 | 21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 24 | endforeach(ffi_plugin) 25 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import path_provider_foundation 9 | import sqflite 10 | import url_launcher_macos 11 | 12 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 13 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 14 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 15 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 16 | } 17 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = movies_app 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.moviesApp 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController.init() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /screenshots/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/screenshots/01.png -------------------------------------------------------------------------------- /screenshots/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/screenshots/02.png -------------------------------------------------------------------------------- /screenshots/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/screenshots/03.png -------------------------------------------------------------------------------- /screenshots/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/screenshots/04.png -------------------------------------------------------------------------------- /screenshots/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/screenshots/05.png -------------------------------------------------------------------------------- /screenshots/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/screenshots/06.png -------------------------------------------------------------------------------- /screenshots/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/screenshots/07.png -------------------------------------------------------------------------------- /screenshots/08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/screenshots/08.png -------------------------------------------------------------------------------- /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 in the flutter_test package. 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:movies_app/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 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | movies_app 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movies_app", 3 | "short_name": "movies_app", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | #include 10 | 11 | void RegisterPlugins(flutter::PluginRegistry* registry) { 12 | UrlLauncherWindowsRegisterWithRegistrar( 13 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 14 | } 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | url_launcher_windows 7 | ) 8 | 9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 10 | ) 11 | 12 | set(PLUGIN_BUNDLED_LIBRARIES) 13 | 14 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 19 | endforeach(plugin) 20 | 21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 24 | endforeach(ffi_plugin) 25 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 37 | 38 | # Run the Flutter tool portions of the build. This must not be removed. 39 | add_dependencies(${BINARY_NAME} flutter_assemble) 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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"movies_app", 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamadayash22/flutter-movie-app/6050506d62795b4fd96ac012b5453ee1ba2439b4/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | std::string utf8_string; 52 | if (target_length == 0 || target_length > utf8_string.max_size()) { 53 | return utf8_string; 54 | } 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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------