415 lines
18 KiB
Dart
415 lines
18 KiB
Dart
import 'dart:ui';
|
|
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:flutter/foundation.dart'; // import kIsWeb
|
|
import 'package:http/http.dart' as http;
|
|
import '../utils/session_manager.dart';
|
|
import '../utils/api_config.dart';
|
|
import '../main.dart'; // import themeNotifier
|
|
|
|
class ProfileScreen extends StatefulWidget {
|
|
const ProfileScreen({super.key});
|
|
|
|
@override
|
|
State<ProfileScreen> createState() => _ProfileScreenState();
|
|
}
|
|
|
|
class _ProfileScreenState extends State<ProfileScreen> {
|
|
bool _isUploading = false;
|
|
|
|
// Fungsi untuk mengunggah foto profil
|
|
Future<void> _pickAndUploadPhoto() async {
|
|
final picker = ImagePicker();
|
|
final pickedFile = await picker.pickImage(source: ImageSource.gallery, imageQuality: 70);
|
|
|
|
if (pickedFile != null && mounted) {
|
|
setState(() => _isUploading = true);
|
|
|
|
try {
|
|
var request = http.MultipartRequest('POST', Uri.parse('${ApiConfig.baseUrl}/upload_photo.php'));
|
|
request.fields['id_users'] = SessionManager.userId.toString();
|
|
if (kIsWeb) {
|
|
// Web needs readAsBytes since path is just a blob URL
|
|
final bytes = await pickedFile.readAsBytes();
|
|
request.files.add(http.MultipartFile.fromBytes(
|
|
'photo',
|
|
bytes,
|
|
filename: pickedFile.name,
|
|
));
|
|
} else {
|
|
// Android/Mobile can use path
|
|
request.files.add(await http.MultipartFile.fromPath('photo', pickedFile.path));
|
|
}
|
|
|
|
var response = await request.send();
|
|
if (response.statusCode == 200) {
|
|
var responseData = await response.stream.bytesToString();
|
|
var data = jsonDecode(responseData);
|
|
|
|
// Check for 'status' instead of 'success' to match backend
|
|
if (data['status'] == 'success') {
|
|
setState(() {
|
|
SessionManager.userPhoto = data['photo_url']; // Save url photo
|
|
});
|
|
if(mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Foto profil diunggah!')));
|
|
} else {
|
|
if(mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Gagal: ${data['message']}')));
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if(mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Gagal unggah foto: $e')));
|
|
}
|
|
|
|
setState(() => _isUploading = false);
|
|
}
|
|
}
|
|
|
|
// Fungsi Hapus Akun
|
|
void _deleteAccount() {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext ctx) {
|
|
return AlertDialog(
|
|
title: const Text("Hapus Akun Permanen", style: TextStyle(color: Colors.red)),
|
|
content: const Text("Apakah Anda yakin ingin menghapus akun ini secara permanen? Data yang dihapus tidak dapat dipulihkan."),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text("Batal", style: TextStyle(color: Colors.grey)),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
Navigator.pop(ctx); // Tutup dialog konfirmasi
|
|
try {
|
|
final response = await http.post(
|
|
Uri.parse('${ApiConfig.baseUrl}/delete_account.php'),
|
|
body: {'id_users': SessionManager.userId.toString()},
|
|
);
|
|
final data = jsonDecode(response.body);
|
|
if (data['status'] == 'success' && mounted) {
|
|
// Kosongkan sesi lokal
|
|
SessionManager.userId = null;
|
|
SessionManager.userName = null;
|
|
SessionManager.userEmail = null;
|
|
SessionManager.userPhoto = null;
|
|
|
|
// Tampilkan pop-up keberhasilan
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
|
title: Column(
|
|
children: [
|
|
Icon(Icons.check_circle_outline, color: Colors.greenAccent[400], size: 70),
|
|
const SizedBox(height: 16),
|
|
const Text("Akun Berhasil Dihapus", style: TextStyle(fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
content: const Text(
|
|
"Akun Anda telah dihapus secara permanen.",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 15),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
// Berikan jeda 2.5 detik agar user sempat membaca popup
|
|
await Future.delayed(const Duration(milliseconds: 2500));
|
|
|
|
if (mounted) {
|
|
Navigator.pushNamedAndRemoveUntil(context, '/welcome', (route) => false);
|
|
}
|
|
} else {
|
|
if(mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Gagal menghapus: ${data['message']}')));
|
|
}
|
|
} catch(e) {
|
|
if(mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Terjadi kesalahan koneksi saat menghapus akun')));
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
|
|
child: const Text("Hapus", style: TextStyle(color: Colors.white)),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
// Fungsi untuk dummy dialog pusat bantuan
|
|
void _showInfoDialog(String title, String content) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
content: Text(content),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Tutup")),
|
|
],
|
|
)
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return Scaffold(
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
body: SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
// --- KARTU HEADER PROFIL DENGAN GRADIENT ---
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.only(top: 70, left: 24, right: 24, bottom: 40),
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [Color(0xFF2196F3), Color(0xFF00BCD4)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: Radius.circular(30),
|
|
bottomRight: Radius.circular(30),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// Avatar dengan GestureDetector untuk Edit Photo
|
|
GestureDetector(
|
|
onTap: _isUploading ? null : _pickAndUploadPhoto,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 38,
|
|
backgroundColor: Colors.blueAccent,
|
|
backgroundImage: (SessionManager.userPhoto != null && SessionManager.userPhoto!.isNotEmpty)
|
|
? NetworkImage("${ApiConfig.baseUrl}/${SessionManager.userPhoto}?t=${DateTime.now().millisecondsSinceEpoch}")
|
|
: null,
|
|
child: (SessionManager.userPhoto == null || SessionManager.userPhoto!.isEmpty)
|
|
? const Icon(Icons.person, size: 45, color: Colors.white)
|
|
: null,
|
|
),
|
|
if (_isUploading)
|
|
const CircularProgressIndicator(color: Colors.white)
|
|
else
|
|
Positioned(
|
|
bottom: 0,
|
|
right: 0,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: const BoxDecoration(color: Colors.black54, shape: BoxShape.circle),
|
|
child: const Icon(Icons.camera_alt, color: Colors.white, size: 14),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 20),
|
|
// Info nama dan email
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
SessionManager.userName ?? "Pengguna",
|
|
style: const TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
SessionManager.userEmail ?? "Tidak ada data",
|
|
style: const TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// --- KONTEN MENU PROFIL ---
|
|
Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
children: [
|
|
// --- BAGIAN PENGATURAN AKUN ---
|
|
_buildSectionHeader("Akun", isDark),
|
|
_buildGlassMenuTile(Icons.person_outline, "Edit Profil", () async {
|
|
final updated = await Navigator.pushNamed(context, '/edit_profile');
|
|
// Jika data kembali bernilai true, refresh halaman profil untuk menampilkan nama baru
|
|
if (updated == true) {
|
|
setState(() {});
|
|
}
|
|
}, isDark),
|
|
// Dark Mode Switch ditaruh di Akun
|
|
_buildGlassMenuTile(
|
|
themeNotifier.value == ThemeMode.dark ? Icons.dark_mode : Icons.light_mode,
|
|
"Mode Gelap",
|
|
() {
|
|
setState(() {
|
|
if(themeNotifier.value == ThemeMode.dark) {
|
|
themeNotifier.value = ThemeMode.light;
|
|
} else {
|
|
themeNotifier.value = ThemeMode.dark;
|
|
}
|
|
});
|
|
},
|
|
isDark,
|
|
trailingWidget: Switch(
|
|
value: themeNotifier.value == ThemeMode.dark,
|
|
onChanged: (val) {
|
|
setState(() {
|
|
themeNotifier.value = val ? ThemeMode.dark : ThemeMode.light;
|
|
});
|
|
},
|
|
activeColor: Colors.blueAccent,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// --- BAGIAN DUKUNGAN & LAINNYA ---
|
|
_buildSectionHeader("Dukungan & Lainnya", isDark),
|
|
_buildGlassMenuTile(Icons.help_outline, "Pusat Bantuan", () => _showInfoDialog("Pusat Bantuan", "Jika Anda memiliki kendala teknis atau saran perbaikan, hubungi tim kami di cs@smartcycling.com atau hubungi layanan Hotline bebas pulsa kami 1500-099."), isDark),
|
|
_buildGlassMenuTile(Icons.info_outline, "Tentang Aplikasi", () => _showInfoDialog("Tentang Aplikasi", "Smart Cycling App Versi 1.0.\nDikembangkan secara khusus untuk memberikan rekomendasi rute bagi pesepeda secara efisien."), isDark),
|
|
_buildGlassMenuTile(Icons.privacy_tip_outlined, "Kebijakan Privasi", () => _showInfoDialog("Kebijakan Privasi", "Informasi lokasi dan preferensi rute Anda tersimpan dengan aman hanya pada sesi aktif. Kami tidak memperjualbelikan rekam jejak pengguna."), isDark),
|
|
const SizedBox(height: 32),
|
|
|
|
// --- FITUR HAPUS AKUN & KELUAR ---
|
|
_buildGlassMenuTile(
|
|
Icons.delete_forever,
|
|
"Hapus Akun",
|
|
_deleteAccount,
|
|
isDark,
|
|
iconColor: Colors.orangeAccent,
|
|
textColor: Colors.orangeAccent,
|
|
showChevron: false,
|
|
),
|
|
_buildGlassMenuTile(
|
|
Icons.logout,
|
|
"Keluar",
|
|
() {
|
|
// Tampilkan pop-up konfirmasi
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text("Konfirmasi Keluar", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red)),
|
|
content: const Text("Apakah anda yakin ingin keluar?"),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(), // Tutup dialog
|
|
child: const Text("Batal", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.bold)),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop(); // Tutup dialog
|
|
// Kosongkan sesi lokal
|
|
SessionManager.userId = null;
|
|
SessionManager.userName = null;
|
|
SessionManager.userEmail = null;
|
|
SessionManager.userPhoto = null;
|
|
Navigator.pushNamedAndRemoveUntil(context, '/welcome', (route) => false); // Arahkan ke welcome & bersihkan stack
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.redAccent,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
child: const Text("Keluar", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
isDark,
|
|
iconColor: Colors.redAccent,
|
|
textColor: Colors.redAccent,
|
|
showChevron: false, // Hilangkan panah khusus untuk tombol keluar
|
|
),
|
|
const SizedBox(height: 24),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// --- HELPER WIDGETS ---
|
|
|
|
Widget _buildSectionHeader(String title, bool isDark) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: 16.0, bottom: 8.0, top: 4.0),
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: isDark ? Colors.white70 : Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildGlassMenuTile(IconData icon, String title, VoidCallback onTap, bool isDark, {Color? iconColor, Color? textColor, bool showChevron = true, Widget? trailingWidget}) {
|
|
Color defaultTxColor = isDark ? Colors.white : Colors.black87;
|
|
Color defaultIcColor = isDark ? Colors.white70 : Colors.black54;
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? Colors.white.withOpacity(0.1) : Colors.white.withOpacity(0.55), // Transparansi efek kaca yang disesuaikan tema
|
|
borderRadius: BorderRadius.circular(15),
|
|
border: Border.all(color: isDark ? Colors.white.withOpacity(0.2) : Colors.white.withOpacity(0.9), width: 1.5),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.02),
|
|
blurRadius: 10,
|
|
spreadRadius: 1,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(15),
|
|
child: BackdropFilter(
|
|
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), // Fitur blur khas glassmorphism
|
|
child: ListTile(
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 4.0),
|
|
leading: Icon(icon, color: iconColor ?? defaultIcColor, size: 26),
|
|
title: Text(
|
|
title,
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: textColor ?? defaultTxColor),
|
|
),
|
|
trailing: trailingWidget ?? (showChevron ? Icon(Icons.chevron_right, color: isDark ? Colors.white38 : Colors.black38, size: 20) : null),
|
|
onTap: onTap,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |