Files
login-vault-android/lib/settings_screen.dart

317 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:login_vault_app/auth_service.dart';
import 'package:login_vault_app/auth_screen.dart';
import 'package:login_vault_app/theme_provider.dart';
import 'package:login_vault_app/backup_service.dart';
/// Settings: change app password, switch theme (dark/system), session timeout,
/// fingerprint toggle, and Exit.
class SettingsScreen extends StatefulWidget {
final AuthService auth;
final ThemeProvider theme;
const SettingsScreen({super.key, required this.auth, required this.theme});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
final _oldCtrl = TextEditingController();
final _newCtrl = TextEditingController();
String? _msg;
Color _msgColor = Colors.grey;
bool _biometricOn = false;
bool _biometricAvailable = false;
int _timeout = 3;
final List<int> _timeoutOptions = [3, 5, 10, 15, 30, 60, 0];
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
final on = await widget.auth.isBiometricEnabled();
final avail = await widget.auth.canUseBiometrics();
final t = await widget.auth.getTimeoutMinutes();
if (mounted) {
setState(() {
_biometricOn = on && avail;
_biometricAvailable = avail;
_timeout = t;
});
}
}
Future<void> _changePassword() async {
final oldP = _oldCtrl.text;
final newP = _newCtrl.text;
if (newP.isEmpty) {
setState(() {
_msg = 'Password baru wajib diisi';
_msgColor = Colors.red;
});
return;
}
final stored = await widget.auth.getAppPassword();
if (stored != null && stored != '__google__' && stored != oldP) {
setState(() {
_msg = 'Password lama salah';
_msgColor = Colors.red;
});
return;
}
await widget.auth.changePassword(newP);
setState(() {
_msg = 'Password aplikasi berhasil diubah';
_msgColor = Colors.green;
});
_oldCtrl.clear();
_newCtrl.clear();
}
Future<void> _backup() async {
try {
final path = await BackupService.exportEncrypted(widget.auth);
if (mounted) {
setState(() {
_msg = 'Backup tersimpan: $path';
_msgColor = Colors.green;
});
}
} catch (e) {
if (mounted) {
setState(() {
_msg = 'Backup gagal: $e';
_msgColor = Colors.red;
});
}
}
}
Future<void> _restore() async {
try {
final backups = await BackupService.listBackups();
if (backups.isEmpty) {
if (mounted) {
setState(() {
_msg = 'Tidak ada file backup di folder Download/loginvault';
_msgColor = Colors.red;
});
}
return;
}
if (!mounted) return;
final chosen = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Pilih file backup'),
content: SizedBox(
width: double.maxFinite,
child: ListView.builder(
shrinkWrap: true,
itemCount: backups.length,
itemBuilder: (_, i) => ListTile(
title: Text(backups[i].path.split('/').last),
onTap: () => Navigator.of(ctx).pop(backups[i].path),
),
),
),
),
);
if (chosen == null) return;
final count = await BackupService.importEncrypted(widget.auth, chosen);
if (mounted) {
setState(() {
_msg = 'Restore berhasil: $count entri';
_msgColor = Colors.green;
});
}
} catch (e) {
if (mounted) {
setState(() {
_msg = 'Restore gagal: $e';
_msgColor = Colors.red;
});
}
}
}
Future<void> _toggleBiometric(bool value) async {
if (value && !_biometricAvailable) {
setState(() {
_msg = 'Perangkat tidak mendukung biometrik';
_msgColor = Colors.red;
});
return;
}
// When turning ON, ask for confirmation first. If cancelled, keep OFF.
if (value) {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Aktifkan Sidik Jari?'),
content: const Text(
'Saat login, Anda akan diminta verifikasi sidik jari '
'untuk masuk tanpa mengisi email & password.'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Batal'),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Aktifkan'),
),
],
),
);
if (confirmed != true) {
// cancelled -> revert switch to OFF
if (mounted) setState(() => _biometricOn = false);
return;
}
}
// Settings only sets the flag; actual biometric prompt happens at login.
await widget.auth.setBiometricEnabled(value);
if (mounted) {
setState(() {
_biometricOn = value;
_msg = value
? 'Sidik jari aktif. Akan diminta saat login.'
: 'Sidik jari nonaktif.';
_msgColor = value ? Colors.green : Colors.grey;
});
}
}
Future<void> _setTimeout(int? value) async {
if (value == null) return;
await widget.auth.setTimeoutMinutes(value);
if (mounted) setState(() => _timeout = value);
widget.auth.openSession(); // activity
}
String _timeoutLabel(int m) => m == 0 ? 'Selamanya' : '$m menit';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pengaturan')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text('Tema', style: TextStyle(fontWeight: FontWeight.bold)),
ListTile(
title: const Text('Gelap (Dark)'),
leading: const Icon(Icons.dark_mode),
trailing: Radio<ThemeMode>(
value: ThemeMode.dark,
groupValue: widget.theme.mode,
onChanged: (m) => widget.theme.setMode(m!),
),
onTap: () => widget.theme.setMode(ThemeMode.dark),
),
ListTile(
title: const Text('Sistem (System)'),
leading: const Icon(Icons.brightness_auto),
trailing: Radio<ThemeMode>(
value: ThemeMode.system,
groupValue: widget.theme.mode,
onChanged: (m) => widget.theme.setMode(m!),
),
onTap: () => widget.theme.setMode(ThemeMode.system),
),
const Divider(),
const Text('Session', style: TextStyle(fontWeight: FontWeight.bold)),
ListTile(
title: const Text('Timeout (tanpa aktivitas)'),
subtitle: Text('Logout otomatis setelah: ${_timeoutLabel(_timeout)}'),
trailing: PopupMenuButton<int>(
initialValue: _timeoutOptions.contains(_timeout) ? _timeout : _timeoutOptions.first,
onSelected: _setTimeout,
itemBuilder: (ctx) => _timeoutOptions
.map((m) => PopupMenuItem(value: m, child: Text(_timeoutLabel(m))))
.toList(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(_timeoutLabel(_timeout)),
const Icon(Icons.arrow_drop_down),
],
),
),
),
SwitchListTile(
title: const Text('Login dengan sidik jari'),
subtitle: Text(_biometricAvailable
? 'Gunakan fingerprint untuk membuka app'
: 'Perangkat tidak mendukung biometrik'),
value: _biometricOn,
onChanged: _toggleBiometric,
),
const Divider(),
const Text('Ubah Password Aplikasi',
style: TextStyle(fontWeight: FontWeight.bold)),
TextField(
controller: _oldCtrl,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password lama'),
),
TextField(
controller: _newCtrl,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password baru'),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: _changePassword,
child: const Text('Simpan Password'),
),
if (_msg != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(_msg!, style: TextStyle(color: _msgColor)),
),
const Divider(),
const Text('Backup & Restore',
style: TextStyle(fontWeight: FontWeight.bold)),
ListTile(
leading: const Icon(Icons.backup),
title: const Text('Backup ke file (lokal)'),
subtitle: const Text(
'Simpan vault terenkripsi ke folder Download. Bisa dipindah ke Drive manual.'),
onTap: _backup,
),
ListTile(
leading: const Icon(Icons.restore),
title: const Text('Restore dari file'),
subtitle: const Text('Pilih file backup .enc untuk dikembalikan.'),
onTap: _restore,
),
const Divider(),
ListTile(
leading: const Icon(Icons.exit_to_app),
title: const Text('Keluar (Exit)'),
onTap: () async {
await widget.auth.closeSession();
// restart to a fresh AuthScreen (re-evaluates registered +
// session state) instead of just popping back to Dashboard.
if (!mounted) return;
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (_) => AuthScreen(auth: widget.auth),
),
(r) => false,
);
},
),
],
),
);
}
}