first commit

This commit is contained in:
Lucas Saburido
2026-05-13 16:26:45 +01:00
commit cabf2025cd
252 changed files with 13524 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/supabase/supabase_providers.dart';
import '../../data/services/feed_service.dart';
import '../../domain/models/feed_post_model.dart';
final feedServiceProvider = Provider<FeedService>((ref) {
final client = ref.watch(supabaseProvider);
return FeedService(client);
});
final feedPostsProvider = FutureProvider<List<FeedPostModel>>((ref) {
return ref.watch(feedServiceProvider).fetchFeed();
});
final feedControllerProvider =
AutoDisposeAsyncNotifierProvider<FeedController, void>(
FeedController.new,
);
class FeedController extends AutoDisposeAsyncNotifier<void> {
@override
Future<void> build() async {}
Future<void> createPost({
required String caption,
required Uint8List imageBytes,
required String extension,
}) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(feedServiceProvider).createPost(
caption: caption,
imageBytes: imageBytes,
extension: extension,
);
ref.invalidate(feedPostsProvider);
});
}
Future<void> toggleLike(FeedPostModel post) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(feedServiceProvider).toggleLike(
postId: post.id,
currentlyLiked: post.isLiked,
currentLikesCount: post.likesCount,
);
ref.invalidate(feedPostsProvider);
});
}
Future<void> deleteOwnPost(String postId) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(feedServiceProvider).deleteOwnPost(postId);
ref.invalidate(feedPostsProvider);
});
}
}

View File

@@ -0,0 +1,59 @@
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/supabase/supabase_providers.dart';
import '../../data/services/post_service.dart';
import '../../domain/models/feed_post_model.dart';
final postServiceProvider = Provider<PostService>((ref) {
final client = ref.watch(supabaseProvider);
return PostService(client);
});
final feedPostsProvider = FutureProvider<List<FeedPostModel>>((ref) {
return ref.watch(postServiceProvider).fetchFeed();
});
final postControllerProvider = AutoDisposeAsyncNotifierProvider<PostController, void>(
PostController.new,
);
class PostController extends AutoDisposeAsyncNotifier<void> {
@override
Future<void> build() async {}
Future<void> createPost({
required String caption,
required Uint8List imageBytes,
required String extension,
}) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(postServiceProvider).uploadPost(
caption: caption,
imageBytes: imageBytes,
extension: extension,
);
ref.invalidate(feedPostsProvider);
});
}
Future<void> toggleLike(FeedPostModel post) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(postServiceProvider).toggleLike(
postId: post.id,
currentlyLiked: post.isLiked,
currentLikesCount: post.likesCount,
);
ref.invalidate(feedPostsProvider);
});
}
Future<void> deletePost(String postId) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(postServiceProvider).deletePost(postId);
ref.invalidate(feedPostsProvider);
});
}
}

View File

@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/router/app_routes.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/widgets/riotz_scaffold.dart';
import '../../../auth/presentation/providers/auth_provider.dart';
import '../providers/post_providers.dart';
import '../widgets/post_card.dart';
class FeedScreen extends ConsumerWidget {
const FeedScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final postsAsync = ref.watch(feedPostsProvider);
final currentUser = ref.watch(authServiceProvider).currentUser;
return RiotzScaffold(
appBar: AppBar(
title: const Text('RIOTZ // FEED'),
actions: [
IconButton(
icon: const Icon(Icons.add_box_outlined, color: AppColors.white),
onPressed: () => context.push(AppRoutes.uploadPost),
),
],
),
body: RefreshIndicator(
color: AppColors.neonRed,
backgroundColor: AppColors.black,
onRefresh: () async => ref.refresh(feedPostsProvider),
child: postsAsync.when(
data: (posts) {
if (posts.isEmpty) {
return const Center(
child: Text('NO CHAOS YET. START A RIOT.'),
);
}
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return PostCard(
post: post,
isOwnPost: post.userId == currentUser?.id,
onLike: () => ref.read(postControllerProvider.notifier).toggleLike(post),
onDelete: () => _confirmDelete(context, ref, post.id),
);
},
);
},
loading: () => const Center(
child: CircularProgressIndicator(color: AppColors.neonRed),
),
error: (error, _) => Center(
child: Text('SIGNAL LOST: $error'),
),
),
),
floatingActionButton: FloatingActionButton(
backgroundColor: AppColors.neonRed,
child: const Icon(Icons.add, color: AppColors.white),
onPressed: () => context.push(AppRoutes.uploadPost),
),
);
}
void _confirmDelete(BuildContext context, WidgetRef ref, String postId) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColors.surface,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('ERASE TRANSMISSION?'),
content: const Text('THIS ACTION CANNOT BE UNDONE.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('CANCEL', style: TextStyle(color: AppColors.white)),
),
TextButton(
onPressed: () {
ref.read(postControllerProvider.notifier).deletePost(postId);
Navigator.pop(context);
},
child: const Text('DELETE', style: TextStyle(color: AppColors.neonRed)),
),
],
),
);
}
}

