import 'package:cached_network_image/cached_network_image.dart'; 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 'package:supabase_flutter/supabase_flutter.dart'; import '../../app/theme.dart'; class Profile { const Profile({required this.id, this.name, this.phone, this.hatti, this.avatarUrl, this.role = 'listener'}); final String id; final String? name; final String? phone; final String? hatti; final String? avatarUrl; final String role; } final profileProvider = FutureProvider.autoDispose((ref) async { final client = Supabase.instance.client; final user = client.auth.currentUser; if (user == null) return null; final r = await client .from('profiles') .select('id, display_name, phone, hatti_name, avatar_url, role') .eq('id', user.id) .maybeSingle(); if (r == null) return Profile(id: user.id); return Profile( id: r['id'] as String, name: r['display_name'] as String?, phone: r['phone'] as String?, hatti: r['hatti_name'] as String?, avatarUrl: r['avatar_url'] as String?, role: r['role'] as String? ?? 'listener', ); }); class ProfileScreen extends ConsumerWidget { const ProfileScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final profile = ref.watch(profileProvider); return Scaffold( appBar: AppBar(title: const Text('Your profile')), body: profile.when( loading: () => const Center(child: CircularProgressIndicator()), error: (_, _) => Center( child: TextButton(onPressed: () => ref.invalidate(profileProvider), child: const Text('Could not load. Retry')), ), data: (p) => p == null ? const SizedBox.shrink() : _ProfileForm(profile: p), ), ); } } class _ProfileForm extends ConsumerStatefulWidget { const _ProfileForm({required this.profile}); final Profile profile; @override ConsumerState<_ProfileForm> createState() => _ProfileFormState(); } class _ProfileFormState extends ConsumerState<_ProfileForm> { late final _name = TextEditingController(text: widget.profile.name); late final _phone = TextEditingController(text: widget.profile.phone); late final _hatti = TextEditingController(text: widget.profile.hatti); late String? _avatarUrl = widget.profile.avatarUrl; bool _saving = false; bool _uploading = false; SupabaseClient get _client => Supabase.instance.client; @override void dispose() { _name.dispose(); _phone.dispose(); _hatti.dispose(); super.dispose(); } void _toast(String message) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); String? _clean(TextEditingController c) => c.text.trim().isEmpty ? null : c.text.trim(); Future _save() async { setState(() => _saving = true); try { await _client.from('profiles').update({ 'display_name': _clean(_name), 'phone': _clean(_phone), 'hatti_name': _clean(_hatti), 'updated_at': DateTime.now().toUtc().toIso8601String(), }).eq('id', widget.profile.id); ref.invalidate(profileProvider); _toast('Saved'); } catch (_) { _toast('Could not save. Check your connection.'); } finally { if (mounted) setState(() => _saving = false); } } Future _pickPhoto() async { final file = await ImagePicker().pickImage(source: ImageSource.gallery, maxWidth: 512, maxHeight: 512, imageQuality: 85); if (file == null) return; setState(() => _uploading = true); try { final path = '${widget.profile.id}/avatar-${DateTime.now().millisecondsSinceEpoch}.jpg'; await _client.storage .from('avatars') .uploadBinary(path, await file.readAsBytes(), fileOptions: const FileOptions(contentType: 'image/jpeg')); final url = _client.storage.from('avatars').getPublicUrl(path); await _client.from('profiles').update({'avatar_url': url}).eq('id', widget.profile.id); setState(() => _avatarUrl = url); ref.invalidate(profileProvider); } catch (_) { _toast('Could not upload the photo.'); } finally { if (mounted) setState(() => _uploading = false); } } Future _changePassword() async { final password = await showDialog(context: context, builder: (_) => const _PasswordDialog()); if (password == null) return; try { await _client.auth.updateUser(UserAttributes(password: password)); _toast('Password updated. Use it next time you sign in.'); } on AuthException catch (e) { _toast(e.message); } } Future _signOut() async { await _client.auth.signOut(); if (mounted) context.go('/login'); } Future _deleteAccount() async { final sure = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Delete your account?'), content: const Text( 'Your profile, likes and playlists are removed for good. Songs in the app are not affected. ' 'This cannot be undone.', ), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), FilledButton( style: FilledButton.styleFrom(backgroundColor: NkColors.like), onPressed: () => Navigator.pop(context, true), child: const Text('Delete'), ), ], ), ); if (sure != true) return; try { await _client.rpc('delete_my_account'); await _client.auth.signOut(); if (mounted) context.go('/welcome'); } catch (_) { _toast('Could not delete the account. Please try again.'); } } @override Widget build(BuildContext context) { final email = _client.auth.currentUser?.email ?? ''; final initial = (_name.text.trim().isNotEmpty ? _name.text.trim() : email).characters.first.toUpperCase(); return ListView( padding: const EdgeInsets.fromLTRB(20, 8, 20, 32), children: [ Center( child: Stack( children: [ CircleAvatar( radius: 48, backgroundColor: NkColors.emblemGreen, backgroundImage: _avatarUrl == null ? null : CachedNetworkImageProvider(_avatarUrl!), child: _avatarUrl == null ? Text(initial, style: const TextStyle(fontSize: 36, fontWeight: FontWeight.w800, color: NkColors.cream)) : null, ), Positioned( right: 0, bottom: 0, child: IconButton.filled( tooltip: 'Change photo', onPressed: _uploading ? null : _pickPhoto, icon: _uploading ? const SizedBox.square(dimension: 18, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.photo_camera_outlined, size: 20), ), ), ], ), ), const SizedBox(height: 8), Center(child: Text(email, style: const TextStyle(color: NkColors.muted))), if (widget.profile.role != 'listener') Center( child: Padding( padding: const EdgeInsets.only(top: 8), child: Chip(label: Text(widget.profile.role == 'admin' ? 'Admin' : 'Verified creator')), ), ), const SizedBox(height: 24), TextField( controller: _name, textCapitalization: TextCapitalization.words, decoration: const InputDecoration(labelText: 'Name'), ), const SizedBox(height: 12), TextField( controller: _hatti, textCapitalization: TextCapitalization.words, decoration: const InputDecoration(labelText: 'Village / hatti (optional)'), ), const SizedBox(height: 12), TextField( controller: _phone, keyboardType: TextInputType.phone, decoration: const InputDecoration( labelText: 'Phone number (optional)', helperText: 'Only visible to the Nanga Kathai team.', ), ), const SizedBox(height: 20), FilledButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox.square(dimension: 22, child: CircularProgressIndicator(strokeWidth: 2.5)) : const Text('Save'), ), const Divider(height: 40, color: NkColors.outline), ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.lock_outline), title: const Text('Set or change password'), onTap: _changePassword, ), ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.info_outline), title: const Text('About Nanga Kathai'), onTap: () => context.push('/about'), ), ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.logout), title: const Text('Sign out'), onTap: _signOut, ), ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.delete_outline, color: NkColors.like), title: const Text('Delete account', style: TextStyle(color: NkColors.like)), onTap: _deleteAccount, ), ], ); } } class _PasswordDialog extends StatefulWidget { const _PasswordDialog(); @override State<_PasswordDialog> createState() => _PasswordDialogState(); } class _PasswordDialogState extends State<_PasswordDialog> { final _a = TextEditingController(); final _b = TextEditingController(); @override void dispose() { _a.dispose(); _b.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final ok = _a.text.length >= 8 && _a.text == _b.text; return AlertDialog( title: const Text('New password'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _a, obscureText: true, autofillHints: const [AutofillHints.newPassword], onChanged: (_) => setState(() {}), decoration: const InputDecoration(labelText: 'New password', helperText: 'At least 8 characters'), ), const SizedBox(height: 12), TextField( controller: _b, obscureText: true, onChanged: (_) => setState(() {}), decoration: InputDecoration( labelText: 'Repeat password', errorText: _b.text.isNotEmpty && _a.text != _b.text ? 'Passwords do not match' : null, ), ), ], ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), FilledButton(onPressed: ok ? () => Navigator.pop(context, _a.text) : null, child: const Text('Save')), ], ); } }