Skip to content

Commit 55742e7

Browse files
committed
Refactor HomePage UI, add SearchPage, and update DetailsPage
1 parent 331d51d commit 55742e7

File tree

4 files changed

+169
-42
lines changed

4 files changed

+169
-42
lines changed

lib/app/cubit/home_cubit.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:tmdb_flutter/app/cubit/home_state.dart';
55
class HomeCubit extends Cubit<HomeState> {
66
HomeCubit(this._repository) : super(HomeInitial());
77
final MoviesRepository _repository;
8+
MoviesRepository get repository => _repository;
89

910
Future<void> loadHomeData() async {
1011
try {

lib/app/view/details_page.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,7 @@ class _DetailsPageState extends State<DetailsPage> {
137137
if (movieDetails != null) ...[
138138
const SizedBox(height: 4),
139139
Text(
140-
movieDetails.genres
141-
.map((g) => g.name)
142-
.join(', '),
140+
movieDetails.genres.map((g) => g.name).join(', '),
143141
style: const TextStyle(
144142
color: Colors.white,
145143
fontSize: 16,

lib/app/view/home_page.dart

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_bloc/flutter_bloc.dart';
3-
import 'package:tmdb_flutter/app/data/remote/models/movie_responses.dart';
43
import 'package:tmdb_flutter/app/cubit/favorite_movies_cubit.dart';
54
import 'package:tmdb_flutter/app/cubit/home_cubit.dart';
65
import 'package:tmdb_flutter/app/cubit/home_state.dart';
6+
import 'package:tmdb_flutter/app/data/remote/models/movie_responses.dart';
77
import 'package:tmdb_flutter/app/view/details_page.dart';
8+
import 'package:tmdb_flutter/app/view/search_page.dart';
89

910
class HomePage extends StatelessWidget {
1011
const HomePage({super.key});
@@ -88,6 +89,18 @@ class HomePage extends StatelessWidget {
8889
contentPadding: EdgeInsets.zero,
8990
),
9091
style: const TextStyle(color: Colors.black),
92+
readOnly: true,
93+
onTap: () {
94+
final repository =
95+
context.read<HomeCubit>().repository;
96+
Navigator.push(
97+
context,
98+
MaterialPageRoute<void>(
99+
builder: (_) =>
100+
SearchPage(repository: repository),
101+
),
102+
);
103+
},
91104
),
92105
const SizedBox(height: 16),
93106
SizedBox(
@@ -200,7 +213,7 @@ class _CategoryChip extends StatelessWidget {
200213
@override
201214
Widget build(BuildContext context) {
202215
return Padding(
203-
padding: const EdgeInsets.only(right: 8.0),
216+
padding: const EdgeInsets.only(right: 8),
204217
child: ChoiceChip(
205218
label: Text(label),
206219
selected: selected,
@@ -286,8 +299,7 @@ class _MovieCardState extends State<_MovieCard> {
286299
crossAxisAlignment: CrossAxisAlignment.start,
287300
children: [
288301
ClipRRect(
289-
borderRadius:
290-
const BorderRadius.vertical(top: Radius.circular(22)),
302+
borderRadius: BorderRadius.circular(22),
291303
child: Image.network(
292304
'https://image.tmdb.org/t/p/w500${widget.movie.posterPath}',
293305
height: widget.small ? 180 : 320,
@@ -303,41 +315,6 @@ class _MovieCardState extends State<_MovieCard> {
303315
},
304316
),
305317
),
306-
if (!widget.small) ...[
307-
Padding(
308-
padding: const EdgeInsets.all(8.0),
309-
child: Column(
310-
crossAxisAlignment: CrossAxisAlignment.start,
311-
children: [
312-
Text(
313-
widget.movie.title,
314-
style: const TextStyle(
315-
color: Colors.black,
316-
fontSize: 16,
317-
fontWeight: FontWeight.bold,
318-
),
319-
maxLines: 2,
320-
overflow: TextOverflow.ellipsis,
321-
),
322-
const SizedBox(height: 4),
323-
Row(
324-
children: [
325-
const Icon(Icons.star,
326-
size: 16, color: Colors.amber),
327-
const SizedBox(width: 4),
328-
Text(
329-
widget.movie.voteAverage.toStringAsFixed(1),
330-
style: const TextStyle(
331-
color: Colors.black87,
332-
fontSize: 14,
333-
),
334-
),
335-
],
336-
),
337-
],
338-
),
339-
),
340-
],
341318
],
342319
),
343320
Positioned(

lib/app/view/search_page.dart

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:tmdb_flutter/app/data/remote/models/movie_responses.dart';
4+
import 'package:tmdb_flutter/app/data/repository/movies_repository.dart';
5+
import 'package:tmdb_flutter/app/view/details_page.dart';
6+
7+
class SearchCubit extends Cubit<List<Movie>> {
8+
SearchCubit(this.repository) : super([]);
9+
final MoviesRepository repository;
10+
11+
Future<void> search(String query) async {
12+
if (query.isEmpty) {
13+
emit([]);
14+
return;
15+
}
16+
final response = await repository.searchMovie(query: query);
17+
emit(response.results);
18+
}
19+
}
20+
21+
class SearchPage extends StatelessWidget {
22+
const SearchPage({required this.repository, super.key});
23+
24+
final MoviesRepository repository;
25+
26+
@override
27+
Widget build(BuildContext context) {
28+
return BlocProvider(
29+
create: (_) => SearchCubit(repository),
30+
child: Scaffold(
31+
backgroundColor: const Color(0xFFF9F3FF),
32+
appBar: AppBar(
33+
backgroundColor: Colors.transparent,
34+
elevation: 0,
35+
title: _SearchBar(),
36+
),
37+
body: BlocBuilder<SearchCubit, List<Movie>>(
38+
builder: (context, movies) {
39+
if (movies.isEmpty) {
40+
return const Center(child: Text('No results'));
41+
}
42+
return ListView.separated(
43+
padding: const EdgeInsets.all(16),
44+
itemCount: movies.length,
45+
separatorBuilder: (_, __) => const SizedBox(height: 16),
46+
itemBuilder: (context, index) {
47+
final movie = movies[index];
48+
return GestureDetector(
49+
onTap: () {
50+
Navigator.push(
51+
context,
52+
MaterialPageRoute<void>(
53+
builder: (_) => DetailsPage(movie: movie),
54+
),
55+
);
56+
},
57+
child: Container(
58+
decoration: BoxDecoration(
59+
color: Colors.white.withOpacity(0.7),
60+
borderRadius: BorderRadius.circular(20),
61+
),
62+
child: Row(
63+
crossAxisAlignment: CrossAxisAlignment.start,
64+
children: [
65+
ClipRRect(
66+
borderRadius: const BorderRadius.only(
67+
topLeft: Radius.circular(20),
68+
bottomLeft: Radius.circular(20),
69+
),
70+
child: Image.network(
71+
'https://image.tmdb.org/t/p/w300${movie.posterPath}',
72+
width: 120,
73+
height: 120,
74+
fit: BoxFit.cover,
75+
errorBuilder: (context, error, stackTrace) =>
76+
Container(
77+
width: 120,
78+
height: 120,
79+
color: Colors.grey[300],
80+
child: const Icon(Icons.error_outline),
81+
),
82+
),
83+
),
84+
Expanded(
85+
child: Padding(
86+
padding: const EdgeInsets.all(12.0),
87+
child: Column(
88+
crossAxisAlignment: CrossAxisAlignment.start,
89+
children: [
90+
Text(
91+
movie.title,
92+
style: const TextStyle(
93+
fontSize: 20,
94+
fontWeight: FontWeight.bold,
95+
),
96+
),
97+
const SizedBox(height: 8),
98+
Text(
99+
movie.overview.isNotEmpty
100+
? movie.overview
101+
: '-',
102+
maxLines: 3,
103+
overflow: TextOverflow.ellipsis,
104+
),
105+
const SizedBox(height: 8),
106+
Wrap(
107+
spacing: 8,
108+
children: [
109+
if (movie is MovieDetailsResponse)
110+
...movie.genres
111+
.map((g) => Chip(label: Text(g.name)))
112+
],
113+
),
114+
],
115+
),
116+
),
117+
),
118+
],
119+
),
120+
),
121+
);
122+
},
123+
);
124+
},
125+
),
126+
),
127+
);
128+
}
129+
}
130+
131+
class _SearchBar extends StatelessWidget {
132+
@override
133+
Widget build(BuildContext context) {
134+
return TextField(
135+
autofocus: true,
136+
decoration: InputDecoration(
137+
hintText: 'Search',
138+
border: InputBorder.none,
139+
suffixIcon: IconButton(
140+
icon: const Icon(Icons.clear),
141+
onPressed: () {
142+
context.read<SearchCubit>().search('');
143+
},
144+
),
145+
),
146+
onChanged: (query) {
147+
context.read<SearchCubit>().search(query);
148+
},
149+
);
150+
}
151+
}

0 commit comments

Comments
 (0)