mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-24 18:52:55 +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:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import 'app/views/responsive_dialog.dart';
|
||||||
import 'core/state.dart';
|
import 'core/state.dart';
|
||||||
import 'desktop/state.dart';
|
import 'desktop/state.dart';
|
||||||
|
|
||||||
@ -14,44 +15,36 @@ class AboutPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Scaffold(
|
return ResponsiveDialog(
|
||||||
appBar: AppBar(
|
title: const Text('About Yubico Authenticator'),
|
||||||
title: const Text('About Yubico Authenticator'),
|
child: Column(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
body: Center(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Padding(
|
children: [
|
||||||
padding: const EdgeInsets.all(8.0),
|
// TODO: Store the version number elsewhere
|
||||||
child: Column(
|
const Text('Yubico Authenticator version: 6.0.0-alpha.1'),
|
||||||
mainAxisSize: MainAxisSize.min,
|
if (isDesktop)
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Text('ykman version: ${ref.watch(rpcStateProvider).version}'),
|
||||||
children: [
|
Text('Dart version: ${Platform.version}'),
|
||||||
// TODO: Store the version number elsewhere
|
const SizedBox(height: 8.0),
|
||||||
const Text('Yubico Authenticator version: 6.0.0-alpha.1'),
|
const Divider(),
|
||||||
if (isDesktop)
|
if (isDesktop)
|
||||||
Text('ykman version: ${ref.watch(rpcStateProvider).version}'),
|
TextButton(
|
||||||
Text('Dart version: ${Platform.version}'),
|
onPressed: () async {
|
||||||
const SizedBox(height: 8.0),
|
_log.info('Running diagnostics...');
|
||||||
const Divider(),
|
final response =
|
||||||
if (isDesktop)
|
await ref.read(rpcProvider).command('diagnose', []);
|
||||||
TextButton(
|
_log.info('Response', response['diagnostics']);
|
||||||
onPressed: () async {
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
_log.info('Running diagnostics...');
|
const SnackBar(
|
||||||
final response =
|
content: Text('Diagnostics done. See log for results...'),
|
||||||
await ref.read(rpcProvider).command('diagnose', []);
|
duration: Duration(seconds: 2),
|
||||||
_log.info('Response', response['diagnostics']);
|
),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
);
|
||||||
const SnackBar(
|
},
|
||||||
content:
|
child: const Text('Run diagnostics...'),
|
||||||
Text('Diagnostics done. See log for results...'),
|
),
|
||||||
duration: Duration(seconds: 2),
|
],
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Text('Run diagnostics...'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../management/views/management_screen.dart';
|
||||||
import '../../about_page.dart';
|
import '../../about_page.dart';
|
||||||
import '../../settings_page.dart';
|
import '../../settings_page.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
@ -78,12 +79,12 @@ class MainPageDrawer extends ConsumerWidget {
|
|||||||
DrawerItem(
|
DrawerItem(
|
||||||
titleText: 'Toggle applications',
|
titleText: 'Toggle applications',
|
||||||
icon: Icon(Application.management._icon),
|
icon: Icon(Application.management._icon),
|
||||||
selected: Application.management == currentApp,
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref
|
|
||||||
.read(currentAppProvider.notifier)
|
|
||||||
.setCurrentApp(Application.management);
|
|
||||||
if (shouldPop) Navigator.of(context).pop();
|
if (shouldPop) Navigator.of(context).pop();
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ManagementScreen(data),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
@ -103,9 +104,8 @@ class MainPageDrawer extends ConsumerWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
final nav = Navigator.of(context);
|
final nav = Navigator.of(context);
|
||||||
if (shouldPop) nav.pop();
|
if (shouldPop) nav.pop();
|
||||||
nav.push(
|
showDialog(
|
||||||
MaterialPageRoute(builder: (context) => const SettingsPage()),
|
context: context, builder: (context) => const SettingsPage());
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
DrawerItem(
|
DrawerItem(
|
||||||
@ -114,9 +114,8 @@ class MainPageDrawer extends ConsumerWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
final nav = Navigator.of(context);
|
final nav = Navigator.of(context);
|
||||||
if (shouldPop) nav.pop();
|
if (shouldPop) nav.pop();
|
||||||
nav.push(
|
showDialog(
|
||||||
MaterialPageRoute(builder: (context) => const AboutPage()),
|
context: context, builder: (context) => const AboutPage());
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -36,26 +36,27 @@ class MainPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder(
|
||||||
final query = MediaQuery.of(context);
|
builder: (context, constraints) {
|
||||||
if (query.size.width < 540) {
|
if (constraints.maxWidth < 540) {
|
||||||
// Single column layout
|
// Single column layout
|
||||||
return _buildScaffold(context, ref, true);
|
return _buildScaffold(context, ref, true);
|
||||||
} else {
|
} else {
|
||||||
// Two-column layout
|
// Two-column layout
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 240,
|
width: 240,
|
||||||
child: MainPageDrawer(shouldPop: false),
|
child: MainPageDrawer(shouldPop: false),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildScaffold(context, ref, false),
|
child: _buildScaffold(context, ref, false),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold _buildScaffold(BuildContext context, WidgetRef ref, bool hasDrawer) {
|
Scaffold _buildScaffold(BuildContext context, WidgetRef ref, bool hasDrawer) {
|
||||||
final deviceNode = ref.watch(currentDeviceProvider);
|
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/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:yubico_authenticator/app/views/app_loading_screen.dart';
|
|
||||||
|
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
|
import '../../app/state.dart';
|
||||||
import '../../app/views/app_failure_screen.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 '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
|
|
||||||
@ -92,37 +94,22 @@ class _ModeFormState extends State<_ModeForm> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CapabilitiesForm extends StatefulWidget {
|
class _CapabilitiesForm extends StatelessWidget {
|
||||||
final DeviceInfo info;
|
final Map<Transport, int> supported;
|
||||||
final Function(Map<Transport, int>) onSubmit;
|
final Map<Transport, int> enabled;
|
||||||
|
final Function(Map<Transport, int> enabled) onChanged;
|
||||||
|
|
||||||
const _CapabilitiesForm(this.info, {required this.onSubmit, Key? key})
|
const _CapabilitiesForm({
|
||||||
: super(key: key);
|
required this.onChanged,
|
||||||
|
required this.supported,
|
||||||
@override
|
required this.enabled,
|
||||||
State<StatefulWidget> createState() => _CapabilitiesFormState();
|
Key? key,
|
||||||
}
|
}) : super(key: key);
|
||||||
|
|
||||||
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};
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final usbCapabilities =
|
final usbCapabilities = supported[Transport.usb] ?? 0;
|
||||||
widget.info.supportedCapabilities[Transport.usb] ?? 0;
|
final nfcCapabilities = supported[Transport.nfc] ?? 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;
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -134,11 +121,9 @@ class _CapabilitiesFormState extends State<_CapabilitiesForm> {
|
|||||||
),
|
),
|
||||||
_CapabilityForm(
|
_CapabilityForm(
|
||||||
capabilities: usbCapabilities,
|
capabilities: usbCapabilities,
|
||||||
enabled: _enabled[Transport.usb] ?? 0,
|
enabled: enabled[Transport.usb] ?? 0,
|
||||||
onChanged: (enabled) {
|
onChanged: (value) {
|
||||||
setState(() {
|
onChanged({...enabled, Transport.usb: value});
|
||||||
_enabled[Transport.usb] = enabled;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (nfcCapabilities != 0)
|
if (nfcCapabilities != 0)
|
||||||
@ -148,74 +133,87 @@ class _CapabilitiesFormState extends State<_CapabilitiesForm> {
|
|||||||
),
|
),
|
||||||
_CapabilityForm(
|
_CapabilityForm(
|
||||||
capabilities: nfcCapabilities,
|
capabilities: nfcCapabilities,
|
||||||
enabled: _enabled[Transport.nfc] ?? 0,
|
enabled: enabled[Transport.nfc] ?? 0,
|
||||||
onChanged: (enabled) {
|
onChanged: (value) {
|
||||||
setState(() {
|
onChanged({...enabled, Transport.nfc: value});
|
||||||
_enabled[Transport.nfc] = enabled;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
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;
|
final YubiKeyData deviceData;
|
||||||
const ManagementScreen(this.deviceData, {Key? key}) : super(key: key);
|
const ManagementScreen(this.deviceData, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
Widget _buildCapabilitiesForm(
|
@override
|
||||||
BuildContext context, WidgetRef ref, DeviceInfo info) =>
|
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||||
_CapabilitiesForm(info, onSubmit: (enabled) async {
|
_ManagementScreenState();
|
||||||
final bool reboot;
|
}
|
||||||
if (deviceData.node is UsbYubiKeyNode) {
|
|
||||||
// Reboot if USB device descriptor is changed.
|
|
||||||
final oldInterfaces = UsbInterfaces.forCapabilites(
|
|
||||||
info.config.enabledCapabilities[Transport.usb] ?? 0);
|
|
||||||
final newInterfaces =
|
|
||||||
UsbInterfaces.forCapabilites(enabled[Transport.usb] ?? 0);
|
|
||||||
reboot = oldInterfaces != newInterfaces;
|
|
||||||
} else {
|
|
||||||
reboot = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Function()? close;
|
class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||||
try {
|
late Map<Transport, int> _enabled;
|
||||||
if (reboot) {
|
|
||||||
// This will take longer, show a message
|
@override
|
||||||
close = ScaffoldMessenger.of(context)
|
void initState() {
|
||||||
.showSnackBar(const SnackBar(
|
super.initState();
|
||||||
content: Text('Reconfiguring YubiKey...'),
|
_enabled = widget.deviceData.info.config.enabledCapabilities;
|
||||||
duration: Duration(seconds: 8),
|
}
|
||||||
))
|
|
||||||
.close;
|
Widget _buildCapabilitiesForm(
|
||||||
}
|
BuildContext context, WidgetRef ref, DeviceInfo info) {
|
||||||
await ref
|
return _CapabilitiesForm(
|
||||||
.read(managementStateProvider(deviceData.node.path).notifier)
|
supported: widget.deviceData.info.supportedCapabilities,
|
||||||
.writeConfig(
|
enabled: _enabled,
|
||||||
info.config.copyWith(enabledCapabilities: enabled),
|
onChanged: (enabled) {
|
||||||
reboot: reboot,
|
setState(() {
|
||||||
);
|
_enabled = enabled;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
});
|
||||||
content: Text('Configuration updated'),
|
},
|
||||||
duration: Duration(seconds: 2),
|
);
|
||||||
));
|
}
|
||||||
} finally {
|
|
||||||
close?.call();
|
void _submitCapabilitiesForm() async {
|
||||||
}
|
final bool reboot;
|
||||||
});
|
if (widget.deviceData.node is UsbYubiKeyNode) {
|
||||||
|
// Reboot if USB device descriptor is changed.
|
||||||
|
final oldInterfaces = UsbInterfaces.forCapabilites(
|
||||||
|
widget.deviceData.info.config.enabledCapabilities[Transport.usb] ??
|
||||||
|
0);
|
||||||
|
final newInterfaces =
|
||||||
|
UsbInterfaces.forCapabilites(_enabled[Transport.usb] ?? 0);
|
||||||
|
reboot = oldInterfaces != newInterfaces;
|
||||||
|
} else {
|
||||||
|
reboot = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Function()? close;
|
||||||
|
try {
|
||||||
|
if (reboot) {
|
||||||
|
// This will take longer, show a message
|
||||||
|
close = ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(const SnackBar(
|
||||||
|
content: Text('Reconfiguring YubiKey...'),
|
||||||
|
duration: Duration(seconds: 8),
|
||||||
|
))
|
||||||
|
.close;
|
||||||
|
}
|
||||||
|
await ref
|
||||||
|
.read(managementStateProvider(widget.deviceData.node.path).notifier)
|
||||||
|
.writeConfig(
|
||||||
|
widget.deviceData.info.config
|
||||||
|
.copyWith(enabledCapabilities: _enabled),
|
||||||
|
reboot: reboot,
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||||
|
content: Text('Configuration updated'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
));
|
||||||
|
} finally {
|
||||||
|
close?.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildModeForm(BuildContext context, WidgetRef ref, DeviceInfo info) =>
|
Widget _buildModeForm(BuildContext context, WidgetRef ref, DeviceInfo info) =>
|
||||||
_ModeForm(
|
_ModeForm(
|
||||||
@ -229,15 +227,41 @@ class ManagementScreen extends ConsumerWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) =>
|
Widget build(BuildContext context) {
|
||||||
ref.watch(managementStateProvider(deviceData.node.path)).when(
|
ref.listen<DeviceNode?>(currentDeviceProvider, (_, __) {
|
||||||
none: () => const AppLoadingScreen(),
|
//TODO: This can probably be checked better to make sure it's the main page.
|
||||||
failure: (reason) => AppFailureScreen(reason),
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
success: (info) => ListView(
|
});
|
||||||
children: [
|
|
||||||
info.version.major > 4
|
bool changed = false;
|
||||||
? _buildCapabilitiesForm(context, ref, info)
|
|
||||||
: _buildModeForm(context, ref, info),
|
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) {
|
||||||
|
// 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',
|
text: 'Add credential',
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
action: (context) {
|
action: (context) {
|
||||||
Navigator.of(context).push(
|
showDialog(
|
||||||
MaterialPageRoute(
|
context: context,
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
OathAddAccountPage(device: device),
|
OathAddAccountPage(device: device),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -152,9 +152,10 @@ mixin AccountMixin {
|
|||||||
Future<bool> deleteCredential(BuildContext context, WidgetRef ref) async {
|
Future<bool> deleteCredential(BuildContext context, WidgetRef ref) async {
|
||||||
final node = ref.read(currentDeviceProvider)!;
|
final node = ref.read(currentDeviceProvider)!;
|
||||||
return await showDialog(
|
return await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => DeleteAccountDialog(node, credential),
|
builder: (context) => DeleteAccountDialog(node, credential),
|
||||||
);
|
) ??
|
||||||
|
false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
|
@ -3,10 +3,11 @@ import 'dart:math';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:yubico_authenticator/oath/models.dart';
|
|
||||||
|
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
|
import '../../app/views/responsive_dialog.dart';
|
||||||
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'utils.dart';
|
import 'utils.dart';
|
||||||
|
|
||||||
@ -15,21 +16,21 @@ final _secretFormatterPattern =
|
|||||||
|
|
||||||
enum _QrScanState { none, scanning, success, failed }
|
enum _QrScanState { none, scanning, success, failed }
|
||||||
|
|
||||||
class AddAccountForm extends ConsumerStatefulWidget {
|
class OathAddAccountPage extends ConsumerStatefulWidget {
|
||||||
final Function(CredentialData, bool) onSubmit;
|
const OathAddAccountPage({required this.device, Key? key}) : super(key: key);
|
||||||
const AddAccountForm({Key? key, required this.onSubmit}) : super(key: key);
|
final DeviceNode device;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<ConsumerStatefulWidget> createState() => _AddAccountFormState();
|
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||||
|
_OathAddAccountPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||||
final _issuerController = TextEditingController();
|
final _issuerController = TextEditingController();
|
||||||
final _accountController = TextEditingController();
|
final _accountController = TextEditingController();
|
||||||
final _secretController = TextEditingController();
|
final _secretController = TextEditingController();
|
||||||
final _periodController = TextEditingController(text: '$defaultPeriod');
|
final _periodController = TextEditingController(text: '$defaultPeriod');
|
||||||
bool _touch = false;
|
bool _touch = false;
|
||||||
bool _advanced = false;
|
|
||||||
OathType _oathType = defaultOathType;
|
OathType _oathType = defaultOathType;
|
||||||
HashAlgorithm _hashAlgorithm = defaultHashAlgorithm;
|
HashAlgorithm _hashAlgorithm = defaultHashAlgorithm;
|
||||||
int _digits = defaultDigits;
|
int _digits = defaultDigits;
|
||||||
@ -83,6 +84,12 @@ class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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 period = int.tryParse(_periodController.text) ?? -1;
|
||||||
final remaining = getRemainingKeySpace(
|
final remaining = getRemainingKeySpace(
|
||||||
oathType: _oathType,
|
oathType: _oathType,
|
||||||
@ -100,277 +107,237 @@ class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
|||||||
|
|
||||||
final qrScanner = ref.watch(qrScannerProvider);
|
final qrScanner = ref.watch(qrScannerProvider);
|
||||||
|
|
||||||
return Column(
|
return ResponsiveDialog(
|
||||||
children: [
|
title: const Text('Add account'),
|
||||||
Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.all(16.0),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Text(
|
||||||
children: [
|
'Account details',
|
||||||
TextField(
|
style: Theme.of(context).textTheme.headline6,
|
||||||
controller: _issuerController,
|
),
|
||||||
autofocus: true,
|
TextField(
|
||||||
enabled: issuerRemaining > 0,
|
controller: _issuerController,
|
||||||
maxLength: max(issuerRemaining, 1),
|
autofocus: true,
|
||||||
decoration: const InputDecoration(
|
enabled: issuerRemaining > 0,
|
||||||
labelText: 'Issuer (optional)',
|
maxLength: max(issuerRemaining, 1),
|
||||||
helperText:
|
decoration: const InputDecoration(
|
||||||
'', // Prevents dialog resizing when enabled = false
|
border: OutlineInputBorder(),
|
||||||
),
|
labelText: 'Issuer (optional)',
|
||||||
onChanged: (value) {
|
helperText: '', // Prevents dialog resizing when enabled = false
|
||||||
setState(() {
|
),
|
||||||
// Update maxlengths
|
onChanged: (value) {
|
||||||
});
|
setState(() {
|
||||||
},
|
// Update maxlengths
|
||||||
),
|
});
|
||||||
TextField(
|
},
|
||||||
controller: _accountController,
|
),
|
||||||
maxLength: nameRemaining,
|
TextField(
|
||||||
decoration: const InputDecoration(
|
controller: _accountController,
|
||||||
labelText: 'Account name',
|
maxLength: nameRemaining,
|
||||||
helperText:
|
decoration: const InputDecoration(
|
||||||
'', // Prevents dialog resizing when enabled = false
|
border: OutlineInputBorder(),
|
||||||
),
|
labelText: 'Account name',
|
||||||
onChanged: (value) {
|
helperText: '', // Prevents dialog resizing when enabled = false
|
||||||
setState(() {
|
),
|
||||||
// Update maxlengths
|
onChanged: (value) {
|
||||||
});
|
setState(() {
|
||||||
},
|
// Update maxlengths
|
||||||
),
|
});
|
||||||
TextField(
|
},
|
||||||
controller: _secretController,
|
),
|
||||||
inputFormatters: <TextInputFormatter>[
|
TextField(
|
||||||
FilteringTextInputFormatter.allow(_secretFormatterPattern)
|
controller: _secretController,
|
||||||
|
inputFormatters: <TextInputFormatter>[
|
||||||
|
FilteringTextInputFormatter.allow(_secretFormatterPattern)
|
||||||
|
],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'Secret key',
|
||||||
|
errorText: _validateSecretLength && !secretLengthValid
|
||||||
|
? 'Invalid length'
|
||||||
|
: null),
|
||||||
|
enabled: _qrState != _QrScanState.success,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_validateSecretLength = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (qrScanner != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 24.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
_scanQrCode(qrScanner);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.qr_code),
|
||||||
|
label: const Text('Scan QR code'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8.0),
|
||||||
|
..._buildQrStatus(),
|
||||||
],
|
],
|
||||||
decoration: InputDecoration(
|
),
|
||||||
labelText: 'Secret key',
|
),
|
||||||
errorText: _validateSecretLength && !secretLengthValid
|
const Divider(),
|
||||||
? 'Invalid length'
|
Text(
|
||||||
: null),
|
'Options',
|
||||||
enabled: _qrState != _QrScanState.success,
|
style: Theme.of(context).textTheme.headline6,
|
||||||
onChanged: (value) {
|
),
|
||||||
|
Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
spacing: 4.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: [
|
||||||
|
FilterChip(
|
||||||
|
label: const Text('Require touch'),
|
||||||
|
selected: _touch,
|
||||||
|
onSelected: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_validateSecretLength = false;
|
_touch = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (qrScanner != null)
|
Chip(
|
||||||
Padding(
|
label: DropdownButtonHideUnderline(
|
||||||
padding: const EdgeInsets.only(top: 24.0),
|
child: DropdownButton<OathType>(
|
||||||
child: Row(
|
value: _oathType,
|
||||||
children: [
|
isDense: true,
|
||||||
OutlinedButton.icon(
|
underline: null,
|
||||||
onPressed: () {
|
items: OathType.values
|
||||||
_scanQrCode(qrScanner);
|
.map((e) => DropdownMenuItem(
|
||||||
},
|
value: e,
|
||||||
icon: const Icon(Icons.qr_code),
|
child: Text(e.name.toUpperCase()),
|
||||||
label: const Text('Scan QR code'),
|
))
|
||||||
),
|
.toList(),
|
||||||
const SizedBox(width: 8.0),
|
onChanged: _qrState != _QrScanState.success
|
||||||
..._buildQrStatus(),
|
? (type) {
|
||||||
],
|
setState(() {
|
||||||
|
_oathType = type ?? OathType.totp;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Chip(
|
||||||
|
label: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<HashAlgorithm>(
|
||||||
|
value: _hashAlgorithm,
|
||||||
|
isDense: true,
|
||||||
|
underline: null,
|
||||||
|
items: HashAlgorithm.values
|
||||||
|
.map((e) => DropdownMenuItem(
|
||||||
|
value: e,
|
||||||
|
child: Text(e.name.toUpperCase()),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
onChanged: _qrState != _QrScanState.success
|
||||||
|
? (type) {
|
||||||
|
setState(() {
|
||||||
|
_hashAlgorithm = type ?? HashAlgorithm.sha1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_oathType == OathType.totp)
|
||||||
|
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 sec'),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
onChanged: _qrState != _QrScanState.success
|
||||||
|
? (period) {
|
||||||
|
setState(() {
|
||||||
|
_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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
]
|
||||||
CheckboxListTile(
|
.map((e) => Padding(
|
||||||
title: const Text('Require touch'),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
child: e,
|
||||||
value: _touch,
|
))
|
||||||
onChanged: (value) {
|
.toList(),
|
||||||
setState(() {
|
),
|
||||||
_touch = value ?? false;
|
actions: [
|
||||||
});
|
TextButton(
|
||||||
},
|
onPressed: isValid
|
||||||
),
|
? () {
|
||||||
CheckboxListTile(
|
if (secretLengthValid) {
|
||||||
title: const Text('Show advanced settings'),
|
final issuer = _issuerController.text;
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
value: _advanced,
|
final cred = CredentialData(
|
||||||
onChanged: (value) {
|
issuer: issuer.isEmpty ? null : issuer,
|
||||||
setState(() {
|
name: _accountController.text,
|
||||||
_advanced = value ?? false;
|
secret: secret,
|
||||||
});
|
oathType: _oathType,
|
||||||
},
|
hashAlgorithm: _hashAlgorithm,
|
||||||
),
|
digits: _digits,
|
||||||
if (_advanced)
|
period: period,
|
||||||
Padding(
|
);
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Column(
|
ref
|
||||||
children: [
|
.read(
|
||||||
Row(
|
credentialListProvider(widget.device.path).notifier)
|
||||||
mainAxisSize: MainAxisSize.max,
|
.addAccount(cred.toUri(), requireTouch: _touch);
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Navigator.of(context).pop();
|
||||||
children: [
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
Expanded(
|
const SnackBar(
|
||||||
child: DropdownButtonFormField<OathType>(
|
content: Text('Account added'),
|
||||||
decoration: const InputDecoration(labelText: 'Type'),
|
duration: Duration(seconds: 2),
|
||||||
value: _oathType,
|
|
||||||
items: OathType.values
|
|
||||||
.map((e) => DropdownMenuItem(
|
|
||||||
value: e,
|
|
||||||
child: Text(e.name.toUpperCase()),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
onChanged: _qrState != _QrScanState.success
|
|
||||||
? (type) {
|
|
||||||
setState(() {
|
|
||||||
_oathType = type ?? OathType.totp;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
const SizedBox(
|
} else {
|
||||||
width: 8.0,
|
setState(() {
|
||||||
),
|
_validateSecretLength = true;
|
||||||
Expanded(
|
});
|
||||||
child: DropdownButtonFormField<HashAlgorithm>(
|
|
||||||
decoration:
|
|
||||||
const InputDecoration(labelText: 'Algorithm'),
|
|
||||||
value: _hashAlgorithm,
|
|
||||||
items: HashAlgorithm.values
|
|
||||||
.map((e) => DropdownMenuItem(
|
|
||||||
value: e,
|
|
||||||
child: Text(e.name.toUpperCase()),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
onChanged: _qrState != _QrScanState.success
|
|
||||||
? (type) {
|
|
||||||
setState(() {
|
|
||||||
_hashAlgorithm = type ?? HashAlgorithm.sha1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
: 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]
|
|
||||||
.map((e) => DropdownMenuItem(
|
|
||||||
value: e,
|
|
||||||
child: Text(e.toString()),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
onChanged: _qrState != _QrScanState.success
|
|
||||||
? (value) {
|
|
||||||
setState(() {
|
|
||||||
_digits = value ?? defaultDigits;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: isValid
|
|
||||||
? () {
|
|
||||||
if (secretLengthValid) {
|
|
||||||
final issuer = _issuerController.text;
|
|
||||||
widget.onSubmit(
|
|
||||||
CredentialData(
|
|
||||||
issuer: issuer.isEmpty ? null : issuer,
|
|
||||||
name: _accountController.text,
|
|
||||||
secret: secret,
|
|
||||||
oathType: _oathType,
|
|
||||||
hashAlgorithm: _hashAlgorithm,
|
|
||||||
digits: _digits,
|
|
||||||
period: period,
|
|
||||||
),
|
|
||||||
_touch,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_validateSecretLength = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
: null,
|
}
|
||||||
child: const Text('Add account'),
|
: null,
|
||||||
),
|
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/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/views/responsive_dialog.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
@ -23,28 +24,27 @@ class DeleteAccountDialog extends ConsumerWidget {
|
|||||||
? '${credential.issuer} (${credential.name})'
|
? '${credential.issuer} (${credential.name})'
|
||||||
: credential.name;
|
: credential.name;
|
||||||
|
|
||||||
return AlertDialog(
|
return ResponsiveDialog(
|
||||||
title: Text('Delete $label?'),
|
title: const Text('Delete account'),
|
||||||
content: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'Warning! This action will delete the account from your YubiKey.'),
|
'Warning! This action will delete the account from your YubiKey.'),
|
||||||
const Text(''),
|
|
||||||
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.',
|
'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,
|
style: Theme.of(context).textTheme.bodyText1,
|
||||||
),
|
),
|
||||||
],
|
Text('Account: $label'),
|
||||||
|
]
|
||||||
|
.map((e) => Padding(
|
||||||
|
child: e,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop(false);
|
|
||||||
},
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await ref
|
await ref
|
||||||
.read(credentialListProvider(device.path).notifier)
|
.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/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/views/responsive_dialog.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
|
|
||||||
class ManagePasswordDialog extends ConsumerStatefulWidget {
|
class _ManagePasswordForm extends ConsumerStatefulWidget {
|
||||||
final DeviceNode device;
|
final DevicePath path;
|
||||||
const ManagePasswordDialog(this.device, {Key? key}) : super(key: key);
|
final OathState state;
|
||||||
|
const _ManagePasswordForm(this.path, this.state, {Key? key})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<ConsumerStatefulWidget> createState() =>
|
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||||
_ManagePasswordDialogState();
|
_ManagePasswordFormState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
class _ManagePasswordFormState extends ConsumerState<_ManagePasswordForm> {
|
||||||
String _currentPassword = '';
|
String _currentPassword = '';
|
||||||
String _newPassword = '';
|
String _newPassword = '';
|
||||||
String _confirmPassword = '';
|
String _confirmPassword = '';
|
||||||
@ -22,185 +26,42 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// If current device changes, we need to pop back to the main Page.
|
return Column(
|
||||||
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Navigator.of(context).pop();
|
children: [
|
||||||
});
|
if (widget.state.hasKey) ...[
|
||||||
|
Text(
|
||||||
return ref.watch(oathStateProvider(widget.device.path)).maybeWhen(
|
'Current password',
|
||||||
success: (state) => AlertDialog(
|
style: Theme.of(context).textTheme.headline6,
|
||||||
title: const Text('Manage password'),
|
),
|
||||||
content: Column(
|
TextField(
|
||||||
mainAxisSize: MainAxisSize.min,
|
autofocus: true,
|
||||||
children: [
|
obscureText: true,
|
||||||
if (state.hasKey)
|
decoration: InputDecoration(
|
||||||
Column(
|
border: const OutlineInputBorder(),
|
||||||
children: [
|
labelText: 'Current password',
|
||||||
if (state.remembered)
|
errorText: _currentIsWrong ? 'Wrong password' : null),
|
||||||
// TODO: This is temporary, to be able to forget a password.
|
onChanged: (value) {
|
||||||
Padding(
|
setState(() {
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
_currentPassword = value;
|
||||||
child: Column(
|
});
|
||||||
children: [
|
},
|
||||||
Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
Wrap(
|
||||||
children: const [
|
spacing: 8.0,
|
||||||
Text(
|
children: [
|
||||||
'Your password is remembered by the app.',
|
|
||||||
style:
|
|
||||||
TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
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(
|
|
||||||
autofocus: true,
|
|
||||||
obscureText: true,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Current',
|
|
||||||
errorText: _currentIsWrong
|
|
||||||
? 'Wrong password'
|
|
||||||
: null),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_currentPassword = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
OutlinedButton(
|
|
||||||
child: const Text('Remove'),
|
|
||||||
onPressed: _currentPassword.isNotEmpty
|
|
||||||
? () async {
|
|
||||||
final result = await ref
|
|
||||||
.read(oathStateProvider(
|
|
||||||
widget.device.path)
|
|
||||||
.notifier)
|
|
||||||
.unsetPassword(_currentPassword);
|
|
||||||
if (result) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content:
|
|
||||||
Text('Password removed'),
|
|
||||||
duration: Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_currentIsWrong = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 16.0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Text(
|
|
||||||
'Enter your new password. A password may contain letters, numbers and other characters.'),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
autofocus: !state.hasKey,
|
|
||||||
obscureText: true,
|
|
||||||
decoration:
|
|
||||||
const InputDecoration(labelText: 'Password'),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_newPassword = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
obscureText: true,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Confirm',
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_confirmPassword = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () {
|
child: const Text('Remove password'),
|
||||||
Navigator.of(context).pop();
|
onPressed: _currentPassword.isNotEmpty
|
||||||
},
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _newPassword.isNotEmpty &&
|
|
||||||
_newPassword == _confirmPassword &&
|
|
||||||
(!state.hasKey || _currentPassword.isNotEmpty)
|
|
||||||
? () async {
|
? () async {
|
||||||
final result = await ref
|
final result = await ref
|
||||||
.read(
|
.read(oathStateProvider(widget.path).notifier)
|
||||||
oathStateProvider(widget.device.path).notifier)
|
.unsetPassword(_currentPassword);
|
||||||
.setPassword(_currentPassword, _newPassword);
|
|
||||||
if (result) {
|
if (result) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Password set'),
|
content: Text('Password removed'),
|
||||||
duration: Duration(seconds: 2),
|
duration: Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -211,10 +72,117 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: const Text('Save'),
|
),
|
||||||
)
|
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 Divider(),
|
||||||
|
],
|
||||||
|
Text(
|
||||||
|
'New password',
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
autofocus: !widget.state.hasKey,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'New password',
|
||||||
|
enabled: !widget.state.hasKey || _currentPassword.isNotEmpty,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_newPassword = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'Confirm password',
|
||||||
|
enabled: _newPassword.isNotEmpty,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_confirmPassword = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _newPassword.isNotEmpty &&
|
||||||
|
_newPassword == _confirmPassword &&
|
||||||
|
(!widget.state.hasKey || _currentPassword.isNotEmpty)
|
||||||
|
? () async {
|
||||||
|
final result = await ref
|
||||||
|
.read(oathStateProvider(widget.path).notifier)
|
||||||
|
.setPassword(_currentPassword, _newPassword);
|
||||||
|
if (result) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Password set'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_currentIsWrong = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: 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: () {
|
orElse: () {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Attempted to show password dialog without an OathState');
|
'Attempted to show password dialog without an OathState');
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/views/responsive_dialog.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
@ -53,11 +54,12 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
final nameRemaining = remaining.second;
|
final nameRemaining = remaining.second;
|
||||||
final isValid = _account.isNotEmpty;
|
final isValid = _account.isNotEmpty;
|
||||||
|
|
||||||
return AlertDialog(
|
return ResponsiveDialog(
|
||||||
title: Text('Rename $label?'),
|
title: const Text('Rename account'),
|
||||||
content: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Text('Rename $label?'),
|
||||||
const Text(
|
const Text(
|
||||||
'This will change how the account is displayed in the list.'),
|
'This will change how the account is displayed in the list.'),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@ -65,6 +67,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
enabled: issuerRemaining > 0,
|
enabled: issuerRemaining > 0,
|
||||||
maxLength: issuerRemaining > 0 ? issuerRemaining : null,
|
maxLength: issuerRemaining > 0 ? issuerRemaining : null,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
labelText: 'Issuer (optional)',
|
labelText: 'Issuer (optional)',
|
||||||
helperText: '', // Prevents dialog resizing when enabled = false
|
helperText: '', // Prevents dialog resizing when enabled = false
|
||||||
),
|
),
|
||||||
@ -78,6 +81,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
initialValue: _account,
|
initialValue: _account,
|
||||||
maxLength: nameRemaining,
|
maxLength: nameRemaining,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
labelText: 'Account name',
|
labelText: 'Account name',
|
||||||
helperText: '', // Prevents dialog resizing when enabled = false
|
helperText: '', // Prevents dialog resizing when enabled = false
|
||||||
errorText: isValid ? null : 'Your account must have a name',
|
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: [
|
actions: [
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: isValid
|
onPressed: isValid
|
||||||
? () async {
|
? () async {
|
||||||
final renamed = await ref
|
final renamed = await ref
|
||||||
@ -113,7 +116,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: const Text('Rename account'),
|
child: const Text('Save'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/views/responsive_dialog.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
@ -16,28 +17,25 @@ class ResetDialog extends ConsumerWidget {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
});
|
});
|
||||||
|
|
||||||
return AlertDialog(
|
return ResponsiveDialog(
|
||||||
title: const Text('Reset to defaults?'),
|
title: const Text('Factory reset'),
|
||||||
content: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'Warning! This will irrevocably delete all OATH TOTP/HOTP accounts from your YubiKey.'),
|
'Warning! This will irrevocably delete all OATH TOTP/HOTP accounts from your YubiKey.'),
|
||||||
const Text(''),
|
|
||||||
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.',
|
'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,
|
style: Theme.of(context).textTheme.bodyText1,
|
||||||
),
|
),
|
||||||
],
|
]
|
||||||
|
.map((e) => Padding(
|
||||||
|
child: e,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await ref.read(oathStateProvider(device.path).notifier).reset();
|
await ref.read(oathStateProvider(device.path).notifier).reset();
|
||||||
Navigator.of(context).pop();
|
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/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:logging/logging.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';
|
import 'core/state.dart';
|
||||||
|
|
||||||
final _log = Logger('settings');
|
final _log = Logger('settings');
|
||||||
@ -12,48 +13,41 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Scaffold(
|
return ResponsiveDialog(
|
||||||
appBar: AppBar(
|
title: const Text('Settings'),
|
||||||
title: const Text('Settings'),
|
child: Column(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
body: Center(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Padding(
|
children: [
|
||||||
padding: const EdgeInsets.all(8.0),
|
DropdownButtonFormField<ThemeMode>(
|
||||||
child: Column(
|
decoration: const InputDecoration(labelText: 'Theme'),
|
||||||
mainAxisSize: MainAxisSize.min,
|
value: ref.watch(themeModeProvider),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
items: [ThemeMode.system, ThemeMode.dark, ThemeMode.light]
|
||||||
children: [
|
.map((e) => DropdownMenuItem(
|
||||||
DropdownButtonFormField<ThemeMode>(
|
value: e,
|
||||||
decoration: const InputDecoration(labelText: 'Theme'),
|
child:
|
||||||
value: ref.watch(themeModeProvider),
|
Text(e.name[0].toUpperCase() + e.name.substring(1)),
|
||||||
items: [ThemeMode.system, ThemeMode.dark, ThemeMode.light]
|
))
|
||||||
.map((e) => DropdownMenuItem(
|
.toList(),
|
||||||
value: e,
|
onChanged: (mode) {
|
||||||
child: Text(
|
ref.read(themeModeProvider.notifier).setThemeMode(mode!);
|
||||||
e.name[0].toUpperCase() + e.name.substring(1)),
|
},
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
onChanged: (mode) {
|
|
||||||
ref.read(themeModeProvider.notifier).setThemeMode(mode!);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
DropdownButtonFormField<Level>(
|
|
||||||
decoration: const InputDecoration(labelText: 'Logging'),
|
|
||||||
value: ref.watch(logLevelProvider),
|
|
||||||
items: [Level.INFO, Level.CONFIG, Level.FINE]
|
|
||||||
.map((e) => DropdownMenuItem(
|
|
||||||
value: e,
|
|
||||||
child: Text(e.name.toUpperCase()),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
onChanged: (level) {
|
|
||||||
ref.read(logLevelProvider.notifier).setLogLevel(level!);
|
|
||||||
_log.config('Log level set to $level');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
DropdownButtonFormField<Level>(
|
||||||
|
decoration: const InputDecoration(labelText: 'Logging'),
|
||||||
|
value: ref.watch(logLevelProvider),
|
||||||
|
items: [Level.INFO, Level.CONFIG, Level.FINE]
|
||||||
|
.map((e) => DropdownMenuItem(
|
||||||
|
value: e,
|
||||||
|
child: Text(e.name.toUpperCase()),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (level) {
|
||||||
|
ref.read(logLevelProvider.notifier).setLogLevel(level!);
|
||||||
|
_log.config('Log level set to $level');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user