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,283 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/widgets/riotz_scaffold.dart';
import '../../../../core/widgets/riotz_button.dart';
import '../../../auth/presentation/providers/auth_provider.dart';
import '../../../music/presentation/providers/music_providers.dart';
import '../../../music/presentation/widgets/track_card.dart';
import '../../domain/models/profile_model.dart';
import '../../domain/models/profile_stats_model.dart';
import '../providers/profile_providers.dart';
class ProfilePage extends ConsumerStatefulWidget {
const ProfilePage({super.key});
@override
ConsumerState<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends ConsumerState<ProfilePage> with SingleTickerProviderStateMixin {
late TabController _tabController;
final _usernameController = TextEditingController();
final _bioController = TextEditingController();
final _picker = ImagePicker();
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
_usernameController.dispose();
_bioController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
ref.listen(profileControllerProvider, (_, next) {
next.whenOrNull(
data: (_) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
backgroundColor: AppColors.success,
content: Text('IDENTITY UPDATED.', style: TextStyle(color: AppColors.black, fontWeight: FontWeight.bold)),
),
);
ref.invalidate(myProfileStatsProvider);
},
error: (error, _) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: AppColors.bloodRed,
content: Text(error.toString().toUpperCase()),
),
);
},
);
});
final profileAsync = ref.watch(myProfileProvider);
final statsAsync = ref.watch(myProfileStatsProvider);
final updating = ref.watch(profileControllerProvider).isLoading;
final currentUser = ref.watch(authServiceProvider).currentUser;
return RiotzScaffold(
appBar: AppBar(
title: const Text('RIOTZ // IDENTITY'),
),
body: profileAsync.when(
loading: () => const Center(child: CircularProgressIndicator(color: AppColors.neonRed)),
error: (error, _) => Center(child: Text('ERROR: $error')),
data: (profile) {
_syncControllers(profile);
return Column(
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24),
children: [
// Profile Header
Center(
child: Stack(
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
border: Border.all(color: AppColors.neonRed, width: 2),
color: AppColors.surfaceLight,
),
child: profile.avatarUrl.isNotEmpty
? Image.network(profile.avatarUrl, fit: BoxFit.cover)
: const Icon(Icons.person, size: 60, color: AppColors.grey),
),
Positioned(
right: -4,
bottom: -4,
child: IconButton(
onPressed: updating ? null : _pickAndUploadAvatar,
icon: Container(
padding: const EdgeInsets.all(4),
color: AppColors.neonRed,
child: const Icon(Icons.edit, color: AppColors.white, size: 18),
),
),
),
],
),
),
const SizedBox(height: 32),
_StatsSection(statsAsync: statsAsync),
const SizedBox(height: 32),
// Tabs
TabBar(
controller: _tabController,
indicatorColor: AppColors.neonRed,
labelColor: AppColors.white,
unselectedLabelColor: AppColors.grey,
tabs: const [
Tab(text: 'IDENTITY'),
Tab(text: 'TRACKS'),
],
),
const SizedBox(height: 24),
SizedBox(
height: 400, // Fixed height for tab content in ListView
child: TabBarView(
controller: _tabController,
children: [
// Identity Tab
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('USERNAME', style: theme.textTheme.labelLarge?.copyWith(color: AppColors.grey)),
const SizedBox(height: 8),
TextField(
controller: _usernameController,
decoration: const InputDecoration(hintText: 'ENTER ALIAS'),
),
const SizedBox(height: 24),
Text('BIO // MANIFESTO', style: theme.textTheme.labelLarge?.copyWith(color: AppColors.grey)),
const SizedBox(height: 8),
TextField(
controller: _bioController,
decoration: const InputDecoration(hintText: 'DESCRIBE THE CHAOS'),
minLines: 3,
maxLines: 5,
),
const SizedBox(height: 32),
RiotzButton(
label: 'UPDATE IDENTITY',
isLoading: updating,
onPressed: () async {
await ref
.read(profileControllerProvider.notifier)
.saveProfile(
username: _usernameController.text,
bio: _bioController.text,
);
},
),
],
),
),
// Tracks Tab
currentUser == null
? const Center(child: Text('LOG IN TO SEE TRACKS'))
: Consumer(
builder: (context, ref, child) {
final tracksAsync = ref.watch(userTracksProvider(currentUser.id));
return tracksAsync.when(
loading: () => const Center(child: CircularProgressIndicator(color: AppColors.neonRed)),
error: (err, _) => Center(child: Text('ERROR: $err')),
data: (tracks) {
if (tracks.isEmpty) {
return const Center(child: Text('NO TRACKS BROADCASTED.'));
}
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: tracks.length,
itemBuilder: (context, index) => TrackCard(track: tracks[index]),
);
},
);
},
),
],
),
),
],
),
),
],
);
},
),
);
}
void _syncControllers(ProfileModel profile) {
if (_usernameController.text != profile.username) {
_usernameController.text = profile.username;
}
if (_bioController.text != profile.bio) {
_bioController.text = profile.bio;
}
}
Future<void> _pickAndUploadAvatar() async {
final picked = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 85,
maxWidth: 1200,
);
if (picked == null) return;
final bytes = await picked.readAsBytes();
final ext = picked.name.split('.').last.toLowerCase();
if (!mounted) return;
await ref.read(profileControllerProvider.notifier).uploadAvatar(
bytes: bytes,
extension: ext.isEmpty ? 'jpg' : ext,
);
}
}
class _StatsSection extends StatelessWidget {
const _StatsSection({required this.statsAsync});
final AsyncValue<ProfileStatsModel> statsAsync;
@override
Widget build(BuildContext context) {
return statsAsync.when(
loading: () => const Center(child: CircularProgressIndicator(color: AppColors.neonRed)),
error: (error, _) => Text('STATS OFFLINE: $error'),
data: (stats) => Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_StatItem(label: 'POSTS', value: stats.postsCount.toString()),
_StatItem(label: 'CHAOS', value: stats.commentsCount.toString()),
_StatItem(label: 'TRACKS', value: stats.tracksCount.toString()),
],
),
);
}
}
class _StatItem extends StatelessWidget {
const _StatItem({
required this.label,
required this.value,
});
final String label;
final String value;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
children: [
Text(value, style: theme.textTheme.headlineLarge),
Text(label, style: theme.textTheme.labelLarge?.copyWith(color: AppColors.grey, fontSize: 10)),
],
);
}
}