diff --git a/.gitignore b/.gitignore index 521fb4c..fa12cb4 100644 --- a/.gitignore +++ b/.gitignore @@ -106,5 +106,5 @@ lib/generated_plugin_registrant.dart !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages - +constants.dart diff --git a/README.md b/README.md index a7c53c1..2105c3b 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,21 @@ Before using this project, you will need to have Appwrite instance with Almost Netflix project ready. You can visit Project setup [GitHub repository](https://github.com/Meldiron/almost-netflix-project-setup) or [Dev.to post](https://dev.to/appwrite/did-we-just-build-a-netflix-clone-with-appwrite-28ok). -## Usage +## Setup -```bash -$ git clone https://github.com/appwrite/demo-almost-netflix-for-flutter.git -$ cd demo-almost-netflix-for-flutter -$ open -a Simulator.app -$ flutter run -``` +1. Create `constants.dart` using `constants.dart.example` as a template and update the values with your own. +2. Add a new Flutter Platform to your Appwrite Project: + - Android: `io.appwrite.netflix_clone` + - iOS: `io.appwrite.netflixClone` -Make sure to update Endpoint and ProjectID in `lib/api/client.dart`. +## Run the App -The application will be listening on port `3000`. You can visit in on URL `http://localhost:3000`. +```shell +flutter pub get +flutter run +``` +## Project Structure ### `assets` diff --git a/android/app/build.gradle b/android/app/build.gradle index bf54544..08bcfe8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 33 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/appwrite.json b/appwrite.json new file mode 100644 index 0000000..5949b9e --- /dev/null +++ b/appwrite.json @@ -0,0 +1,301 @@ +{ + "projectId": "almost-netflix", + "projectName": "Almost Netflix", + "databases": [ + { + "$id": "default", + "name": "default", + "$createdAt": "2023-03-29T00:16:26.726+00:00", + "$updatedAt": "2023-03-29T00:16:26.726+00:00" + } + ], + "collections": [ + { + "$id": "movies", + "$permissions": [ + "read(\"any\")" + ], + "databaseId": "default", + "name": "Movies", + "enabled": true, + "documentSecurity": false, + "attributes": [ + { + "key": "genres", + "type": "string", + "status": "available", + "required": false, + "array": true, + "size": 255, + "default": null + }, + { + "key": "ageRestriction", + "type": "string", + "status": "available", + "required": true, + "array": false, + "elements": [ + "AR7", + "AR13", + "AR16", + "AR18" + ], + "format": "enum", + "default": null + }, + { + "key": "trendingIndex", + "type": "double", + "status": "available", + "required": true, + "array": false, + "min": -1.7976931348623157e+308, + "max": 1.7976931348623157e+308, + "default": null + }, + { + "key": "tags", + "type": "string", + "status": "available", + "required": false, + "array": true, + "size": 255, + "default": null + }, + { + "key": "releaseDate", + "type": "datetime", + "status": "available", + "required": false, + "array": false, + "format": "", + "default": null + }, + { + "key": "isOriginal", + "type": "boolean", + "status": "available", + "required": true, + "array": false, + "default": null + }, + { + "key": "netflixReleaseDate", + "type": "datetime", + "status": "available", + "required": false, + "array": false, + "format": "", + "default": null + }, + { + "key": "name", + "type": "string", + "status": "available", + "required": true, + "array": false, + "size": 255, + "default": null + }, + { + "key": "thumbnailImageId", + "type": "string", + "status": "available", + "required": true, + "array": false, + "size": 255, + "default": null + }, + { + "key": "durationMinutes", + "type": "integer", + "status": "available", + "required": true, + "array": false, + "min": 1, + "max": 1000, + "default": null + }, + { + "key": "cast", + "type": "string", + "status": "available", + "required": false, + "array": true, + "size": 255, + "default": null + }, + { + "key": "description", + "type": "string", + "status": "available", + "required": false, + "array": false, + "size": 5000, + "default": null + } + ], + "indexes": [ + { + "key": "isOriginalDESC", + "type": "key", + "status": "available", + "attributes": [ + "isOriginal" + ], + "orders": [ + "DESC" + ] + }, + { + "key": "releaseDateDESC", + "type": "key", + "status": "available", + "attributes": [ + "releaseDate" + ], + "orders": [ + "DESC" + ] + }, + { + "key": "nameFULLTEXT", + "type": "fulltext", + "status": "available", + "attributes": [ + "name" + ], + "orders": [ + "ASC" + ] + }, + { + "key": "genresFULLTEXT", + "type": "fulltext", + "status": "available", + "attributes": [ + "genres" + ], + "orders": [ + "ASC" + ] + }, + { + "key": "trendingIndexDESC", + "type": "key", + "status": "available", + "attributes": [ + "trendingIndex" + ], + "orders": [ + "DESC" + ] + }, + { + "key": "castFULLTEXT", + "type": "fulltext", + "status": "available", + "attributes": [ + "cast" + ], + "orders": [ + "ASC" + ] + }, + { + "key": "tagsFULLTEXT", + "type": "fulltext", + "status": "available", + "attributes": [ + "tags" + ], + "orders": [ + "ASC" + ] + }, + { + "key": "durationMinutesDESC", + "type": "key", + "status": "available", + "attributes": [ + "durationMinutes" + ], + "orders": [ + "DESC" + ] + } + ] + }, + { + "$id": "watchlists", + "$permissions": [ + "create(\"users\")" + ], + "databaseId": "default", + "name": "Watchlists", + "enabled": true, + "documentSecurity": true, + "attributes": [ + { + "key": "movie", + "type": "relationship", + "status": "available", + "required": false, + "array": false, + "relatedCollection": "movies", + "relationType": "manyToOne", + "twoWay": false, + "twoWayKey": "watchlists", + "onDelete": "setNull", + "side": "parent" + }, + { + "key": "userId", + "type": "string", + "status": "available", + "required": true, + "array": false, + "size": 255, + "default": null + } + ], + "indexes": [ + { + "key": "userIdASC", + "type": "key", + "status": "available", + "attributes": [ + "userId" + ], + "orders": [ + "ASC" + ] + } + ] + } + ], + "buckets": [ + { + "$id": "posters", + "$createdAt": "2023-04-07T21:23:16.876+00:00", + "$updatedAt": "2023-04-07T21:23:16.876+00:00", + "$permissions": [ + "read(\"any\")" + ], + "fileSecurity": false, + "name": "Posters", + "enabled": true, + "maximumFileSize": 5000000, + "allowedFileExtensions": [ + "jpg", + "png", + "heic", + "jpeg" + ], + "compression": "none", + "encryption": true, + "antivirus": true + } + ] +} \ No newline at end of file diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 8d4492f..9625e10 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/ios/Podfile b/ios/Podfile index 1e8c3c9..88359b2 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5b20dde..bff68f0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,45 +2,52 @@ PODS: - device_info_plus (0.0.1): - Flutter - Flutter (1.0.0) - - flutter_web_auth (0.4.0): + - flutter_web_auth_2 (1.1.1): - Flutter - package_info_plus (0.4.5): - Flutter - - path_provider_ios (0.0.1): + - path_provider_foundation (0.0.1): - Flutter + - FlutterMacOS - shared_preferences_ios (0.0.1): - Flutter + - url_launcher_ios (0.0.1): + - Flutter DEPENDENCIES: - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - - flutter_web_auth (from `.symlinks/plugins/flutter_web_auth/ios`) + - flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) EXTERNAL SOURCES: device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" Flutter: :path: Flutter - flutter_web_auth: - :path: ".symlinks/plugins/flutter_web_auth/ios" + flutter_web_auth_2: + :path: ".symlinks/plugins/flutter_web_auth_2/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_ios: - :path: ".symlinks/plugins/path_provider_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/ios" shared_preferences_ios: :path: ".symlinks/plugins/shared_preferences_ios/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - flutter_web_auth: fd071763f61703882adbb2524f5cd5251883118c + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + flutter_web_auth_2: a1bc00762c408a8f80b72a538cd7ff5b601c3e71 package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 - shared_preferences_ios: aef470a42dc4675a1cdd50e3158b42e3d1232b32 + path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 -PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c +PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index cf2e009..8cf9989 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -340,7 +340,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -417,7 +417,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -466,7 +466,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index abf22d6..d5d619c 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -43,5 +43,7 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + diff --git a/lib/api/client.dart b/lib/api/client.dart index fd401b5..af3452a 100644 --- a/lib/api/client.dart +++ b/lib/api/client.dart @@ -10,21 +10,22 @@ import 'package:appwrite/appwrite.dart'; +import '/constants.dart'; + class ApiClient { Client get _client { Client client = Client(); client - .setEndpoint('https://demo.appwrite.io/v1') - .setProject('almostNetflix2') - .setSelfSigned(); + .setEndpoint(AppwriteConstants.endpoint) + .setProject(AppwriteConstants.projectId) + .setSelfSigned(status: AppwriteConstants.selfSigned); return client; } static Account get account => Account(_instance._client); - static Databases get database => - Databases(_instance._client); + static Databases get database => Databases(_instance._client); static Storage get storage => Storage(_instance._client); static final ApiClient _instance = ApiClient._internal(); diff --git a/lib/constants.dart.default b/lib/constants.dart.default new file mode 100644 index 0000000..9068d2d --- /dev/null +++ b/lib/constants.dart.default @@ -0,0 +1,10 @@ +class AppwriteConstants { + static const endpoint = 'https://[HOSTNAME/IP]/v1'; + static const projectId = 'test'; + static const selfSigned = true; + static const databaseId = 'default'; + static const moviesCollectionId = 'movies'; + static const usersCollectionId = 'users'; + static const watchlistCollectionId = 'watchlists'; + static const postersBucketId = 'posters'; +} diff --git a/lib/data/entry.dart b/lib/data/movie.dart similarity index 56% rename from lib/data/entry.dart rename to lib/data/movie.dart index 232c205..a89f560 100644 --- a/lib/data/entry.dart +++ b/lib/data/movie.dart @@ -1,16 +1,16 @@ // -// entry.dart +// movie.dart // appflix -// +// // Author: wess (me@wess.io) // Created: 01/03/2022 -// +// // Copywrite (c) 2022 Wess.io // -import 'package:netflix_clone/extensions/datetime.dart'; +import '/constants.dart'; -class Entry { +class Movie { final String id; final String name; final String? description; @@ -26,14 +26,18 @@ class Entry { final List cast; bool isEmpty() { - if(id.isEmpty || name.isEmpty) { + if (id.isEmpty || name.isEmpty) { return true; } return false; } - Entry({ + String get thumbnailImageUrl => thumbnailImageId.isEmpty + ? "" + : "${AppwriteConstants.endpoint}/storage/buckets/${AppwriteConstants.postersBucketId}/files/$thumbnailImageId/view?project=${AppwriteConstants.projectId}"; + + Movie({ required this.id, required this.name, this.description, @@ -49,8 +53,8 @@ class Entry { required this.cast, }); - static Entry empty() { - return Entry( + static Movie empty() { + return Movie( id: '', name: '', description: '', @@ -65,8 +69,8 @@ class Entry { ); } - static Entry fromJson(Map data) { - return Entry( + factory Movie.fromJson(Map data) { + return Movie( id: data['\$id'], name: data['name'], description: data['description'], @@ -75,11 +79,31 @@ class Entry { thumbnailImageId: data['thumbnailImageId'], genres: data['genres'].cast(), tags: data['tags'].cast(), - netflixReleaseDate: data['netflixReleaseDate'] != null ? DateTimeExt.fromUnixTimestampInt(data['netflixReleaseDate']) : null, - releaseDate: data['releaseDate'] != null ? DateTimeExt.fromUnixTimestampInt(data['releaseDate']) : null, + netflixReleaseDate: data['netflixReleaseDate'] != null + ? DateTime.parse(data['netflixReleaseDate']) + : null, + releaseDate: data['releaseDate'] != null + ? DateTime.parse(data['releaseDate']) + : null, trendingIndex: data['trendingIndex'], isOriginal: data['isOriginal'], cast: data['cast'].cast(), ); } + + Map toJson() => { + '\$id': id, + 'name': name, + 'description': description, + 'ageRestriction': ageRestriction, + 'durationMinutes': durationMinutes.inMinutes, + 'thumbnailImageId': thumbnailImageId, + 'genres': genres, + 'tags': tags, + 'netflixReleaseDate': netflixReleaseDate?.toUtc().toIso8601String(), + 'releaseDate': releaseDate?.toUtc().toIso8601String(), + 'trendingIndex': trendingIndex, + 'isOriginal': isOriginal, + 'cast': cast, + }; } diff --git a/lib/data/watchlist.dart b/lib/data/watchlist.dart index e69de29..0768a4c 100644 --- a/lib/data/watchlist.dart +++ b/lib/data/watchlist.dart @@ -0,0 +1,45 @@ +import 'movie.dart'; + +class Watchlist { + final Movie movie; + final String userId; + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final List permissions; + final String databaseId; + final String collectionId; + + Watchlist({ + required this.movie, + required this.userId, + required this.id, + required this.createdAt, + required this.updatedAt, + required this.permissions, + required this.databaseId, + required this.collectionId, + }); + + factory Watchlist.fromJson(Map json) => Watchlist( + movie: Movie.fromJson(json["movie"]), + userId: json["userId"], + id: json["\u0024id"], + createdAt: DateTime.parse(json["\u0024createdAt"]), + updatedAt: DateTime.parse(json["\u0024updatedAt"]), + permissions: List.from(json["\u0024permissions"].map((x) => x)), + databaseId: json["\u0024databaseId"], + collectionId: json["\u0024collectionId"], + ); + + Map toJson() => { + "movie": movie.toJson(), + "userId": userId, + "\u0024id": id, + "\u0024createdAt": createdAt.toUtc().toIso8601String(), + "\u0024updatedAt": updatedAt.toUtc().toIso8601String(), + "\u0024permissions": List.from(permissions.map((x) => x)), + "\u0024databaseId": databaseId, + "\u0024collectionId": collectionId, + }; +} diff --git a/lib/main.dart b/lib/main.dart index 1c8b6e2..5032ac5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,41 +1,61 @@ // // main.dart // appflix -// +// // Author: wess (me@wess.io) // Created: 12/29/2021 -// +// // Copywrite (c) 2022 Wess.io // -import 'package:netflix_clone/providers/account.dart'; -import 'package:netflix_clone/providers/entry.dart'; -import 'package:netflix_clone/providers/watchlist.dart'; -import 'package:netflix_clone/screens/navigation.dart'; -import 'package:netflix_clone/screens/onboarding.dart'; +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '/constants.dart'; +import '/providers/account.dart'; +import '/providers/movies.dart'; +import '/providers/watchlist.dart'; +import '/screens/navigation.dart'; +import '/screens/onboarding.dart'; + Future main() async { WidgetsFlutterBinding.ensureInitialized(); - runApp( - MultiProvider( - providers: [ - ChangeNotifierProvider(create: (context) => AccountProvider()), - ChangeNotifierProvider(create: (context) => EntryProvider()), - ChangeNotifierProvider(create: (context) => WatchListProvider()), - ], - child: const Main(), - ) - ); + if (AppwriteConstants.selfSigned) { + debugNetworkImageHttpClientProvider = () => HttpClient() + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + } + + runApp(MultiProvider( + providers: [ + ChangeNotifierProvider(create: (context) => AccountProvider()), + ChangeNotifierProvider(create: (context) => MoviesProvider()), + ChangeNotifierProvider(create: (context) => WatchListProvider()), + ], + child: const Main(), + )); } -class Main extends StatelessWidget { +class Main extends StatefulWidget { const Main({Key? key}) : super(key: key); + @override + State
createState() => _MainState(); +} + +class _MainState extends State
{ + @override + void initState() { + context.read().isValid(); + super.initState(); + } + @override Widget build(BuildContext context) { + final user = context.watch(); return MaterialApp( title: 'Appflix', theme: ThemeData( @@ -43,10 +63,7 @@ class Main extends StatelessWidget { primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: FutureBuilder( - future: context.read().isValid(), - builder: (context, snapshot) => context.watch().session == null ? const OnboardingScreen() : const NavScreen(), - ) + home: user.session == null ? const OnboardingScreen() : const NavScreen(), ); } -} \ No newline at end of file +} diff --git a/lib/providers/account.dart b/lib/providers/account.dart index 5a578e3..87e64ed 100644 --- a/lib/providers/account.dart +++ b/lib/providers/account.dart @@ -10,15 +10,16 @@ import 'dart:convert'; -import 'package:appwrite/appwrite.dart' as appwrite; -import 'package:netflix_clone/api/client.dart'; +import 'package:appwrite/appwrite.dart'; import 'package:appwrite/models.dart'; import 'package:flutter/material.dart'; -import 'package:netflix_clone/data/store.dart'; + +import '/api/client.dart'; +import '/data/store.dart'; class AccountProvider extends ChangeNotifier { - Account? _current; - Account? get current => _current; + User? _current; + User? get current => _current; Session? _session; Session? get session => _session; @@ -44,17 +45,23 @@ class AccountProvider extends ChangeNotifier { _session = cached; } + notifyListeners(); + return _session != null; } Future register(String email, String password, String? name) async { try { final result = await ApiClient.account.create( - userId: appwrite.ID.unique(), email: email, password: password, name: name); + userId: ID.unique(), + email: email, + password: password, + name: name, + ); _current = result; - notifyListeners(); + await login(email, password); } catch (e) { throw Exception("Failed to register"); } diff --git a/lib/providers/entry.dart b/lib/providers/entry.dart deleted file mode 100644 index fb11a9b..0000000 --- a/lib/providers/entry.dart +++ /dev/null @@ -1,84 +0,0 @@ -// -// entry.dart -// appflix -// -// Author: wess (me@wess.io) -// Created: 01/03/2022 -// -// Copywrite (c) 2022 Wess.io -// - -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appwrite/appwrite.dart'; -import 'package:netflix_clone/api/client.dart'; -import 'package:netflix_clone/data/entry.dart'; -import 'package:flutter/material.dart'; - -class EntryProvider extends ChangeNotifier { - final Map _imageCache = {}; - - static final String _databaseId = ID.custom("default2"); - static final String _collectionId = ID.custom("movies"); - static final String _bucketId = ID.custom("default1"); - - Entry? _selected; - Entry? get selected => _selected; - - Entry _featured = Entry.empty(); - Entry get featured => _featured; - - List _entries = []; - List get entries => _entries; - List get originals => _entries.where((e) => e.isOriginal).toList(); - List get animations => _entries - .where((e) => e.genres.contains('animation')) - .toList(); - List get newReleases => _entries - .where((e) => - e.releaseDate != null && - e.releaseDate!.isAfter(DateTime.parse('2018-01-01'))) - .toList(); - - List get trending { - var trending = _entries; - - trending.sort((a, b) => b.trendingIndex.compareTo(a.trendingIndex)); - - return trending; - } - - void setSelected(Entry entry) { - _selected = entry; - - notifyListeners(); - } - - Future list() async { - var result = - await ApiClient.database.listDocuments(databaseId: _databaseId, collectionId: _collectionId); - - _entries = result.documents - .map((document) => Entry.fromJson(document.data)) - .toList(); - _featured = _entries.isEmpty ? Entry.empty() : _entries[0]; - - notifyListeners(); - } - - Future imageFor(Entry entry) async { - if (_imageCache.containsKey(entry.thumbnailImageId)) { - return _imageCache[entry.thumbnailImageId]!; - } - - final result = await ApiClient.storage.getFileView( - bucketId: _bucketId, - fileId: entry.thumbnailImageId, - ); - - _imageCache[entry.thumbnailImageId] = result; - - return result; - } -} diff --git a/lib/providers/movies.dart b/lib/providers/movies.dart new file mode 100644 index 0000000..2956283 --- /dev/null +++ b/lib/providers/movies.dart @@ -0,0 +1,67 @@ +// +// movies.dart +// appflix +// +// Author: wess (me@wess.io) +// Created: 01/03/2022 +// +// Copywrite (c) 2022 Wess.io +// + +import 'dart:async'; + +import 'package:appwrite/appwrite.dart'; +import 'package:flutter/material.dart'; + +import '/api/client.dart'; +import '/constants.dart'; +import '/data/movie.dart'; + +class MoviesProvider extends ChangeNotifier { + static final String _databaseId = ID.custom(AppwriteConstants.databaseId); + static final String _collectionId = ID.custom( + AppwriteConstants.moviesCollectionId, + ); + + Movie? _selected; + Movie? get selected => _selected; + + Movie _featured = Movie.empty(); + Movie get featured => _featured; + + List _movies = []; + List get movies => _movies; + List get originals => _movies.where((e) => e.isOriginal).toList(); + List get animations => + _movies.where((e) => e.genres.contains('Animation')).toList(); + List get newReleases => _movies.where((e) { + final thirtyDaysAgo = DateTime.now().subtract(const Duration(days: 30)); + return e.releaseDate != null && e.releaseDate!.isAfter(thirtyDaysAgo); + }).toList(); + + List get trending { + var trending = _movies; + + trending.sort((a, b) => b.trendingIndex.compareTo(a.trendingIndex)); + + return trending; + } + + void setSelected(Movie movie) { + _selected = movie; + + notifyListeners(); + } + + Future list() async { + var result = await ApiClient.database + .listDocuments(databaseId: _databaseId, collectionId: _collectionId); + + _movies = result.documents + .map((document) => Movie.fromJson(document.data)) + .toList(); + _featured = _movies.isEmpty ? Movie.empty() : _movies[0]; + + notifyListeners(); + } +} diff --git a/lib/providers/watchlist.dart b/lib/providers/watchlist.dart index dd84788..e243104 100644 --- a/lib/providers/watchlist.dart +++ b/lib/providers/watchlist.dart @@ -8,95 +8,77 @@ // Copywrite (c) 2022 Appwrite.io // -import 'dart:async'; -import 'dart:typed_data'; -import 'package:appwrite/appwrite.dart' as appwrite; +import 'dart:collection'; + +import 'package:appwrite/appwrite.dart'; import 'package:appwrite/models.dart'; -import 'package:netflix_clone/api/client.dart'; -import 'package:netflix_clone/data/entry.dart'; import 'package:flutter/material.dart'; -class WatchListProvider extends ChangeNotifier { +import '/api/client.dart'; +import '/constants.dart'; +import '/data/movie.dart'; +import '/data/watchlist.dart'; - static final String _databaseId = appwrite.ID.custom("default2"); - final String _collectionId = appwrite.ID.custom("watchlists"); - static final String _bucketId = appwrite.ID.custom("default1"); +class WatchListProvider extends ChangeNotifier { + final String _databaseId = ID.custom(AppwriteConstants.databaseId); + final String _collectionId = ID.custom( + AppwriteConstants.watchlistCollectionId, + ); - List _entries = []; - List get entries => _entries; + final Map _watchlists = {}; + UnmodifiableMapView get watchlists => + UnmodifiableMapView(_watchlists); - Future get user { + Future get user { return ApiClient.account.get(); } - Future> list() async { + Future> list() async { final user = await this.user; - final watchlist = await ApiClient.database.listDocuments( + final documentList = await ApiClient.database.listDocuments( databaseId: _databaseId, collectionId: _collectionId, + queries: [ + Query.equal("userId", user.$id), + ], ); - final movieIds = watchlist.documents - .map((document) => document.data["movieId"]) - .toList(); - final entries = - (await ApiClient.database.listDocuments(databaseId: _databaseId, collectionId: appwrite.ID.custom('movies'))) - .documents - .map((document) => Entry.fromJson(document.data)) - .toList(); - final filtered = - entries.where((entry) => movieIds.contains(entry.id)).toList(); + _watchlists.clear(); - _entries = filtered; + for (final document in documentList.documents) { + final watchlist = Watchlist.fromJson(document.data); + _watchlists[watchlist.movie.id] = watchlist; + } notifyListeners(); - return _entries; + return watchlists; } - Future add(Entry entry) async { + Future add(Movie movie) async { final user = await this.user; - var result = await ApiClient.database.createDocument( - databaseId: _databaseId, - collectionId: _collectionId, - documentId: appwrite.ID.unique(), - data: { - "userId": user.$id, - "movieId": entry.id, - }); - - // _entries.add(Entry.fromJson(result.data)); + await ApiClient.database.createDocument( + databaseId: _databaseId, + collectionId: _collectionId, + documentId: ID.unique(), + data: { + "userId": user.$id, + "movie": movie.id, + }, + ); list(); } - Future remove(Entry entry) async { - final user = await this.user; - - final result = await ApiClient.database.listDocuments( - databaseId: _databaseId, - collectionId: _collectionId, - queries: [ - appwrite.Query.equal("userId", user.$id), - appwrite.Query.equal("movieId", entry.id) - ]); - - final id = result.documents.first.$id; - - await ApiClient.database - .deleteDocument(databaseId: _databaseId, collectionId: _collectionId, documentId: id); + Future remove(Watchlist watchlist) async { + await ApiClient.database.deleteDocument( + databaseId: _databaseId, + collectionId: _collectionId, + documentId: watchlist.id, + ); list(); } - - Future imageFor(Entry entry) async { - return await ApiClient.storage.getFileView( - bucketId: _bucketId, - fileId: entry.thumbnailImageId, - ); - } - - bool isOnList(Entry entry) => _entries.any((e) => e.id == entry.id); } diff --git a/lib/screens/details.dart b/lib/screens/details.dart index e62ba44..6188a0d 100644 --- a/lib/screens/details.dart +++ b/lib/screens/details.dart @@ -1,35 +1,36 @@ // // detail.dart // Netflix Clone -// +// // Author: wess (wess@appwrite.io) // Created: 01/19/2022 -// +// // Copywrite (c) 2022 Appwrite.io // -import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:netflix_clone/providers/watchlist.dart'; -import 'package:netflix_clone/widgets/buttons/icon.dart'; import 'package:provider/provider.dart'; -import 'package:netflix_clone/data/entry.dart'; -import 'package:netflix_clone/providers/entry.dart'; +import '/providers/watchlist.dart'; +import '/widgets/buttons/icon.dart'; +import '../data/movie.dart'; class DetailsScreen extends StatefulWidget { - final Entry _entry; + final Movie _movie; - const DetailsScreen({Key? key, required Entry entry,}) : _entry = entry, super(key: key); + const DetailsScreen({ + Key? key, + required Movie movie, + }) : _movie = movie, + super(key: key); @override State createState() => _DetailsScreenState(); } class _DetailsScreenState extends State { - @override void initState() { super.initState(); @@ -42,146 +43,115 @@ class _DetailsScreenState extends State { @override Widget build(BuildContext context) { + final watchListProvider = context.watch(); + final watchlist = watchListProvider.watchlists[widget._movie.id]; return Scaffold( - body: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _DetailHeader(featured: widget._entry), - const SizedBox(height: 20,), - - Expanded( + body: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _DetailHeader(featured: widget._movie), + const SizedBox( + height: 20, + ), + Expanded( child: Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: Text( - widget._entry.description ?? "", - style: const TextStyle( - fontSize: 14, - color: Colors.white - ) - ), - ) - ), - const SizedBox(height: 20), - Expanded( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: Text(widget._movie.description ?? "", + style: const TextStyle(fontSize: 14, color: Colors.white)), + )), + const SizedBox(height: 20), + Expanded( child: Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: Text( - "Starring: ${widget._entry.cast.join(",")}", - style: const TextStyle( - fontSize: 12, - color: Colors.white - ) - ), - ) - ), - const Spacer(), - Expanded( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: Text("Starring: ${widget._movie.cast.join(", ")}", + style: const TextStyle(fontSize: 12, color: Colors.white)), + )), + const Spacer(), + Expanded( child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - VerticalIconButton( - icon: context.read().isOnList(widget._entry) ? Icons.check : Icons.add, - title: "My List", - tap: () { - - if(context.read().isOnList(widget._entry)){ - context.read().remove(widget._entry); - } else { - context.read().add(widget._entry); - } - - Navigator.of(context).pop(); + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + VerticalIconButton( + icon: watchlist != null ? Icons.check : Icons.add, + title: "My List", + tap: () { + if (watchlist != null) { + context.read().remove(watchlist); + } else { + context.read().add(widget._movie); } - ), - const Spacer(), - VerticalIconButton( - icon: Icons.thumb_up, - title: "Rate", - tap: () {} - ), - const Spacer(), - VerticalIconButton( - icon: Icons.share, - title: "Share", - tap: () {} - ), - const Spacer(), - ], - ) - ), - const Spacer(), - ], - ) - ); + }), + const Spacer(), + VerticalIconButton(icon: Icons.thumb_up, title: "Rate", tap: () {}), + const Spacer(), + VerticalIconButton(icon: Icons.share, title: "Share", tap: () {}), + const Spacer(), + ], + )), + const Spacer(), + ], + )); } } - class _DetailHeader extends StatelessWidget { - final Entry featured; - + final Movie featured; + const _DetailHeader({Key? key, required this.featured}) : super(key: key); @override Widget build(BuildContext context) { - return FutureBuilder( - future: context.read().imageFor(featured), - builder: (context, snapshot) { - if(snapshot.hasData == false || snapshot.data == null) { - return const SizedBox( + return Stack( + fit: StackFit.passthrough, + alignment: Alignment.center, + children: [ + Container( height: 500, - child: Center(child: CircularProgressIndicator(),), - ); - } - - return Stack( - fit: StackFit.passthrough, - alignment: Alignment.center, - children: [ - Container( - height: 500, - decoration: BoxDecoration( - image: DecorationImage( - fit: BoxFit.cover, - image: Image.memory((snapshot.data! as Uint8List)).image, - ), - ), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), - child: Container( - height: 500, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.6) - ), - ), + decoration: BoxDecoration( + image: featured.thumbnailImageUrl.isEmpty + ? null + : DecorationImage( + fit: BoxFit.cover, + image: NetworkImage(featured.thumbnailImageUrl), + ), + ), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Container( + height: 500, + decoration: BoxDecoration(color: Colors.black.withOpacity(0.6)), ), ), - Positioned( + ), + Positioned( top: 1, left: 10, child: IconButton( - icon: const Icon(Icons.close, color: Colors.white, size: 30,), + icon: const Icon( + Icons.close, + color: Colors.white, + size: 30, + ), onPressed: () => Navigator.of(context).pop(), - ) - ), - - Positioned( + )), + Positioned( bottom: 160, child: Container( height: 300, width: 200, decoration: BoxDecoration( - image: DecorationImage( - fit: BoxFit.cover, - image: Image.memory((snapshot.data! as Uint8List)).image, + image: featured.thumbnailImageUrl.isEmpty + ? null + : DecorationImage( + fit: BoxFit.cover, + image: NetworkImage(featured.thumbnailImageUrl), + ), ), - ), - ) - ), - Positioned( + )), + Positioned( bottom: 120, child: Row( mainAxisAlignment: MainAxisAlignment.start, @@ -192,101 +162,93 @@ class _DetailHeader extends StatelessWidget { child: Text( "96% Match", style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.green - ), + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.green), ), ), - const SizedBox(width: 10,), + const SizedBox( + width: 10, + ), Padding( padding: const EdgeInsets.all(5.0), child: Text( - featured.releaseDate == null - ? "2020" - : featured.netflixReleaseDate!.year.toString(), + featured.releaseDate == null + ? "2020" + : featured.netflixReleaseDate!.year.toString(), style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.white - ), + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.white), ), ), - const SizedBox(width: 10,), + const SizedBox( + width: 10, + ), Container( color: Colors.black.withAlpha(180), padding: const EdgeInsets.all(5), child: Text( - featured.ageRestriction == "AR13" - ? "13+" - : "18+", + featured.ageRestriction == "AR13" ? "13+" : "18+", style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.white - ), + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.white), ), ), - const SizedBox(width: 10,), + const SizedBox( + width: 10, + ), Padding( padding: const EdgeInsets.all(5), child: Text( "${(featured.durationMinutes.inMinutes / 60).floor().toStringAsFixed(2).replaceAll('.', 'h')}m", style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.white - ), + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.white), ), ), ], - ) - ), - - Positioned( + )), + Positioned( bottom: 10, right: 10, left: 10, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, - children: [ MaterialButton( - color: Colors.white, - onPressed: () {}, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon(Icons.play_arrow), - SizedBox(width: 8), - Text("Play") - ], - ) - ), + color: Colors.white, + onPressed: () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.play_arrow), + SizedBox(width: 8), + Text("Play") + ], + )), MaterialButton( - color: Colors.white.withAlpha(40), - onPressed: () {}, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon(Icons.download, color: Colors.white,), - SizedBox(width: 8), - Text( - "Download", - style: TextStyle( - color: Colors.white + color: Colors.white.withAlpha(40), + onPressed: () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon( + Icons.download, + color: Colors.white, ), - ) - ], - ) - ), + SizedBox(width: 8), + Text( + "Download", + style: TextStyle(color: Colors.white), + ) + ], + )), ], - ) - ) - ] - ); - } - ); + )) + ]); } -} \ No newline at end of file +} diff --git a/lib/screens/home.dart b/lib/screens/home.dart index f34492c..159ccac 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -1,26 +1,25 @@ // // home.dart // appflix -// +// // Author: wess (me@wess.io) // Created: 01/03/2022 -// +// // Copywrite (c) 2022 Wess.io // -import 'package:netflix_clone/providers/entry.dart'; -import 'package:netflix_clone/widgets/content/bar.dart'; -import 'package:netflix_clone/widgets/content/header.dart'; -import 'package:netflix_clone/widgets/content/list.dart'; -import 'package:netflix_clone/widgets/previews.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; - +import '/providers/movies.dart'; +import '/providers/watchlist.dart'; +import '/widgets/content/bar.dart'; +import '/widgets/content/header.dart'; +import '/widgets/content/list.dart'; +import '/widgets/previews.dart'; class HomeScreen extends StatefulWidget { - - const HomeScreen({required Key key}) : super(key: key); + const HomeScreen({required Key key}) : super(key: key); @override State createState() => _HomeScreenState(); @@ -33,6 +32,8 @@ class _HomeScreenState extends State { @override void initState() { + context.read().list(); + _scrollController = ScrollController() ..addListener(() { setState(() { @@ -40,7 +41,6 @@ class _HomeScreenState extends State { }); }); - super.initState(); } @@ -66,7 +66,9 @@ class _HomeScreenState extends State { controller: _scrollController, slivers: [ SliverToBoxAdapter( - child: ContentHeader(featured: context.watch().featured), + child: ContentHeader( + featured: context.watch().featured, + ), ), const SliverPadding( padding: EdgeInsets.only(top: 20), @@ -82,7 +84,7 @@ class _HomeScreenState extends State { padding: const EdgeInsets.all(10), child: ContentList( title: 'Only on Almost Netflix', - contentList: context.watch().entries, + contentList: context.watch().originals, isOriginal: false, ), ), @@ -90,7 +92,7 @@ class _HomeScreenState extends State { SliverToBoxAdapter( child: ContentList( title: 'New releases', - contentList: context.watch().originals, + contentList: context.watch().newReleases, isOriginal: true, ), ), @@ -99,7 +101,7 @@ class _HomeScreenState extends State { sliver: SliverToBoxAdapter( child: ContentList( title: 'Animation', - contentList: context.watch().animations, + contentList: context.watch().animations, isOriginal: false, ), ), diff --git a/lib/screens/navigation.dart b/lib/screens/navigation.dart index 009538c..7487fa7 100644 --- a/lib/screens/navigation.dart +++ b/lib/screens/navigation.dart @@ -1,20 +1,19 @@ // // navigation.dart // appflix -// +// // Author: wess (me@wess.io) // Created: 01/03/2022 -// +// // Copywrite (c) 2022 Wess.io // import 'package:flutter/material.dart'; -import 'package:netflix_clone/data/entry.dart'; -import 'package:netflix_clone/screens/watchlist.dart'; import 'package:provider/provider.dart'; -import 'package:netflix_clone/providers/entry.dart'; -import 'package:netflix_clone/Screens/home.dart'; +import '/providers/movies.dart'; +import '/screens/home.dart'; +import '/screens/watchlist.dart'; class NavScreen extends StatefulWidget { const NavScreen({Key? key}) : super(key: key); @@ -24,15 +23,12 @@ class NavScreen extends StatefulWidget { } class _NavScreenState extends State { - Widget home() => const HomeScreen(key: PageStorageKey('homescreen')); @override Widget build(BuildContext context) { - Entry? selected = context.watch().selected; - return FutureBuilder( - future: context.read().list(), + future: context.read().list(), builder: (context, snapshot) { return Scaffold( body: home(), @@ -41,12 +37,10 @@ class _NavScreenState extends State { unselectedItemColor: Colors.white, currentIndex: 0, onTap: (index) async { - if(index == 1) { + if (index == 1) { await showDialog( - context: context, - builder: (context) => const WatchlistScreen() - ); - + context: context, + builder: (context) => const WatchlistScreen()); } }, items: const [ @@ -61,7 +55,7 @@ class _NavScreenState extends State { ], ), ); - } + }, ); } } diff --git a/lib/screens/onboarding.dart b/lib/screens/onboarding.dart index e83e334..c8e1063 100644 --- a/lib/screens/onboarding.dart +++ b/lib/screens/onboarding.dart @@ -1,20 +1,20 @@ // // onboarding.dart // appflix -// +// // Author: wess (me@wess.io) // Created: 01/03/2022 -// +// // Copywrite (c) 2022 Wess.io // -import 'package:netflix_clone/providers/account.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class OnboardingScreen extends StatefulWidget { +import '/providers/account.dart'; - const OnboardingScreen({Key? key}) : super(key: key); +class OnboardingScreen extends StatefulWidget { + const OnboardingScreen({Key? key}) : super(key: key); @override State createState() => _OnboardingScreenState(); @@ -37,265 +37,259 @@ class _OnboardingScreenState extends State { super.dispose(); } - Widget _renderSignIn() { return Container( - padding: const EdgeInsets.fromLTRB(60, 0, 60, 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: Image.asset('assets/logo.png', width: 200), - ), - const SizedBox(height: 60), - TextField( - controller: _emailController, - autofocus: false, - autocorrect: false, - enableSuggestions: false, - decoration: const InputDecoration( - filled: true, - fillColor: Colors.grey, - labelText: 'Email', - floatingLabelStyle: TextStyle(color: Colors.black), - focusedBorder: InputBorder.none, - border: InputBorder.none, - ), - ), - Container( - height: 0.1, - color: Colors.black, + padding: const EdgeInsets.fromLTRB(60, 0, 60, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Image.asset('assets/logo.png', width: 200), + ), + const SizedBox(height: 60), + TextField( + controller: _emailController, + autofocus: false, + autocorrect: false, + enableSuggestions: false, + decoration: const InputDecoration( + filled: true, + fillColor: Colors.grey, + labelText: 'Email', + floatingLabelStyle: TextStyle(color: Colors.black), + focusedBorder: InputBorder.none, + border: InputBorder.none, + ), + ), + Container( + height: 0.1, + color: Colors.black, + ), + TextField( + controller: _passwordController, + obscureText: true, + autofocus: false, + autocorrect: false, + enableSuggestions: false, + decoration: const InputDecoration( + filled: true, + fillColor: Colors.grey, + labelText: 'Password', + floatingLabelStyle: TextStyle(color: Colors.black), + focusedBorder: InputBorder.none, + border: InputBorder.none, + ), + ), + const SizedBox(height: 20.0), + SizedBox( + width: double.infinity, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + side: const BorderSide(width: 1.0, color: Colors.grey), ), - TextField( - controller: _passwordController, - obscureText: true, - autofocus: false, - autocorrect: false, - enableSuggestions: false, - decoration: const InputDecoration( - filled: true, - fillColor: Colors.grey, - labelText: 'Password', - floatingLabelStyle: TextStyle(color: Colors.black), - focusedBorder: InputBorder.none, - border: InputBorder.none, - ), + child: const Text( + "Sign in", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 22.0), ), - const SizedBox(height: 20.0), - SizedBox( - width: double.infinity, - child: OutlinedButton( - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), - side: const BorderSide(width: 1.0, color: Colors.grey), - ), - child: const Text( - "Sign in", - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 22.0 - ), - ), - onPressed: () async { - final api = context.read(); - final email = _emailController.text; - final password = _passwordController.text; + onPressed: () async { + final api = context.read(); + final email = _emailController.text; + final password = _passwordController.text; - if (email.isEmpty || password.isEmpty) { - showDialog(context: context, builder: (_) => AlertDialog( - title: const Text('Error'), - content: const Text('Please enter your email and password'), - actions: [ - TextButton( - child: const Text('OK'), - onPressed: () => Navigator.of(context).pop(), - ) - ], - )); + if (email.isEmpty || password.isEmpty) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Error'), + content: const Text( + 'Please enter your email and password'), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(), + ) + ], + )); - return; - } - - await api.login(email, password); + return; + } - }, - ), - ), - const SizedBox(height: 40.0), - MaterialButton( - child: const Text( - "Don't have an account? Sign up", - style: TextStyle(color: Colors.white), - ), - onPressed: () { - setState(() { - _selectedIndex = 1; - }); - }, - ), - const SizedBox(height: 10.0), - MaterialButton( - child: const Text( - "Forgot your password?", - style: TextStyle(color: Colors.white), - ), - onPressed: () {}, - ), - ], - ), - ); + await api.login(email, password); + }, + ), + ), + const SizedBox(height: 40.0), + MaterialButton( + child: const Text( + "Don't have an account? Sign up", + style: TextStyle(color: Colors.white), + ), + onPressed: () { + setState(() { + _selectedIndex = 1; + }); + }, + ), + const SizedBox(height: 10.0), + MaterialButton( + child: const Text( + "Forgot your password?", + style: TextStyle(color: Colors.white), + ), + onPressed: () {}, + ), + ], + ), + ); } - + Widget _renderSignUp() { - return Container( + return SingleChildScrollView( + child: Container( padding: const EdgeInsets.fromLTRB(40, 0, 40, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - Center( - child: Image.asset('assets/logo.png', width: 200), + Center( + child: Image.asset('assets/logo.png', width: 200), + ), + const SizedBox(height: 60), + TextField( + controller: _nameController, + autofocus: false, + autocorrect: false, + enableSuggestions: false, + decoration: const InputDecoration( + filled: true, + fillColor: Colors.grey, + labelText: 'Your name', + floatingLabelStyle: TextStyle(color: Colors.black), + focusedBorder: InputBorder.none, + border: InputBorder.none, ), - const SizedBox(height: 60), - TextField( - controller: _nameController, - autofocus: false, - autocorrect: false, - enableSuggestions: false, - decoration: const InputDecoration( - filled: true, - fillColor: Colors.grey, - labelText: 'Your name', - floatingLabelStyle: TextStyle(color: Colors.black), - focusedBorder: InputBorder.none, - border: InputBorder.none, - ), + ), + Container( + height: 0.1, + color: Colors.black, + ), + TextField( + controller: _emailController, + autofocus: false, + autocorrect: false, + enableSuggestions: false, + decoration: const InputDecoration( + filled: true, + fillColor: Colors.grey, + labelText: 'Email address', + floatingLabelStyle: TextStyle(color: Colors.black), + focusedBorder: InputBorder.none, + border: InputBorder.none, ), - Container( - height: 0.1, - color: Colors.black, + ), + Container( + height: 0.1, + color: Colors.black, + ), + TextField( + controller: _passwordController, + obscureText: true, + autofocus: false, + autocorrect: false, + enableSuggestions: false, + decoration: const InputDecoration( + filled: true, + fillColor: Colors.grey, + labelText: 'Password', + floatingLabelStyle: TextStyle(color: Colors.black), + focusedBorder: InputBorder.none, + border: InputBorder.none, ), - TextField( - controller: _emailController, - autofocus: false, - autocorrect: false, - enableSuggestions: false, - decoration: const InputDecoration( - filled: true, - fillColor: Colors.grey, - labelText: 'Email address', - floatingLabelStyle: TextStyle(color: Colors.black), - focusedBorder: InputBorder.none, - border: InputBorder.none, + ), + const SizedBox(height: 20.0), + SizedBox( + width: double.infinity, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + side: const BorderSide(width: 1.0, color: Colors.grey), ), - ), - Container( - height: 0.1, - color: Colors.black, - ), - TextField( - controller: _passwordController, - obscureText: true, - autofocus: false, - autocorrect: false, - enableSuggestions: false, - decoration: const InputDecoration( - filled: true, - fillColor: Colors.grey, - labelText: 'Password', - floatingLabelStyle: TextStyle(color: Colors.black), - focusedBorder: InputBorder.none, - border: InputBorder.none, + child: const Text( + "Sign up", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 22.0), ), - ), - const SizedBox(height: 20.0), - SizedBox( - width: double.infinity, - child: OutlinedButton( - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), - side: const BorderSide(width: 1.0, color: Colors.grey), - ), - child: const Text( - "Sign up", - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 22.0 - ), - ), - onPressed: () async { - final api = context.read(); - final name = _nameController.text; - final email = _emailController.text; - final password = _passwordController.text; + onPressed: () async { + final api = context.read(); + final name = _nameController.text; + final email = _emailController.text; + final password = _passwordController.text; - if (email.isEmpty || password.isEmpty) { - showDialog(context: context, builder: (_) => AlertDialog( - title: const Text('Error'), - content: const Text('Please enter your email and password'), - actions: [ - TextButton( - child: const Text('OK'), - onPressed: () => Navigator.of(context).pop(), - ) - ], - )); + if (email.isEmpty || password.isEmpty) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Error'), + content: const Text( + 'Please enter your email and password'), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(), + ) + ], + )); - return; - } - - await api.register(email, password, name); + return; + } - }, - ), + await api.register(email, password, name); + }, ), - const SizedBox(height: 10.0), - MaterialButton( - child: const Text( - "Forgot your password?", - style: TextStyle(color: Colors.white), - ), - onPressed: () {}, + ), + const SizedBox(height: 10.0), + MaterialButton( + child: const Text( + "Forgot your password?", + style: TextStyle(color: Colors.white), ), - const SizedBox(height: 40.0), - MaterialButton( - child: const Text( - "Already have an account? Sign in", - style: TextStyle(color: Colors.white), - ), - onPressed: () { - setState(() { - _selectedIndex = 0; - }); - }, + onPressed: () {}, + ), + const SizedBox(height: 40.0), + MaterialButton( + child: const Text( + "Already have an account? Sign in", + style: TextStyle(color: Colors.white), ), - ], + onPressed: () { + setState(() { + _selectedIndex = 0; + }); + }, + ), + ], ), - ); + ), + ); } @override Widget build(BuildContext context) { - final Size screenSize = MediaQuery.of(context).size; - - final current = context.watch().current; - - _emailController.text = current?.email ?? ""; - return Scaffold( - extendBodyBehindAppBar: true, - body: IndexedStack( - index: _selectedIndex, - children: [ - _renderSignIn(), - _renderSignUp(), - ], - ) - ); + extendBodyBehindAppBar: true, + body: IndexedStack( + index: _selectedIndex, + children: [ + _renderSignIn(), + _renderSignUp(), + ], + )); } } - - diff --git a/lib/screens/watchlist.dart b/lib/screens/watchlist.dart index a341fbe..cda6214 100644 --- a/lib/screens/watchlist.dart +++ b/lib/screens/watchlist.dart @@ -1,23 +1,20 @@ // // watchlist.dart // Netflix Clone -// +// // Author: wess (wess@appwrite.io) // Created: 01/19/2022 -// +// // Copywrite (c) 2022 Appwrite.io // -import 'dart:typed_data'; - import 'package:flutter/material.dart'; -import 'package:netflix_clone/providers/watchlist.dart'; -import 'package:netflix_clone/screens/details.dart'; -import 'package:netflix_clone/widgets/buttons/icon.dart'; import 'package:provider/provider.dart'; -import 'package:netflix_clone/data/entry.dart'; -import 'package:netflix_clone/providers/entry.dart'; +import '/data/watchlist.dart'; +import '/providers/watchlist.dart'; +import '/screens/details.dart'; +import '/widgets/buttons/icon.dart'; class WatchlistScreen extends StatefulWidget { const WatchlistScreen({Key? key}) : super(key: key); @@ -27,7 +24,6 @@ class WatchlistScreen extends StatefulWidget { } class _WatchlistScreenState extends State { - @override void initState() { super.initState(); @@ -38,60 +34,58 @@ class _WatchlistScreenState extends State { super.dispose(); } - Widget _row(Entry entry) { + Widget _row(Watchlist w) { return Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - FutureBuilder( - future: context.read().imageFor(entry), - builder: (context, snapshot) => snapshot.hasData && snapshot.data != null - ? Container( - width: 80, - height: 80, - decoration: BoxDecoration( - image: DecorationImage( - fit: BoxFit.contain, - image: Image.memory(snapshot.data!).image, - ), - ), - ) - : Container(), - ), - const SizedBox(width: 10,), + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + image: w.movie.thumbnailImageUrl.isEmpty + ? null + : DecorationImage( + fit: BoxFit.contain, + image: NetworkImage(w.movie.thumbnailImageUrl), + ), + ), + ), + const SizedBox( + width: 10, + ), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - entry.name, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + w.movie.name, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, ), - Text( - ((entry.description ?? "").length < 51) ? (entry.description ?? "") : "${(entry.description ?? "").substring(0, 50)}...", - style: const TextStyle( - color: Colors.grey, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + ), + Text( + ((w.movie.description ?? "").length < 51) + ? (w.movie.description ?? "") + : "${(w.movie.description ?? "").substring(0, 50)}...", + style: const TextStyle( + color: Colors.grey, + fontSize: 14, + fontWeight: FontWeight.w500, ), - ], - ) - ), - + ), + ], + )), Padding( padding: const EdgeInsets.fromLTRB(20, 20, 30, 20), child: VerticalIconButton( - icon: Icons.delete, + icon: Icons.delete, title: '', tap: () async { - await context.read().remove(entry); - setState(() {}); - } + await context.read().remove(w); + }, ), ), ], @@ -100,6 +94,8 @@ class _WatchlistScreenState extends State { @override Widget build(BuildContext context) { + final watchlists = context.watch().watchlists; + return Scaffold( appBar: AppBar( backgroundColor: Colors.transparent, @@ -113,29 +109,22 @@ class _WatchlistScreenState extends State { ), ], ), - body: FutureBuilder>( - future: context.read().list(), - builder: (context, snapshot) { - return snapshot.hasData == false || snapshot.data == null - ? const Padding( - padding: EdgeInsets.all(60), - child: Center( - child: CircularProgressIndicator() + body: ListView( + children: watchlists.entries + .map( + (entry) => GestureDetector( + child: _row(entry.value), + onTap: () async { + await showDialog( + context: context, + builder: (context) => + DetailsScreen(movie: entry.value.movie), + ); + }, + ), ) - ) - : ListView( - children: snapshot.data!.map((entry) => GestureDetector( - child: _row(entry), - onTap:() async { - await showDialog( - context: context, - builder: (context) => DetailsScreen(entry: entry) - ); - } - )).toList(), - ); - } - ) + .toList(), + ), ); } } diff --git a/lib/widgets/buttons/icon.dart b/lib/widgets/buttons/icon.dart index ade646f..d46c906 100644 --- a/lib/widgets/buttons/icon.dart +++ b/lib/widgets/buttons/icon.dart @@ -1,10 +1,10 @@ // // icon.dart // appflix -// +// // Author: wess (me@wess.io) // Created: 01/03/2022 -// +// // Copywrite (c) 2022 Wess.io // @@ -22,7 +22,9 @@ class VerticalIconButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTapUp: (_) { tap(); }, + onTapUp: (_) { + tap(); + }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/widgets/content/bar.dart b/lib/widgets/content/bar.dart index 41b13bb..6465b29 100644 --- a/lib/widgets/content/bar.dart +++ b/lib/widgets/content/bar.dart @@ -1,16 +1,17 @@ // // bar.dart // appflix -// +// // Author: wess (me@wess.io) // Created: 01/03/2022 -// +// // Copywrite (c) 2022 Wess.io // import 'package:flutter/material.dart'; -import 'package:netflix_clone/assets.dart'; -import 'package:netflix_clone/screens/watchlist.dart'; + +import '/assets.dart'; +import '/screens/watchlist.dart'; class ContentBar extends StatelessWidget { final double scrollOffset; @@ -40,8 +41,8 @@ class ContentBar extends StatelessWidget { const Spacer(), _AppBarButton('My List', () async { await showDialog( - context: context, - builder: (context) => const WatchlistScreen() + context: context, + builder: (context) => const WatchlistScreen(), ); }), ], @@ -63,7 +64,9 @@ class _AppBarButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () { function(); }, + onTap: () { + function(); + }, child: Text( title, style: const TextStyle( diff --git a/lib/widgets/content/header.dart b/lib/widgets/content/header.dart index 1f8eb75..e7afea2 100644 --- a/lib/widgets/content/header.dart +++ b/lib/widgets/content/header.dart @@ -1,145 +1,120 @@ // // header.dart // appflix -// +// // Author: wess (me@wess.io) // Created: 01/03/2022 -// +// // Copywrite (c) 2022 Wess.io // -import 'dart:typed_data'; - -import 'package:netflix_clone/data/entry.dart'; -import 'package:netflix_clone/providers/entry.dart'; -import 'package:netflix_clone/providers/watchlist.dart'; -import 'package:netflix_clone/screens/details.dart'; -import 'package:netflix_clone/widgets/buttons/icon.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '/data/movie.dart'; +import '/providers/watchlist.dart'; +import '/screens/details.dart'; +import '/widgets/buttons/icon.dart'; + class ContentHeader extends StatelessWidget { - final Entry featured; - + final Movie featured; + const ContentHeader({Key? key, required this.featured}) : super(key: key); @override Widget build(BuildContext context) { - - return FutureBuilder( - future: context.read().imageFor(featured), - builder: (context, snapshot) { - if(snapshot.hasData == false || snapshot.data == null) { - return const SizedBox( - height: 500, - child: Center(child: CircularProgressIndicator(),), - ); - } - - return Stack( - alignment: Alignment.center, - children: [ - Container( - height: 500, - decoration: BoxDecoration( - image: DecorationImage( + final watchListProvider = context.watch(); + final watchlist = watchListProvider.watchlists[featured.id]; + return Stack(alignment: Alignment.center, children: [ + Container( + height: 500, + decoration: BoxDecoration( + image: featured.thumbnailImageUrl.isEmpty + ? null + : DecorationImage( fit: BoxFit.cover, - image: Image.memory((snapshot.data! as Uint8List)).image, + image: NetworkImage(featured.thumbnailImageUrl), ), + ), + ), + Container( + height: 500, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.black, Colors.transparent], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + ), + ), + ), + Positioned( + bottom: 120, + child: SizedBox( + width: 250, + child: Text( + featured.name, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, ), - ), - Container( - height: 500, - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Colors.black, Colors.transparent], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - ), - ), - ), - Positioned( - bottom: 120, - child: SizedBox( - width: 250, - child: Text( - featured.name, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ) + )), + ), + Positioned( + bottom: 88, + child: SizedBox( + width: 250, + child: Text( + featured.tags.join(" • "), + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.normal, ), + )), + ), + Positioned( + right: 0, + left: 0, + bottom: 20, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Spacer(), + VerticalIconButton( + icon: watchlist != null ? Icons.check : Icons.add, + title: 'Watchlist', + tap: () { + if (watchlist != null) { + watchListProvider.remove(watchlist); + } else { + watchListProvider.add(featured); + } + }, ), - Positioned( - bottom: 88, - child: SizedBox( - width: 250, - child: Text( - featured.tags.join(" • "), - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.normal, - ), - ) - ), + const SizedBox(width: 40), + MaterialButton( + color: Colors.white, + child: Row( + children: const [Icon(Icons.play_arrow), Text("Play")], + ), + onPressed: () {}), + const SizedBox(width: 40), + VerticalIconButton( + icon: Icons.info, + title: 'Info', + tap: () async { + await showDialog( + context: context, + builder: (context) => DetailsScreen(movie: featured)); + }, ), - Positioned( - right: 0, - left: 0, - bottom: 20, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const Spacer(), - VerticalIconButton( - icon: context.read().isOnList(featured) ? Icons.check : Icons.add, - title: 'Watchlist', - tap: () { - if(context.read().isOnList(featured)){ - context.read().remove(featured); - } else { - context.read().add(featured); - } - }, - ), - - const SizedBox(width: 40), - - MaterialButton( - color: Colors.white, - child: Row( - children: const [ - Icon(Icons.play_arrow), - Text("Play") - ], - ), - onPressed: (){} - ), - - const SizedBox(width: 40), - - VerticalIconButton( - icon: Icons.info, - title: 'Info', - tap: () async { - await showDialog( - context: context, - builder: (context) => DetailsScreen(entry: featured) - ); - }, - ), - const Spacer(), - ], - ), - ) - ] - ); - } - ); + const Spacer(), + ], + ), + ) + ]); } -} \ No newline at end of file +} diff --git a/lib/widgets/content/list.dart b/lib/widgets/content/list.dart index 94693bb..e502218 100644 --- a/lib/widgets/content/list.dart +++ b/lib/widgets/content/list.dart @@ -1,35 +1,29 @@ // // list.dart // appflix -// +// // Author: wess (me@wess.io) // Created: 01/03/2022 -// +// // Copywrite (c) 2022 Wess.io // -import 'dart:typed_data'; - import 'package:flutter/material.dart'; -import 'package:netflix_clone/screens/details.dart'; -import 'package:provider/provider.dart'; -import 'package:netflix_clone/data/entry.dart'; -import 'package:netflix_clone/providers/entry.dart'; +import '/data/movie.dart'; +import '/screens/details.dart'; class ContentList extends StatelessWidget { final String title; - final List contentList; - bool isOriginal; - final bool _rounded; + final List contentList; + final bool isOriginal; - ContentList({ + const ContentList({ Key? key, required this.title, required this.contentList, required this.isOriginal, - bool rounded = false, - }) : _rounded = rounded, super(key: key); + }) : super(key: key); @override Widget build(BuildContext context) { @@ -41,9 +35,9 @@ class ContentList extends StatelessWidget { child: Text( title, style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 20.0 + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 20.0, ), ), ), @@ -53,28 +47,26 @@ class ContentList extends StatelessWidget { scrollDirection: Axis.horizontal, itemCount: contentList.length, itemBuilder: (context, int count) { - final Entry current = contentList[count]; + final current = contentList[count]; return GestureDetector( onTap: () async { await showDialog( - context: context, - builder: (context) => DetailsScreen(entry: current) + context: context, + builder: (context) => DetailsScreen(movie: current), ); }, child: Container( height: 100, - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), - child: FutureBuilder( - future: context.read().imageFor(current), - builder: (context, snapshot) => snapshot.hasData - ? Image.memory( - snapshot.data!, - fit: BoxFit.cover, - ) - : Container( - color: Colors.black, - ), - ) + margin: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 4, + ), + child: current.thumbnailImageUrl.isEmpty + ? null + : Image( + image: NetworkImage(current.thumbnailImageUrl), + fit: BoxFit.cover, + ), ), ); }, diff --git a/lib/widgets/previews.dart b/lib/widgets/previews.dart index e208f6c..67e2820 100644 --- a/lib/widgets/previews.dart +++ b/lib/widgets/previews.dart @@ -1,25 +1,23 @@ // // previews.dart // appflix -// +// // Author: wess (me@wess.io) // Created: 01/03/2022 -// +// // Copywrite (c) 2022 Wess.io // -import 'dart:typed_data'; - import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:netflix_clone/data/entry.dart'; -import 'package:netflix_clone/providers/entry.dart'; -import 'package:netflix_clone/screens/details.dart'; +import '/data/movie.dart'; +import '/providers/movies.dart'; +import '/screens/details.dart'; class Previews extends StatefulWidget { final String title; - + const Previews({ Key? key, required this.title, @@ -30,87 +28,69 @@ class Previews extends StatefulWidget { } class _PreviewsState extends State { - - Widget _renderStack(Entry entry) { - return FutureBuilder( - future: context.read().imageFor(entry), - builder: (context, snapshot) { - if(snapshot.connectionState == ConnectionState.waiting) { - return const SizedBox( - height: 130, - width: 130, - child: Center(child: CircularProgressIndicator(),), - ); - } - - return Stack( - alignment: Alignment.center, - children: [ - snapshot.hasData && snapshot.data != null - ? Container( - margin: const EdgeInsets.symmetric( - horizontal: 16.0, vertical: 8.0), - height: 130, - width: 130, - decoration: BoxDecoration( - image: DecorationImage( - image: Image.memory((snapshot.data! as Uint8List)).image, + Widget _renderStack(Movie movie) { + return Stack( + alignment: Alignment.center, + children: [ + Container( + margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + height: 130, + width: 130, + decoration: BoxDecoration( + image: movie.thumbnailImageUrl.isEmpty + ? null + : DecorationImage( + image: NetworkImage(movie.thumbnailImageUrl), fit: BoxFit.cover, ), - shape: BoxShape.circle, - border: - Border.all(color: Colors.white.withAlpha(40), width: 4.0)), - ) - : const CircularProgressIndicator(), - Container( - height: 130, - width: 130, - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ - Colors.black87, - Colors.black45, - Colors.transparent - ], - stops: [ - 0, - 0.25, - 1 - ], - begin: Alignment.bottomCenter, - end: Alignment.topCenter), - shape: BoxShape.circle, - border: Border.all(color: Colors.white.withAlpha(40), width: 4.0), - ), + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withAlpha(40), + width: 4.0, ), - Positioned( - bottom: 0, - right: 0, - left: 0, - child: SizedBox( - height: 60, - child: Text( - entry.name.length > 14 - ? entry.name.substring(0, 14) - : entry.name, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ) + ), + ), + Container( + height: 130, + width: 130, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Colors.black87, Colors.black45, Colors.transparent], + stops: [0, 0.25, 1], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + ), + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withAlpha(40), + width: 4.0, + ), + ), + ), + Positioned( + bottom: 0, + right: 0, + left: 0, + child: SizedBox( + height: 60, + child: Text( + movie.name.length > 14 ? movie.name.substring(0, 14) : movie.name, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, ), ), - ], - ); - } + ), + ), + ], ); } @override Widget build(BuildContext context) { - var entries = context.read().entries; + var movies = context.watch().movies; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -120,30 +100,30 @@ class _PreviewsState extends State { child: Text( 'Popular this week', style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 20.0 - ), + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 20.0), ), ), SizedBox( height: 165.0, child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: entries.length, - itemBuilder: (BuildContext context, int index) { - final Entry entry = entries[index]; + scrollDirection: Axis.horizontal, + itemCount: movies.length, + itemBuilder: (BuildContext context, int index) { + final Movie movie = movies[index]; - return GestureDetector( - onTap: () async { - await showDialog( - context: context, - builder: (context) => DetailsScreen(entry: entry) - ); - }, - child: _renderStack(entry), - ); - }), + return GestureDetector( + onTap: () async { + await showDialog( + context: context, + builder: (context) => DetailsScreen(movie: movie), + ); + }, + child: _renderStack(movie), + ); + }, + ), ), ], ); diff --git a/pubspec.lock b/pubspec.lock index a8afba2..4a0c692 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,14 +21,14 @@ packages: name: appwrite url: "https://pub.dartlang.org" source: hosted - version: "6.0.0" + version: "9.0.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -42,7 +42,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -56,7 +56,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: transitive description: @@ -78,6 +78,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.2" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.2" cupertino_icons: dependency: "direct main" description: @@ -91,42 +98,14 @@ packages: name: device_info_plus url: "https://pub.dartlang.org" source: hosted - version: "3.2.4" - device_info_plus_linux: - dependency: transitive - description: - name: device_info_plus_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - device_info_plus_macos: - dependency: transitive - description: - name: device_info_plus_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.3" + version: "8.1.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.3.0+1" - device_info_plus_web: - dependency: transitive - description: - name: device_info_plus_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - device_info_plus_windows: - dependency: transitive - description: - name: device_info_plus_windows - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" + version: "7.0.0" dynamic_color: dependency: transitive description: @@ -140,14 +119,14 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "2.0.1" file: dependency: transitive description: @@ -179,25 +158,39 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_web_auth: + flutter_web_auth_2: dependency: transitive description: - name: flutter_web_auth + name: flutter_web_auth_2 url: "https://pub.dartlang.org" source: hosted - version: "0.4.1" + version: "2.1.2" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.2" http: dependency: transitive description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.13.4" + version: "0.13.5" http_parser: dependency: transitive description: @@ -239,21 +232,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" nested: dependency: transitive description: @@ -267,56 +260,28 @@ packages: name: package_info_plus url: "https://pub.dartlang.org" source: hosted - version: "1.4.2" - package_info_plus_linux: - dependency: transitive - description: - name: package_info_plus_linux - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" - package_info_plus_macos: - dependency: transitive - description: - name: package_info_plus_macos - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" + version: "3.0.3" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" - package_info_plus_web: - dependency: transitive - description: - name: package_info_plus_web - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" - package_info_plus_windows: - dependency: transitive - description: - name: package_info_plus_windows - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" + version: "2.0.1" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: transitive description: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.14" path_provider_android: dependency: transitive description: @@ -324,13 +289,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.16" - path_provider_ios: + path_provider_foundation: dependency: transitive description: - name: path_provider_ios + name: path_provider_foundation url: "https://pub.dartlang.org" source: hosted - version: "2.0.10" + version: "2.2.0" path_provider_linux: dependency: transitive description: @@ -338,13 +303,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" path_provider_platform_interface: dependency: transitive description: @@ -358,7 +316,7 @@ packages: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.1.5" platform: dependency: transitive description: @@ -454,7 +412,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -475,21 +433,21 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" typed_data: dependency: transitive description: @@ -497,6 +455,76 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + universal_html: + dependency: transitive + description: + name: universal_html + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + universal_io: + dependency: transitive + description: + name: universal_io + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.10" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.26" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.16" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" vector_math: dependency: transitive description: @@ -510,14 +538,21 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.3.0" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "3.1.3" + window_to_front: + dependency: transitive + description: + name: window_to_front + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3" xdg_directories: dependency: transitive description: @@ -526,5 +561,5 @@ packages: source: hosted version: "0.2.0+1" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" + dart: ">=2.18.0 <3.0.0" + flutter: ">=3.3.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5bf161f..c83f54b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A new Flutter project. # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -30,7 +30,6 @@ dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 @@ -38,7 +37,7 @@ dependencies: fluro: ^2.0.3 shared_preferences: ^2.0.11 adaptive_dialog: ^1.3.0 - appwrite: ^8.1.0 + appwrite: ^9.0.0 dev_dependencies: flutter_test: @@ -56,7 +55,6 @@ dev_dependencies: # The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. diff --git a/test/widget_test.dart b/test/widget_test.dart index ffbe3a7..29bbbf6 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - import 'package:netflix_clone/main.dart'; void main() {