Merge PR #132.
BIN
assets/fonts/Roboto-Light.ttf
Normal file
BIN
assets/fonts/Roboto-Regular.ttf
Normal file
BIN
assets/fonts/Roboto-Thin.ttf
Normal file
BIN
assets/graphics/no-accounts.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/graphics/no-discoverable.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/graphics/no-fingerprints.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
assets/graphics/no-permission.png
Executable file
After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 695 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 770 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 428 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 629 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 720 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 646 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 326 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 796 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 497 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 520 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 462 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 774 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 13 KiB |
@ -26,7 +26,14 @@
|
|||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
from .base import RpcNode, child, action, NoSuchNodeException, ChildResetException
|
from .base import (
|
||||||
|
RpcNode,
|
||||||
|
child,
|
||||||
|
action,
|
||||||
|
RpcException,
|
||||||
|
NoSuchNodeException,
|
||||||
|
ChildResetException,
|
||||||
|
)
|
||||||
from .oath import OathNode
|
from .oath import OathNode
|
||||||
from .fido import Ctap2Node
|
from .fido import Ctap2Node
|
||||||
from .yubiotp import YubiOtpNode
|
from .yubiotp import YubiOtpNode
|
||||||
@ -47,6 +54,7 @@ from yubikit.logging import LOG_LEVEL
|
|||||||
|
|
||||||
from ykman.pcsc import list_devices, YK_READER_NAME
|
from ykman.pcsc import list_devices, YK_READER_NAME
|
||||||
from smartcard.Exceptions import SmartcardException
|
from smartcard.Exceptions import SmartcardException
|
||||||
|
from smartcard.pcsc.PCSCExceptions import EstablishContextException
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from typing import Mapping, Tuple
|
from typing import Mapping, Tuple
|
||||||
@ -65,6 +73,15 @@ def _is_admin():
|
|||||||
return os.getuid() == 0
|
return os.getuid() == 0
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionException(RpcException):
|
||||||
|
def __init__(self, connection, exc_type):
|
||||||
|
super().__init__(
|
||||||
|
"connection-error",
|
||||||
|
f"Error connecting to {connection} interface",
|
||||||
|
dict(connection=connection, exc_type=type(exc_type).__name__),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RootNode(RpcNode):
|
class RootNode(RpcNode):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -127,9 +144,16 @@ class ReadersNode(RpcNode):
|
|||||||
return self.list_children()
|
return self.list_children()
|
||||||
|
|
||||||
def list_children(self):
|
def list_children(self):
|
||||||
|
try:
|
||||||
devices = [
|
devices = [
|
||||||
d for d in list_devices("") if YK_READER_NAME not in d.reader.name.lower()
|
d
|
||||||
|
for d in list_devices("")
|
||||||
|
if YK_READER_NAME not in d.reader.name.lower()
|
||||||
]
|
]
|
||||||
|
except EstablishContextException:
|
||||||
|
logger.warning("Unable to list readers", exc_info=True)
|
||||||
|
return {}
|
||||||
|
|
||||||
state = {d.reader.name for d in devices}
|
state = {d.reader.name for d in devices}
|
||||||
if self._state != state:
|
if self._state != state:
|
||||||
self._readers = {}
|
self._readers = {}
|
||||||
@ -271,15 +295,27 @@ class UsbDeviceNode(AbstractDeviceNode):
|
|||||||
|
|
||||||
@child(condition=lambda self: self._supports_connection(SmartCardConnection))
|
@child(condition=lambda self: self._supports_connection(SmartCardConnection))
|
||||||
def ccid(self):
|
def ccid(self):
|
||||||
|
try:
|
||||||
return self._create_connection(SmartCardConnection)
|
return self._create_connection(SmartCardConnection)
|
||||||
|
except (ValueError, SmartcardException) as e:
|
||||||
|
logger.warning("Error opening connection", exc_info=True)
|
||||||
|
raise ConnectionException("ccid", e)
|
||||||
|
|
||||||
@child(condition=lambda self: self._supports_connection(OtpConnection))
|
@child(condition=lambda self: self._supports_connection(OtpConnection))
|
||||||
def otp(self):
|
def otp(self):
|
||||||
|
try:
|
||||||
return self._create_connection(OtpConnection)
|
return self._create_connection(OtpConnection)
|
||||||
|
except (ValueError, OSError) as e:
|
||||||
|
logger.warning("Error opening connection", exc_info=True)
|
||||||
|
raise ConnectionException("otp", e)
|
||||||
|
|
||||||
@child(condition=lambda self: self._supports_connection(FidoConnection))
|
@child(condition=lambda self: self._supports_connection(FidoConnection))
|
||||||
def fido(self):
|
def fido(self):
|
||||||
|
try:
|
||||||
return self._create_connection(FidoConnection)
|
return self._create_connection(FidoConnection)
|
||||||
|
except (ValueError, OSError) as e:
|
||||||
|
logger.warning("Error opening connection", exc_info=True)
|
||||||
|
raise ConnectionException("fido", e)
|
||||||
|
|
||||||
|
|
||||||
class ReaderDeviceNode(AbstractDeviceNode):
|
class ReaderDeviceNode(AbstractDeviceNode):
|
||||||
|
@ -185,19 +185,19 @@ class _QrScannerViewState extends State<QrScannerView> {
|
|||||||
Text('Looking for a code...',
|
Text('Looking for a code...',
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.headline6
|
.titleLarge
|
||||||
?.copyWith(color: Colors.black)),
|
?.copyWith(color: Colors.black)),
|
||||||
if (_status == _ScanStatus.success)
|
if (_status == _ScanStatus.success)
|
||||||
Text('Found a valid code',
|
Text('Found a valid code',
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.headline6
|
.titleLarge
|
||||||
?.copyWith(color: Colors.white)),
|
?.copyWith(color: Colors.white)),
|
||||||
if (_status == _ScanStatus.error)
|
if (_status == _ScanStatus.error)
|
||||||
Text('This code is not valid, try again.',
|
Text('This code is not valid, try again.',
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.headline6
|
.titleLarge
|
||||||
?.copyWith(color: Colors.white)),
|
?.copyWith(color: Colors.white)),
|
||||||
]),
|
]),
|
||||||
Row(
|
Row(
|
||||||
|
83
lib/app/views/app_failure_page.dart
Executable file
@ -0,0 +1,83 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../desktop/models.dart';
|
||||||
|
import '../../desktop/state.dart';
|
||||||
|
import '../../theme.dart';
|
||||||
|
import '../message.dart';
|
||||||
|
import 'graphics.dart';
|
||||||
|
import 'message_page.dart';
|
||||||
|
|
||||||
|
class AppFailurePage extends ConsumerWidget {
|
||||||
|
final Widget? title;
|
||||||
|
final Object cause;
|
||||||
|
const AppFailurePage({this.title, required this.cause, super.key}) : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final reason = cause;
|
||||||
|
|
||||||
|
Widget? graphic = const Icon(Icons.error);
|
||||||
|
String? header = 'An error has occured';
|
||||||
|
String? message = reason.toString();
|
||||||
|
List<Widget> actions = [];
|
||||||
|
|
||||||
|
if (reason is RpcError) {
|
||||||
|
if (reason.status == 'connection-error') {
|
||||||
|
switch (reason.body['connection']) {
|
||||||
|
case 'ccid':
|
||||||
|
header = 'Failed to open smart card connection';
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
message = 'Try to remove and re-insert your YubiKey.';
|
||||||
|
} else if (Platform.isLinux) {
|
||||||
|
message = 'Make sure pcscd is running.';
|
||||||
|
} else {
|
||||||
|
message = 'Make sure your smart card service is functioning.';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'fido':
|
||||||
|
if (Platform.isWindows &&
|
||||||
|
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||||
|
graphic = noPermission;
|
||||||
|
header = null;
|
||||||
|
message = 'WebAuthn management requires elevated privileges.';
|
||||||
|
actions = [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
label: const Text('Unlock'),
|
||||||
|
icon: const Icon(Icons.lock_open),
|
||||||
|
style: AppTheme.primaryOutlinedButtonStyle(context),
|
||||||
|
onPressed: () async {
|
||||||
|
final controller = showMessage(
|
||||||
|
context, 'Elevating permissions...',
|
||||||
|
duration: const Duration(seconds: 30));
|
||||||
|
try {
|
||||||
|
if (await ref.read(rpcProvider).elevate()) {
|
||||||
|
ref.refresh(rpcProvider);
|
||||||
|
} else {
|
||||||
|
showMessage(context, 'Permission denied');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
header = 'Failed to open connection';
|
||||||
|
message = 'Try to remove and re-insert your YubiKey.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MessagePage(
|
||||||
|
title: title,
|
||||||
|
graphic: graphic,
|
||||||
|
header: header,
|
||||||
|
message: message,
|
||||||
|
actions: actions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class AppFailureScreen extends StatelessWidget {
|
|
||||||
final String reason;
|
|
||||||
const AppFailureScreen(this.reason, {super.key}) : super();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
reason,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,14 +8,15 @@ class AppPage extends ConsumerWidget {
|
|||||||
final Key _scaffoldKey = GlobalKey();
|
final Key _scaffoldKey = GlobalKey();
|
||||||
final Widget? title;
|
final Widget? title;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Widget? floatingActionButton;
|
final List<Widget> actions;
|
||||||
final bool centered;
|
final bool centered;
|
||||||
AppPage(
|
AppPage({
|
||||||
{super.key,
|
super.key,
|
||||||
this.title,
|
this.title,
|
||||||
required this.child,
|
required this.child,
|
||||||
this.floatingActionButton,
|
this.actions = const [],
|
||||||
this.centered = false});
|
this.centered = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder(
|
Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder(
|
||||||
@ -29,7 +30,7 @@ class AppPage extends ConsumerWidget {
|
|||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 240,
|
width: 280,
|
||||||
child: ListTileTheme(
|
child: ListTileTheme(
|
||||||
style: ListTileStyle.drawer,
|
style: ListTileStyle.drawer,
|
||||||
child: MainPageDrawer(shouldPop: false)),
|
child: MainPageDrawer(shouldPop: false)),
|
||||||
@ -46,11 +47,26 @@ class AppPage extends ConsumerWidget {
|
|||||||
|
|
||||||
Widget _buildScrollView() => SafeArea(
|
Widget _buildScrollView() => SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
// Make sure FAB doesn't block content
|
child: Builder(builder: (context) {
|
||||||
padding: floatingActionButton != null
|
return Column(
|
||||||
? const EdgeInsets.only(bottom: 72)
|
children: [
|
||||||
: null,
|
child,
|
||||||
child: child,
|
if (actions.isNotEmpty)
|
||||||
|
Align(
|
||||||
|
alignment:
|
||||||
|
centered ? Alignment.center : Alignment.centerLeft,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: actions,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -58,13 +74,14 @@ class AppPage extends ConsumerWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
titleSpacing: 8,
|
||||||
title: title,
|
title: title,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
|
titleTextStyle: Theme.of(context).textTheme.titleLarge,
|
||||||
actions: const [DeviceButton()],
|
actions: const [DeviceButton()],
|
||||||
),
|
),
|
||||||
drawer: hasDrawer ? const MainPageDrawer() : null,
|
drawer: hasDrawer ? const MainPageDrawer() : null,
|
||||||
body: centered ? Center(child: _buildScrollView()) : _buildScrollView(),
|
body: centered ? Center(child: _buildScrollView()) : _buildScrollView(),
|
||||||
floatingActionButton: floatingActionButton,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ class DeviceAvatar extends StatelessWidget {
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 22,
|
radius: 22,
|
||||||
backgroundColor: selected
|
backgroundColor: selected
|
||||||
? Theme.of(context).colorScheme.secondary
|
? Theme.of(context).colorScheme.primary
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
backgroundColor: Theme.of(context).colorScheme.background,
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
|
@ -33,6 +33,7 @@ class DeviceButton extends ConsumerWidget {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
|
tooltip: 'Select YubiKey or device',
|
||||||
icon: OverflowBox(
|
icon: OverflowBox(
|
||||||
maxHeight: 44,
|
maxHeight: 44,
|
||||||
maxWidth: 44,
|
maxWidth: 44,
|
||||||
|
8
lib/app/views/graphics.dart
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final Image noAccounts = _graphic('no-accounts');
|
||||||
|
final Image noDiscoverable = _graphic('no-discoverable');
|
||||||
|
final Image noFingerprints = _graphic('no-fingerprints');
|
||||||
|
final Image noPermission = _graphic('no-permission');
|
||||||
|
|
||||||
|
Image _graphic(String name) => Image.asset('assets/graphics/$name.png');
|
@ -38,17 +38,20 @@ class MainPageDrawer extends ConsumerWidget {
|
|||||||
final data = ref.watch(currentDeviceDataProvider);
|
final data = ref.watch(currentDeviceDataProvider);
|
||||||
final currentApp = ref.watch(currentAppProvider);
|
final currentApp = ref.watch(currentAppProvider);
|
||||||
|
|
||||||
|
MediaQuery? mediaQuery =
|
||||||
|
context.findAncestorWidgetOfExactType<MediaQuery>();
|
||||||
|
final width = mediaQuery?.data.size.width ?? 400;
|
||||||
|
|
||||||
return Drawer(
|
return Drawer(
|
||||||
|
width: width < 357 ? 0.85 * width : null,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topRight: Radius.circular(20.0),
|
||||||
|
bottomRight: Radius.circular(20.0))),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
primary: false, //Prevents conflict with the MainPage scroll view.
|
primary: false, //Prevents conflict with the MainPage scroll view.
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
const SizedBox(height: 24.0),
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Text(
|
|
||||||
'Yubico Authenticator',
|
|
||||||
style: Theme.of(context).textTheme.headline6,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (data != null) ...[
|
if (data != null) ...[
|
||||||
// Normal YubiKey Applications
|
// Normal YubiKey Applications
|
||||||
...supportedApps
|
...supportedApps
|
||||||
@ -68,14 +71,6 @@ class MainPageDrawer extends ConsumerWidget {
|
|||||||
if (supportedApps.contains(Application.management) &&
|
if (supportedApps.contains(Application.management) &&
|
||||||
Application.management.getAvailability(data) ==
|
Application.management.getAvailability(data) ==
|
||||||
Availability.enabled) ...[
|
Availability.enabled) ...[
|
||||||
const Divider(),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Text(
|
|
||||||
'Configuration',
|
|
||||||
style: Theme.of(context).textTheme.bodyText2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DrawerItem(
|
DrawerItem(
|
||||||
titleText: 'Toggle applications',
|
titleText: 'Toggle applications',
|
||||||
icon: Icon(Application.management._icon),
|
icon: Icon(Application.management._icon),
|
||||||
@ -87,17 +82,10 @@ class MainPageDrawer extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(),
|
|
||||||
],
|
],
|
||||||
|
const Divider(indent: 16.0, endIndent: 28.0),
|
||||||
],
|
],
|
||||||
// Non-YubiKey pages
|
// Non-YubiKey pages
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Text(
|
|
||||||
'Application',
|
|
||||||
style: Theme.of(context).textTheme.bodyText2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DrawerItem(
|
DrawerItem(
|
||||||
titleText: 'Settings',
|
titleText: 'Settings',
|
||||||
icon: const Icon(Icons.settings),
|
icon: const Icon(Icons.settings),
|
||||||
@ -110,7 +98,7 @@ class MainPageDrawer extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
DrawerItem(
|
DrawerItem(
|
||||||
titleText: 'Help and feedback',
|
titleText: 'Help and feedback',
|
||||||
icon: const Icon(Icons.help_outline),
|
icon: const Icon(Icons.help),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final nav = Navigator.of(context);
|
final nav = Navigator.of(context);
|
||||||
if (shouldPop) nav.pop();
|
if (shouldPop) nav.pop();
|
||||||
@ -173,17 +161,22 @@ class DrawerItem extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 8),
|
padding: const EdgeInsets.only(left: 12.0, right: 12.0),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.horizontal(right: Radius.circular(20)),
|
borderRadius: BorderRadius.all(Radius.circular(30)),
|
||||||
),
|
),
|
||||||
dense: true,
|
dense: true,
|
||||||
|
minLeadingWidth: 24,
|
||||||
|
minVerticalPadding: 18,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
selectedColor: Theme.of(context).backgroundColor,
|
selectedColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
selectedTileColor: Theme.of(context).colorScheme.secondary,
|
selectedTileColor: Theme.of(context).colorScheme.primary,
|
||||||
leading: icon,
|
leading: IconTheme.merge(
|
||||||
|
data: const IconThemeData(size: 24),
|
||||||
|
child: icon,
|
||||||
|
),
|
||||||
title: Text(titleText),
|
title: Text(titleText),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
),
|
),
|
||||||
|
@ -4,29 +4,40 @@ import 'app_page.dart';
|
|||||||
|
|
||||||
class MessagePage extends StatelessWidget {
|
class MessagePage extends StatelessWidget {
|
||||||
final Widget? title;
|
final Widget? title;
|
||||||
final String header;
|
final Widget? graphic;
|
||||||
final String message;
|
final String? header;
|
||||||
final Widget? floatingActionButton;
|
final String? message;
|
||||||
|
final List<Widget> actions;
|
||||||
|
|
||||||
const MessagePage({
|
const MessagePage({
|
||||||
super.key,
|
super.key,
|
||||||
this.title,
|
this.title,
|
||||||
required this.header,
|
this.graphic,
|
||||||
required this.message,
|
this.header,
|
||||||
this.floatingActionButton,
|
this.message,
|
||||||
|
this.actions = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => AppPage(
|
Widget build(BuildContext context) => AppPage(
|
||||||
title: title,
|
title: title,
|
||||||
centered: true,
|
centered: true,
|
||||||
floatingActionButton: floatingActionButton,
|
actions: actions,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(header, style: Theme.of(context).textTheme.headline6),
|
if (graphic != null) graphic!,
|
||||||
|
if (header != null)
|
||||||
|
Text(header!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium),
|
||||||
const SizedBox(height: 12.0),
|
const SizedBox(height: 12.0),
|
||||||
Text(message, textAlign: TextAlign.center),
|
if (message != null) ...[
|
||||||
|
Text(message!, textAlign: TextAlign.center),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,25 +5,29 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import '../../core/models.dart';
|
import '../../core/models.dart';
|
||||||
import '../../desktop/state.dart';
|
import '../../desktop/state.dart';
|
||||||
|
import '../../theme.dart';
|
||||||
import '../message.dart';
|
import '../message.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import 'app_page.dart';
|
|
||||||
import 'device_avatar.dart';
|
import 'device_avatar.dart';
|
||||||
|
import 'graphics.dart';
|
||||||
|
import 'message_page.dart';
|
||||||
|
|
||||||
class NoDeviceScreen extends ConsumerWidget {
|
class NoDeviceScreen extends ConsumerWidget {
|
||||||
final DeviceNode? node;
|
final DeviceNode? node;
|
||||||
const NoDeviceScreen(this.node, {super.key});
|
const NoDeviceScreen(this.node, {super.key});
|
||||||
|
|
||||||
List<Widget> _buildUsbPid(BuildContext context, WidgetRef ref, UsbPid pid) {
|
Widget _buildUsbPid(BuildContext context, WidgetRef ref, UsbPid pid) {
|
||||||
if (pid.usbInterfaces == UsbInterface.fido.value) {
|
if (pid.usbInterfaces == UsbInterface.fido.value) {
|
||||||
if (Platform.isWindows &&
|
if (Platform.isWindows &&
|
||||||
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||||
return [
|
return MessagePage(
|
||||||
const DeviceAvatar(child: Icon(Icons.lock)),
|
graphic: noPermission,
|
||||||
const Text('WebAuthn management requires elevated privileges.'),
|
message: 'Managing this device requires elevated privileges.',
|
||||||
|
actions: [
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
icon: const Icon(Icons.lock_open),
|
style: AppTheme.primaryOutlinedButtonStyle(context),
|
||||||
label: const Text('Unlock'),
|
label: const Text('Unlock'),
|
||||||
|
icon: const Icon(Icons.lock_open),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final controller = showMessage(
|
final controller = showMessage(
|
||||||
context, 'Elevating permissions...',
|
context, 'Elevating permissions...',
|
||||||
@ -37,43 +41,31 @@ class NoDeviceScreen extends ConsumerWidget {
|
|||||||
} finally {
|
} finally {
|
||||||
controller.close();
|
controller.close();
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
]
|
|
||||||
.map((e) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
||||||
child: e,
|
|
||||||
))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
const DeviceAvatar(child: Icon(Icons.usb_off)),
|
|
||||||
const Text(
|
|
||||||
'This YubiKey cannot be accessed',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
];
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return const MessagePage(
|
||||||
|
graphic: DeviceAvatar(child: Icon(Icons.usb_off)),
|
||||||
|
message: 'This YubiKey cannot be accessed',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return AppPage(
|
return node?.map(usbYubiKey: (node) {
|
||||||
centered: true,
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: node?.map(usbYubiKey: (node) {
|
|
||||||
return _buildUsbPid(context, ref, node.pid);
|
return _buildUsbPid(context, ref, node.pid);
|
||||||
}, nfcReader: (node) {
|
}, nfcReader: (node) {
|
||||||
return const [
|
return const MessagePage(
|
||||||
DeviceAvatar(child: Icon(Icons.wifi)),
|
graphic: DeviceAvatar(child: Icon(Icons.wifi)),
|
||||||
Text('Place your YubiKey on the NFC reader'),
|
message: 'Place your YubiKey on the NFC reader',
|
||||||
];
|
);
|
||||||
}) ??
|
}) ??
|
||||||
const [
|
const MessagePage(
|
||||||
DeviceAvatar(child: Icon(Icons.usb)),
|
graphic: DeviceAvatar(child: Icon(Icons.usb)),
|
||||||
Text('Insert your YubiKey'),
|
message: 'Insert your YubiKey',
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,19 +133,21 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('Step 1/2: Capture fingerprint'),
|
const Text('Step 1/2: Capture fingerprint'),
|
||||||
Card(
|
Column(
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
AnimatedBuilder(
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(36.0),
|
||||||
|
child: AnimatedBuilder(
|
||||||
animation: _color,
|
animation: _color,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return Icon(
|
return Icon(
|
||||||
_fingerprint == null ? Icons.fingerprint : Icons.check,
|
_fingerprint == null ? Icons.fingerprint : Icons.check,
|
||||||
size: 200.0,
|
size: 128.0,
|
||||||
color: _color.value,
|
color: _color.value,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
LinearProgressIndicator(value: progress),
|
LinearProgressIndicator(value: progress),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
@ -153,7 +155,6 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const Text('Step 2/2: Name fingerprint'),
|
const Text('Step 2/2: Name fingerprint'),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
focusNode: _nameFocus,
|
focusNode: _nameFocus,
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
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/message.dart';
|
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/views/app_failure_screen.dart';
|
import '../../app/views/app_failure_page.dart';
|
||||||
import '../../app/views/app_loading_screen.dart';
|
import '../../app/views/app_loading_screen.dart';
|
||||||
import '../../app/views/app_page.dart';
|
import '../../app/views/app_page.dart';
|
||||||
import '../../app/views/device_avatar.dart';
|
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
import '../../desktop/state.dart';
|
|
||||||
import '../../management/models.dart';
|
import '../../management/models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'locked_page.dart';
|
import 'locked_page.dart';
|
||||||
@ -51,52 +46,10 @@ class FidoScreen extends ConsumerWidget {
|
|||||||
'WebAuthn requires the FIDO2 application to be enabled on your YubiKey',
|
'WebAuthn requires the FIDO2 application to be enabled on your YubiKey',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (Platform.isWindows) {
|
|
||||||
if (!ref
|
return AppFailurePage(
|
||||||
.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
|
||||||
return AppPage(
|
|
||||||
title: const Text('WebAuthn'),
|
title: const Text('WebAuthn'),
|
||||||
centered: true,
|
cause: error,
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const DeviceAvatar(child: Icon(Icons.lock)),
|
|
||||||
const Text(
|
|
||||||
'WebAuthn management requires elevated privileges.',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
icon: const Icon(Icons.lock_open),
|
|
||||||
label: const Text('Unlock'),
|
|
||||||
onPressed: () async {
|
|
||||||
final controller = showMessage(
|
|
||||||
context, 'Elevating permissions...',
|
|
||||||
duration: const Duration(seconds: 30));
|
|
||||||
try {
|
|
||||||
if (await ref.read(rpcProvider).elevate()) {
|
|
||||||
ref.refresh(rpcProvider);
|
|
||||||
} else {
|
|
||||||
showMessage(context, 'Permission denied');
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
controller.close();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
.map((e) => Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(vertical: 8.0),
|
|
||||||
child: e,
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return AppPage(
|
|
||||||
title: const Text('WebAuthn'),
|
|
||||||
centered: true,
|
|
||||||
child: AppFailureScreen('$error'),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
data: (fidoState) {
|
data: (fidoState) {
|
||||||
|
@ -4,7 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/views/app_page.dart';
|
import '../../app/views/app_page.dart';
|
||||||
|
import '../../app/views/graphics.dart';
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
|
import '../../theme.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'pin_dialog.dart';
|
import 'pin_dialog.dart';
|
||||||
@ -22,23 +24,26 @@ class FidoLockedPage extends ConsumerWidget {
|
|||||||
if (state.bioEnroll != null) {
|
if (state.bioEnroll != null) {
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
title: const Text('WebAuthn'),
|
title: const Text('WebAuthn'),
|
||||||
|
graphic: noFingerprints,
|
||||||
header: 'No fingerprints',
|
header: 'No fingerprints',
|
||||||
message: 'Set a PIN to register fingerprints',
|
message: 'Set a PIN to register fingerprints.',
|
||||||
floatingActionButton: _buildFab(context),
|
actions: _buildActions(context),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
title: const Text('WebAuthn'),
|
title: const Text('WebAuthn'),
|
||||||
|
graphic: noDiscoverable,
|
||||||
header: 'No discoverable accounts',
|
header: 'No discoverable accounts',
|
||||||
message:
|
message:
|
||||||
'Optionally set a PIN to protect access to your YubiKey\nRegister as a Security Key on websites',
|
'Optionally set a PIN to protect access to your YubiKey\nRegister as a Security Key on websites',
|
||||||
floatingActionButton: _buildFab(context),
|
actions: _buildActions(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppPage(
|
return AppPage(
|
||||||
title: const Text('WebAuthn'),
|
title: const Text('WebAuthn'),
|
||||||
|
actions: _buildActions(context),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const ListTile(title: Text('Unlock')),
|
const ListTile(title: Text('Unlock')),
|
||||||
@ -48,20 +53,28 @@ class FidoLockedPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
FloatingActionButton _buildFab(BuildContext context) {
|
List<Widget> _buildActions(BuildContext context) => [
|
||||||
return FloatingActionButton.extended(
|
if (!state.hasPin)
|
||||||
icon: Icon(state.bioEnroll != null ? Icons.fingerprint : Icons.pin),
|
OutlinedButton.icon(
|
||||||
label: const Text('Setup'),
|
style: AppTheme.primaryOutlinedButtonStyle(context),
|
||||||
|
label: const Text('Set PIN'),
|
||||||
|
icon: const Icon(Icons.pin),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => FidoPinDialog(node.path, state),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
label: const Text('Options'),
|
||||||
|
icon: const Icon(Icons.tune),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showBottomMenu(context, [
|
showBottomMenu(context, [
|
||||||
if (state.bioEnroll != null)
|
if (state.hasPin)
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: 'Add fingerprint',
|
text: 'Change PIN',
|
||||||
icon: const Icon(Icons.fingerprint),
|
icon: const Icon(Icons.pin),
|
||||||
),
|
|
||||||
MenuAction(
|
|
||||||
text: 'Set PIN',
|
|
||||||
icon: const Icon(Icons.pin_outlined),
|
|
||||||
action: (context) {
|
action: (context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -71,7 +84,7 @@ class FidoLockedPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: 'Reset FIDO',
|
text: 'Reset FIDO',
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete),
|
||||||
action: (context) {
|
action: (context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -81,8 +94,8 @@ class FidoLockedPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
}
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PinEntryForm extends ConsumerStatefulWidget {
|
class _PinEntryForm extends ConsumerStatefulWidget {
|
||||||
@ -149,34 +162,6 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
|||||||
onSubmitted: (_) => _submit(),
|
onSubmitted: (_) => _submit(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Wrap(
|
|
||||||
spacing: 4.0,
|
|
||||||
runSpacing: 8.0,
|
|
||||||
children: [
|
|
||||||
OutlinedButton.icon(
|
|
||||||
icon: const Icon(Icons.pin_outlined),
|
|
||||||
label: const Text('Change PIN'),
|
|
||||||
onPressed: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) =>
|
|
||||||
FidoPinDialog(widget._deviceNode.path, widget._state),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
icon: const Icon(Icons.delete_outlined),
|
|
||||||
label: const Text('Reset FIDO'),
|
|
||||||
onPressed: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ResetDialog(widget._deviceNode),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16.0),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading:
|
leading:
|
||||||
noFingerprints ? const Icon(Icons.warning_amber_rounded) : null,
|
noFingerprints ? const Icon(Icons.warning_amber_rounded) : null,
|
||||||
|
@ -50,7 +50,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
if (hasPin) ...[
|
if (hasPin) ...[
|
||||||
Text(
|
Text(
|
||||||
'Current PIN',
|
'Current PIN',
|
||||||
style: Theme.of(context).textTheme.headline6,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: _currentPin,
|
initialValue: _currentPin,
|
||||||
@ -70,7 +70,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
],
|
],
|
||||||
Text(
|
Text(
|
||||||
'New PIN',
|
'New PIN',
|
||||||
style: Theme.of(context).textTheme.headline6,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: _newPin,
|
initialValue: _newPin,
|
||||||
|
@ -94,7 +94,7 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
|||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: Text(_getMessage(),
|
child: Text(_getMessage(),
|
||||||
style: Theme.of(context).textTheme.headline6),
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
.map((e) => Padding(
|
.map((e) => Padding(
|
||||||
|
@ -4,7 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/views/app_page.dart';
|
import '../../app/views/app_page.dart';
|
||||||
|
import '../../app/views/graphics.dart';
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
|
import '../../theme.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'add_fingerprint_dialog.dart';
|
import 'add_fingerprint_dialog.dart';
|
||||||
@ -29,10 +31,23 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
? [
|
? [
|
||||||
const ListTile(title: Text('Credentials')),
|
const ListTile(title: Text('Credentials')),
|
||||||
...creds.map((cred) => ListTile(
|
...creds.map((cred) => ListTile(
|
||||||
leading:
|
leading: CircleAvatar(
|
||||||
const CircleAvatar(child: Icon(Icons.link)),
|
foregroundColor:
|
||||||
title: Text(cred.userName),
|
Theme.of(context).colorScheme.onPrimary,
|
||||||
subtitle: Text(cred.rpId),
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
child: const Icon(Icons.person),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
cred.userName,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
cred.rpId,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -45,7 +60,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
node.path, cred),
|
node.path, cred),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.delete_outlined)),
|
icon: const Icon(Icons.delete_outline)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
@ -59,9 +74,18 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
? [
|
? [
|
||||||
const ListTile(title: Text('Fingerprints')),
|
const ListTile(title: Text('Fingerprints')),
|
||||||
...fingerprints.map((fp) => ListTile(
|
...fingerprints.map((fp) => ListTile(
|
||||||
leading: const CircleAvatar(
|
leading: CircleAvatar(
|
||||||
child: Icon(Icons.fingerprint)),
|
foregroundColor:
|
||||||
title: Text(fp.label),
|
Theme.of(context).colorScheme.onSecondary,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.secondary,
|
||||||
|
child: const Icon(Icons.fingerprint),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
fp.label,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -74,7 +98,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
node.path, fp),
|
node.path, fp),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.edit)),
|
icon: const Icon(Icons.edit_outlined)),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
@ -84,7 +108,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
node.path, fp),
|
node.path, fp),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.delete_outlined)),
|
icon: const Icon(Icons.delete_outline)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
@ -97,50 +121,57 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
if (children.isNotEmpty) {
|
if (children.isNotEmpty) {
|
||||||
return AppPage(
|
return AppPage(
|
||||||
title: const Text('WebAuthn'),
|
title: const Text('WebAuthn'),
|
||||||
floatingActionButton: _buildFab(context),
|
actions: _buildActions(context),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: children,
|
children: children,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.bioEnroll == false) {
|
if (state.bioEnroll != null) {
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
title: const Text('WebAuthn'),
|
title: const Text('WebAuthn'),
|
||||||
|
graphic: noFingerprints,
|
||||||
header: 'No fingerprints',
|
header: 'No fingerprints',
|
||||||
message: 'Add one or more (up to five) fingerprints',
|
message: 'Add one or more (up to five) fingerprints',
|
||||||
floatingActionButton: _buildFab(context),
|
actions: _buildActions(context, fingerprintPrimary: true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
title: const Text('WebAuthn'),
|
title: const Text('WebAuthn'),
|
||||||
|
graphic: noDiscoverable,
|
||||||
header: 'No discoverable accounts',
|
header: 'No discoverable accounts',
|
||||||
message: 'Register as a Security Key on websites',
|
message: 'Register as a Security Key on websites',
|
||||||
floatingActionButton: _buildFab(context),
|
actions: _buildActions(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
FloatingActionButton _buildFab(BuildContext context) {
|
List<Widget> _buildActions(BuildContext context,
|
||||||
return FloatingActionButton.extended(
|
{bool fingerprintPrimary = false}) =>
|
||||||
icon: Icon(state.bioEnroll != null ? Icons.fingerprint : Icons.pin),
|
[
|
||||||
label: const Text('Setup'),
|
|
||||||
onPressed: () {
|
|
||||||
showBottomMenu(context, [
|
|
||||||
if (state.bioEnroll != null)
|
if (state.bioEnroll != null)
|
||||||
MenuAction(
|
OutlinedButton.icon(
|
||||||
text: 'Add fingerprint',
|
style: fingerprintPrimary
|
||||||
|
? AppTheme.primaryOutlinedButtonStyle(context)
|
||||||
|
: null,
|
||||||
|
label: const Text('Add fingerprint'),
|
||||||
icon: const Icon(Icons.fingerprint),
|
icon: const Icon(Icons.fingerprint),
|
||||||
action: (context) {
|
onPressed: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AddFingerprintDialog(node.path),
|
builder: (context) => AddFingerprintDialog(node.path),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
label: const Text('Options'),
|
||||||
|
icon: const Icon(Icons.tune),
|
||||||
|
onPressed: () {
|
||||||
|
showBottomMenu(context, [
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: 'Change PIN',
|
text: 'Change PIN',
|
||||||
icon: const Icon(Icons.pin_outlined),
|
icon: const Icon(Icons.pin),
|
||||||
action: (context) {
|
action: (context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -149,8 +180,8 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: 'Delete all data',
|
text: 'Reset FIDO',
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete),
|
||||||
action: (context) {
|
action: (context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -160,6 +191,6 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
}
|
];
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import 'package:collection/collection.dart';
|
|||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../app/views/app_failure_screen.dart';
|
|
||||||
import '../../app/views/app_loading_screen.dart';
|
import '../../app/views/app_loading_screen.dart';
|
||||||
import '../../core/models.dart';
|
import '../../core/models.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
@ -27,8 +26,8 @@ class _CapabilityForm extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 4.0,
|
spacing: 8,
|
||||||
runSpacing: 8.0,
|
runSpacing: 16,
|
||||||
children: Capability.values
|
children: Capability.values
|
||||||
.where((c) => capabilities & c.value != 0)
|
.where((c) => capabilities & c.value != 0)
|
||||||
.map((c) => FilterChip(
|
.map((c) => FilterChip(
|
||||||
@ -85,10 +84,12 @@ class _CapabilitiesForm extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (usbCapabilities != 0)
|
if (usbCapabilities != 0) ...[
|
||||||
const ListTile(
|
const ListTile(
|
||||||
leading: Icon(Icons.usb),
|
leading: Icon(Icons.usb),
|
||||||
title: Text('USB applications'),
|
title: Text('USB'),
|
||||||
|
contentPadding: EdgeInsets.only(bottom: 8),
|
||||||
|
horizontalTitleGap: 0,
|
||||||
),
|
),
|
||||||
_CapabilityForm(
|
_CapabilityForm(
|
||||||
capabilities: usbCapabilities,
|
capabilities: usbCapabilities,
|
||||||
@ -97,10 +98,18 @@ class _CapabilitiesForm extends StatelessWidget {
|
|||||||
onChanged({...enabled, Transport.usb: value});
|
onChanged({...enabled, Transport.usb: value});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (nfcCapabilities != 0)
|
],
|
||||||
|
if (nfcCapabilities != 0) ...[
|
||||||
|
if (usbCapabilities != 0)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 24, bottom: 12),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
const ListTile(
|
const ListTile(
|
||||||
leading: Icon(Icons.wifi),
|
leading: Icon(Icons.wifi),
|
||||||
title: Text('NFC applications'),
|
title: Text('NFC'),
|
||||||
|
contentPadding: EdgeInsets.only(bottom: 8),
|
||||||
|
horizontalTitleGap: 0,
|
||||||
),
|
),
|
||||||
_CapabilityForm(
|
_CapabilityForm(
|
||||||
capabilities: nfcCapabilities,
|
capabilities: nfcCapabilities,
|
||||||
@ -109,6 +118,7 @@ class _CapabilitiesForm extends StatelessWidget {
|
|||||||
onChanged({...enabled, Transport.nfc: value});
|
onChanged({...enabled, Transport.nfc: value});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -230,7 +240,18 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
|||||||
final child =
|
final child =
|
||||||
ref.watch(managementStateProvider(widget.deviceData.node.path)).when(
|
ref.watch(managementStateProvider(widget.deviceData.node.path)).when(
|
||||||
loading: () => const AppLoadingScreen(),
|
loading: () => const AppLoadingScreen(),
|
||||||
error: (error, _) => AppFailureScreen('$error'),
|
error: (error, _) => Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
error.toString(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
data: (info) {
|
data: (info) {
|
||||||
bool hasConfig = info.version.major > 4;
|
bool hasConfig = info.version.major > 4;
|
||||||
if (hasConfig) {
|
if (hasConfig) {
|
||||||
|
@ -3,7 +3,9 @@ import 'dart:async';
|
|||||||
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/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
|
import '../../core/models.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../../widgets/dialog_frame.dart';
|
import '../../widgets/dialog_frame.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
@ -44,17 +46,38 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
|
|||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Pair<Color?, Color?> _getColors(BuildContext context, MenuAction action) {
|
||||||
|
final theme =
|
||||||
|
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
|
||||||
|
return action.text.startsWith('Copy')
|
||||||
|
? Pair(theme.primary, theme.onPrimary)
|
||||||
|
: (action.text.startsWith('Delete')
|
||||||
|
? Pair(theme.error, theme.onError)
|
||||||
|
: Pair(theme.secondary, theme.onSecondary));
|
||||||
|
}
|
||||||
|
|
||||||
List<Widget> _buildActions(BuildContext context, WidgetRef ref) {
|
List<Widget> _buildActions(BuildContext context, WidgetRef ref) {
|
||||||
return buildActions(context, ref).map((e) {
|
return buildActions(context, ref).map((e) {
|
||||||
final action = e.action;
|
final action = e.action;
|
||||||
return IconButton(
|
final colors = _getColors(context, e);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
child: CircleAvatar(
|
||||||
|
// TODO: Hardcoded color
|
||||||
|
backgroundColor: action != null ? colors.first : Colors.grey.shade900,
|
||||||
|
foregroundColor: colors.second,
|
||||||
|
child: IconButton(
|
||||||
icon: e.icon,
|
icon: e.icon,
|
||||||
|
iconSize: 22,
|
||||||
tooltip: e.text,
|
tooltip: e.text,
|
||||||
|
disabledColor: Colors.white70,
|
||||||
onPressed: action != null
|
onPressed: action != null
|
||||||
? () {
|
? () {
|
||||||
action(context);
|
action(context);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
@ -69,20 +92,62 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
|
|||||||
}
|
}
|
||||||
return DialogFrame(
|
return DialogFrame(
|
||||||
child: AlertDialog(
|
child: AlertDialog(
|
||||||
title: Text(title),
|
title: Center(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24.0),
|
child: Text(
|
||||||
actionsAlignment: MainAxisAlignment.center,
|
title,
|
||||||
actionsPadding: EdgeInsets.zero,
|
overflow: TextOverflow.fade,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
content: Column(
|
content: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(subtitle ?? ''),
|
if (subtitle != null)
|
||||||
const SizedBox(height: 8.0),
|
Text(
|
||||||
Center(child: FittedBox(child: buildCodeView(ref, big: true))),
|
subtitle!,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: false,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12.0),
|
||||||
|
DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.rectangle,
|
||||||
|
color: CardTheme.of(context).color,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(30.0)),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: FittedBox(
|
||||||
|
child: DefaultTextStyle.merge(
|
||||||
|
style: const TextStyle(fontSize: 28),
|
||||||
|
child: IconTheme(
|
||||||
|
data: IconTheme.of(context).copyWith(size: 24),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: buildCodeView(ref),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [FittedBox(child: Row(children: _buildActions(context, ref)))],
|
actionsPadding: const EdgeInsets.only(top: 10.0, right: -16.0),
|
||||||
|
actions: [
|
||||||
|
Center(
|
||||||
|
child: FittedBox(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Row(children: _buildActions(context, ref)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -94,9 +94,11 @@ class _AccountListState extends ConsumerState<AccountList> {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
if (pinnedCreds.isNotEmpty)
|
if (pinnedCreds.isNotEmpty)
|
||||||
const ListTile(
|
ListTile(
|
||||||
|
minVerticalPadding: 16,
|
||||||
title: Text(
|
title: Text(
|
||||||
'Pinned',
|
'PINNED',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...pinnedCreds.map(
|
...pinnedCreds.map(
|
||||||
@ -106,9 +108,11 @@ class _AccountListState extends ConsumerState<AccountList> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (creds.isNotEmpty)
|
if (creds.isNotEmpty)
|
||||||
const ListTile(
|
ListTile(
|
||||||
|
minVerticalPadding: 16,
|
||||||
title: Text(
|
title: Text(
|
||||||
'Accounts',
|
'ACCOUNTS',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...creds.map(
|
...creds.map(
|
||||||
|
@ -147,6 +147,16 @@ mixin AccountMixin {
|
|||||||
final pinned = isPinned(ref);
|
final pinned = isPinned(ref);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
MenuAction(
|
||||||
|
text: 'Copy to clipboard',
|
||||||
|
icon: const Icon(Icons.copy),
|
||||||
|
action: code == null || expired
|
||||||
|
? null
|
||||||
|
: (context) {
|
||||||
|
Clipboard.setData(ClipboardData(text: code.value));
|
||||||
|
showMessage(context, 'Code copied to clipboard');
|
||||||
|
},
|
||||||
|
),
|
||||||
if (manual)
|
if (manual)
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: 'Calculate',
|
text: 'Calculate',
|
||||||
@ -157,27 +167,19 @@ mixin AccountMixin {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
MenuAction(
|
|
||||||
text: 'Copy to clipboard',
|
|
||||||
icon: const Icon(Icons.copy),
|
|
||||||
action: code == null || expired
|
|
||||||
? null
|
|
||||||
: (context) {
|
|
||||||
Clipboard.setData(ClipboardData(text: code.value));
|
|
||||||
showMessage(context, 'Code copied to clipboard');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: pinned ? 'Unpin account' : 'Pin account',
|
text: pinned ? 'Unpin account' : 'Pin account',
|
||||||
//TODO: Replace this with a custom icon.
|
//TODO: Replace this with a custom icon.
|
||||||
icon: pinned
|
icon: pinned
|
||||||
? CustomPaint(
|
? Builder(builder: (context) {
|
||||||
|
return CustomPaint(
|
||||||
painter: _StrikethroughPainter(
|
painter: _StrikethroughPainter(
|
||||||
Theme.of(context).iconTheme.color ?? Colors.black),
|
IconTheme.of(context).color ?? Colors.black),
|
||||||
child: ClipPath(
|
child: ClipPath(
|
||||||
clipper: _StrikethroughClipper(),
|
clipper: _StrikethroughClipper(),
|
||||||
child: const Icon(Icons.push_pin_outlined)),
|
child: const Icon(Icons.push_pin)),
|
||||||
)
|
);
|
||||||
|
})
|
||||||
: const Icon(Icons.push_pin_outlined),
|
: const Icon(Icons.push_pin_outlined),
|
||||||
action: (context) {
|
action: (context) {
|
||||||
ref.read(favoritesProvider.notifier).toggleFavorite(credential.id);
|
ref.read(favoritesProvider.notifier).toggleFavorite(credential.id);
|
||||||
@ -194,7 +196,7 @@ mixin AccountMixin {
|
|||||||
),
|
),
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: 'Delete account',
|
text: 'Delete account',
|
||||||
icon: const Icon(Icons.delete_outlined),
|
icon: const Icon(Icons.delete_outline),
|
||||||
action: (context) async {
|
action: (context) async {
|
||||||
await deleteCredential(context, ref);
|
await deleteCredential(context, ref);
|
||||||
},
|
},
|
||||||
@ -203,23 +205,14 @@ mixin AccountMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Widget buildCodeView(WidgetRef ref, {bool big = false}) {
|
Widget buildCodeView(WidgetRef ref) {
|
||||||
final code = getCode(ref);
|
final code = getCode(ref);
|
||||||
final expired = isExpired(code, ref);
|
final expired = isExpired(code, ref);
|
||||||
return DecoratedBox(
|
return AnimatedSize(
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.rectangle,
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(30.0)),
|
|
||||||
border: Border.all(width: 1.0, color: Colors.grey.shade500),
|
|
||||||
),
|
|
||||||
child: AnimatedSize(
|
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
duration: const Duration(milliseconds: 100),
|
duration: const Duration(milliseconds: 100),
|
||||||
child: Padding(
|
child: Builder(builder: (context) {
|
||||||
padding: big
|
return Row(
|
||||||
? const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0)
|
|
||||||
: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 2.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: code == null
|
children: code == null
|
||||||
@ -228,9 +221,8 @@ mixin AccountMixin {
|
|||||||
credential.oathType == OathType.hotp
|
credential.oathType == OathType.hotp
|
||||||
? Icons.refresh
|
? Icons.refresh
|
||||||
: Icons.touch_app,
|
: Icons.touch_app,
|
||||||
size: big ? 36 : 18,
|
|
||||||
),
|
),
|
||||||
Text('', style: TextStyle(fontSize: big ? 32.0 : 22.0)),
|
const Text(''),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
if (credential.oathType == OathType.totp) ...[
|
if (credential.oathType == OathType.totp) ...[
|
||||||
@ -243,7 +235,8 @@ mixin AccountMixin {
|
|||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
SizedBox.square(
|
SizedBox.square(
|
||||||
dimension: big ? 32 : 16,
|
dimension:
|
||||||
|
(IconTheme.of(context).size ?? 18) * 0.8,
|
||||||
child: CircleTimer(
|
child: CircleTimer(
|
||||||
code.validFrom * 1000,
|
code.validFrom * 1000,
|
||||||
code.validTo * 1000,
|
code.validTo * 1000,
|
||||||
@ -256,16 +249,15 @@ mixin AccountMixin {
|
|||||||
opacity: expired ? 0.4 : 1.0,
|
opacity: expired ? 0.4 : 1.0,
|
||||||
child: Text(
|
child: Text(
|
||||||
formatCode(code),
|
formatCode(code),
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: big ? 32.0 : 22.0,
|
fontFeatures: [FontFeature.tabularFigures()],
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
//fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
),
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,11 @@ class AccountView extends ConsumerWidget with AccountMixin {
|
|||||||
items: _buildPopupMenu(context, ref),
|
items: _buildPopupMenu(context, ref),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: ListTile(
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
|
final showAvatar = constraints.maxWidth >= 315;
|
||||||
|
return ListTile(
|
||||||
|
shape:
|
||||||
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
@ -111,7 +115,8 @@ class AccountView extends ConsumerWidget with AccountMixin {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
leading: CircleAvatar(
|
leading: showAvatar
|
||||||
|
? CircleAvatar(
|
||||||
foregroundColor: darkMode ? Colors.black : Colors.white,
|
foregroundColor: darkMode ? Colors.black : Colors.white,
|
||||||
backgroundColor: _iconColor(darkMode ? 300 : 400),
|
backgroundColor: _iconColor(darkMode ? 300 : 400),
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -119,12 +124,15 @@ class AccountView extends ConsumerWidget with AccountMixin {
|
|||||||
.characters
|
.characters
|
||||||
.first
|
.first
|
||||||
.toUpperCase(),
|
.toUpperCase(),
|
||||||
style: const TextStyle(fontSize: 18),
|
style: const TextStyle(
|
||||||
),
|
fontSize: 16, fontWeight: FontWeight.w300),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
title: Text(
|
title: Text(
|
||||||
title,
|
title,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
),
|
),
|
||||||
@ -132,12 +140,28 @@ class AccountView extends ConsumerWidget with AccountMixin {
|
|||||||
? Text(
|
? Text(
|
||||||
subtitle!,
|
subtitle!,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
trailing: buildCodeView(ref),
|
trailing: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.rectangle,
|
||||||
|
color: CardTheme.of(context).color,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(30.0)),
|
||||||
),
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 10.0, vertical: 2.0),
|
||||||
|
child: DefaultTextStyle.merge(
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
child: buildCodeView(ref),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,7 +196,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Account details',
|
'Account details',
|
||||||
style: Theme.of(context).textTheme.headline6,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
key: const Key('issuer'),
|
key: const Key('issuer'),
|
||||||
@ -280,7 +280,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
const Divider(),
|
const Divider(),
|
||||||
Text(
|
Text(
|
||||||
'Options',
|
'Options',
|
||||||
style: Theme.of(context).textTheme.headline6,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
Wrap(
|
Wrap(
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
@ -297,6 +297,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
Chip(
|
Chip(
|
||||||
|
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||||
label: DropdownButtonHideUnderline(
|
label: DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<OathType>(
|
child: DropdownButton<OathType>(
|
||||||
value: _oathType,
|
value: _oathType,
|
||||||
@ -319,6 +320,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Chip(
|
Chip(
|
||||||
|
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||||
label: DropdownButtonHideUnderline(
|
label: DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<HashAlgorithm>(
|
child: DropdownButton<HashAlgorithm>(
|
||||||
value: _hashAlgorithm,
|
value: _hashAlgorithm,
|
||||||
@ -342,6 +344,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
),
|
),
|
||||||
if (_oathType == OathType.totp)
|
if (_oathType == OathType.totp)
|
||||||
Chip(
|
Chip(
|
||||||
|
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||||
label: DropdownButtonHideUnderline(
|
label: DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<int>(
|
child: DropdownButton<int>(
|
||||||
value: int.tryParse(_periodController.text) ??
|
value: int.tryParse(_periodController.text) ??
|
||||||
@ -366,6 +369,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Chip(
|
Chip(
|
||||||
|
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||||
label: DropdownButtonHideUnderline(
|
label: DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<int>(
|
child: DropdownButton<int>(
|
||||||
value: _digits,
|
value: _digits,
|
||||||
|
@ -64,7 +64,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
if (widget.state.hasKey) ...[
|
if (widget.state.hasKey) ...[
|
||||||
Text(
|
Text(
|
||||||
'Current password',
|
'Current password',
|
||||||
style: Theme.of(context).textTheme.headline6,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@ -120,7 +120,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
],
|
],
|
||||||
Text(
|
Text(
|
||||||
'New password',
|
'New password',
|
||||||
style: Theme.of(context).textTheme.headline6,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
autofocus: !widget.state.hasKey,
|
autofocus: !widget.state.hasKey,
|
||||||
|
@ -6,10 +6,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/views/app_failure_screen.dart';
|
import '../../app/views/app_failure_page.dart';
|
||||||
import '../../app/views/app_loading_screen.dart';
|
import '../../app/views/app_loading_screen.dart';
|
||||||
import '../../app/views/app_page.dart';
|
import '../../app/views/app_page.dart';
|
||||||
|
import '../../app/views/graphics.dart';
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
|
import '../../theme.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'account_list.dart';
|
import 'account_list.dart';
|
||||||
@ -29,10 +31,9 @@ class OathScreen extends ConsumerWidget {
|
|||||||
centered: true,
|
centered: true,
|
||||||
child: const AppLoadingScreen(),
|
child: const AppLoadingScreen(),
|
||||||
),
|
),
|
||||||
error: (error, _) => AppPage(
|
error: (error, _) => AppFailurePage(
|
||||||
title: const Text('Authenticator'),
|
title: const Text('Authenticator'),
|
||||||
centered: true,
|
cause: error,
|
||||||
child: AppFailureScreen('$error'),
|
|
||||||
),
|
),
|
||||||
data: (oathState) => oathState.locked
|
data: (oathState) => oathState.locked
|
||||||
? _LockedView(devicePath, oathState)
|
? _LockedView(devicePath, oathState)
|
||||||
@ -50,12 +51,42 @@ class _LockedView extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) => AppPage(
|
Widget build(BuildContext context, WidgetRef ref) => AppPage(
|
||||||
title: const Text('Authenticator'),
|
title: const Text('Authenticator'),
|
||||||
|
actions: [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
label: const Text('Options'),
|
||||||
|
icon: const Icon(Icons.tune),
|
||||||
|
onPressed: () {
|
||||||
|
showBottomMenu(context, [
|
||||||
|
MenuAction(
|
||||||
|
text: 'Manage password',
|
||||||
|
icon: const Icon(Icons.password),
|
||||||
|
action: (context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
ManagePasswordDialog(devicePath, oathState),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuAction(
|
||||||
|
text: 'Reset OATH',
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
action: (context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ResetDialog(devicePath),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const ListTile(title: Text('Unlock')),
|
const ListTile(title: Text('Unlock')),
|
||||||
_UnlockForm(
|
_UnlockForm(
|
||||||
devicePath,
|
devicePath,
|
||||||
oathState,
|
|
||||||
keystore: oathState.keystore,
|
keystore: oathState.keystore,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -76,9 +107,9 @@ class _UnlockedView extends ConsumerWidget {
|
|||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
title: const Text('Authenticator'),
|
title: const Text('Authenticator'),
|
||||||
|
graphic: noAccounts,
|
||||||
header: 'No accounts',
|
header: 'No accounts',
|
||||||
message: 'Follow the instructions on a website to add an account',
|
actions: _buildActions(context, true),
|
||||||
floatingActionButton: _buildFab(context),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,6 +127,7 @@ class _UnlockedView extends ConsumerWidget {
|
|||||||
return TextFormField(
|
return TextFormField(
|
||||||
key: const Key('search_accounts'),
|
key: const Key('search_accounts'),
|
||||||
initialValue: ref.read(searchProvider),
|
initialValue: ref.read(searchProvider),
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: 'Search accounts',
|
hintText: 'Search accounts',
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
@ -110,21 +142,18 @@ class _UnlockedView extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
floatingActionButton: _buildFab(context),
|
actions: _buildActions(context, false),
|
||||||
child: AccountList(devicePath, oathState),
|
child: AccountList(devicePath, oathState),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
FloatingActionButton _buildFab(BuildContext context) {
|
List<Widget> _buildActions(BuildContext context, bool isEmpty) {
|
||||||
final fab = FloatingActionButton.extended(
|
return [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
style: isEmpty ? AppTheme.primaryOutlinedButtonStyle(context) : null,
|
||||||
|
label: const Text('Add account'),
|
||||||
icon: const Icon(Icons.person_add_alt_1),
|
icon: const Icon(Icons.person_add_alt_1),
|
||||||
label: const Text('Setup'),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showBottomMenu(context, [
|
|
||||||
MenuAction(
|
|
||||||
text: 'Add account',
|
|
||||||
icon: const Icon(Icons.person_add_alt),
|
|
||||||
action: (context) {
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => OathAddAccountPage(
|
builder: (context) => OathAddAccountPage(
|
||||||
@ -134,6 +163,11 @@ class _UnlockedView extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
label: const Text('Options'),
|
||||||
|
icon: const Icon(Icons.tune),
|
||||||
|
onPressed: () {
|
||||||
|
showBottomMenu(context, [
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: oathState.hasKey ? 'Manage password' : 'Set password',
|
text: oathState.hasKey ? 'Manage password' : 'Set password',
|
||||||
icon: const Icon(Icons.password),
|
icon: const Icon(Icons.password),
|
||||||
@ -147,7 +181,7 @@ class _UnlockedView extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: 'Reset OATH',
|
text: 'Reset OATH',
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete),
|
||||||
action: (context) {
|
action: (context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -157,17 +191,15 @@ class _UnlockedView extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
return fab;
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UnlockForm extends ConsumerStatefulWidget {
|
class _UnlockForm extends ConsumerStatefulWidget {
|
||||||
final DevicePath _devicePath;
|
final DevicePath _devicePath;
|
||||||
final OathState _oathState;
|
|
||||||
final KeystoreState keystore;
|
final KeystoreState keystore;
|
||||||
const _UnlockForm(this._devicePath, this._oathState,
|
const _UnlockForm(this._devicePath, {required this.keystore});
|
||||||
{required this.keystore});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<_UnlockForm> createState() => _UnlockFormState();
|
ConsumerState<_UnlockForm> createState() => _UnlockFormState();
|
||||||
@ -224,33 +256,6 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
|
|||||||
onChanged: (_) => setState(() {}), // Update state on change
|
onChanged: (_) => setState(() {}), // Update state on change
|
||||||
onSubmitted: (_) => _submit(),
|
onSubmitted: (_) => _submit(),
|
||||||
),
|
),
|
||||||
Wrap(
|
|
||||||
spacing: 4.0,
|
|
||||||
runSpacing: 8.0,
|
|
||||||
children: [
|
|
||||||
OutlinedButton.icon(
|
|
||||||
icon: const Icon(Icons.password),
|
|
||||||
label: const Text('Manage password'),
|
|
||||||
onPressed: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ManagePasswordDialog(
|
|
||||||
widget._devicePath, widget._oathState),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
icon: const Icon(Icons.delete_outlined),
|
|
||||||
label: const Text('Reset OATH'),
|
|
||||||
onPressed: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ResetDialog(widget._devicePath),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
154
lib/theme.dart
@ -1,8 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const primaryGreen = Color(0xffa8c86c);
|
const primaryGreen = Color(0xffaed581);
|
||||||
const accentGreen = Color(0xff9aca3c);
|
const accentGreen = Color(0xff9aca3c);
|
||||||
const primaryBlue = Color(0xff325f74);
|
const primaryBlue = Color(0xff325f74);
|
||||||
|
const primaryRed = Color(0xffea4335);
|
||||||
|
|
||||||
class AppTheme {
|
class AppTheme {
|
||||||
static ThemeData get lightTheme => ThemeData(
|
static ThemeData get lightTheme => ThemeData(
|
||||||
@ -14,26 +15,71 @@ class AppTheme {
|
|||||||
secondary: accentGreen,
|
secondary: accentGreen,
|
||||||
background: Colors.grey.shade200,
|
background: Colors.grey.shade200,
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.white,
|
iconTheme: IconThemeData(
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
size: 18.0,
|
||||||
|
),
|
||||||
|
//backgroundColor: Colors.white,
|
||||||
toggleableActiveColor: accentGreen,
|
toggleableActiveColor: accentGreen,
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
elevation: 0.5,
|
elevation: 0,
|
||||||
backgroundColor: Colors.white,
|
toolbarHeight: 48,
|
||||||
|
//shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
foregroundColor: Colors.grey.shade800,
|
foregroundColor: Colors.grey.shade800,
|
||||||
),
|
),
|
||||||
|
// Mainly used for the OATH dialog view at the moment
|
||||||
|
buttonTheme: ButtonThemeData(
|
||||||
|
colorScheme: ColorScheme.light(
|
||||||
|
secondary: Colors.grey.shade300,
|
||||||
|
onSecondary: Colors.grey.shade900,
|
||||||
|
primary: primaryGreen,
|
||||||
|
onPrimary: Colors.grey.shade900,
|
||||||
|
error: primaryRed,
|
||||||
|
onError: Colors.grey.shade100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
primary: Colors.grey.shade800,
|
||||||
|
side: BorderSide(width: 1, color: Colors.grey.shade400),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||||
|
)),
|
||||||
|
cardTheme: CardTheme(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
),
|
||||||
|
chipTheme: ChipThemeData(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
selectedColor: const Color(0xffd2dbdf),
|
||||||
|
side: BorderSide(width: 1, color: Colors.grey.shade400),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||||
backgroundColor: primaryBlue,
|
backgroundColor: primaryBlue,
|
||||||
),
|
),
|
||||||
textTheme: TextTheme(
|
fontFamily: 'Roboto',
|
||||||
bodyText1: TextStyle(
|
textTheme: const TextTheme(
|
||||||
color: Colors.grey.shade600,
|
//bodySmall: TextStyle(color: Colors.grey.shade500),
|
||||||
),
|
//bodyLarge: const TextStyle(color: Colors.white70),
|
||||||
bodyText2: TextStyle(
|
//bodyMedium: TextStyle(color: Colors.grey.shade200),
|
||||||
color: Colors.grey.shade800,
|
//labelSmall: TextStyle(color: Colors.grey.shade500),
|
||||||
),
|
//labelMedium: TextStyle(color: Colors.cyan.shade200),
|
||||||
headline2: TextStyle(
|
//labelLarge: TextStyle(color: Colors.cyan.shade500),
|
||||||
color: Colors.grey.shade800,
|
//titleSmall: TextStyle(color: Colors.grey.shade600),
|
||||||
),
|
//titleMedium: const TextStyle(),
|
||||||
|
titleLarge: TextStyle(
|
||||||
|
//color: Colors.grey.shade500,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 18),
|
||||||
|
headlineSmall: TextStyle(
|
||||||
|
//color: Colors.grey.shade200,
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
fontSize: 16),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -43,23 +89,85 @@ class AppTheme {
|
|||||||
colorScheme:
|
colorScheme:
|
||||||
ColorScheme.fromSwatch(brightness: Brightness.dark).copyWith(
|
ColorScheme.fromSwatch(brightness: Brightness.dark).copyWith(
|
||||||
primary: primaryGreen,
|
primary: primaryGreen,
|
||||||
secondary: primaryGreen,
|
onPrimary: Colors.black,
|
||||||
|
secondary: const Color(0xff5d7d90),
|
||||||
|
),
|
||||||
|
iconTheme: const IconThemeData(
|
||||||
|
color: Colors.white70,
|
||||||
|
size: 18.0,
|
||||||
),
|
),
|
||||||
toggleableActiveColor: primaryGreen,
|
toggleableActiveColor: primaryGreen,
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
elevation: 0,
|
||||||
|
toolbarHeight: 48,
|
||||||
|
//shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
foregroundColor: Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
// Mainly used for the OATH dialog view at the moment
|
||||||
|
buttonTheme: ButtonThemeData(
|
||||||
|
colorScheme: ColorScheme.dark(
|
||||||
|
secondary: Colors.grey.shade800,
|
||||||
|
onSecondary: Colors.white70,
|
||||||
|
primary: primaryGreen,
|
||||||
|
onPrimary: Colors.grey.shade900,
|
||||||
|
error: primaryRed,
|
||||||
|
onError: Colors.grey.shade100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
primary: Colors.white70,
|
||||||
|
side: const BorderSide(width: 1, color: Colors.white12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||||
|
)),
|
||||||
|
cardTheme: CardTheme(
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
chipTheme: ChipThemeData(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
selectedColor: Colors.white12,
|
||||||
|
side: const BorderSide(width: 1, color: Colors.white12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dialogTheme: const DialogTheme(
|
||||||
|
backgroundColor: Color(0xff323232),
|
||||||
|
),
|
||||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||||
foregroundColor: Colors.grey.shade900,
|
foregroundColor: Colors.grey.shade900,
|
||||||
backgroundColor: primaryGreen,
|
backgroundColor: primaryGreen,
|
||||||
),
|
),
|
||||||
|
fontFamily: 'Roboto',
|
||||||
textTheme: TextTheme(
|
textTheme: TextTheme(
|
||||||
bodyText1: TextStyle(
|
bodySmall: TextStyle(color: Colors.grey.shade500),
|
||||||
color: Colors.grey.shade400,
|
bodyLarge: const TextStyle(color: Colors.white70),
|
||||||
),
|
bodyMedium: TextStyle(color: Colors.grey.shade200),
|
||||||
bodyText2: TextStyle(
|
labelSmall: TextStyle(color: Colors.grey.shade500),
|
||||||
|
labelMedium: TextStyle(color: Colors.cyan.shade200),
|
||||||
|
labelLarge: TextStyle(color: Colors.cyan.shade500),
|
||||||
|
titleSmall: TextStyle(color: Colors.grey.shade600),
|
||||||
|
titleMedium: const TextStyle(),
|
||||||
|
titleLarge: TextStyle(
|
||||||
color: Colors.grey.shade500,
|
color: Colors.grey.shade500,
|
||||||
),
|
fontWeight: FontWeight.w400,
|
||||||
headline2: TextStyle(
|
fontSize: 18),
|
||||||
color: Colors.grey.shade100,
|
headlineSmall: TextStyle(
|
||||||
),
|
color: Colors.grey.shade200,
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
fontSize: 16),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static ButtonStyle primaryOutlinedButtonStyle(BuildContext context) =>
|
||||||
|
OutlinedButton.styleFrom(
|
||||||
|
primary: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
side:
|
||||||
|
BorderSide(width: 1, color: Theme.of(context).colorScheme.primary),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,8 @@ class _CircleTimerState extends State<CircleTimer>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ProgressCircle(Colors.grey, _progress.value);
|
return ProgressCircle(
|
||||||
|
Theme.of(context).iconTheme.color ?? Colors.grey.shade600,
|
||||||
|
_progress.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,6 @@ class _ResponsiveDialogState extends State<ResponsiveDialog> {
|
|||||||
: 'Cancel';
|
: 'Cancel';
|
||||||
return DialogFrame(
|
return DialogFrame(
|
||||||
child: AlertDialog(
|
child: AlertDialog(
|
||||||
insetPadding: EdgeInsets.zero,
|
|
||||||
title: widget.title,
|
title: widget.title,
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
|
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
Executable file → Normal file
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 5.9 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
Executable file → Normal file
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 719 B |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
Executable file → Normal file
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 13 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
Executable file → Normal file
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
Executable file → Normal file
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
Executable file → Normal file
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 2.6 KiB |
12
pubspec.yaml
@ -88,7 +88,7 @@ flutter:
|
|||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
assets:
|
assets:
|
||||||
- assets/product-images/
|
- assets/product-images/
|
||||||
|
- assets/graphics/
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||||
@ -115,3 +115,13 @@ flutter:
|
|||||||
#
|
#
|
||||||
# For details regarding fonts from package dependencies,
|
# For details regarding fonts from package dependencies,
|
||||||
# see https://flutter.dev/custom-fonts/#from-packages
|
# see https://flutter.dev/custom-fonts/#from-packages
|
||||||
|
|
||||||
|
fonts:
|
||||||
|
- family: Roboto
|
||||||
|
fonts:
|
||||||
|
- asset: assets/fonts/Roboto-Regular.ttf
|
||||||
|
weight: 400
|
||||||
|
- asset: assets/fonts/Roboto-Light.ttf
|
||||||
|
weight: 300
|
||||||
|
- asset: assets/fonts/Roboto-Thin.ttf
|
||||||
|
weight: 100
|
||||||
|