mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-24 02:33:44 +03:00
Merge branch 'main' into feature/android-native
This commit is contained in:
commit
a9f7a2eef6
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'app/views/responsive_dialog.dart';
|
||||
import 'core/state.dart';
|
||||
import 'desktop/state.dart';
|
||||
|
||||
@ -14,13 +15,8 @@ class AboutPage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
return ResponsiveDialog(
|
||||
title: const Text('About Yubico Authenticator'),
|
||||
),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -41,8 +37,7 @@ class AboutPage extends ConsumerWidget {
|
||||
_log.info('Response', response['diagnostics']);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content:
|
||||
Text('Diagnostics done. See log for results...'),
|
||||
content: Text('Diagnostics done. See log for results...'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@ -51,8 +46,6 @@ class AboutPage extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../management/views/management_screen.dart';
|
||||
import '../../about_page.dart';
|
||||
import '../../settings_page.dart';
|
||||
import '../models.dart';
|
||||
@ -78,12 +79,12 @@ class MainPageDrawer extends ConsumerWidget {
|
||||
DrawerItem(
|
||||
titleText: 'Toggle applications',
|
||||
icon: Icon(Application.management._icon),
|
||||
selected: Application.management == currentApp,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(currentAppProvider.notifier)
|
||||
.setCurrentApp(Application.management);
|
||||
if (shouldPop) Navigator.of(context).pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagementScreen(data),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
@ -103,9 +104,8 @@ class MainPageDrawer extends ConsumerWidget {
|
||||
onTap: () {
|
||||
final nav = Navigator.of(context);
|
||||
if (shouldPop) nav.pop();
|
||||
nav.push(
|
||||
MaterialPageRoute(builder: (context) => const SettingsPage()),
|
||||
);
|
||||
showDialog(
|
||||
context: context, builder: (context) => const SettingsPage());
|
||||
},
|
||||
),
|
||||
DrawerItem(
|
||||
@ -114,9 +114,8 @@ class MainPageDrawer extends ConsumerWidget {
|
||||
onTap: () {
|
||||
final nav = Navigator.of(context);
|
||||
if (shouldPop) nav.pop();
|
||||
nav.push(
|
||||
MaterialPageRoute(builder: (context) => const AboutPage()),
|
||||
);
|
||||
showDialog(
|
||||
context: context, builder: (context) => const AboutPage());
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -36,9 +36,9 @@ class MainPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final query = MediaQuery.of(context);
|
||||
if (query.size.width < 540) {
|
||||
Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 540) {
|
||||
// Single column layout
|
||||
return _buildScaffold(context, ref, true);
|
||||
} else {
|
||||
@ -55,7 +55,8 @@ class MainPage extends ConsumerWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Scaffold _buildScaffold(BuildContext context, WidgetRef ref, bool hasDrawer) {
|
||||
final deviceNode = ref.watch(currentDeviceProvider);
|
||||
|
51
lib/app/views/responsive_dialog.dart
Executable file
51
lib/app/views/responsive_dialog.dart
Executable file
@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ResponsiveDialog extends StatelessWidget {
|
||||
final Widget? title;
|
||||
final Widget child;
|
||||
final List<Widget> actions;
|
||||
|
||||
const ResponsiveDialog(
|
||||
{Key? key, required this.child, this.title, this.actions = const []})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
LayoutBuilder(builder: ((context, constraints) {
|
||||
if (constraints.maxWidth < 540) {
|
||||
// Fullscreen
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(0),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: title,
|
||||
actions: actions,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
child: child,
|
||||
),
|
||||
));
|
||||
} else {
|
||||
// Dialog
|
||||
return AlertDialog(
|
||||
insetPadding: EdgeInsets.zero,
|
||||
title: title,
|
||||
scrollable: true,
|
||||
content: SizedBox(
|
||||
width: 380,
|
||||
child: child,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
...actions
|
||||
],
|
||||
);
|
||||
}
|
||||
}));
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:yubico_authenticator/app/views/app_loading_screen.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/app_failure_screen.dart';
|
||||
import '../../app/views/app_loading_screen.dart';
|
||||
import '../../app/views/responsive_dialog.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
|
||||
@ -92,37 +94,22 @@ class _ModeFormState extends State<_ModeForm> {
|
||||
}
|
||||
}
|
||||
|
||||
class _CapabilitiesForm extends StatefulWidget {
|
||||
final DeviceInfo info;
|
||||
final Function(Map<Transport, int>) onSubmit;
|
||||
class _CapabilitiesForm extends StatelessWidget {
|
||||
final Map<Transport, int> supported;
|
||||
final Map<Transport, int> enabled;
|
||||
final Function(Map<Transport, int> enabled) onChanged;
|
||||
|
||||
const _CapabilitiesForm(this.info, {required this.onSubmit, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _CapabilitiesFormState();
|
||||
}
|
||||
|
||||
class _CapabilitiesFormState extends State<_CapabilitiesForm> {
|
||||
late Map<Transport, int> _enabled;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Make sure to copy enabledCapabilites, not mutate the original.
|
||||
_enabled = {...widget.info.config.enabledCapabilities};
|
||||
}
|
||||
const _CapabilitiesForm({
|
||||
required this.onChanged,
|
||||
required this.supported,
|
||||
required this.enabled,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final usbCapabilities =
|
||||
widget.info.supportedCapabilities[Transport.usb] ?? 0;
|
||||
final nfcCapabilities =
|
||||
widget.info.supportedCapabilities[Transport.nfc] ?? 0;
|
||||
|
||||
final changed =
|
||||
!_mapEquals(widget.info.config.enabledCapabilities, _enabled);
|
||||
final valid = changed && (_enabled[Transport.usb] ?? 0) > 0;
|
||||
final usbCapabilities = supported[Transport.usb] ?? 0;
|
||||
final nfcCapabilities = supported[Transport.nfc] ?? 0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -134,11 +121,9 @@ class _CapabilitiesFormState extends State<_CapabilitiesForm> {
|
||||
),
|
||||
_CapabilityForm(
|
||||
capabilities: usbCapabilities,
|
||||
enabled: _enabled[Transport.usb] ?? 0,
|
||||
onChanged: (enabled) {
|
||||
setState(() {
|
||||
_enabled[Transport.usb] = enabled;
|
||||
});
|
||||
enabled: enabled[Transport.usb] ?? 0,
|
||||
onChanged: (value) {
|
||||
onChanged({...enabled, Transport.usb: value});
|
||||
},
|
||||
),
|
||||
if (nfcCapabilities != 0)
|
||||
@ -148,44 +133,56 @@ class _CapabilitiesFormState extends State<_CapabilitiesForm> {
|
||||
),
|
||||
_CapabilityForm(
|
||||
capabilities: nfcCapabilities,
|
||||
enabled: _enabled[Transport.nfc] ?? 0,
|
||||
onChanged: (enabled) {
|
||||
setState(() {
|
||||
_enabled[Transport.nfc] = enabled;
|
||||
});
|
||||
enabled: enabled[Transport.nfc] ?? 0,
|
||||
onChanged: (value) {
|
||||
onChanged({...enabled, Transport.nfc: value});
|
||||
},
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton(
|
||||
onPressed: valid
|
||||
? () {
|
||||
widget.onSubmit(_enabled);
|
||||
}
|
||||
: null,
|
||||
child: const Text('Apply changes'),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ManagementScreen extends ConsumerWidget {
|
||||
class ManagementScreen extends ConsumerStatefulWidget {
|
||||
final YubiKeyData deviceData;
|
||||
const ManagementScreen(this.deviceData, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||
_ManagementScreenState();
|
||||
}
|
||||
|
||||
class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
late Map<Transport, int> _enabled;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_enabled = widget.deviceData.info.config.enabledCapabilities;
|
||||
}
|
||||
|
||||
Widget _buildCapabilitiesForm(
|
||||
BuildContext context, WidgetRef ref, DeviceInfo info) =>
|
||||
_CapabilitiesForm(info, onSubmit: (enabled) async {
|
||||
BuildContext context, WidgetRef ref, DeviceInfo info) {
|
||||
return _CapabilitiesForm(
|
||||
supported: widget.deviceData.info.supportedCapabilities,
|
||||
enabled: _enabled,
|
||||
onChanged: (enabled) {
|
||||
setState(() {
|
||||
_enabled = enabled;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _submitCapabilitiesForm() async {
|
||||
final bool reboot;
|
||||
if (deviceData.node is UsbYubiKeyNode) {
|
||||
if (widget.deviceData.node is UsbYubiKeyNode) {
|
||||
// Reboot if USB device descriptor is changed.
|
||||
final oldInterfaces = UsbInterfaces.forCapabilites(
|
||||
info.config.enabledCapabilities[Transport.usb] ?? 0);
|
||||
widget.deviceData.info.config.enabledCapabilities[Transport.usb] ??
|
||||
0);
|
||||
final newInterfaces =
|
||||
UsbInterfaces.forCapabilites(enabled[Transport.usb] ?? 0);
|
||||
UsbInterfaces.forCapabilites(_enabled[Transport.usb] ?? 0);
|
||||
reboot = oldInterfaces != newInterfaces;
|
||||
} else {
|
||||
reboot = false;
|
||||
@ -203,9 +200,10 @@ class ManagementScreen extends ConsumerWidget {
|
||||
.close;
|
||||
}
|
||||
await ref
|
||||
.read(managementStateProvider(deviceData.node.path).notifier)
|
||||
.read(managementStateProvider(widget.deviceData.node.path).notifier)
|
||||
.writeConfig(
|
||||
info.config.copyWith(enabledCapabilities: enabled),
|
||||
widget.deviceData.info.config
|
||||
.copyWith(enabledCapabilities: _enabled),
|
||||
reboot: reboot,
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
@ -215,7 +213,7 @@ class ManagementScreen extends ConsumerWidget {
|
||||
} finally {
|
||||
close?.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildModeForm(BuildContext context, WidgetRef ref, DeviceInfo info) =>
|
||||
_ModeForm(
|
||||
@ -229,15 +227,41 @@ class ManagementScreen extends ConsumerWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) =>
|
||||
ref.watch(managementStateProvider(deviceData.node.path)).when(
|
||||
Widget build(BuildContext context) {
|
||||
ref.listen<DeviceNode?>(currentDeviceProvider, (_, __) {
|
||||
//TODO: This can probably be checked better to make sure it's the main page.
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
});
|
||||
|
||||
bool changed = false;
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Toggle applications'),
|
||||
child:
|
||||
ref.watch(managementStateProvider(widget.deviceData.node.path)).when(
|
||||
none: () => const AppLoadingScreen(),
|
||||
failure: (reason) => AppFailureScreen(reason),
|
||||
success: (info) => ListView(
|
||||
success: (info) {
|
||||
// TODO: Check mode for < YK5 intead
|
||||
changed = !_mapEquals(
|
||||
_enabled,
|
||||
info.config.enabledCapabilities,
|
||||
);
|
||||
return Column(
|
||||
children: [
|
||||
info.version.major > 4
|
||||
? _buildCapabilitiesForm(context, ref, info)
|
||||
: _buildModeForm(context, ref, info),
|
||||
],
|
||||
));
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: changed ? _submitCapabilitiesForm : null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -19,11 +19,10 @@ List<MenuAction> buildOathMenuActions(AutoDisposeProviderRef ref) {
|
||||
text: 'Add credential',
|
||||
icon: const Icon(Icons.add),
|
||||
action: (context) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
OathAddAccountPage(device: device),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -154,7 +154,8 @@ mixin AccountMixin {
|
||||
return await showDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteAccountDialog(node, credential),
|
||||
);
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
@protected
|
||||
|
@ -3,10 +3,11 @@ import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:yubico_authenticator/oath/models.dart';
|
||||
|
||||
import '../../app/state.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/views/responsive_dialog.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
@ -15,21 +16,21 @@ final _secretFormatterPattern =
|
||||
|
||||
enum _QrScanState { none, scanning, success, failed }
|
||||
|
||||
class AddAccountForm extends ConsumerStatefulWidget {
|
||||
final Function(CredentialData, bool) onSubmit;
|
||||
const AddAccountForm({Key? key, required this.onSubmit}) : super(key: key);
|
||||
class OathAddAccountPage extends ConsumerStatefulWidget {
|
||||
const OathAddAccountPage({required this.device, Key? key}) : super(key: key);
|
||||
final DeviceNode device;
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _AddAccountFormState();
|
||||
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||
_OathAddAccountPageState();
|
||||
}
|
||||
|
||||
class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
||||
class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
final _issuerController = TextEditingController();
|
||||
final _accountController = TextEditingController();
|
||||
final _secretController = TextEditingController();
|
||||
final _periodController = TextEditingController(text: '$defaultPeriod');
|
||||
bool _touch = false;
|
||||
bool _advanced = false;
|
||||
OathType _oathType = defaultOathType;
|
||||
HashAlgorithm _hashAlgorithm = defaultHashAlgorithm;
|
||||
int _digits = defaultDigits;
|
||||
@ -83,6 +84,12 @@ class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If current device changes, we need to pop back to the main Page.
|
||||
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
||||
//TODO: This can probably be checked better to make sure it's the main page.
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
});
|
||||
|
||||
final period = int.tryParse(_periodController.text) ?? -1;
|
||||
final remaining = getRemainingKeySpace(
|
||||
oathType: _oathType,
|
||||
@ -100,22 +107,24 @@ class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
||||
|
||||
final qrScanner = ref.watch(qrScannerProvider);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Add account'),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Account details',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
TextField(
|
||||
controller: _issuerController,
|
||||
autofocus: true,
|
||||
enabled: issuerRemaining > 0,
|
||||
maxLength: max(issuerRemaining, 1),
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Issuer (optional)',
|
||||
helperText:
|
||||
'', // Prevents dialog resizing when enabled = false
|
||||
helperText: '', // Prevents dialog resizing when enabled = false
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@ -127,9 +136,9 @@ class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
||||
controller: _accountController,
|
||||
maxLength: nameRemaining,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Account name',
|
||||
helperText:
|
||||
'', // Prevents dialog resizing when enabled = false
|
||||
helperText: '', // Prevents dialog resizing when enabled = false
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@ -143,6 +152,7 @@ class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
||||
FilteringTextInputFormatter.allow(_secretFormatterPattern)
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'Secret key',
|
||||
errorText: _validateSecretLength && !secretLengthValid
|
||||
? 'Invalid length'
|
||||
@ -171,42 +181,31 @@ class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const Divider(),
|
||||
Text(
|
||||
'Options',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: const Text('Require touch'),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: _touch,
|
||||
onChanged: (value) {
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
FilterChip(
|
||||
label: const Text('Require touch'),
|
||||
selected: _touch,
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
_touch = value ?? false;
|
||||
_touch = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: const Text('Show advanced settings'),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: _advanced,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_advanced = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_advanced)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<OathType>(
|
||||
decoration: const InputDecoration(labelText: 'Type'),
|
||||
Chip(
|
||||
label: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<OathType>(
|
||||
value: _oathType,
|
||||
isDense: true,
|
||||
underline: null,
|
||||
items: OathType.values
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
@ -222,14 +221,13 @@ class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<HashAlgorithm>(
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Algorithm'),
|
||||
Chip(
|
||||
label: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<HashAlgorithm>(
|
||||
value: _hashAlgorithm,
|
||||
isDense: true,
|
||||
underline: null,
|
||||
items: HashAlgorithm.values
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
@ -245,74 +243,71 @@ class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_oathType == OathType.totp)
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _periodController,
|
||||
enabled: _qrState != _QrScanState.success,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
// Manual alignment to match digits-dropdown.
|
||||
const EdgeInsets.fromLTRB(0, 12, 0, 15),
|
||||
labelText: 'Period',
|
||||
errorText:
|
||||
period > 0 ? null : 'Must be a positive number',
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
// Update maxlengths
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_oathType == OathType.totp)
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<int>(
|
||||
decoration: const InputDecoration(labelText: 'Digits'),
|
||||
value: _digits,
|
||||
items: [6, 7, 8]
|
||||
Chip(
|
||||
label: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
value:
|
||||
int.tryParse(_periodController.text) ?? defaultPeriod,
|
||||
isDense: true,
|
||||
underline: null,
|
||||
items: [20, 30, 45, 60]
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(e.toString()),
|
||||
child: Text('$e sec'),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (value) {
|
||||
? (period) {
|
||||
setState(() {
|
||||
_digits = value ?? defaultDigits;
|
||||
_periodController.text =
|
||||
'${period ?? defaultPeriod}';
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
Chip(
|
||||
label: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
value: _digits,
|
||||
isDense: true,
|
||||
underline: null,
|
||||
items: [6, 7, 8]
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text('$e digits'),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (digits) {
|
||||
setState(() {
|
||||
_digits = digits ?? defaultDigits;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: e,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton(
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isValid
|
||||
? () {
|
||||
if (secretLengthValid) {
|
||||
final issuer = _issuerController.text;
|
||||
widget.onSubmit(
|
||||
CredentialData(
|
||||
|
||||
final cred = CredentialData(
|
||||
issuer: issuer.isEmpty ? null : issuer,
|
||||
name: _accountController.text,
|
||||
secret: secret,
|
||||
@ -320,8 +315,18 @@ class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
||||
hashAlgorithm: _hashAlgorithm,
|
||||
digits: _digits,
|
||||
period: period,
|
||||
);
|
||||
|
||||
ref
|
||||
.read(
|
||||
credentialListProvider(widget.device.path).notifier)
|
||||
.addAccount(cred.toUri(), requireTouch: _touch);
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Account added'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
_touch,
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
@ -330,47 +335,9 @@ class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: const Text('Add account'),
|
||||
),
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OathAddAccountPage extends ConsumerWidget {
|
||||
const OathAddAccountPage({required this.device, Key? key}) : super(key: key);
|
||||
final DeviceNode device;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// If current device changes, we need to pop back to the main Page.
|
||||
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
||||
//TODO: This can probably be checked better to make sure it's the main page.
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Add account'),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
AddAccountForm(
|
||||
onSubmit: (cred, requireTouch) {
|
||||
ref
|
||||
.read(credentialListProvider(device.path).notifier)
|
||||
.addAccount(cred.toUri(), requireTouch: requireTouch);
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Account added'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/views/responsive_dialog.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import '../../app/models.dart';
|
||||
@ -23,28 +24,27 @@ class DeleteAccountDialog extends ConsumerWidget {
|
||||
? '${credential.issuer} (${credential.name})'
|
||||
: credential.name;
|
||||
|
||||
return AlertDialog(
|
||||
title: Text('Delete $label?'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Delete account'),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Warning! This action will delete the account from your YubiKey.'),
|
||||
const Text(''),
|
||||
Text(
|
||||
'You will no longer be able to generate OTPs for this account. Make sure to first disable this credential from the website to avoid being locked out of your account.',
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
],
|
||||
Text('Account: $label'),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(credentialListProvider(device.path).notifier)
|
||||
@ -57,7 +57,7 @@ class DeleteAccountDialog extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Delete account'),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -1,20 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/views/responsive_dialog.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
|
||||
class ManagePasswordDialog extends ConsumerStatefulWidget {
|
||||
final DeviceNode device;
|
||||
const ManagePasswordDialog(this.device, {Key? key}) : super(key: key);
|
||||
class _ManagePasswordForm extends ConsumerStatefulWidget {
|
||||
final DevicePath path;
|
||||
final OathState state;
|
||||
const _ManagePasswordForm(this.path, this.state, {Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||
_ManagePasswordDialogState();
|
||||
_ManagePasswordFormState();
|
||||
}
|
||||
|
||||
class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
class _ManagePasswordFormState extends ConsumerState<_ManagePasswordForm> {
|
||||
String _currentPassword = '';
|
||||
String _newPassword = '';
|
||||
String _confirmPassword = '';
|
||||
@ -22,106 +26,42 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If current device changes, we need to pop back to the main Page.
|
||||
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
|
||||
return ref.watch(oathStateProvider(widget.device.path)).maybeWhen(
|
||||
success: (state) => AlertDialog(
|
||||
title: const Text('Manage password'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state.hasKey)
|
||||
Column(
|
||||
children: [
|
||||
if (state.remembered)
|
||||
// TODO: This is temporary, to be able to forget a password.
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: const [
|
||||
if (widget.state.hasKey) ...[
|
||||
Text(
|
||||
'Your password is remembered by the app.',
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold),
|
||||
'Current password',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(oathStateProvider(
|
||||
widget.device.path)
|
||||
.notifier)
|
||||
.forgetPassword();
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Password forgotten'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Forget'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'Enter your current password to change it. If you don\'t know your password, you\'ll need to reset the YubiKey, then create a new password.'),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Current',
|
||||
errorText: _currentIsWrong
|
||||
? 'Wrong password'
|
||||
: null),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'Current password',
|
||||
errorText: _currentIsWrong ? 'Wrong password' : null),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_currentPassword = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
child: const Text('Remove'),
|
||||
child: const Text('Remove password'),
|
||||
onPressed: _currentPassword.isNotEmpty
|
||||
? () async {
|
||||
final result = await ref
|
||||
.read(oathStateProvider(
|
||||
widget.device.path)
|
||||
.notifier)
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.unsetPassword(_currentPassword);
|
||||
if (result) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content:
|
||||
Text('Password removed'),
|
||||
content: Text('Password removed'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@ -133,41 +73,50 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
if (widget.state.remembered)
|
||||
OutlinedButton(
|
||||
child: const Text('Clear saved password'),
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.forgetPassword();
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Password forgotten'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
Text(
|
||||
'New password',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
const Text(
|
||||
'Enter your new password. A password may contain letters, numbers and other characters.'),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: !state.hasKey,
|
||||
TextField(
|
||||
autofocus: !widget.state.hasKey,
|
||||
obscureText: true,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Password'),
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'New password',
|
||||
enabled: !widget.state.hasKey || _currentPassword.isNotEmpty,
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_newPassword = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
TextField(
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Confirm',
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'Confirm password',
|
||||
enabled: _newPassword.isNotEmpty,
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@ -175,26 +124,15 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: ElevatedButton(
|
||||
onPressed: _newPassword.isNotEmpty &&
|
||||
_newPassword == _confirmPassword &&
|
||||
(!state.hasKey || _currentPassword.isNotEmpty)
|
||||
(!widget.state.hasKey || _currentPassword.isNotEmpty)
|
||||
? () async {
|
||||
final result = await ref
|
||||
.read(
|
||||
oathStateProvider(widget.device.path).notifier)
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.setPassword(_currentPassword, _newPassword);
|
||||
if (result) {
|
||||
Navigator.of(context).pop();
|
||||
@ -212,8 +150,38 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
)
|
||||
],
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: e,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ManagePasswordDialog extends ConsumerWidget {
|
||||
final DeviceNode device;
|
||||
const ManagePasswordDialog(this.device, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// If current device changes, we need to pop back to the main Page.
|
||||
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
|
||||
return ref.watch(oathStateProvider(device.path)).maybeWhen(
|
||||
success: (state) => ResponsiveDialog(
|
||||
title: const Text('Manage password'),
|
||||
child: _ManagePasswordForm(
|
||||
device.path,
|
||||
state,
|
||||
// Prevents from losing state on responsive change.
|
||||
key: GlobalKey(),
|
||||
),
|
||||
),
|
||||
orElse: () {
|
||||
throw Exception(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/views/responsive_dialog.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import '../../app/models.dart';
|
||||
@ -53,11 +54,12 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
final nameRemaining = remaining.second;
|
||||
final isValid = _account.isNotEmpty;
|
||||
|
||||
return AlertDialog(
|
||||
title: Text('Rename $label?'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Rename account'),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Rename $label?'),
|
||||
const Text(
|
||||
'This will change how the account is displayed in the list.'),
|
||||
TextFormField(
|
||||
@ -65,6 +67,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
enabled: issuerRemaining > 0,
|
||||
maxLength: issuerRemaining > 0 ? issuerRemaining : null,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Issuer (optional)',
|
||||
helperText: '', // Prevents dialog resizing when enabled = false
|
||||
),
|
||||
@ -78,6 +81,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
initialValue: _account,
|
||||
maxLength: nameRemaining,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'Account name',
|
||||
helperText: '', // Prevents dialog resizing when enabled = false
|
||||
errorText: isValid ? null : 'Your account must have a name',
|
||||
@ -88,16 +92,15 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
TextButton(
|
||||
onPressed: isValid
|
||||
? () async {
|
||||
final renamed = await ref
|
||||
@ -113,7 +116,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: const Text('Rename account'),
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/views/responsive_dialog.dart';
|
||||
import '../state.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
@ -16,28 +17,25 @@ class ResetDialog extends ConsumerWidget {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text('Reset to defaults?'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Factory reset'),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Warning! This will irrevocably delete all OATH TOTP/HOTP accounts from your YubiKey.'),
|
||||
const Text(''),
|
||||
Text(
|
||||
'Your OATH credentials, as well as any password set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.',
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
],
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref.read(oathStateProvider(device.path).notifier).reset();
|
||||
Navigator.of(context).pop();
|
||||
@ -48,7 +46,7 @@ class ResetDialog extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Reset YubiKey'),
|
||||
child: const Text('Reset'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -1,8 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:yubico_authenticator/app/state.dart';
|
||||
|
||||
import 'app/state.dart';
|
||||
import 'app/views/responsive_dialog.dart';
|
||||
import 'core/state.dart';
|
||||
|
||||
final _log = Logger('settings');
|
||||
@ -12,13 +13,8 @@ class SettingsPage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Settings'),
|
||||
),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -29,8 +25,8 @@ class SettingsPage extends ConsumerWidget {
|
||||
items: [ThemeMode.system, ThemeMode.dark, ThemeMode.light]
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(
|
||||
e.name[0].toUpperCase() + e.name.substring(1)),
|
||||
child:
|
||||
Text(e.name[0].toUpperCase() + e.name.substring(1)),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (mode) {
|
||||
@ -53,8 +49,6 @@ class SettingsPage extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user