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