Skip to content

Commit ee40697

Browse files
committed
feat: Day 6 favorites, history, continue watching + fix category page with genre chips + fix video controls
1 parent 9143d82 commit ee40697

18 files changed

Lines changed: 968 additions & 83 deletions

File tree

devtools_options.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
description: This file stores settings for Dart & Flutter DevTools.
2+
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
3+
extensions:

lib/core/router/app_router.dart

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import 'package:go_router/go_router.dart';
55
import '../../features/category/presentation/bloc/category_bloc.dart';
66
import '../../features/category/presentation/bloc/category_event.dart';
77
import '../../features/category/presentation/pages/category_page.dart';
8+
import '../../features/favorite/presentation/bloc/favorite_bloc.dart';
9+
import '../../features/favorite/presentation/bloc/favorite_event.dart';
810
import '../../features/favorite/presentation/pages/favorites_page.dart';
11+
import '../../features/history/presentation/bloc/history_bloc.dart';
12+
import '../../features/history/presentation/bloc/history_event.dart';
913
import '../../features/history/presentation/pages/history_page.dart';
1014
import '../../features/home/presentation/bloc/home_bloc.dart';
1115
import '../../features/home/presentation/bloc/home_event.dart';
@@ -64,22 +68,31 @@ final GoRouter appRouter = GoRouter(
6468
path: '/category/:type',
6569
builder: (context, state) {
6670
final type = state.pathParameters['type'] ?? 'phim-bo';
71+
final category = state.uri.queryParameters['category'];
6772
return BlocProvider(
68-
create: (_) =>
69-
sl<CategoryBloc>()..add(LoadCategoryMovies(type: type)),
70-
child: CategoryPage(type: type),
73+
create: (_) => sl<CategoryBloc>()
74+
..add(LoadCategoryMovies(type: type, category: category)),
75+
child: CategoryPage(type: type, initialCategory: category),
7176
);
7277
},
7378
),
7479
GoRoute(
7580
path: '/favorites',
76-
pageBuilder: (context, state) =>
77-
const NoTransitionPage(child: FavoritesPage()),
81+
pageBuilder: (context, state) => NoTransitionPage(
82+
child: BlocProvider(
83+
create: (_) => sl<FavoriteBloc>()..add(const LoadFavorites()),
84+
child: const FavoritesPage(),
85+
),
86+
),
7887
),
7988
GoRoute(
8089
path: '/history',
81-
pageBuilder: (context, state) =>
82-
const NoTransitionPage(child: HistoryPage()),
90+
pageBuilder: (context, state) => NoTransitionPage(
91+
child: BlocProvider(
92+
create: (_) => sl<HistoryBloc>()..add(const LoadHistory()),
93+
child: const HistoryPage(),
94+
),
95+
),
8396
),
8497
],
8598
),
@@ -88,8 +101,15 @@ final GoRouter appRouter = GoRouter(
88101
path: '/movie/:slug',
89102
builder: (context, state) {
90103
final slug = state.pathParameters['slug']!;
91-
return BlocProvider(
92-
create: (_) => sl<MovieDetailBloc>()..add(LoadMovieDetail(slug)),
104+
return MultiBlocProvider(
105+
providers: [
106+
BlocProvider(
107+
create: (_) => sl<MovieDetailBloc>()..add(LoadMovieDetail(slug)),
108+
),
109+
BlocProvider(
110+
create: (_) => sl<FavoriteBloc>()..add(const LoadFavorites()),
111+
),
112+
],
93113
child: MovieDetailPage(slug: slug),
94114
);
95115
},
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import 'package:cached_network_image/cached_network_image.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:go_router/go_router.dart';
4+
5+
import '../../../core/theme/app_colors.dart';
6+
import '../../features/watch/data/models/watch_position_model.dart';
7+
8+
/// Widget "Tiếp tục xem" hiển thị trên trang chủ
9+
class ContinueWatchingWidget extends StatelessWidget {
10+
final List<WatchPosition> items;
11+
12+
const ContinueWatchingWidget({super.key, required this.items});
13+
14+
@override
15+
Widget build(BuildContext context) {
16+
if (items.isEmpty) return const SizedBox.shrink();
17+
18+
return Column(
19+
crossAxisAlignment: CrossAxisAlignment.start,
20+
children: [
21+
const Padding(
22+
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
23+
child: Row(
24+
children: [
25+
Icon(Icons.play_circle_fill, color: AppColors.primary, size: 20),
26+
SizedBox(width: 8),
27+
Text(
28+
'Tiếp tục xem',
29+
style: TextStyle(
30+
color: Colors.white,
31+
fontSize: 16,
32+
fontWeight: FontWeight.bold,
33+
),
34+
),
35+
],
36+
),
37+
),
38+
SizedBox(
39+
height: 160,
40+
child: ListView.separated(
41+
scrollDirection: Axis.horizontal,
42+
padding: const EdgeInsets.symmetric(horizontal: 16),
43+
itemCount: items.length,
44+
separatorBuilder: (_, __) => const SizedBox(width: 12),
45+
itemBuilder: (context, index) {
46+
return _ContinueCard(item: items[index]);
47+
},
48+
),
49+
),
50+
],
51+
);
52+
}
53+
}
54+
55+
class _ContinueCard extends StatelessWidget {
56+
final WatchPosition item;
57+
58+
const _ContinueCard({required this.item});
59+
60+
@override
61+
Widget build(BuildContext context) {
62+
return GestureDetector(
63+
onTap: () {
64+
context.push('/watch/${item.movieSlug}/${item.episodeSlug}');
65+
},
66+
child: SizedBox(
67+
width: 140,
68+
child: Column(
69+
crossAxisAlignment: CrossAxisAlignment.start,
70+
children: [
71+
// Thumbnail with progress
72+
ClipRRect(
73+
borderRadius: BorderRadius.circular(8),
74+
child: SizedBox(
75+
height: 80,
76+
width: 140,
77+
child: Stack(
78+
fit: StackFit.expand,
79+
children: [
80+
CachedNetworkImage(
81+
imageUrl: item.posterUrl,
82+
fit: BoxFit.cover,
83+
errorWidget: (_, __, ___) =>
84+
Container(color: AppColors.cardDark),
85+
),
86+
// Play icon overlay
87+
Container(
88+
color: Colors.black26,
89+
child: const Center(
90+
child: Icon(
91+
Icons.play_circle_outline,
92+
color: Colors.white70,
93+
size: 36,
94+
),
95+
),
96+
),
97+
// Progress bar
98+
Positioned(
99+
bottom: 0,
100+
left: 0,
101+
right: 0,
102+
child: LinearProgressIndicator(
103+
value: item.progress.clamp(0.0, 1.0),
104+
backgroundColor: Colors.black54,
105+
valueColor: const AlwaysStoppedAnimation<Color>(
106+
AppColors.primary),
107+
minHeight: 3,
108+
),
109+
),
110+
],
111+
),
112+
),
113+
),
114+
const SizedBox(height: 8),
115+
// Movie name
116+
Text(
117+
item.movieName,
118+
maxLines: 1,
119+
overflow: TextOverflow.ellipsis,
120+
style: const TextStyle(
121+
color: Colors.white,
122+
fontSize: 12,
123+
fontWeight: FontWeight.w500,
124+
),
125+
),
126+
const SizedBox(height: 2),
127+
// Episode name
128+
Text(
129+
item.episodeName,
130+
maxLines: 1,
131+
overflow: TextOverflow.ellipsis,
132+
style: const TextStyle(color: Colors.white38, fontSize: 11),
133+
),
134+
],
135+
),
136+
),
137+
);
138+
}
139+
}

0 commit comments

Comments
 (0)