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() {