// ignore_for_file: use_build_context_synchronously, prefer_const_constructors, deprecated_member_use // lib/features/settings/user_settings_screen.dart import 'dart:async'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../../app/app_cubit.dart'; import '../../app/injection_container.dart'; import '../../core/constants/app_constants.dart'; import '../../core/errors/friendly_error.dart'; import '../../core/network/api_client.dart'; import '../../core/services/haptic_service.dart'; import '../../core/services/tts_service.dart'; import '../../core/storage/secure_storage.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_decorations.dart'; import '../../core/theme/app_text_styles.dart'; Dio get _api => sl().dio; // ─── Colours (inline, tidak butuh import app_colors.dart) ──────────────────── const _kBlue = Color(0xFF1A56DB); const _kRed = Color(0xFFDC2626); const _kMuted = Color(0xFF64748B); // ─── Screen ────────────────────────────────────────────────────────────────── class UserSettingsScreen extends StatefulWidget { const UserSettingsScreen({super.key}); @override State createState() => _UserSettingsScreenState(); } class _UserSettingsScreenState extends State { // ── local state ──────────────────────────────────────────────────────────── bool _loading = true; bool _saving = false; // TTS String _ttsLanguage = 'id-ID'; double _ttsPitch = 1.0; // read-only for user, shown as info double _ttsSpeed = 0.9; // read-only for user, shown as info // Caution bool _warnNoGuardian = true; bool _hapticEnabled = true; // Account info (from SecureStorage) String _displayName = ''; // Pairing status String _pairingStatus = '—'; bool _paired = false; @override void initState() { super.initState(); sl().speak('Settings menu.'); _loadAll(); } Future _loadAll() async { setState(() => _loading = true); await Future.wait([_loadAccount(), _loadSettings(), _loadPairing()]); if (mounted) setState(() => _loading = false); } Future _loadAccount() async { final storage = sl(); _displayName = await storage.getDisplayName() ?? ''; } Future _loadSettings() async { await runFriendlyAction( () async { final res = await _api .get('/user/settings') .timeout(const Duration(seconds: 6)); final data = res.data['data']; if (data is Map) { _ttsLanguage = data['ttsLanguage']?.toString() ?? 'id-ID'; _ttsPitch = (data['ttsPitch'] as num?)?.toDouble() ?? 1.0; _ttsSpeed = (data['ttsSpeed'] as num?)?.toDouble() ?? 0.9; _warnNoGuardian = data['warnNoGuardian'] as bool? ?? true; _hapticEnabled = data['hapticEnabled'] as bool? ?? true; sl().setEnabled(_hapticEnabled); } }, onError: (_) {}, fallback: 'Settings belum bisa dimuat.', ); } Future _loadPairing() async { await runFriendlyAction( () async { final res = await _api .get('/shared/pairing/status') .timeout(const Duration(seconds: 5)); final data = res.data['data']; if (data is Map) { _paired = data['status'] == 'ACTIVE'; final partner = data['pairedWithName'] ?? data['pairedWithEmail'] ?? ''; _pairingStatus = _paired ? 'Terhubung dengan $partner' : data['status'] == 'PENDING' ? 'Menunggu konfirmasi Guardian' : 'Belum paired'; } }, onError: (_) => _pairingStatus = 'Tidak bisa cek status', fallback: 'Tidak bisa cek status', ); } Future _save() async { setState(() => _saving = true); // Apply TTS locally dulu await sl().setLanguage(_ttsLanguage); context.read().setLocaleCode(_ttsLanguage); sl().setEnabled(_hapticEnabled); if (_hapticEnabled) { await sl().success(); } await runFriendlyAction( () async { await _api.put('/user/settings', data: { 'ttsLanguage': _ttsLanguage, 'ttsPitch': _ttsPitch, 'ttsSpeed': _ttsSpeed, 'warnNoGuardian': _warnNoGuardian, 'hapticEnabled': _hapticEnabled, }).timeout(const Duration(seconds: 8)); _snack('Settings tersimpan.'); sl().speak('Settings disimpan.'); }, onError: _snack, fallback: 'Settings lokal sudah diterapkan, gagal sync ke server.', ); if (mounted) setState(() => _saving = false); } Future _logout() async { await sl().clearAll(); context.read().clearSession(); _api.post('/auth/logout').timeout(const Duration(seconds: 3)).ignore(); if (mounted) context.go('/login'); } Future _changeServer() async { await AppConstants.clearServerUrl(); if (mounted) context.go('/server-connect'); } void _snack(String msg) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); } } // ── build ────────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { return DecoratedBox( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [AppColors.softBlueBg, Colors.white], ), ), child: SafeArea( child: _loading ? const Center(child: CircularProgressIndicator()) : ListView( padding: const EdgeInsets.all(16), children: [ // ── header ───────────────────────────────────────────────── Text('Settings', style: AppTextStyles.heading.copyWith( fontWeight: FontWeight.w800, )), const Text('TTS, haptic, pairing, account', style: TextStyle(color: AppColors.muted)), const SizedBox(height: 20), // ── 1. TTS Settings ──────────────────────────────────────── _SectionHeader('1. TTS Settings', Icons.record_voice_over), const SizedBox(height: 10), _Card( child: Column( children: [ // Language (editable) DropdownButtonFormField( value: _ttsLanguage, decoration: const InputDecoration( labelText: 'Bahasa TTS', border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric( horizontal: 12, vertical: 10), ), items: const [ DropdownMenuItem( value: 'id-ID', child: Text('Bahasa Indonesia')), DropdownMenuItem( value: 'en-US', child: Text('English (US)')), ], onChanged: (v) => setState(() => _ttsLanguage = v ?? _ttsLanguage), ), const SizedBox(height: 12), // Pitch — read-only info _InfoRow( label: 'Pitch', value: _ttsPitch.toStringAsFixed(1), note: 'Diatur oleh Guardian', icon: Icons.tune, ), const Divider(height: 20), // Speed — read-only info _InfoRow( label: 'Speed', value: _ttsSpeed.toStringAsFixed(1), note: 'Diatur oleh Guardian', icon: Icons.speed, ), ], ), ), const SizedBox(height: 20), // ── 2. Pairing ───────────────────────────────────────────── _SectionHeader('2. Pairing', Icons.link), const SizedBox(height: 10), _Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( _paired ? Icons.link : Icons.link_off, color: _paired ? const Color(0xFF16A34A) : const Color(0xFFD97706), size: 20, ), const SizedBox(width: 10), Expanded( child: Text(_pairingStatus, style: TextStyle( color: _paired ? const Color(0xFF166534) : const Color(0xFF92400E), fontWeight: FontWeight.w600)), ), IconButton( icon: const Icon(Icons.refresh, size: 18), onPressed: () async { await _loadPairing(); setState(() {}); }, tooltip: 'Refresh status', ), ], ), const SizedBox(height: 8), const Text( 'Kode pairing dibuat dari menu Pairing dan hanya valid sementara. Jangan pakai Unique User ID untuk pairing.', style: TextStyle( color: _kMuted, fontSize: 12, height: 1.35, ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () => context.go('/user/pairing'), icon: const Icon(Icons.manage_accounts_outlined), label: const Text('Buka menu Pairing'), ), ), ], ), ), const SizedBox(height: 20), // ── 3. Manual / Instructions ─────────────────────────────── _SectionHeader('3. Manual & Instruksi', Icons.menu_book), const SizedBox(height: 10), _Card( child: ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.help_outline, color: _kBlue), title: const Text('Daftar Voice Commands & Shortcuts'), subtitle: const Text( 'Lihat semua perintah suara yang tersedia'), trailing: const Icon(Icons.chevron_right), onTap: () { context.go('/user/manual'); }, ), ), const SizedBox(height: 20), // ── 4. Caution Settings ──────────────────────────────────── _SectionHeader('4. Caution Settings', Icons.warning_amber), const SizedBox(height: 10), _Card( child: Column( children: [ SwitchListTile( contentPadding: EdgeInsets.zero, value: _warnNoGuardian, onChanged: (v) => setState(() => _warnNoGuardian = v), title: const Text('Peringatan belum paired'), subtitle: const Text( 'TTS ingatkan jika belum terhubung Guardian'), secondary: const Icon( Icons.notifications_active_outlined, color: _kBlue), ), const Divider(height: 1), SwitchListTile( contentPadding: EdgeInsets.zero, value: _hapticEnabled, onChanged: (v) { sl().setEnabled(v); setState(() => _hapticEnabled = v); }, title: const Text('Haptic feedback'), subtitle: const Text('Getaran saat obstacle terdeteksi'), secondary: const Icon(Icons.vibration, color: _kBlue), ), ], ), ), const SizedBox(height: 20), // ── 5. Account ───────────────────────────────────────────── _SectionHeader('5. Account', Icons.person), const SizedBox(height: 10), _Card( child: Column( children: [ _InfoRow( label: 'Display Name', value: _displayName.isNotEmpty ? _displayName : '—', icon: Icons.badge_outlined, ), const Divider(height: 20), _InfoRow( label: 'Role', value: 'User', icon: Icons.accessibility_new, ), ], ), ), const SizedBox(height: 24), // ── Save button ──────────────────────────────────────────── FilledButton.icon( onPressed: _saving ? null : _save, icon: _saving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white)) : const Icon(Icons.save_outlined), label: Text(_saving ? 'Menyimpan…' : 'Simpan Settings'), style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(48), ), ), const SizedBox(height: 10), // ── Change server ────────────────────────────────────────── OutlinedButton.icon( onPressed: _changeServer, icon: const Icon(Icons.dns_outlined), label: const Text('Ganti Server'), style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(44), ), ), const SizedBox(height: 10), // ── Logout ───────────────────────────────────────────────── OutlinedButton.icon( onPressed: () => _confirmLogout(context), icon: const Icon(Icons.logout, color: _kRed), label: const Text('Logout', style: TextStyle(color: _kRed)), style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(44), side: const BorderSide(color: _kRed), ), ), const SizedBox(height: 32), ], ), ), ); } Future _confirmLogout(BuildContext context) async { final confirm = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Logout?'), content: const Text('Sesi akan diakhiri. Kamu perlu login ulang.'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Batal')), FilledButton( style: FilledButton.styleFrom(backgroundColor: _kRed), onPressed: () => Navigator.pop(ctx, true), child: const Text('Logout'), ), ], ), ); if (confirm == true) await _logout(); } } // ─── Sub-widgets ───────────────────────────────────────────────────────────── class _SectionHeader extends StatelessWidget { final String title; final IconData icon; const _SectionHeader(this.title, this.icon); @override Widget build(BuildContext context) { return Row( children: [ Container( width: 36, height: 36, decoration: AppDecorations.iconCircle(color: AppColors.softBlueBg), child: Icon(icon, size: 18, color: AppColors.primaryBlue), ), const SizedBox(width: 8), Text(title, style: const TextStyle( fontWeight: FontWeight.w800, fontSize: 14, color: AppColors.primaryBlue)), ], ); } } class _Card extends StatelessWidget { final Widget child; const _Card({required this.child}); @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: AppDecorations.cardShadow, ), child: child, ); } } class _InfoRow extends StatelessWidget { final String label; final String value; final IconData icon; final String? note; const _InfoRow( {required this.label, required this.value, required this.icon, this.note}); @override Widget build(BuildContext context) { return Row( children: [ Container( width: 36, height: 36, decoration: AppDecorations.iconCircle(color: AppColors.softBlueBg), child: Icon(icon, size: 18, color: AppColors.primaryBlue), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 12, color: AppColors.muted)), Text(value, style: const TextStyle( fontWeight: FontWeight.w700, color: AppColors.textDark)), ], ), ), if (note != null) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: AppColors.softBlueBg, borderRadius: BorderRadius.circular(50), border: Border.all(color: AppColors.border), ), child: Text(note!, style: const TextStyle(fontSize: 11, color: AppColors.muted)), ), ], ); } }