├── amplify
├── backend
│ ├── storage
│ │ └── netflixmedia
│ │ │ ├── storage-params.json
│ │ │ ├── parameters.json
│ │ │ └── s3-cloudformation-template.json
│ ├── api
│ │ └── netflix
│ │ │ ├── transform.conf.json
│ │ │ ├── parameters.json
│ │ │ ├── schema.graphql
│ │ │ └── stacks
│ │ │ └── CustomResources.json
│ ├── backend-config.json
│ └── auth
│ │ └── netflixd3938a31
│ │ ├── parameters.json
│ │ └── netflixd3938a31-cloudformation-template.yml
├── .config
│ └── project-config.json
└── team-provider-info.json
├── screens
├── HomeScreen
│ ├── index.tsx
│ ├── styles.ts
│ └── HomeScreen.tsx
├── NotFoundScreen.tsx
├── TabTwoScreen.tsx
└── MovieDetailsScreen
│ ├── styles.ts
│ └── index.tsx
├── assets
├── images
│ ├── icon.png
│ ├── splash.png
│ ├── favicon.png
│ └── adaptive-icon.png
├── fonts
│ └── SpaceMono-Regular.ttf
└── data
│ ├── movie.ts
│ └── categories.ts
├── src
├── models
│ ├── schema.d.ts
│ ├── index.js
│ ├── index.d.ts
│ └── schema.js
├── graphql
│ ├── queries.ts
│ ├── subscriptions.ts
│ └── mutations.ts
└── API.ts
├── babel.config.js
├── amplify.json
├── components
├── StyledText.tsx
├── VideoPlayer
│ ├── styles.ts
│ └── index.tsx
├── __tests__
│ └── StyledText-test.js
├── MovieItem
│ ├── styles.ts
│ └── index.tsx
├── HomeCategory
│ ├── styles.ts
│ └── index.tsx
├── EpisodeItem
│ ├── styles.ts
│ └── index.tsx
├── Themed.tsx
└── EditScreenInfo.tsx
├── hooks
├── useColorScheme.web.ts
├── useColorScheme.ts
└── useCachedResources.ts
├── constants
├── Layout.ts
└── Colors.ts
├── tsconfig.json
├── .expo-shared
└── assets.json
├── .graphqlconfig.yml
├── navigation
├── LinkingConfiguration.ts
├── index.tsx
└── BottomTabNavigator.tsx
├── .gitignore
├── types.tsx
├── app.json
├── App.tsx
├── LICENSE
└── package.json
/amplify/backend/storage/netflixmedia/storage-params.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/screens/HomeScreen/index.tsx:
--------------------------------------------------------------------------------
1 | import HomeScreen from './HomeScreen';
2 |
3 | export default HomeScreen;
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VadimNotJustDev/NetflixClone/HEAD/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VadimNotJustDev/NetflixClone/HEAD/assets/images/splash.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VadimNotJustDev/NetflixClone/HEAD/assets/images/favicon.png
--------------------------------------------------------------------------------
/src/models/schema.d.ts:
--------------------------------------------------------------------------------
1 | import { Schema } from '@aws-amplify/datastore';
2 |
3 | export declare const schema: Schema;
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VadimNotJustDev/NetflixClone/HEAD/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VadimNotJustDev/NetflixClone/HEAD/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/amplify.json:
--------------------------------------------------------------------------------
1 | {
2 | "features":
3 | {
4 | "graphqltransformer":
5 | {
6 | "transformerversion": 5
7 | },
8 | "keytransformer":
9 | {
10 | "defaultquery": true
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/screens/HomeScreen/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | const styles = StyleSheet.create({
4 | container: {
5 | flex: 1,
6 | padding: 20,
7 | },
8 | });
9 |
10 | export default styles;
--------------------------------------------------------------------------------
/components/StyledText.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Text, TextProps } from './Themed';
4 |
5 | export function MonoText(props: TextProps) {
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/components/VideoPlayer/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | const styles = StyleSheet.create({
4 | video: {
5 | width: '100%',
6 | aspectRatio: 16/9,
7 |
8 | }
9 | });
10 |
11 | export default styles;
--------------------------------------------------------------------------------
/hooks/useColorScheme.web.ts:
--------------------------------------------------------------------------------
1 | // useColorScheme from react-native does not support web currently. You can replace
2 | // this with react-native-appearance if you would like theme support on web.
3 | export default function useColorScheme() {
4 | return 'light';
5 | }
--------------------------------------------------------------------------------
/amplify/backend/api/netflix/transform.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": 5,
3 | "ElasticsearchWarning": true,
4 | "ResolverConfig": {
5 | "project": {
6 | "ConflictHandler": "AUTOMERGE",
7 | "ConflictDetection": "VERSION"
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/src/models/index.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { initSchema } from '@aws-amplify/datastore';
3 | import { schema } from './schema';
4 |
5 |
6 |
7 | const { Category, Movie, Season, Episode } = initSchema(schema);
8 |
9 | export {
10 | Category,
11 | Movie,
12 | Season,
13 | Episode
14 | };
--------------------------------------------------------------------------------
/constants/Layout.ts:
--------------------------------------------------------------------------------
1 | import { Dimensions } from 'react-native';
2 |
3 | const width = Dimensions.get('window').width;
4 | const height = Dimensions.get('window').height;
5 |
6 | export default {
7 | window: {
8 | width,
9 | height,
10 | },
11 | isSmallDevice: width < 375,
12 | };
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "jsx": "react-native",
5 | "lib": ["dom", "esnext"],
6 | "moduleResolution": "node",
7 | "noEmit": true,
8 | "skipLibCheck": true,
9 | "resolveJsonModule": true,
10 | "strict": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "e997a5256149a4b76e6bfd6cbf519c5e5a0f1d278a3d8fa1253022b03c90473b": true,
3 | "af683c96e0ffd2cf81287651c9433fa44debc1220ca7cb431fe482747f34a505": true,
4 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
5 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
6 | }
7 |
--------------------------------------------------------------------------------
/components/__tests__/StyledText-test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import { MonoText } from '../StyledText';
5 |
6 | it(`renders correctly`, () => {
7 | const tree = renderer.create(Snapshot test!).toJSON();
8 |
9 | expect(tree).toMatchSnapshot();
10 | });
11 |
--------------------------------------------------------------------------------
/amplify/backend/api/netflix/parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppSyncApiName": "netflix",
3 | "DynamoDBBillingMode": "PAY_PER_REQUEST",
4 | "DynamoDBEnableServerSideEncryption": false,
5 | "AuthCognitoUserPoolId": {
6 | "Fn::GetAtt": [
7 | "authnetflixd3938a31",
8 | "Outputs.UserPoolId"
9 | ]
10 | }
11 | }
--------------------------------------------------------------------------------
/.graphqlconfig.yml:
--------------------------------------------------------------------------------
1 | projects:
2 | netflix:
3 | schemaPath: src/graphql/schema.json
4 | includes:
5 | - src/graphql/**/*.ts
6 | excludes:
7 | - ./amplify/**
8 | extensions:
9 | amplify:
10 | codeGenTarget: typescript
11 | generatedFileName: src/API.ts
12 | docsFilePath: src/graphql
13 | extensions:
14 | amplify:
15 | version: 3
16 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | import { ColorSchemeName, useColorScheme as _useColorScheme } from 'react-native';
2 |
3 | // The useColorScheme value is always either light or dark, but the built-in
4 | // type suggests that it can be null. This will not happen in practice, so this
5 | // makes it a bit easier to work with.
6 | export default function useColorScheme(): NonNullable {
7 | return _useColorScheme() as NonNullable;
8 | }
9 |
--------------------------------------------------------------------------------
/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | const tintColorLight = '#2f95dc';
2 | const tintColorDark = '#fff';
3 |
4 | export default {
5 | light: {
6 | text: '#000',
7 | background: '#fff',
8 | tint: tintColorLight,
9 | tabIconDefault: '#ccc',
10 | tabIconSelected: tintColorLight,
11 | },
12 | dark: {
13 | text: '#fff',
14 | background: '#000',
15 | tint: tintColorDark,
16 | tabIconDefault: '#ccc',
17 | tabIconSelected: tintColorDark,
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/components/MovieItem/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | const styles = StyleSheet.create({
4 | container: {
5 | flex: 1,
6 | padding: 20,
7 | },
8 | image: {
9 | width: 100,
10 | height: 170,
11 | resizeMode: 'cover',
12 | borderRadius: 5,
13 | margin: 5,
14 | },
15 | title: {
16 | fontSize: 20,
17 | fontWeight: 'bold',
18 | }
19 | });
20 |
21 | export default styles;
--------------------------------------------------------------------------------
/components/HomeCategory/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | const styles = StyleSheet.create({
4 | container: {
5 | flex: 1,
6 | padding: 20,
7 | },
8 | image: {
9 | width: 100,
10 | height: 170,
11 | resizeMode: 'cover',
12 | borderRadius: 5,
13 | margin: 5,
14 | },
15 | title: {
16 | fontSize: 20,
17 | fontWeight: 'bold',
18 | }
19 | });
20 |
21 | export default styles;
--------------------------------------------------------------------------------
/amplify/.config/project-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "Netflix",
3 | "version": "3.0",
4 | "frontend": "javascript",
5 | "javascript": {
6 | "framework": "react-native",
7 | "config": {
8 | "SourceDir": "/",
9 | "DistributionDir": "/",
10 | "BuildCommand": "npm run-script build",
11 | "StartCommand": "npm run-script start"
12 | }
13 | },
14 | "providers": [
15 | "awscloudformation"
16 | ]
17 | }
--------------------------------------------------------------------------------
/navigation/LinkingConfiguration.ts:
--------------------------------------------------------------------------------
1 | import * as Linking from 'expo-linking';
2 |
3 | export default {
4 | prefixes: [Linking.makeUrl('/')],
5 | config: {
6 | screens: {
7 | Root: {
8 | screens: {
9 | TabOne: {
10 | screens: {
11 | TabOneScreen: 'one',
12 | },
13 | },
14 | TabTwo: {
15 | screens: {
16 | TabTwoScreen: 'two',
17 | },
18 | },
19 | },
20 | },
21 | NotFound: '*',
22 | },
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p8
6 | *.p12
7 | *.key
8 | *.mobileprovision
9 | *.orig.*
10 | web-build/
11 |
12 | # macOS
13 | .DS_Store
14 |
15 | #amplify
16 | amplify/\#current-cloud-backend
17 | amplify/.config/local-*
18 | amplify/mock-data
19 | amplify/backend/amplify-meta.json
20 | amplify/backend/awscloudformation
21 | build/
22 | dist/
23 | node_modules/
24 | aws-exports.js
25 | awsconfiguration.json
26 | amplifyconfiguration.json
27 | amplify-build-config.json
28 | amplify-gradle-config.json
29 | amplifytools.xcconfig
--------------------------------------------------------------------------------
/types.tsx:
--------------------------------------------------------------------------------
1 | export type RootStackParamList = {
2 | Root: undefined;
3 | NotFound: undefined;
4 | };
5 |
6 | export type BottomTabParamList = {
7 | Home: undefined;
8 | Coming_Soon: undefined;
9 | Search: undefined;
10 | Downloads: undefined;
11 | };
12 |
13 | export type HomeParamList = {
14 | HomeScreen: undefined;
15 | MovieDetailsScreen: undefined;
16 | };
17 |
18 | export type TabTwoParamList = {
19 | TabTwoScreen: undefined;
20 | };
21 |
22 | export type Episode = {
23 | id: string,
24 | title: string,
25 | poster: string,
26 | duration: string,
27 | plot: string,
28 | video: string,
29 | }
30 |
--------------------------------------------------------------------------------
/components/EpisodeItem/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | const styles = StyleSheet.create({
4 | row: {
5 | flexDirection: 'row',
6 | justifyContent: 'space-between',
7 | alignItems: 'center',
8 | marginBottom: 5,
9 | },
10 | image: {
11 | height: 75,
12 | aspectRatio: 16/9,
13 | resizeMode: 'cover',
14 | borderRadius: 3,
15 | },
16 | titleContainer: {
17 | flex: 1,
18 | padding: 5,
19 | justifyContent: 'center',
20 | },
21 | title: {
22 |
23 | },
24 | duration: {
25 | color: 'darkgrey',
26 | fontSize: 10,
27 | },
28 | plot: {
29 | color: 'darkgrey'
30 | }
31 | })
32 |
33 | export default styles;
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "Netflix",
4 | "slug": "Netflix",
5 | "version": "1.0.0",
6 | "icon": "./assets/images/icon.png",
7 | "scheme": "myapp",
8 | "userInterfaceStyle": "dark",
9 | "splash": {
10 | "image": "./assets/images/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "updates": {
15 | "fallbackToCacheTimeout": 0
16 | },
17 | "assetBundlePatterns": [
18 | "**/*"
19 | ],
20 | "ios": {
21 | "supportsTablet": true
22 | },
23 | "android": {
24 | "adaptiveIcon": {
25 | "foregroundImage": "./assets/images/adaptive-icon.png",
26 | "backgroundColor": "#FFFFFF"
27 | }
28 | },
29 | "web": {
30 | "favicon": "./assets/images/favicon.png"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/amplify/backend/backend-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "auth": {
3 | "netflixd3938a31": {
4 | "service": "Cognito",
5 | "providerPlugin": "awscloudformation",
6 | "dependsOn": [],
7 | "customAuth": false
8 | }
9 | },
10 | "api": {
11 | "netflix": {
12 | "service": "AppSync",
13 | "providerPlugin": "awscloudformation",
14 | "output": {
15 | "authConfig": {
16 | "defaultAuthentication": {
17 | "authenticationType": "AMAZON_COGNITO_USER_POOLS",
18 | "userPoolConfig": {
19 | "userPoolId": "authnetflixd3938a31_userpool_d3938a31"
20 | }
21 | },
22 | "additionalAuthenticationProviders": [
23 | {
24 | "authenticationType": "AWS_IAM"
25 | }
26 | ]
27 | }
28 | }
29 | }
30 | },
31 | "storage": {
32 | "netflixmedia": {
33 | "service": "S3",
34 | "providerPlugin": "awscloudformation"
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import { StatusBar } from 'expo-status-bar';
2 | import React from 'react';
3 | import { SafeAreaProvider } from 'react-native-safe-area-context';
4 | import Amplify from 'aws-amplify';
5 | import { withAuthenticator } from 'aws-amplify-react-native';
6 |
7 | import useCachedResources from './hooks/useCachedResources';
8 | import useColorScheme from './hooks/useColorScheme';
9 | import Navigation from './navigation';
10 | import config from './aws-exports';
11 |
12 | Amplify.configure(config);
13 |
14 | function App() {
15 | const isLoadingComplete = useCachedResources();
16 | const colorScheme = useColorScheme();
17 |
18 | if (!isLoadingComplete) {
19 | return null;
20 | } else {
21 | return (
22 |
23 |
24 |
25 |
26 | );
27 | }
28 | }
29 |
30 | export default withAuthenticator(App);
31 |
--------------------------------------------------------------------------------
/amplify/team-provider-info.json:
--------------------------------------------------------------------------------
1 | {
2 | "dev": {
3 | "awscloudformation": {
4 | "AuthRoleName": "amplify-netflix-dev-151224-authRole",
5 | "UnauthRoleArn": "arn:aws:iam::704219588443:role/amplify-netflix-dev-151224-unauthRole",
6 | "AuthRoleArn": "arn:aws:iam::704219588443:role/amplify-netflix-dev-151224-authRole",
7 | "Region": "eu-west-1",
8 | "DeploymentBucketName": "amplify-netflix-dev-151224-deployment",
9 | "UnauthRoleName": "amplify-netflix-dev-151224-unauthRole",
10 | "StackName": "amplify-netflix-dev-151224",
11 | "StackId": "arn:aws:cloudformation:eu-west-1:704219588443:stack/amplify-netflix-dev-151224/850eb300-88c5-11eb-ae05-06fe7da2d029",
12 | "AmplifyAppId": "d29vqtvfcgauub"
13 | },
14 | "categories": {
15 | "auth": {
16 | "netflixd3938a31": {}
17 | }
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/screens/HomeScreen/HomeScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Image, FlatList } from 'react-native';
3 | import { DataStore } from 'aws-amplify';
4 |
5 | import { Text, View } from '../../components/Themed';
6 |
7 | import styles from './styles';
8 | import HomeCategory from '../../components/HomeCategory';
9 | import { Category } from '../../src/models'
10 |
11 | const HomeScreen = () => {
12 | const [categories, setCategories] = useState([]);
13 |
14 | useEffect(() => {
15 | const fetchCategories = async () => {
16 | setCategories(await DataStore.query(Category));
17 |
18 | };
19 | fetchCategories();
20 | }, []);
21 |
22 | return (
23 |
24 | {/* List of categories */}
25 | }
28 | />
29 |
30 | );
31 | }
32 |
33 | export default HomeScreen;
34 |
--------------------------------------------------------------------------------
/components/MovieItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState} from 'react'
2 | import { Pressable, Image } from 'react-native'
3 | import { useNavigation } from '@react-navigation/native';
4 | import { Storage } from 'aws-amplify';
5 |
6 | import { Movie } from '../../src/models';
7 | import styles from './styles';
8 |
9 | const MovieItem = ({ movie }: {movie: Movie}) => {
10 | const navigation = useNavigation();
11 | const [imageUrl, setImageUrl] = useState('');
12 |
13 | const onMoviePress = () => {
14 | navigation.navigate('MovieDetailsScreen', { id: movie.id })
15 | }
16 |
17 | useEffect(() => {
18 | if (movie.poster.startsWith('http')) {
19 | setImageUrl(movie.poster);
20 | return;
21 | }
22 |
23 | Storage.get(movie.poster).then(setImageUrl)
24 | }, [])
25 |
26 | return (
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | export default MovieItem
34 |
--------------------------------------------------------------------------------
/hooks/useCachedResources.ts:
--------------------------------------------------------------------------------
1 | import { Ionicons } from '@expo/vector-icons';
2 | import * as Font from 'expo-font';
3 | import * as SplashScreen from 'expo-splash-screen';
4 | import * as React from 'react';
5 |
6 | export default function useCachedResources() {
7 | const [isLoadingComplete, setLoadingComplete] = React.useState(false);
8 |
9 | // Load any resources or data that we need prior to rendering the app
10 | React.useEffect(() => {
11 | async function loadResourcesAndDataAsync() {
12 | try {
13 | SplashScreen.preventAutoHideAsync();
14 |
15 | // Load fonts
16 | await Font.loadAsync({
17 | ...Ionicons.font,
18 | 'space-mono': require('../assets/fonts/SpaceMono-Regular.ttf'),
19 | });
20 | } catch (e) {
21 | // We might want to provide this error information to an error reporting service
22 | console.warn(e);
23 | } finally {
24 | setLoadingComplete(true);
25 | SplashScreen.hideAsync();
26 | }
27 | }
28 |
29 | loadResourcesAndDataAsync();
30 | }, []);
31 |
32 | return isLoadingComplete;
33 | }
34 |
--------------------------------------------------------------------------------
/amplify/backend/api/netflix/schema.graphql:
--------------------------------------------------------------------------------
1 | type Category @model {
2 | id: ID!
3 | title: String!
4 |
5 | movies: [Movie] @connection(keyName: "byCategory", fields: ["id"])
6 | }
7 |
8 | type Movie @model
9 | @key(name: "byCategory", fields: ["categoryID"]){
10 | id: ID!
11 | title: String!
12 | poster: String!
13 | year: Int
14 | numberOfSeasons: Int
15 |
16 | plot: String
17 | cast: String
18 | creator: String
19 |
20 | categoryID: ID!
21 |
22 | seasons: [Season] @connection(keyName: "byMovie", fields: ["id"])
23 | }
24 |
25 | type Season @model
26 | @key(name: "byMovie", fields: ["movieID"]){
27 | id: ID!
28 | name: String!
29 |
30 | movieID: ID!
31 | movie: Movie @connection(fields: ["movieID"])
32 |
33 | episodes: [Episode] @connection(keyName: "bySeason", fields: ["id"])
34 | }
35 |
36 | type Episode @model
37 | @key(name: "bySeason", fields: ["seasonID"])
38 | {
39 | id: ID!
40 | title: String!
41 | poster: String!
42 | duration: String!
43 | plot: String
44 | video: String!
45 |
46 | seasonID: ID!
47 | season: Season @connection(fields: ["seasonID"])
48 | }
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Vadim Savin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/screens/NotFoundScreen.tsx:
--------------------------------------------------------------------------------
1 | import { StackScreenProps } from '@react-navigation/stack';
2 | import * as React from 'react';
3 | import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
4 |
5 | import { RootStackParamList } from '../types';
6 |
7 | export default function NotFoundScreen({
8 | navigation,
9 | }: StackScreenProps) {
10 | return (
11 |
12 | This screen doesn't exist.
13 | navigation.replace('Root')} style={styles.link}>
14 | Go to home screen!
15 |
16 |
17 | );
18 | }
19 |
20 | const styles = StyleSheet.create({
21 | container: {
22 | flex: 1,
23 | backgroundColor: '#fff',
24 | alignItems: 'center',
25 | justifyContent: 'center',
26 | padding: 20,
27 | },
28 | title: {
29 | fontSize: 20,
30 | fontWeight: 'bold',
31 | },
32 | link: {
33 | marginTop: 15,
34 | paddingVertical: 15,
35 | },
36 | linkText: {
37 | fontSize: 14,
38 | color: '#2e78b7',
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/screens/TabTwoScreen.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Pressable, StyleSheet } from 'react-native';
3 | import { Auth } from 'aws-amplify';
4 |
5 | import EditScreenInfo from '../components/EditScreenInfo';
6 | import { Text, View } from '../components/Themed';
7 |
8 | export default function TabTwoScreen() {
9 | const onLogout = () => {
10 | Auth.signOut();
11 | }
12 |
13 | return (
14 |
15 | Tab Two
16 |
17 |
18 |
19 | Logout
20 |
21 |
22 | );
23 | }
24 |
25 | const styles = StyleSheet.create({
26 | container: {
27 | flex: 1,
28 | alignItems: 'center',
29 | justifyContent: 'center',
30 | },
31 | title: {
32 | fontSize: 20,
33 | fontWeight: 'bold',
34 | },
35 | separator: {
36 | marginVertical: 30,
37 | height: 1,
38 | width: '80%',
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/amplify/backend/storage/netflixmedia/parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "bucketName": "netflix49b040bd779644519ba8d91c478e9c9a",
3 | "authPolicyName": "s3_amplify_36a03f8a",
4 | "unauthPolicyName": "s3_amplify_36a03f8a",
5 | "authRoleName": {
6 | "Ref": "AuthRoleName"
7 | },
8 | "unauthRoleName": {
9 | "Ref": "UnauthRoleName"
10 | },
11 | "selectedGuestPermissions": [
12 | "s3:GetObject",
13 | "s3:ListBucket"
14 | ],
15 | "selectedAuthenticatedPermissions": [
16 | "s3:GetObject",
17 | "s3:ListBucket"
18 | ],
19 | "s3PermissionsAuthenticatedPublic": "s3:GetObject",
20 | "s3PublicPolicy": "Public_policy_b6db8f9f",
21 | "s3PermissionsAuthenticatedUploads": "DISALLOW",
22 | "s3UploadsPolicy": "Uploads_policy_b6db8f9f",
23 | "s3PermissionsAuthenticatedProtected": "s3:GetObject",
24 | "s3ProtectedPolicy": "Protected_policy_b6db8f9f",
25 | "s3PermissionsAuthenticatedPrivate": "s3:GetObject",
26 | "s3PrivatePolicy": "Private_policy_b6db8f9f",
27 | "AuthenticatedAllowList": "ALLOW",
28 | "s3ReadPolicy": "read_policy_b6db8f9f",
29 | "s3PermissionsGuestPublic": "DISALLOW",
30 | "s3PermissionsGuestUploads": "DISALLOW",
31 | "GuestAllowList": "DISALLOW",
32 | "triggerFunction": "NONE"
33 | }
--------------------------------------------------------------------------------
/components/HomeCategory/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Image, FlatList, Pressable } from 'react-native';
3 | import { Text } from '../../components/Themed';
4 | import MovieItem from '../../components/MovieItem';
5 | import { Storage } from 'aws-amplify';
6 |
7 | import styles from './styles';
8 | import { Category, Movie } from '../../src/models';
9 | import { DataStore } from '@aws-amplify/datastore';
10 |
11 | interface HomeCategoryProps {
12 | category: Category,
13 | }
14 |
15 | const HomeCategory = (props: HomeCategoryProps) => {
16 | const { category } = props;
17 |
18 | const [movies, setMovies] = useState([]);
19 |
20 |
21 | useEffect(() => {
22 | const fetchMovies = async () => {
23 | const result = (await DataStore.query(Movie))
24 | .filter((movie) => movie.categoryID === category.id)
25 | setMovies(result);
26 | };
27 |
28 | fetchMovies();
29 | }, [])
30 |
31 | return (
32 | <>
33 | {category.title}
34 | }
37 | horizontal
38 | showsHorizontalScrollIndicator={false}
39 | />
40 | >
41 | );
42 | }
43 |
44 | export default HomeCategory;
45 |
--------------------------------------------------------------------------------
/components/Themed.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Text as DefaultText, View as DefaultView } from 'react-native';
3 |
4 | import Colors from '../constants/Colors';
5 | import useColorScheme from '../hooks/useColorScheme';
6 |
7 | export function useThemeColor(
8 | props: { light?: string; dark?: string },
9 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark
10 | ) {
11 | const theme = useColorScheme();
12 | const colorFromProps = props[theme];
13 |
14 | if (colorFromProps) {
15 | return colorFromProps;
16 | } else {
17 | return Colors[theme][colorName];
18 | }
19 | }
20 |
21 | type ThemeProps = {
22 | lightColor?: string;
23 | darkColor?: string;
24 | };
25 |
26 | export type TextProps = ThemeProps & DefaultText['props'];
27 | export type ViewProps = ThemeProps & DefaultView['props'];
28 |
29 | export function Text(props: TextProps) {
30 | const { style, lightColor, darkColor, ...otherProps } = props;
31 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
32 |
33 | return ;
34 | }
35 |
36 | export function View(props: ViewProps) {
37 | const { style, lightColor, darkColor, ...otherProps } = props;
38 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
39 |
40 | return ;
41 | }
42 |
--------------------------------------------------------------------------------
/navigation/index.tsx:
--------------------------------------------------------------------------------
1 | import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native';
2 | import { createStackNavigator } from '@react-navigation/stack';
3 | import * as React from 'react';
4 | import { ColorSchemeName } from 'react-native';
5 |
6 | import NotFoundScreen from '../screens/NotFoundScreen';
7 | import { RootStackParamList } from '../types';
8 | import BottomTabNavigator from './BottomTabNavigator';
9 | import LinkingConfiguration from './LinkingConfiguration';
10 |
11 | // If you are not familiar with React Navigation, we recommend going through the
12 | // "Fundamentals" guide: https://reactnavigation.org/docs/getting-started
13 | export default function Navigation({ colorScheme }: { colorScheme: ColorSchemeName }) {
14 | return (
15 |
18 |
19 |
20 | );
21 | }
22 |
23 | // A root stack navigator is often used for displaying modals on top of all other content
24 | // Read more here: https://reactnavigation.org/docs/modal
25 | const Stack = createStackNavigator();
26 |
27 | function RootNavigator() {
28 | return (
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/screens/MovieDetailsScreen/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | const styles = StyleSheet.create({
4 | image: {
5 | width: '100%',
6 | aspectRatio: 16/9,
7 | resizeMode: 'cover',
8 | },
9 | title: {
10 | fontSize: 24,
11 | fontWeight: 'bold'
12 | },
13 | match: {
14 | color: '#59d467',
15 | fontWeight: 'bold',
16 | marginRight: 5,
17 | },
18 | year: {
19 | color: '#757575',
20 | marginRight: 5,
21 | },
22 | ageContainer: {
23 | backgroundColor: '#e6e229',
24 | justifyContent: 'center',
25 | alignItems: 'center',
26 | borderRadius: 2,
27 | paddingHorizontal: 3,
28 | marginRight: 5,
29 | },
30 | age: {
31 | color: 'black',
32 | fontWeight: 'bold'
33 | },
34 |
35 | // Button
36 | playButton: {
37 | backgroundColor: 'white',
38 | justifyContent: 'center',
39 | alignItems: 'center',
40 | padding: 5,
41 | borderRadius: 3,
42 | marginVertical: 5,
43 | },
44 | playButtonText: {
45 | color: 'black',
46 | fontSize: 16,
47 | fontWeight: 'bold'
48 | },
49 | downloadButton: {
50 | backgroundColor: '#2b2b2b',
51 | justifyContent: 'center',
52 | alignItems: 'center',
53 | padding: 5,
54 | borderRadius: 3,
55 | marginVertical: 5,
56 | },
57 | downloadButtonText: {
58 | color: 'white',
59 | fontSize: 16,
60 | fontWeight: 'bold'
61 | }
62 | })
63 |
64 | export default styles;
65 |
--------------------------------------------------------------------------------
/components/EpisodeItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Image, Pressable } from 'react-native';
3 | import { AntDesign } from '@expo/vector-icons';
4 | import { Text, View } from '../../components/Themed';
5 | import styles from './styles';
6 | import { Episode } from '../../types';
7 |
8 | // {
9 | // id: 'episode1',
10 | // title: '1. Pilot Part 1 & 2',
11 | // poster: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/netflix/ep0.jpg',
12 | // duration: '1h 21m',
13 | // plot: 'When Harvey\'s promotion requires him to recruit and hire a graduate of Harvard Law, he chooses Mike Ross. But Mike doesn\'t actualy have a law degree',
14 | // video: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
15 | // }
16 |
17 | interface EpisodeItemProps {
18 | episode: Episode;
19 | onPress: (eppisode: Episode) => {}
20 | }
21 |
22 | const EpisodeItem = (props: EpisodeItemProps) => {
23 | const { episode, onPress } = props;
24 |
25 | return (
26 | onPress(episode)}>
27 |
28 |
29 |
30 |
31 | {episode.title}
32 | {episode.duration}
33 |
34 |
35 |
36 |
37 |
38 | {episode.plot}
39 |
40 | )
41 | };
42 |
43 | export default EpisodeItem;
44 |
45 |
--------------------------------------------------------------------------------
/src/models/index.d.ts:
--------------------------------------------------------------------------------
1 | import { ModelInit, MutableModel, PersistentModelConstructor } from "@aws-amplify/datastore";
2 |
3 |
4 |
5 |
6 |
7 | export declare class Category {
8 | readonly id: string;
9 | readonly title: string;
10 | readonly movies?: Movie[];
11 | constructor(init: ModelInit);
12 | static copyOf(source: Category, mutator: (draft: MutableModel) => MutableModel | void): Category;
13 | }
14 |
15 | export declare class Movie {
16 | readonly id: string;
17 | readonly title: string;
18 | readonly poster: string;
19 | readonly year?: number;
20 | readonly numberOfSeasons?: number;
21 | readonly plot?: string;
22 | readonly cast?: string;
23 | readonly creator?: string;
24 | readonly categoryID: string;
25 | readonly seasons?: Season[];
26 | constructor(init: ModelInit);
27 | static copyOf(source: Movie, mutator: (draft: MutableModel) => MutableModel | void): Movie;
28 | }
29 |
30 | export declare class Season {
31 | readonly id: string;
32 | readonly name: string;
33 | readonly movie?: Movie;
34 | readonly episodes?: Episode[];
35 | constructor(init: ModelInit);
36 | static copyOf(source: Season, mutator: (draft: MutableModel) => MutableModel | void): Season;
37 | }
38 |
39 | export declare class Episode {
40 | readonly id: string;
41 | readonly title: string;
42 | readonly poster: string;
43 | readonly duration: string;
44 | readonly plot?: string;
45 | readonly video: string;
46 | readonly season?: Season;
47 | constructor(init: ModelInit);
48 | static copyOf(source: Episode, mutator: (draft: MutableModel) => MutableModel | void): Episode;
49 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "node_modules/expo/AppEntry.js",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "web": "expo start --web",
8 | "eject": "expo eject",
9 | "test": "jest --watchAll"
10 | },
11 | "jest": {
12 | "preset": "jest-expo"
13 | },
14 | "dependencies": {
15 | "@expo/vector-icons": "^12.0.0",
16 | "@react-native-community/masked-view": "0.1.10",
17 | "@react-native-community/netinfo": "^6.0.0",
18 | "@react-native-picker/picker": "1.9.2",
19 | "@react-navigation/bottom-tabs": "5.11.2",
20 | "@react-navigation/native": "~5.8.10",
21 | "@react-navigation/stack": "~5.12.8",
22 | "amazon-cognito-identity-js": "^4.6.0",
23 | "aws-amplify": "^3.3.25",
24 | "aws-amplify-react-native": "^4.3.2",
25 | "expo": "~40.0.0",
26 | "expo-asset": "~8.2.1",
27 | "expo-av": "~8.7.0",
28 | "expo-constants": "~9.3.0",
29 | "expo-font": "~8.4.0",
30 | "expo-linking": "~2.0.0",
31 | "expo-splash-screen": "~0.8.0",
32 | "expo-status-bar": "~1.0.3",
33 | "expo-web-browser": "~8.6.0",
34 | "react": "16.13.1",
35 | "react-dom": "16.13.1",
36 | "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
37 | "react-native-gesture-handler": "~1.8.0",
38 | "react-native-safe-area-context": "3.1.9",
39 | "react-native-screens": "~2.15.0",
40 | "react-native-web": "~0.13.12"
41 | },
42 | "devDependencies": {
43 | "@babel/core": "~7.9.0",
44 | "@types/react": "~16.9.35",
45 | "@types/react-native": "~0.63.2",
46 | "jest-expo": "~40.0.0",
47 | "typescript": "~4.0.0"
48 | },
49 | "private": true
50 | }
51 |
--------------------------------------------------------------------------------
/amplify/backend/auth/netflixd3938a31/parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "identityPoolName": "netflixd3938a31_identitypool_d3938a31",
3 | "allowUnauthenticatedIdentities": false,
4 | "resourceNameTruncated": "netflid3938a31",
5 | "userPoolName": "netflixd3938a31_userpool_d3938a31",
6 | "autoVerifiedAttributes": [
7 | "email"
8 | ],
9 | "mfaConfiguration": "OFF",
10 | "mfaTypes": [
11 | "SMS Text Message"
12 | ],
13 | "smsAuthenticationMessage": "Your authentication code is {####}",
14 | "smsVerificationMessage": "Your verification code is {####}",
15 | "emailVerificationSubject": "Your verification code",
16 | "emailVerificationMessage": "Your verification code is {####}",
17 | "defaultPasswordPolicy": false,
18 | "passwordPolicyMinLength": 8,
19 | "passwordPolicyCharacters": [],
20 | "requiredAttributes": [
21 | "email"
22 | ],
23 | "userpoolClientGenerateSecret": true,
24 | "userpoolClientRefreshTokenValidity": 30,
25 | "userpoolClientWriteAttributes": [
26 | "email"
27 | ],
28 | "userpoolClientReadAttributes": [
29 | "email"
30 | ],
31 | "userpoolClientLambdaRole": "netflid3938a31_userpoolclient_lambda_role",
32 | "userpoolClientSetAttributes": false,
33 | "sharedId": "d3938a31",
34 | "resourceName": "netflixd3938a31",
35 | "authSelections": "identityPoolAndUserPool",
36 | "authRoleArn": {
37 | "Fn::GetAtt": [
38 | "AuthRole",
39 | "Arn"
40 | ]
41 | },
42 | "unauthRoleArn": {
43 | "Fn::GetAtt": [
44 | "UnauthRole",
45 | "Arn"
46 | ]
47 | },
48 | "useDefault": "default",
49 | "userPoolGroupList": [],
50 | "dependsOn": []
51 | }
--------------------------------------------------------------------------------
/amplify/backend/api/netflix/stacks/CustomResources.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSTemplateFormatVersion": "2010-09-09",
3 | "Description": "An auto-generated nested stack.",
4 | "Metadata": {},
5 | "Parameters": {
6 | "AppSyncApiId": {
7 | "Type": "String",
8 | "Description": "The id of the AppSync API associated with this project."
9 | },
10 | "AppSyncApiName": {
11 | "Type": "String",
12 | "Description": "The name of the AppSync API",
13 | "Default": "AppSyncSimpleTransform"
14 | },
15 | "env": {
16 | "Type": "String",
17 | "Description": "The environment name. e.g. Dev, Test, or Production",
18 | "Default": "NONE"
19 | },
20 | "S3DeploymentBucket": {
21 | "Type": "String",
22 | "Description": "The S3 bucket containing all deployment assets for the project."
23 | },
24 | "S3DeploymentRootKey": {
25 | "Type": "String",
26 | "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory."
27 | }
28 | },
29 | "Resources": {
30 | "EmptyResource": {
31 | "Type": "Custom::EmptyResource",
32 | "Condition": "AlwaysFalse"
33 | }
34 | },
35 | "Conditions": {
36 | "HasEnvironmentParameter": {
37 | "Fn::Not": [
38 | {
39 | "Fn::Equals": [
40 | {
41 | "Ref": "env"
42 | },
43 | "NONE"
44 | ]
45 | }
46 | ]
47 | },
48 | "AlwaysFalse": {
49 | "Fn::Equals": ["true", "false"]
50 | }
51 | },
52 | "Outputs": {
53 | "EmptyOutput": {
54 | "Description": "An empty output. You may delete this if you have at least one resource above.",
55 | "Value": ""
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/components/VideoPlayer/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useEffect } from 'react'
2 | import { View, Text } from 'react-native'
3 | import { Video } from 'expo-av';
4 | import { Storage } from 'aws-amplify';
5 | import { Episode } from '../../types';
6 | import styles from './styles';
7 | import { Playback } from 'expo-av/build/AV';
8 |
9 | interface VideoPlayerProps {
10 | episode: Episode;
11 | }
12 |
13 | const VideoPlayer = (props: VideoPlayerProps) => {
14 | const { episode } = props;
15 | const [videoURL, setVideoURL] = useState('');
16 |
17 | const [status, setStatus] = useState({});
18 | const video = useRef(null);
19 |
20 | useEffect(() => {
21 | if (episode.video.startsWith('http')) {
22 | setVideoURL(episode.video);
23 | return;
24 | }
25 | Storage.get(episode.video).then(setVideoURL);
26 | }, [episode])
27 |
28 | useEffect(() => {
29 | if (!video) {
30 | return;
31 | }
32 | (async () => {
33 | await video?.current?.unloadAsync();
34 | await video?.current?.loadAsync(
35 | { uri: videoURL },
36 | {},
37 | false
38 | );
39 | })();
40 | }, [videoURL])
41 |
42 | console.log(videoURL);
43 |
44 | if (videoURL === '') {
45 | return null;
46 | }
47 |
48 | return (
49 |