Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions lib/core/hive/favorite_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:hive/hive.dart';

part 'favorite_model.g.dart';

@HiveType(typeId: 0)
class Favorite extends HiveObject {
@HiveField(0)
late int id;

@HiveField(1)
late int itemId;

@HiveField(2)
late String title;

@HiveField(3)
late String posterPath;

@HiveField(4)
late String type; // 'movie', 'tv', or 'celebrity'

@HiveField(5)
String? overview;

@HiveField(6)
String? releaseDate;

Favorite({
required this.id,
required this.itemId,
required this.title,
required this.posterPath,
required this.type,
this.overview,
this.releaseDate,
});
}
61 changes: 61 additions & 0 deletions lib/core/hive/hive_helper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:flutter_movie_clean_architecture/core/hive/favorite_model.dart';

class HiveHelper {
static const String _boxName = 'favorites_box';
static Box<Favorite>? _box;

static Future<void> init() async {
// Initialize Hive
await Hive.initFlutter();

// Register adapter
Hive.registerAdapter(FavoriteAdapter());

// Open box
_box = await Hive.openBox<Favorite>(_boxName);
}

static Box<Favorite> _getBox() {
if (_box == null) {
throw Exception('HiveHelper not initialized. Call init() first.');
}
return _box!;
}

// Insert a favorite
static Future<void> insertFavorite(Favorite favorite) async {
final box = _getBox();
await box.put('${favorite.itemId}_${favorite.type}', favorite);
}

// Delete a favorite
static Future<void> deleteFavorite(int itemId, String type) async {
final box = _getBox();
await box.delete('${itemId}_$type');
}

// Check if an item is favorite
static Future<bool> isFavorite(int itemId, String type) async {
final box = _getBox();
return box.containsKey('${itemId}_$type');
}

// Get all favorites
static Future<List<Favorite>> getAllFavorites() async {
final box = _getBox();
return box.values.toList();
}

// Get favorites by type
static Future<List<Favorite>> getFavoritesByType(String type) async {
final box = _getBox();
return box.values.where((favorite) => favorite.type == type).toList();
}

// Close Hive
static Future<void> close() async {
await _box?.close();
}
}
226 changes: 226 additions & 0 deletions lib/features/favorites/favorites_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import 'package:flutter/material.dart';
import 'package:flutter_movie_clean_architecture/core/config/app_constant.dart';
import 'package:flutter_movie_clean_architecture/core/hive/favorite_model.dart';
import 'package:flutter_movie_clean_architecture/core/hive/hive_helper.dart';
import 'package:go_router/go_router.dart';

class FavoritesPage extends StatefulWidget {
const FavoritesPage({super.key});

@override
State<FavoritesPage> createState() => _FavoritesPageState();
}

class _FavoritesPageState extends State<FavoritesPage> {
int _currentIndex = 0;
late Future<List<Favorite>> _favoritesFuture;

@override
void initState() {
super.initState();
_loadFavorites();
}

Future<void> _loadFavorites() async {
setState(() {
_favoritesFuture = _getFavoritesByType();
});
}

Future<List<Favorite>> _getFavoritesByType() async {
switch (_currentIndex) {
case 0:
return await HiveHelper.getFavoritesByType('movie');
case 1:
return await HiveHelper.getFavoritesByType('tv');
case 2:
return await HiveHelper.getFavoritesByType('celebrity');
default:
return await HiveHelper.getAllFavorites();
}
}

Future<void> _refreshFavorites() async {
await _loadFavorites();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<Favorite>>(
future: _favoritesFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}

if (snapshot.hasError) {
return Center(
child: Text('Error loading favorites: ${snapshot.error}'),
);
}

final favorites = snapshot.data ?? [];

if (favorites.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite_border, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'No favorites yet',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
SizedBox(height: 8),
Text(
'Start adding favorites from movie, TV series, or artist details',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
],
),
);
}

return RefreshIndicator(
onRefresh: _refreshFavorites,
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.7,
),
itemCount: favorites.length,
itemBuilder: (context, index) {
final favorite = favorites[index];
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: () {
switch (favorite.type) {
case 'movie':
context.push('/movie/${favorite.itemId}');
break;
case 'tv':
context.push('/tv/${favorite.itemId}');
break;
case 'celebrity':
context.push('/artistId/${favorite.itemId}');
break;
}
},
borderRadius: BorderRadius.circular(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
child: favorite.posterPath.isNotEmpty
? Image.network(
'$IMAGE_URL${favorite.posterPath}',
fit: BoxFit.cover,
width: double.infinity,
errorBuilder: (_, __, ___) =>
_buildPlaceholderImage(),
)
: _buildPlaceholderImage(),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
favorite.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
favorite.type.toUpperCase(),
style: TextStyle(
color: Colors.grey[600],
fontSize: 10,
),
),
IconButton(
icon: const Icon(
Icons.favorite,
color: Colors.red,
size: 18,
),
padding: EdgeInsets.zero,
onPressed: () async {
await HiveHelper.deleteFavorite(
favorite.itemId, favorite.type);
_refreshFavorites();
},
),
],
),
],
),
),
],
),
),
);
},
),
);
},
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
_loadFavorites();
});
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.movie),
label: 'Movies',
),
BottomNavigationBarItem(
icon: Icon(Icons.tv),
label: 'TV Series',
),
BottomNavigationBarItem(
icon: Icon(Icons.people),
label: 'Celebrities',
),
],
),
);
}

Widget _buildPlaceholderImage() {
return Container(
width: double.infinity,
height: double.infinity,
color: Colors.grey[300],
child: const Icon(Icons.image, color: Colors.grey),
);
}
}
2 changes: 2 additions & 0 deletions lib/features/movie/data/models/credit_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class Cast with _$Cast {
String? character,
@JsonKey(name: 'credit_id') String? creditId,
int? order,
@JsonKey(name: 'media_type') String? mediaType,
@JsonKey(name: 'first_air_date') String? firstAirDate,
}) = _Cast;

factory Cast.fromJson(Map<String, dynamic> json) => _$CastFromJson(json);
Expand Down
Loading