208 lines
6.6 KiB
Dart
208 lines
6.6 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:login_vault_app/auth_service.dart';
|
|
import 'package:login_vault_app/theme_provider.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.green;
|
|
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> _toggleBiometric(bool value) async {
|
|
if (value && !_biometricAvailable) {
|
|
setState(() {
|
|
_msg = 'Perangkat tidak mendukung biometrik';
|
|
_msgColor = Colors.red;
|
|
});
|
|
return;
|
|
}
|
|
// optimistic: move switch immediately so it feels responsive
|
|
if (mounted) setState(() => _biometricOn = value);
|
|
if (value) {
|
|
final (ok, err) = await widget.auth.authenticateWithBiometrics();
|
|
if (!ok) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_biometricOn = false;
|
|
_msg = 'Verifikasi biometrik gagal: ${err ?? "dibatalkan"}';
|
|
_msgColor = Colors.red;
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
await widget.auth.setBiometricEnabled(value);
|
|
}
|
|
|
|
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(),
|
|
ListTile(
|
|
leading: const Icon(Icons.exit_to_app),
|
|
title: const Text('Keluar (Exit)'),
|
|
onTap: () async {
|
|
await widget.auth.closeSession();
|
|
Navigator.of(context).popUntil((r) => r.isFirst);
|
|
if (Platform.isAndroid) {
|
|
SystemNavigator.pop();
|
|
} else {
|
|
exit(0);
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|