348 lines
12 KiB
Dart
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),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|