Files
YungR1otz/lib/features/home/presentation/pages/home_page.dart
2026-05-20 22:08:30 +01:00

348 lines
12 KiB
Dart

import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import '../../../../core/router/app_routes.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/widgets/riotz_scaffold.dart';
import '../../../../core/widgets/riotz_button.dart';
import '../../../../features/auth/presentation/providers/auth_providers.dart';
import '../../../../features/admin/presentation/providers/admin_providers.dart';
import '../../../../features/feed/domain/models/feed_post_model.dart';
import '../../../../features/feed/presentation/providers/feed_providers.dart';
class HomePage extends ConsumerStatefulWidget {
const HomePage({super.key});
@override
ConsumerState<HomePage> createState() => _HomePageState();
}
class _HomePageState extends ConsumerState<HomePage> {
final _captionController = TextEditingController();
final _picker = ImagePicker();
Uint8List? _selectedImageBytes;
String _selectedImageExt = 'jpg';
@override
void dispose() {
_captionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final user = ref.watch(currentUserProvider);
final isAdmin = ref.watch(isAdminProvider);
final feedAsync = ref.watch(feedPostsProvider);
final feedActionState = ref.watch(feedControllerProvider);
final isBusy = feedActionState.isLoading;
ref.listen(feedControllerProvider, (_, next) {
next.whenOrNull(
data: (_) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
backgroundColor: AppColors.success,
content: Text('CHAOS PROPAGATED.', style: TextStyle(color: AppColors.black, fontWeight: FontWeight.bold)),
),
);
},
error: (error, _) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: AppColors.bloodRed,
content: Text(error.toString().toUpperCase()),
),
);
},
);
});
return RiotzScaffold(
appBar: AppBar(
title: const Text('RIOTZ // FEED'),
actions: [
IconButton(
icon: const Icon(Icons.logout, color: AppColors.white),
onPressed: () async {
await ref.read(authControllerProvider.notifier).logout();
if (context.mounted) context.go(AppRoutes.login);
},
),
],
),
body: RefreshIndicator(
color: AppColors.neonRed,
backgroundColor: AppColors.black,
onRefresh: () async => ref.invalidate(feedPostsProvider),
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
children: [
Text(
'WELCOME, ${user?.email?.split('@').first.toUpperCase() ?? 'RIOTER'}',
style: theme.textTheme.labelLarge?.copyWith(color: AppColors.neonRed),
),
const SizedBox(height: 24),
// Composer
_PostComposer(
captionController: _captionController,
selectedImageBytes: _selectedImageBytes,
onPickImage: isBusy ? null : _pickImage,
onCreatePost: isBusy ? null : _createPost,
isBusy: isBusy,
),
const SizedBox(height: 32),
// Navigation Shortcuts (Chaotic Grid)
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_NavChip(label: 'PROFILE', onTap: () => context.push(AppRoutes.profile)),
_NavChip(label: 'MUSIC', onTap: () => context.push(AppRoutes.music)),
_NavChip(label: 'DISCOVER', onTap: () => context.push(AppRoutes.discover)),
if (isAdmin) _NavChip(label: 'ADMIN', onTap: () => context.push(AppRoutes.admin), isAccent: true),
],
),
const SizedBox(height: 32),
feedAsync.when(
loading: () => const Center(child: CircularProgressIndicator(color: AppColors.neonRed)),
error: (error, _) => Center(child: Text('SYSTEM ERROR: $error')),
data: (posts) {
if (posts.isEmpty) {
return const Center(child: Text('NO CHAOS YET. START A RIOT.'));
}
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: posts.length,
separatorBuilder: (_, _) => const SizedBox(height: 24),
itemBuilder: (context, index) {
final post = posts[index];
return _FeedPostCard(
post: post,
isOwnPost: user?.id == post.userId,
onLike: () => ref.read(feedControllerProvider.notifier).toggleLike(post),
onDelete: () => ref.read(feedControllerProvider.notifier).deleteOwnPost(post.id),
);
},
);
},
),
],
),
),
);
}
Future<void> _pickImage() async {
final image = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 80);
if (image == null) return;
final bytes = await image.readAsBytes();
setState(() {
_selectedImageBytes = bytes;
_selectedImageExt = image.name.split('.').last;
});
}
Future<void> _createPost() async {
if (_selectedImageBytes == null) return;
await ref.read(feedControllerProvider.notifier).createPost(
caption: _captionController.text,
imageBytes: _selectedImageBytes!,
extension: _selectedImageExt,
);
_captionController.clear();
setState(() => _selectedImageBytes = null);
}
}
class _NavChip extends StatelessWidget {
const _NavChip({required this.label, required this.onTap, this.isAccent = false});
final String label;
final VoidCallback onTap;
final bool isAccent;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: isAccent ? AppColors.neonRed : AppColors.white),
),
child: Text(
label,
style: TextStyle(
color: isAccent ? AppColors.neonRed : AppColors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
);
}
}
class _PostComposer extends StatelessWidget {
const _PostComposer({
required this.captionController,
required this.selectedImageBytes,
required this.onPickImage,
required this.onCreatePost,
required this.isBusy,
});
final TextEditingController captionController;
final Uint8List? selectedImageBytes;
final VoidCallback? onPickImage;
final VoidCallback? onCreatePost;
final bool isBusy;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: AppColors.border),
color: AppColors.surface,
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: captionController,
maxLines: 3,
decoration: const InputDecoration(
hintText: 'WHAT\'S THE WORD ON THE STREET?',
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
),
),
if (selectedImageBytes != null) ...[
const SizedBox(height: 12),
Image.memory(selectedImageBytes!, height: 200, width: double.infinity, fit: BoxFit.cover),
],
const SizedBox(height: 16),
Row(
children: [
IconButton(
icon: const Icon(Icons.add_a_photo_outlined, color: AppColors.white),
onPressed: onPickImage,
),
const Spacer(),
RiotzButton(
label: 'POST',
onPressed: onCreatePost,
isLoading: isBusy,
fullWidth: false,
),
],
),
],
),
);
}
}
class _FeedPostCard extends StatelessWidget {
const _FeedPostCard({
required this.post,
required this.isOwnPost,
required this.onLike,
required this.onDelete,
});
final FeedPostModel post;
final bool isOwnPost;
final VoidCallback onLike;
final VoidCallback onDelete;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: AppColors.border)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.surfaceLight,
border: Border.all(color: AppColors.border),
),
child: post.avatarUrl.isNotEmpty
? Image.network(post.avatarUrl, fit: BoxFit.cover)
: const Icon(Icons.person, color: AppColors.grey),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(post.username.toUpperCase(), style: theme.textTheme.labelLarge),
Text('RIOTER', style: theme.textTheme.bodySmall?.copyWith(color: AppColors.neonRed, fontSize: 10)),
],
),
const Spacer(),
if (isOwnPost)
IconButton(
icon: const Icon(Icons.more_vert, color: AppColors.grey),
onPressed: onDelete,
),
],
),
const SizedBox(height: 16),
if (post.imageUrl.isNotEmpty)
Image.network(post.imageUrl, width: double.infinity, fit: BoxFit.cover),
const SizedBox(height: 16),
if (post.caption.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(post.caption, style: theme.textTheme.bodyLarge),
),
Row(
children: [
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
icon: Icon(
post.isLiked ? Icons.favorite : Icons.favorite_border,
color: post.isLiked ? AppColors.neonRed : AppColors.white,
size: 20,
),
onPressed: onLike,
),
const SizedBox(width: 8),
Text('${post.likesCount}', style: theme.textTheme.labelLarge),
const SizedBox(width: 24),
const Icon(Icons.chat_bubble_outline, color: AppColors.white, size: 20),
const SizedBox(width: 8),
const Text('0', style: TextStyle(fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 16),
],
),
);
}
}