View File

@@ -0,0 +1,134 @@
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/theme/app_colors.dart';
import '../../../../core/widgets/riotz_button.dart';
import '../../../../core/widgets/riotz_scaffold.dart';
import '../providers/post_providers.dart';
class UploadPostScreen extends ConsumerStatefulWidget {
const UploadPostScreen({super.key});
@override
ConsumerState<UploadPostScreen> createState() => _UploadPostScreenState();
}
class _UploadPostScreenState extends ConsumerState<UploadPostScreen> {
final _captionController = TextEditingController();
final _picker = ImagePicker();
Uint8List? _imageBytes;
String? _extension;
@override
void dispose() {
_captionController.dispose();
super.dispose();
}
Future<void> _pickImage() async {
final image = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
maxWidth: 1080,
);
if (image != null) {
final bytes = await image.readAsBytes();
setState(() {
_imageBytes = bytes;
_extension = image.name.split('.').last.toLowerCase();
});
}
}
Future<void> _upload() async {
if (_imageBytes == null) return;
await ref.read(postControllerProvider.notifier).createPost(
caption: _captionController.text,
imageBytes: _imageBytes!,
extension: _extension ?? 'jpg',
);
if (mounted) {
context.pop();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final loading = ref.watch(postControllerProvider).isLoading;
return RiotzScaffold(
appBar: AppBar(
title: const Text('NEW TRANSMISSION'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => context.pop(),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image Picker Area
GestureDetector(
onTap: loading ? null : _pickImage,
child: AspectRatio(
aspectRatio: 1,
child: Container(
decoration: BoxDecoration(
color: AppColors.surface,
border: Border.all(color: AppColors.border, width: 2),
),
child: _imageBytes != null
? Image.memory(_imageBytes!, fit: BoxFit.cover)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.add_a_photo_outlined, size: 48, color: AppColors.grey),
const SizedBox(height: 12),
Text(
'SELECT VISUALS',
style: theme.textTheme.labelLarge?.copyWith(color: AppColors.grey),
),
],
),
),
),
),
const SizedBox(height: 32),
// Caption Field
Text(
'MANIFESTO',
style: theme.textTheme.labelLarge?.copyWith(color: AppColors.neonRed),
),
const SizedBox(height: 8),
TextField(
controller: _captionController,
maxLines: 4,
decoration: const InputDecoration(
hintText: 'DESCRIBE THE CHAOS...',
),
),
const SizedBox(height: 48),
// Post Button
RiotzButton(
label: 'PROPAGATE',
isLoading: loading,
onPressed: _imageBytes == null ? null : _upload,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,117 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/widgets/riotz_card.dart';
import '../../domain/models/feed_post_model.dart';
class PostCard extends StatelessWidget {
const PostCard({
required this.post,
required this.isOwnPost,
required this.onLike,
required this.onDelete,
super.key,
});
final FeedPostModel post;
final bool isOwnPost;
final VoidCallback onLike;
final VoidCallback onDelete;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final timeStr = DateFormat('HH:mm // dd.MM.yy').format(post.createdAt);
return RiotzCard(
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.surfaceLight,
border: Border.all(color: AppColors.border),
),
child: post.avatarUrl.isNotEmpty
? CachedNetworkImage(
imageUrl: post.avatarUrl,
fit: BoxFit.cover,
errorWidget: (_, __, ___) => const Icon(Icons.person, color: AppColors.grey),
)
: const Icon(Icons.person, color: AppColors.grey),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(post.username.toUpperCase(), style: theme.textTheme.labelLarge),
Text(timeStr, style: theme.textTheme.bodySmall),
],
),
),
if (isOwnPost)
IconButton(
icon: const Icon(Icons.close, color: AppColors.grey, size: 20),
onPressed: onDelete,
),
],
),
),
// Visuals
CachedNetworkImage(
imageUrl: post.imageUrl,
width: double.infinity,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
height: 300,
color: AppColors.surfaceLight,
child: const Center(child: CircularProgressIndicator(color: AppColors.neonRed)),
),
),
// Footer
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
IconButton(
onPressed: onLike,
icon: Icon(
post.isLiked ? Icons.favorite : Icons.favorite_border,
color: post.isLiked ? AppColors.neonRed : AppColors.white,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
const SizedBox(width: 8),
Text('${post.likesCount}', style: theme.textTheme.titleMedium),
const SizedBox(width: 24),
const Icon(Icons.chat_bubble_outline, color: AppColors.white, size: 20),
],
),
if (post.caption.isNotEmpty) ...[
const SizedBox(height: 12),
Text(post.caption, style: theme.textTheme.bodyLarge),
],
],
),
),
],
),
);
}
}