Created
April 16, 2023 13:04
-
-
Save haneulee/d3963533668e7d862dd20a1fec51ca27 to your computer and use it in GitHub Desktop.
netflix-flutter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:convert'; | |
import 'dart:io'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter_rating_bar/flutter_rating_bar.dart'; | |
import 'package:http/http.dart' as http; | |
import 'package:shared_preferences/shared_preferences.dart'; | |
import 'package:url_launcher/url_launcher_string.dart'; | |
class MyHttpOverrides extends HttpOverrides { | |
@override | |
HttpClient createHttpClient(SecurityContext? context) { | |
return super.createHttpClient(context) | |
..userAgent = | |
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'; | |
} | |
} | |
void main() { | |
HttpOverrides.global = MyHttpOverrides(); | |
runApp(const App()); | |
} | |
class App extends StatelessWidget { | |
const App({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData( | |
colorScheme: const ColorScheme.light( | |
background: Color.fromARGB(255, 26, 24, 24)), | |
), | |
home: HomeScreen(), | |
); | |
} | |
} | |
class MovieModel { | |
final String title, id, thumb; | |
MovieModel.fromJson(Map<String, dynamic> json) | |
: title = json['title'], | |
id = json['id'].toString(), | |
thumb = json['poster_path']; | |
} | |
class MovieDetailModel { | |
final String title, thumb, homepage, overview, date; | |
final List<dynamic> genres; | |
final int id, runtime; | |
final double vote; | |
final bool adult; | |
MovieDetailModel.fromJson(Map<String, dynamic> json) | |
: id = json['id'], | |
title = json['title'], | |
thumb = json['poster_path'], | |
homepage = json['homepage'], | |
overview = json['overview'], | |
date = json['release_date'], | |
vote = json['vote_average'], | |
adult = json['adult'], | |
genres = json['genres'], | |
runtime = json['runtime']; | |
} | |
class ApiService { | |
static const String baseUrl = "https://movies-api.nomadcoders.workers.dev"; | |
static const String popular = "popular"; | |
static const String nowPlaying = "now-playing"; | |
static const String comingSoon = "coming-soon"; | |
static Future<List<MovieModel>> getPopularMovie() async { | |
List<MovieModel> movieInstances = []; | |
final url = Uri.parse('$baseUrl/$popular'); | |
final response = await http.get(url); | |
if (response.statusCode == 200) { | |
final Map<String, dynamic> decoded = jsonDecode(response.body); | |
final List<dynamic> movies = decoded['results']; | |
for (var movie in movies) { | |
final instance = MovieModel.fromJson(movie); | |
movieInstances.add(instance); | |
} | |
return movieInstances; | |
} | |
throw Error(); | |
} | |
static Future<List<MovieModel>> getNowPlayingMovie() async { | |
List<MovieModel> movieInstances = []; | |
final url = Uri.parse('$baseUrl/$nowPlaying'); | |
final response = await http.get(url); | |
if (response.statusCode == 200) { | |
final Map<String, dynamic> decoded = jsonDecode(response.body); | |
final List<dynamic> movies = decoded['results']; | |
for (var movie in movies) { | |
final instance = MovieModel.fromJson(movie); | |
movieInstances.add(instance); | |
} | |
return movieInstances; | |
} | |
throw Error(); | |
} | |
static Future<List<MovieModel>> getComingSoonMovie() async { | |
List<MovieModel> movieInstances = []; | |
final url = Uri.parse('$baseUrl/$comingSoon'); | |
final response = await http.get(url); | |
if (response.statusCode == 200) { | |
final Map<String, dynamic> decoded = jsonDecode(response.body); | |
final List<dynamic> movies = decoded['results']; | |
for (var movie in movies) { | |
final instance = MovieModel.fromJson(movie); | |
movieInstances.add(instance); | |
} | |
return movieInstances; | |
} | |
throw Error(); | |
} | |
static Future<MovieDetailModel> getDetailMovie(String id) async { | |
final url = Uri.parse("$baseUrl/movie?id=$id"); | |
final response = await http.get(url); | |
if (response.statusCode == 200) { | |
final movie = jsonDecode(response.body); | |
return MovieDetailModel.fromJson(movie); | |
} | |
throw Error(); | |
} | |
} | |
class HomeScreen extends StatelessWidget { | |
HomeScreen({super.key}); | |
final Future<List<MovieModel>> popularMovies = ApiService.getPopularMovie(); | |
final Future<List<MovieModel>> nowPlayingMovies = | |
ApiService.getNowPlayingMovie(); | |
final Future<List<MovieModel>> comingSoonMovies = | |
ApiService.getComingSoonMovie(); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
backgroundColor: Theme.of(context).colorScheme.background, | |
appBar: AppBar( | |
elevation: 10, | |
backgroundColor: Theme.of(context).colorScheme.background, | |
foregroundColor: Colors.red, | |
title: const Text( | |
"NETFLIX", | |
style: TextStyle( | |
fontSize: 28, fontWeight: FontWeight.w700, letterSpacing: 0.1), | |
), | |
), | |
body: FutureBuilder( | |
future: | |
Future.wait([popularMovies, nowPlayingMovies, comingSoonMovies]), | |
builder: ( | |
context, | |
AsyncSnapshot<List<dynamic>> snapshot, | |
) { | |
if (snapshot.hasData) { | |
final popularMovies = snapshot.data![0]; | |
final nowPlayingMovies = snapshot.data![0]; | |
final comingSoonMovies = snapshot.data![0]; | |
if (popularMovies.isEmpty || | |
nowPlayingMovies.isEmpty || | |
comingSoonMovies.isEmpty) { | |
return const Text('loading...'); | |
} | |
return SingleChildScrollView( | |
scrollDirection: Axis.vertical, | |
child: Column( | |
children: [ | |
const SizedBox( | |
height: 40, | |
), | |
Container( | |
alignment: Alignment.centerLeft, | |
padding: const EdgeInsets.only(left: 20, bottom: 10), | |
child: const Text( | |
'Popular Movies', | |
style: TextStyle( | |
fontSize: 22, | |
fontWeight: FontWeight.w600, | |
color: Colors.white), | |
)), | |
SizedBox( | |
height: 280, | |
child: makeList(popularMovies, 'popular'), | |
), | |
Container( | |
alignment: Alignment.centerLeft, | |
padding: const EdgeInsets.only(left: 20, bottom: 10), | |
child: const Text( | |
'Now in Cinemas', | |
style: TextStyle( | |
fontSize: 22, | |
fontWeight: FontWeight.w600, | |
color: Colors.white), | |
)), | |
SizedBox( | |
height: 240, | |
child: makeList(nowPlayingMovies, 'nowPlaying'), | |
), | |
Container( | |
alignment: Alignment.centerLeft, | |
padding: const EdgeInsets.only(left: 20, bottom: 10), | |
child: const Text( | |
'Coming soon', | |
style: TextStyle( | |
fontSize: 22, | |
fontWeight: FontWeight.w600, | |
color: Colors.white), | |
)), | |
SizedBox( | |
height: 260, | |
child: makeList(comingSoonMovies, 'comingSoon'), | |
), | |
], | |
), | |
); | |
} | |
return const Center( | |
child: CircularProgressIndicator(), | |
); | |
}, | |
), | |
); | |
} | |
ListView makeList(List<MovieModel> movies, String type) { | |
return ListView.separated( | |
scrollDirection: Axis.horizontal, | |
itemCount: movies.length, | |
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), | |
itemBuilder: (context, index) { | |
var webtoon = movies[index]; | |
return Movie( | |
title: webtoon.title, | |
thumb: webtoon.thumb, | |
id: webtoon.id, | |
type: type, | |
); | |
}, | |
separatorBuilder: (context, index) => const SizedBox(width: 20), | |
); | |
} | |
} | |
class DetailScreen extends StatefulWidget { | |
final String title, thumb, id; | |
const DetailScreen({ | |
super.key, | |
required this.title, | |
required this.thumb, | |
required this.id, | |
}); | |
@override | |
State<DetailScreen> createState() => _DetailScreenState(); | |
} | |
class _DetailScreenState extends State<DetailScreen> { | |
late Future<MovieDetailModel> movie; | |
late SharedPreferences prefs; | |
bool isLiked = false; | |
Future initPrefs() async { | |
prefs = await SharedPreferences.getInstance(); | |
final likedMovies = prefs.getStringList('likedMovies'); | |
if (likedMovies != null) { | |
if (likedMovies.contains(widget.id) == true) { | |
setState(() { | |
isLiked = true; | |
}); | |
} | |
} else { | |
await prefs.setStringList('likedMovies', []); | |
} | |
} | |
@override | |
void initState() { | |
super.initState(); | |
movie = ApiService.getDetailMovie(widget.id); | |
initPrefs(); | |
} | |
onHeartTap() async { | |
final likedMovies = prefs.getStringList('likedMovies'); | |
if (likedMovies != null) { | |
if (isLiked) { | |
likedMovies.remove(widget.id); | |
} else { | |
likedMovies.add(widget.id); | |
} | |
await prefs.setStringList('likedMovies', likedMovies); | |
setState(() { | |
isLiked = !isLiked; | |
}); | |
} | |
} | |
String format(int minute, String type) { | |
var duration = Duration(minutes: minute); | |
var result = | |
duration.toString().split(".").first.substring(0, 4).split(":"); | |
return type == 'hour' ? result[0] : result[1]; | |
} | |
String genreFormat(List<dynamic> genres) { | |
var text = genres.map((item) => item['name']).join(', '); | |
return text; | |
} | |
onClickBuyTicket(String url) async { | |
await launchUrlString(url); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
backgroundColor: Theme.of(context).colorScheme.background, | |
appBar: AppBar( | |
elevation: 10, | |
backgroundColor: Theme.of(context).colorScheme.background, | |
foregroundColor: Colors.red, | |
actions: [ | |
IconButton( | |
onPressed: onHeartTap, | |
icon: Icon( | |
isLiked ? Icons.favorite : Icons.favorite_outline, | |
), | |
) | |
], | |
title: Text( | |
widget.title, | |
style: const TextStyle( | |
fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 0.1), | |
), | |
), | |
body: Container( | |
constraints: const BoxConstraints.expand(), | |
decoration: BoxDecoration( | |
image: DecorationImage( | |
image: NetworkImage( | |
"https://image.tmdb.org/t/p/w500/${widget.thumb}", | |
), | |
fit: BoxFit.cover, | |
colorFilter: ColorFilter.mode( | |
Colors.black.withOpacity(0.5), BlendMode.dstATop), | |
), | |
), | |
child: Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 30), | |
child: FutureBuilder( | |
future: movie, | |
builder: (context, snapshot) { | |
if (snapshot.hasData) { | |
return Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
mainAxisAlignment: MainAxisAlignment.end, | |
children: [ | |
Text( | |
snapshot.data!.title, | |
style: const TextStyle( | |
fontSize: 30, | |
color: Colors.white, | |
fontWeight: FontWeight.w700), | |
), | |
const SizedBox( | |
height: 15, | |
), | |
Row( | |
children: [ | |
Icon( | |
snapshot.data!.adult | |
? Icons.no_adult_content | |
: Icons.family_restroom, | |
color: | |
snapshot.data!.adult ? Colors.red : Colors.green, | |
size: 20, | |
), | |
const SizedBox(width: 5), | |
const Text( | |
'|', | |
style: TextStyle(fontSize: 16, color: Colors.white), | |
), | |
const SizedBox(width: 5), | |
RatingBar.builder( | |
initialRating: snapshot.data!.vote / 2, | |
minRating: 1, | |
direction: Axis.horizontal, | |
allowHalfRating: true, | |
itemSize: 20, | |
itemCount: 5, | |
itemBuilder: (context, _) => const Icon( | |
Icons.star, | |
color: Colors.yellow, | |
), | |
onRatingUpdate: (rating) { | |
print(rating); | |
}, | |
), | |
const SizedBox(width: 5), | |
const Text( | |
'|', | |
style: TextStyle(fontSize: 16, color: Colors.white), | |
), | |
const SizedBox(width: 5), | |
Text( | |
format(snapshot.data!.runtime, 'hour'), | |
style: const TextStyle( | |
fontSize: 16, color: Colors.white), | |
), | |
const Text( | |
'h ', | |
style: TextStyle(fontSize: 16, color: Colors.white), | |
), | |
Text( | |
format(snapshot.data!.runtime, 'min'), | |
style: const TextStyle( | |
fontSize: 16, color: Colors.white), | |
), | |
const Text( | |
'min', | |
style: TextStyle(fontSize: 16, color: Colors.white), | |
), | |
], | |
), | |
SizedBox( | |
height: 20, | |
child: ListView( | |
scrollDirection: Axis.horizontal, | |
shrinkWrap: true, | |
children: [ | |
Text( | |
genreFormat(snapshot.data!.genres), | |
style: const TextStyle( | |
fontSize: 16, color: Colors.white), | |
), | |
], | |
), | |
), | |
const SizedBox( | |
height: 15, | |
), | |
const Text( | |
'Storyline', | |
style: TextStyle( | |
fontSize: 30, | |
color: Colors.white, | |
fontWeight: FontWeight.w600), | |
), | |
const SizedBox( | |
height: 5, | |
), | |
Text( | |
snapshot.data!.overview, | |
style: const TextStyle(fontSize: 16, color: Colors.white), | |
), | |
Container( | |
margin: const EdgeInsets.symmetric( | |
vertical: 50, horizontal: 30), | |
child: TextButton( | |
style: TextButton.styleFrom( | |
padding: const EdgeInsets.symmetric( | |
horizontal: 80, vertical: 20), | |
foregroundColor: | |
const Color.fromARGB(255, 43, 36, 36), | |
backgroundColor: Colors.yellow, | |
), | |
onPressed: () => | |
onClickBuyTicket(snapshot.data!.homepage), | |
child: const Text( | |
'Buy ticket', | |
style: TextStyle( | |
fontSize: 20, | |
color: Colors.black, | |
fontWeight: FontWeight.w600), | |
), | |
), | |
) | |
], | |
); | |
} | |
return const Text("..."); | |
}, | |
), | |
), | |
), | |
); | |
} | |
} | |
class Movie extends StatelessWidget { | |
final String title, thumb, id, type; | |
const Movie({ | |
super.key, | |
required this.title, | |
required this.thumb, | |
required this.id, | |
required this.type, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return GestureDetector( | |
onTap: () { | |
Navigator.push( | |
context, | |
MaterialPageRoute( | |
builder: (context) => DetailScreen( | |
title: title, | |
thumb: thumb, | |
id: id, | |
), | |
fullscreenDialog: true, | |
), | |
); | |
}, | |
child: Column( | |
children: [ | |
ClipRect( | |
child: Hero( | |
tag: '$type-$id', | |
child: Container( | |
clipBehavior: Clip.hardEdge, | |
decoration: BoxDecoration( | |
borderRadius: BorderRadius.circular(10), | |
), | |
child: Align( | |
alignment: Alignment.center, | |
widthFactor: type == 'popular' ? 0.6 : 0.3, | |
heightFactor: type == 'popular' ? 0.3 : 0.2, | |
child: | |
Image.network('https://image.tmdb.org/t/p/w500/$thumb'), | |
), | |
), | |
), | |
), | |
Container( | |
width: 140, | |
padding: const EdgeInsets.only(top: 10), | |
child: Text( | |
type == 'popular' ? '' : title, | |
style: const TextStyle( | |
color: Colors.white, | |
fontSize: 15, | |
fontWeight: FontWeight.w600, | |
height: 1.3), | |
), | |
), | |
], | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment