mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-19 08:02:12 +03:00
235 lines
7.8 KiB
Dart
Executable File
235 lines
7.8 KiB
Dart
Executable File
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../../app/state.dart';
|
|
import '../../core/state.dart';
|
|
import '../../widgets/list_title.dart';
|
|
import '../../widgets/responsive_dialog.dart';
|
|
|
|
const String _prefNfcOpenApp = 'prefNfcOpenApp';
|
|
const String _prefNfcBypassTouch = 'prefNfcBypassTouch';
|
|
const String _prefNfcCopyOtp = 'prefNfcCopyOtp';
|
|
const String _prefClipKbdLayout = 'prefClipKbdLayout';
|
|
|
|
// TODO: Get these from Android
|
|
const List<String> _keyboardLayouts = ['US', 'DE', 'DE-CH'];
|
|
const String _defaultClipKbdLayout = 'US';
|
|
|
|
enum _TapAction {
|
|
launch,
|
|
copy,
|
|
both;
|
|
|
|
String get description {
|
|
switch (this) {
|
|
case _TapAction.launch:
|
|
return 'Launch Yubico Authenticator';
|
|
case _TapAction.copy:
|
|
return 'Copy OTP to clipboard';
|
|
case _TapAction.both:
|
|
return 'Launch app and copy OTP';
|
|
}
|
|
}
|
|
|
|
Key get key {
|
|
switch (this) {
|
|
case _TapAction.launch:
|
|
return const Key('android.settings.on_nfc_tap.launch');
|
|
case _TapAction.copy:
|
|
return const Key('android.settings.on_nfc_tap.copy');
|
|
case _TapAction.both:
|
|
return const Key('android.settings.on_nfc_tap.both');
|
|
}
|
|
}
|
|
|
|
static _TapAction load(SharedPreferences prefs) {
|
|
final launchApp = prefs.getBool(_prefNfcOpenApp) ?? true;
|
|
final copyOtp = prefs.getBool(_prefNfcCopyOtp) ?? false;
|
|
if (launchApp && copyOtp) {
|
|
return both;
|
|
}
|
|
if (copyOtp) {
|
|
return copy;
|
|
}
|
|
// This is the default value if both are false.
|
|
return launch;
|
|
}
|
|
|
|
void save(SharedPreferences prefs) {
|
|
prefs.setBool(_prefNfcOpenApp, this != copy);
|
|
prefs.setBool(_prefNfcCopyOtp, this != launch);
|
|
}
|
|
}
|
|
|
|
extension on ThemeMode {
|
|
String get displayName {
|
|
switch (this) {
|
|
case ThemeMode.system:
|
|
return 'System default';
|
|
case ThemeMode.light:
|
|
return 'Light theme';
|
|
case ThemeMode.dark:
|
|
return 'Dark theme';
|
|
}
|
|
}
|
|
}
|
|
|
|
class AndroidSettingsPage extends ConsumerStatefulWidget {
|
|
const AndroidSettingsPage({super.key});
|
|
|
|
@override
|
|
ConsumerState<ConsumerStatefulWidget> createState() =>
|
|
_AndroidSettingsPageState();
|
|
}
|
|
|
|
class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final prefs = ref.watch(prefProvider);
|
|
|
|
final tapAction = _TapAction.load(prefs);
|
|
final clipKbdLayout =
|
|
prefs.getString(_prefClipKbdLayout) ?? _defaultClipKbdLayout;
|
|
final nfcBypassTouch = prefs.getBool(_prefNfcBypassTouch) ?? false;
|
|
final themeMode = ref.watch(themeModeProvider);
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
return ResponsiveDialog(
|
|
title: const Text('Settings'),
|
|
child: Theme(
|
|
// Make the headers use the primary color to pop a bit.
|
|
// Once M3 is implemented this will probably not be needed.
|
|
data: theme.copyWith(
|
|
textTheme: theme.textTheme.copyWith(
|
|
labelLarge: theme.textTheme.labelLarge
|
|
?.copyWith(color: theme.colorScheme.primary)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const ListTitle('NFC options'),
|
|
ListTile(
|
|
title: const Text('On YubiKey NFC tap'),
|
|
subtitle: Text(tapAction.description),
|
|
key: const Key('android.settings.option.on_nfc_tap'),
|
|
onTap: () async {
|
|
final newTapAction = await _selectTapAction(context, tapAction);
|
|
newTapAction.save(prefs);
|
|
setState(() {});
|
|
},
|
|
),
|
|
ListTile(
|
|
title: const Text('Keyboard Layout (for static password)'),
|
|
subtitle: Text(clipKbdLayout),
|
|
key: const Key('android.settings.option.keyboard_layout'),
|
|
enabled: tapAction != _TapAction.launch,
|
|
onTap: () async {
|
|
var newValue = await _selectKbdLayout(context, clipKbdLayout);
|
|
if (newValue != clipKbdLayout) {
|
|
await prefs.setString(_prefClipKbdLayout, newValue);
|
|
setState(() {});
|
|
}
|
|
},
|
|
),
|
|
SwitchListTile(
|
|
title: const Text('Bypass touch requirement'),
|
|
subtitle: nfcBypassTouch
|
|
? const Text(
|
|
'Accounts that require touch are automatically shown over NFC.')
|
|
: const Text(
|
|
'Accounts that require touch need an additional tap over NFC.'),
|
|
value: nfcBypassTouch,
|
|
key: const Key('android.settings.bypass_touch'),
|
|
onChanged: (value) {
|
|
prefs.setBool(_prefNfcBypassTouch, value);
|
|
setState(() {});
|
|
}),
|
|
const ListTitle('Appearance'),
|
|
ListTile(
|
|
title: const Text('App theme'),
|
|
subtitle: Text(themeMode.displayName),
|
|
onTap: () async {
|
|
final newMode = await _selectAppearance(context, themeMode);
|
|
ref.read(themeModeProvider.notifier).setThemeMode(newMode);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<_TapAction> _selectTapAction(
|
|
BuildContext context, _TapAction tapAction) async =>
|
|
await showDialog<_TapAction>(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return SimpleDialog(
|
|
title: const Text('On YubiKey NFC tap'),
|
|
children: _TapAction.values
|
|
.map(
|
|
(e) => RadioListTile<_TapAction>(
|
|
title: Text(e.description),
|
|
key: e.key,
|
|
value: e,
|
|
groupValue: tapAction,
|
|
toggleable: true,
|
|
onChanged: (mode) {
|
|
Navigator.pop(context, e);
|
|
}),
|
|
)
|
|
.toList(),
|
|
);
|
|
}) ??
|
|
_TapAction.launch;
|
|
|
|
Future<String> _selectKbdLayout(
|
|
BuildContext context, String currentKbdLayout) async =>
|
|
await showDialog<String>(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return SimpleDialog(
|
|
title: const Text('Choose keyboard layout'),
|
|
children: _keyboardLayouts
|
|
.map(
|
|
(e) => RadioListTile<String>(
|
|
title: Text(e),
|
|
value: e,
|
|
key: Key('android.settings.keyboard_layout.$e'),
|
|
toggleable: true,
|
|
groupValue: currentKbdLayout,
|
|
onChanged: (mode) {
|
|
Navigator.pop(context, e);
|
|
}),
|
|
)
|
|
.toList(),
|
|
);
|
|
}) ??
|
|
_defaultClipKbdLayout;
|
|
|
|
Future<ThemeMode> _selectAppearance(
|
|
BuildContext context, ThemeMode themeMode) async =>
|
|
await showDialog<ThemeMode>(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return SimpleDialog(
|
|
title: const Text('Choose app theme'),
|
|
children: ThemeMode.values
|
|
.map((e) => RadioListTile(
|
|
title: Text(e.displayName),
|
|
value: e,
|
|
groupValue: themeMode,
|
|
toggleable: true,
|
|
onChanged: (mode) {
|
|
Navigator.pop(context, e);
|
|
},
|
|
))
|
|
.toList(),
|
|
);
|
|
}) ??
|
|
ThemeMode.system;
|
|
}
|