Merge branch 'main' into patryk/product-images
10
.github/workflows/android.yaml
vendored
@ -35,6 +35,12 @@ jobs:
|
||||
with:
|
||||
path: 'app'
|
||||
|
||||
- name: Check app versions
|
||||
run: |
|
||||
python set-version.py
|
||||
git diff --exit-code
|
||||
working-directory: ./app
|
||||
|
||||
- name: Run flutter tests
|
||||
run: |
|
||||
flutter test
|
||||
@ -50,6 +56,10 @@ jobs:
|
||||
YUBIOATH_STORE_PASSWORD: ${{ secrets.YUBIOATH_STORE_PASSWORD }}
|
||||
working-directory: ./app
|
||||
|
||||
- name: Check generated files
|
||||
run: git diff --exit-code
|
||||
working-directory: ./app
|
||||
|
||||
- name: Run android tests
|
||||
run: |
|
||||
./gradlew test
|
||||
|
5
.github/workflows/linux.yml
vendored
@ -18,6 +18,11 @@ jobs:
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check app versions
|
||||
run: |
|
||||
python set-version.py
|
||||
git diff --exit-code
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
5
.github/workflows/macos.yml
vendored
@ -15,6 +15,11 @@ jobs:
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check app versions
|
||||
run: |
|
||||
python set-version.py
|
||||
git diff --exit-code
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew update
|
||||
|
5
.github/workflows/windows.yml
vendored
@ -15,6 +15,11 @@ jobs:
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check app versions
|
||||
run: |
|
||||
python set-version.py
|
||||
git diff --exit-code
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
choco install swig
|
||||
|
@ -23,9 +23,6 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
def authenticatorVersionCode = 59900
|
||||
def authenticatorVersionName = '6.0.0-alpha.2'
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
@ -52,8 +49,8 @@ android {
|
||||
applicationId "com.yubico.yubioath"
|
||||
minSdkVersion project.minSdkVersion
|
||||
targetSdkVersion project.targetSdkVersion
|
||||
versionCode authenticatorVersionCode
|
||||
versionName authenticatorVersionName
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
|
||||
|
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 |
@ -26,7 +26,14 @@
|
||||
# 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 .fido import Ctap2Node
|
||||
from .yubiotp import YubiOtpNode
|
||||
@ -47,6 +54,7 @@ from yubikit.logging import LOG_LEVEL
|
||||
|
||||
from ykman.pcsc import list_devices, YK_READER_NAME
|
||||
from smartcard.Exceptions import SmartcardException
|
||||
from smartcard.pcsc.PCSCExceptions import EstablishContextException
|
||||
from hashlib import sha256
|
||||
from dataclasses import asdict
|
||||
from typing import Mapping, Tuple
|
||||
@ -65,6 +73,15 @@ def _is_admin():
|
||||
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):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@ -127,9 +144,16 @@ class ReadersNode(RpcNode):
|
||||
return self.list_children()
|
||||
|
||||
def list_children(self):
|
||||
devices = [
|
||||
d for d in list_devices("") if YK_READER_NAME not in d.reader.name.lower()
|
||||
]
|
||||
try:
|
||||
devices = [
|
||||
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}
|
||||
if self._state != state:
|
||||
self._readers = {}
|
||||
@ -271,15 +295,27 @@ class UsbDeviceNode(AbstractDeviceNode):
|
||||
|
||||
@child(condition=lambda self: self._supports_connection(SmartCardConnection))
|
||||
def ccid(self):
|
||||
return self._create_connection(SmartCardConnection)
|
||||
try:
|
||||
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))
|
||||
def otp(self):
|
||||
return self._create_connection(OtpConnection)
|
||||
try:
|
||||
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))
|
||||
def fido(self):
|
||||
return self._create_connection(FidoConnection)
|
||||
try:
|
||||
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):
|
||||
|
@ -6,8 +6,8 @@ VSVersionInfo(
|
||||
ffi=FixedFileInfo(
|
||||
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
|
||||
# Set not needed items to zero 0.
|
||||
filevers=(4, 1, 0, 0),
|
||||
prodvers=(4, 1, 0, 0),
|
||||
filevers=(6, 0, 0, 0),
|
||||
prodvers=(6, 0, 0, 0),
|
||||
# Contains a bitmask that specifies the valid bits 'flags'r
|
||||
mask=0x3f,
|
||||
# Contains a bitmask that specifies the Boolean attributes of the file.
|
||||
|
@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:yubico_authenticator/app/state.dart';
|
||||
|
||||
import 'version.dart';
|
||||
import 'app/logging.dart';
|
||||
import 'app/message.dart';
|
||||
import 'core/state.dart';
|
||||
@ -26,8 +27,7 @@ class AboutPage extends ConsumerWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// TODO: Store the version number elsewhere
|
||||
const Text('Yubico Authenticator: 6.0.0-alpha.2'),
|
||||
const Text('Yubico Authenticator: $version'),
|
||||
if (isDesktop)
|
||||
Text('ykman version: ${ref.watch(rpcStateProvider).version}'),
|
||||
Text('Dart version: ${Platform.version}'),
|
||||
@ -41,7 +41,8 @@ class AboutPage extends ConsumerWidget {
|
||||
_log.info('Running diagnostics...');
|
||||
final response =
|
||||
await ref.read(rpcProvider).command('diagnose', []);
|
||||
final data = response['diagnostics'];
|
||||
final data = response['diagnostics'] as List;
|
||||
data.insert(0, {'app_version': version});
|
||||
final text = const JsonEncoder.withIndent(' ').convert(data);
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
await ref.read(withContextProvider)(
|
||||
@ -87,7 +88,7 @@ class LoggingPanel extends ConsumerWidget {
|
||||
OutlinedButton(
|
||||
child: const Text('Copy log'),
|
||||
onPressed: () async {
|
||||
_log.info('Copying log to clipboard...');
|
||||
_log.info('Copying log to clipboard ($version)...');
|
||||
final logs = await ref.read(logLevelProvider.notifier).getLogs();
|
||||
await Clipboard.setData(ClipboardData(text: logs.join('\n')));
|
||||
await ref.read(withContextProvider)(
|
||||
|
@ -185,19 +185,19 @@ class _QrScannerViewState extends State<QrScannerView> {
|
||||
Text('Looking for a code...',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline6
|
||||
.titleLarge
|
||||
?.copyWith(color: Colors.black)),
|
||||
if (_status == _ScanStatus.success)
|
||||
Text('Found a valid code',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline6
|
||||
.titleLarge
|
||||
?.copyWith(color: Colors.white)),
|
||||
if (_status == _ScanStatus.error)
|
||||
Text('This code is not valid, try again.',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline6
|
||||
.titleLarge
|
||||
?.copyWith(color: Colors.white)),
|
||||
]),
|
||||
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 Widget? title;
|
||||
final Widget child;
|
||||
final Widget? floatingActionButton;
|
||||
final List<Widget> actions;
|
||||
final bool centered;
|
||||
AppPage(
|
||||
{super.key,
|
||||
this.title,
|
||||
required this.child,
|
||||
this.floatingActionButton,
|
||||
this.centered = false});
|
||||
AppPage({
|
||||
super.key,
|
||||
this.title,
|
||||
required this.child,
|
||||
this.actions = const [],
|
||||
this.centered = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder(
|
||||
@ -29,7 +30,7 @@ class AppPage extends ConsumerWidget {
|
||||
body: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 240,
|
||||
width: 280,
|
||||
child: ListTileTheme(
|
||||
style: ListTileStyle.drawer,
|
||||
child: MainPageDrawer(shouldPop: false)),
|
||||
@ -46,11 +47,26 @@ class AppPage extends ConsumerWidget {
|
||||
|
||||
Widget _buildScrollView() => SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
// Make sure FAB doesn't block content
|
||||
padding: floatingActionButton != null
|
||||
? const EdgeInsets.only(bottom: 72)
|
||||
: null,
|
||||
child: child,
|
||||
child: Builder(builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
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(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
titleSpacing: 8,
|
||||
title: title,
|
||||
centerTitle: true,
|
||||
titleTextStyle: Theme.of(context).textTheme.titleLarge,
|
||||
actions: const [DeviceButton()],
|
||||
),
|
||||
drawer: hasDrawer ? const MainPageDrawer() : null,
|
||||
body: centered ? Center(child: _buildScrollView()) : _buildScrollView(),
|
||||
floatingActionButton: floatingActionButton,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ class DeviceAvatar extends StatelessWidget {
|
||||
CircleAvatar(
|
||||
radius: 22,
|
||||
backgroundColor: selected
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.transparent,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
|
@ -33,6 +33,7 @@ class DeviceButton extends ConsumerWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: IconButton(
|
||||
tooltip: 'Select YubiKey or device',
|
||||
icon: OverflowBox(
|
||||
maxHeight: 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 currentApp = ref.watch(currentAppProvider);
|
||||
|
||||
MediaQuery? mediaQuery =
|
||||
context.findAncestorWidgetOfExactType<MediaQuery>();
|
||||
final width = mediaQuery?.data.size.width ?? 400;
|
||||
|
||||
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(
|
||||
primary: false, //Prevents conflict with the MainPage scroll view.
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
'Yubico Authenticator',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
if (data != null) ...[
|
||||
// Normal YubiKey Applications
|
||||
...supportedApps
|
||||
@ -68,14 +71,6 @@ class MainPageDrawer extends ConsumerWidget {
|
||||
if (supportedApps.contains(Application.management) &&
|
||||
Application.management.getAvailability(data) ==
|
||||
Availability.enabled) ...[
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'Configuration',
|
||||
style: Theme.of(context).textTheme.bodyText2,
|
||||
),
|
||||
),
|
||||
DrawerItem(
|
||||
titleText: 'Toggle applications',
|
||||
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
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'Application',
|
||||
style: Theme.of(context).textTheme.bodyText2,
|
||||
),
|
||||
),
|
||||
DrawerItem(
|
||||
titleText: 'Settings',
|
||||
icon: const Icon(Icons.settings),
|
||||
@ -110,7 +98,7 @@ class MainPageDrawer extends ConsumerWidget {
|
||||
),
|
||||
DrawerItem(
|
||||
titleText: 'Help and feedback',
|
||||
icon: const Icon(Icons.help_outline),
|
||||
icon: const Icon(Icons.help),
|
||||
onTap: () {
|
||||
final nav = Navigator.of(context);
|
||||
if (shouldPop) nav.pop();
|
||||
@ -173,17 +161,22 @@ class DrawerItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
padding: const EdgeInsets.only(left: 12.0, right: 12.0),
|
||||
child: ListTile(
|
||||
enabled: enabled,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.horizontal(right: Radius.circular(20)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(30)),
|
||||
),
|
||||
dense: true,
|
||||
minLeadingWidth: 24,
|
||||
minVerticalPadding: 18,
|
||||
selected: selected,
|
||||
selectedColor: Theme.of(context).backgroundColor,
|
||||
selectedTileColor: Theme.of(context).colorScheme.secondary,
|
||||
leading: icon,
|
||||
selectedColor: Theme.of(context).colorScheme.onPrimary,
|
||||
selectedTileColor: Theme.of(context).colorScheme.primary,
|
||||
leading: IconTheme.merge(
|
||||
data: const IconThemeData(size: 24),
|
||||
child: icon,
|
||||
),
|
||||
title: Text(titleText),
|
||||
onTap: onTap,
|
||||
),
|
||||
|
@ -4,29 +4,40 @@ import 'app_page.dart';
|
||||
|
||||
class MessagePage extends StatelessWidget {
|
||||
final Widget? title;
|
||||
final String header;
|
||||
final String message;
|
||||
final Widget? floatingActionButton;
|
||||
final Widget? graphic;
|
||||
final String? header;
|
||||
final String? message;
|
||||
final List<Widget> actions;
|
||||
|
||||
const MessagePage({
|
||||
super.key,
|
||||
this.title,
|
||||
required this.header,
|
||||
required this.message,
|
||||
this.floatingActionButton,
|
||||
this.graphic,
|
||||
this.header,
|
||||
this.message,
|
||||
this.actions = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AppPage(
|
||||
title: title,
|
||||
centered: true,
|
||||
floatingActionButton: floatingActionButton,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(header, style: Theme.of(context).textTheme.headline6),
|
||||
const SizedBox(height: 12.0),
|
||||
Text(message, textAlign: TextAlign.center),
|
||||
],
|
||||
actions: actions,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
if (graphic != null) graphic!,
|
||||
if (header != null)
|
||||
Text(header!,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 12.0),
|
||||
if (message != null) ...[
|
||||
Text(message!, textAlign: TextAlign.center),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -5,25 +5,29 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/models.dart';
|
||||
import '../../desktop/state.dart';
|
||||
import '../../theme.dart';
|
||||
import '../message.dart';
|
||||
import '../models.dart';
|
||||
import 'app_page.dart';
|
||||
import 'device_avatar.dart';
|
||||
import 'graphics.dart';
|
||||
import 'message_page.dart';
|
||||
|
||||
class NoDeviceScreen extends ConsumerWidget {
|
||||
final DeviceNode? node;
|
||||
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 (Platform.isWindows &&
|
||||
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||
return [
|
||||
const DeviceAvatar(child: Icon(Icons.lock)),
|
||||
const Text('WebAuthn management requires elevated privileges.'),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.lock_open),
|
||||
return MessagePage(
|
||||
graphic: noPermission,
|
||||
message: 'Managing this device requires elevated privileges.',
|
||||
actions: [
|
||||
OutlinedButton.icon(
|
||||
style: AppTheme.primaryOutlinedButtonStyle(context),
|
||||
label: const Text('Unlock'),
|
||||
icon: const Icon(Icons.lock_open),
|
||||
onPressed: () async {
|
||||
final controller = showMessage(
|
||||
context, 'Elevating permissions...',
|
||||
@ -37,43 +41,31 @@ class NoDeviceScreen extends ConsumerWidget {
|
||||
} finally {
|
||||
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
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AppPage(
|
||||
centered: true,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: node?.map(usbYubiKey: (node) {
|
||||
return _buildUsbPid(context, ref, node.pid);
|
||||
}, nfcReader: (node) {
|
||||
return const [
|
||||
DeviceAvatar(child: Icon(Icons.wifi)),
|
||||
Text('Place your YubiKey on the NFC reader'),
|
||||
];
|
||||
}) ??
|
||||
const [
|
||||
DeviceAvatar(child: Icon(Icons.usb)),
|
||||
Text('Insert your YubiKey'),
|
||||
],
|
||||
),
|
||||
);
|
||||
return node?.map(usbYubiKey: (node) {
|
||||
return _buildUsbPid(context, ref, node.pid);
|
||||
}, nfcReader: (node) {
|
||||
return const MessagePage(
|
||||
graphic: DeviceAvatar(child: Icon(Icons.wifi)),
|
||||
message: 'Place your YubiKey on the NFC reader',
|
||||
);
|
||||
}) ??
|
||||
const MessagePage(
|
||||
graphic: DeviceAvatar(child: Icon(Icons.usb)),
|
||||
message: 'Insert your YubiKey',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,16 @@ Future<Widget> initialize(List<String> argv) async {
|
||||
exe = Uri.file(Platform.resolvedExecutable)
|
||||
.resolve(relativePath)
|
||||
.toFilePath();
|
||||
|
||||
if (Platform.isMacOS && Platform.version.contains('arm64')) {
|
||||
// See if there is an arm64 specific helper on arm64 Mac.
|
||||
final arm64exe = Uri.file(exe)
|
||||
.resolve('../helper-arm64/authenticator-helper')
|
||||
.toFilePath();
|
||||
if (await Directory(arm64exe).exists()) {
|
||||
exe = arm64exe;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_log.info('Starting Helper subprocess: $exe');
|
||||
|
@ -133,26 +133,27 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Step 1/2: Capture fingerprint'),
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(36.0),
|
||||
child: AnimatedBuilder(
|
||||
animation: _color,
|
||||
builder: (context, _) {
|
||||
return Icon(
|
||||
_fingerprint == null ? Icons.fingerprint : Icons.check,
|
||||
size: 200.0,
|
||||
size: 128.0,
|
||||
color: _color.value,
|
||||
);
|
||||
},
|
||||
),
|
||||
LinearProgressIndicator(value: progress),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(_getMessage()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
LinearProgressIndicator(value: progress),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(_getMessage()),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Text('Step 2/2: Name fingerprint'),
|
||||
TextFormField(
|
||||
|
@ -1,16 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/message.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_page.dart';
|
||||
import '../../app/views/device_avatar.dart';
|
||||
import '../../app/views/message_page.dart';
|
||||
import '../../desktop/state.dart';
|
||||
import '../../management/models.dart';
|
||||
import '../state.dart';
|
||||
import 'locked_page.dart';
|
||||
@ -51,52 +46,10 @@ class FidoScreen extends ConsumerWidget {
|
||||
'WebAuthn requires the FIDO2 application to be enabled on your YubiKey',
|
||||
);
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
if (!ref
|
||||
.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||
return AppPage(
|
||||
title: const Text('WebAuthn'),
|
||||
centered: true,
|
||||
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(
|
||||
|
||||
return AppFailurePage(
|
||||
title: const Text('WebAuthn'),
|
||||
centered: true,
|
||||
child: AppFailureScreen('$error'),
|
||||
cause: error,
|
||||
);
|
||||
},
|
||||
data: (fidoState) {
|
||||
|
@ -4,7 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/views/app_page.dart';
|
||||
import '../../app/views/graphics.dart';
|
||||
import '../../app/views/message_page.dart';
|
||||
import '../../theme.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'pin_dialog.dart';
|
||||
@ -22,23 +24,26 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
if (state.bioEnroll != null) {
|
||||
return MessagePage(
|
||||
title: const Text('WebAuthn'),
|
||||
graphic: noFingerprints,
|
||||
header: 'No fingerprints',
|
||||
message: 'Set a PIN to register fingerprints',
|
||||
floatingActionButton: _buildFab(context),
|
||||
message: 'Set a PIN to register fingerprints.',
|
||||
actions: _buildActions(context),
|
||||
);
|
||||
} else {
|
||||
return MessagePage(
|
||||
title: const Text('WebAuthn'),
|
||||
graphic: noDiscoverable,
|
||||
header: 'No discoverable accounts',
|
||||
message:
|
||||
'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(
|
||||
title: const Text('WebAuthn'),
|
||||
actions: _buildActions(context),
|
||||
child: Column(
|
||||
children: [
|
||||
const ListTile(title: Text('Unlock')),
|
||||
@ -48,41 +53,49 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
FloatingActionButton _buildFab(BuildContext context) {
|
||||
return FloatingActionButton.extended(
|
||||
icon: Icon(state.bioEnroll != null ? Icons.fingerprint : Icons.pin),
|
||||
label: const Text('Setup'),
|
||||
onPressed: () {
|
||||
showBottomMenu(context, [
|
||||
if (state.bioEnroll != null)
|
||||
MenuAction(
|
||||
text: 'Add fingerprint',
|
||||
icon: const Icon(Icons.fingerprint),
|
||||
),
|
||||
MenuAction(
|
||||
text: 'Set PIN',
|
||||
icon: const Icon(Icons.pin_outlined),
|
||||
action: (context) {
|
||||
List<Widget> _buildActions(BuildContext context) => [
|
||||
if (!state.hasPin)
|
||||
OutlinedButton.icon(
|
||||
style: AppTheme.primaryOutlinedButtonStyle(context),
|
||||
label: const Text('Set PIN'),
|
||||
icon: const Icon(Icons.pin),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FidoPinDialog(node.path, state),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
text: 'Reset FIDO',
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
action: (context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(node),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
},
|
||||
);
|
||||
}
|
||||
OutlinedButton.icon(
|
||||
label: const Text('Options'),
|
||||
icon: const Icon(Icons.tune),
|
||||
onPressed: () {
|
||||
showBottomMenu(context, [
|
||||
if (state.hasPin)
|
||||
MenuAction(
|
||||
text: 'Change PIN',
|
||||
icon: const Icon(Icons.pin),
|
||||
action: (context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FidoPinDialog(node.path, state),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
text: 'Reset FIDO',
|
||||
icon: const Icon(Icons.delete),
|
||||
action: (context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(node),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
class _PinEntryForm extends ConsumerStatefulWidget {
|
||||
@ -149,34 +162,6 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
||||
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(
|
||||
leading:
|
||||
noFingerprints ? const Icon(Icons.warning_amber_rounded) : null,
|
||||
|
@ -50,7 +50,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
if (hasPin) ...[
|
||||
Text(
|
||||
'Current PIN',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
TextFormField(
|
||||
initialValue: _currentPin,
|
||||
@ -70,7 +70,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
],
|
||||
Text(
|
||||
'New PIN',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
TextFormField(
|
||||
initialValue: _newPin,
|
||||
|
@ -94,7 +94,7 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||
),
|
||||
Center(
|
||||
child: Text(_getMessage(),
|
||||
style: Theme.of(context).textTheme.headline6),
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
|
@ -4,7 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/views/app_page.dart';
|
||||
import '../../app/views/graphics.dart';
|
||||
import '../../app/views/message_page.dart';
|
||||
import '../../theme.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'add_fingerprint_dialog.dart';
|
||||
@ -29,10 +31,23 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
? [
|
||||
const ListTile(title: Text('Credentials')),
|
||||
...creds.map((cred) => ListTile(
|
||||
leading:
|
||||
const CircleAvatar(child: Icon(Icons.link)),
|
||||
title: Text(cred.userName),
|
||||
subtitle: Text(cred.rpId),
|
||||
leading: CircleAvatar(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -45,7 +60,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
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')),
|
||||
...fingerprints.map((fp) => ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Icons.fingerprint)),
|
||||
title: Text(fp.label),
|
||||
leading: CircleAvatar(
|
||||
foregroundColor:
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -74,7 +98,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
node.path, fp),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.edit)),
|
||||
icon: const Icon(Icons.edit_outlined)),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
@ -84,7 +108,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
node.path, fp),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete_outlined)),
|
||||
icon: const Icon(Icons.delete_outline)),
|
||||
],
|
||||
),
|
||||
))
|
||||
@ -97,69 +121,76 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
if (children.isNotEmpty) {
|
||||
return AppPage(
|
||||
title: const Text('WebAuthn'),
|
||||
floatingActionButton: _buildFab(context),
|
||||
actions: _buildActions(context),
|
||||
child: Column(
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.bioEnroll == false) {
|
||||
if (state.bioEnroll != null) {
|
||||
return MessagePage(
|
||||
title: const Text('WebAuthn'),
|
||||
graphic: noFingerprints,
|
||||
header: 'No fingerprints',
|
||||
message: 'Add one or more (up to five) fingerprints',
|
||||
floatingActionButton: _buildFab(context),
|
||||
actions: _buildActions(context, fingerprintPrimary: true),
|
||||
);
|
||||
}
|
||||
|
||||
return MessagePage(
|
||||
title: const Text('WebAuthn'),
|
||||
graphic: noDiscoverable,
|
||||
header: 'No discoverable accounts',
|
||||
message: 'Register as a Security Key on websites',
|
||||
floatingActionButton: _buildFab(context),
|
||||
actions: _buildActions(context),
|
||||
);
|
||||
}
|
||||
|
||||
FloatingActionButton _buildFab(BuildContext context) {
|
||||
return FloatingActionButton.extended(
|
||||
icon: Icon(state.bioEnroll != null ? Icons.fingerprint : Icons.pin),
|
||||
label: const Text('Setup'),
|
||||
onPressed: () {
|
||||
showBottomMenu(context, [
|
||||
if (state.bioEnroll != null)
|
||||
MenuAction(
|
||||
text: 'Add fingerprint',
|
||||
icon: const Icon(Icons.fingerprint),
|
||||
action: (context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AddFingerprintDialog(node.path),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
text: 'Change PIN',
|
||||
icon: const Icon(Icons.pin_outlined),
|
||||
action: (context) {
|
||||
List<Widget> _buildActions(BuildContext context,
|
||||
{bool fingerprintPrimary = false}) =>
|
||||
[
|
||||
if (state.bioEnroll != null)
|
||||
OutlinedButton.icon(
|
||||
style: fingerprintPrimary
|
||||
? AppTheme.primaryOutlinedButtonStyle(context)
|
||||
: null,
|
||||
label: const Text('Add fingerprint'),
|
||||
icon: const Icon(Icons.fingerprint),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FidoPinDialog(node.path, state),
|
||||
builder: (context) => AddFingerprintDialog(node.path),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
text: 'Delete all data',
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
action: (context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(node),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
},
|
||||
);
|
||||
}
|
||||
OutlinedButton.icon(
|
||||
label: const Text('Options'),
|
||||
icon: const Icon(Icons.tune),
|
||||
onPressed: () {
|
||||
showBottomMenu(context, [
|
||||
MenuAction(
|
||||
text: 'Change PIN',
|
||||
icon: const Icon(Icons.pin),
|
||||
action: (context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FidoPinDialog(node.path, state),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
text: 'Reset FIDO',
|
||||
icon: const Icon(Icons.delete),
|
||||
action: (context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(node),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import 'package:collection/collection.dart';
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/app_failure_screen.dart';
|
||||
import '../../app/views/app_loading_screen.dart';
|
||||
import '../../core/models.dart';
|
||||
import '../../widgets/responsive_dialog.dart';
|
||||
@ -27,8 +26,8 @@ class _CapabilityForm extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 8.0,
|
||||
spacing: 8,
|
||||
runSpacing: 16,
|
||||
children: Capability.values
|
||||
.where((c) => capabilities & c.value != 0)
|
||||
.map((c) => FilterChip(
|
||||
@ -85,30 +84,41 @@ class _CapabilitiesForm extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (usbCapabilities != 0)
|
||||
if (usbCapabilities != 0) ...[
|
||||
const ListTile(
|
||||
leading: Icon(Icons.usb),
|
||||
title: Text('USB applications'),
|
||||
title: Text('USB'),
|
||||
contentPadding: EdgeInsets.only(bottom: 8),
|
||||
horizontalTitleGap: 0,
|
||||
),
|
||||
_CapabilityForm(
|
||||
capabilities: usbCapabilities,
|
||||
enabled: enabled[Transport.usb] ?? 0,
|
||||
onChanged: (value) {
|
||||
onChanged({...enabled, Transport.usb: value});
|
||||
},
|
||||
),
|
||||
if (nfcCapabilities != 0)
|
||||
_CapabilityForm(
|
||||
capabilities: usbCapabilities,
|
||||
enabled: enabled[Transport.usb] ?? 0,
|
||||
onChanged: (value) {
|
||||
onChanged({...enabled, Transport.usb: value});
|
||||
},
|
||||
),
|
||||
],
|
||||
if (nfcCapabilities != 0) ...[
|
||||
if (usbCapabilities != 0)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 24, bottom: 12),
|
||||
child: Divider(),
|
||||
),
|
||||
const ListTile(
|
||||
leading: Icon(Icons.wifi),
|
||||
title: Text('NFC applications'),
|
||||
title: Text('NFC'),
|
||||
contentPadding: EdgeInsets.only(bottom: 8),
|
||||
horizontalTitleGap: 0,
|
||||
),
|
||||
_CapabilityForm(
|
||||
capabilities: nfcCapabilities,
|
||||
enabled: enabled[Transport.nfc] ?? 0,
|
||||
onChanged: (value) {
|
||||
onChanged({...enabled, Transport.nfc: value});
|
||||
},
|
||||
),
|
||||
_CapabilityForm(
|
||||
capabilities: nfcCapabilities,
|
||||
enabled: enabled[Transport.nfc] ?? 0,
|
||||
onChanged: (value) {
|
||||
onChanged({...enabled, Transport.nfc: value});
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -126,7 +136,6 @@ class ManagementScreen extends ConsumerStatefulWidget {
|
||||
class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
late Map<Transport, int> _enabled;
|
||||
late int _interfaces;
|
||||
bool _canSave = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -227,46 +236,55 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
});
|
||||
|
||||
var canSave = false;
|
||||
final child =
|
||||
ref.watch(managementStateProvider(widget.deviceData.node.path)).when(
|
||||
loading: () => const AppLoadingScreen(),
|
||||
error: (error, _) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (info) {
|
||||
bool hasConfig = info.version.major > 4;
|
||||
if (hasConfig) {
|
||||
canSave = !_mapEquals(
|
||||
_enabled,
|
||||
info.config.enabledCapabilities,
|
||||
);
|
||||
} else {
|
||||
canSave = _interfaces != 0 &&
|
||||
_interfaces !=
|
||||
UsbInterface.forCapabilites(widget.deviceData.info
|
||||
.config.enabledCapabilities[Transport.usb] ??
|
||||
0);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
hasConfig
|
||||
? _buildCapabilitiesForm(context, ref, info)
|
||||
: _buildModeForm(context, ref, info),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Toggle applications'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _canSave ? _submitForm : null,
|
||||
onPressed: canSave ? _submitForm : null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
child:
|
||||
ref.watch(managementStateProvider(widget.deviceData.node.path)).when(
|
||||
loading: () => const AppLoadingScreen(),
|
||||
error: (error, _) => AppFailureScreen('$error'),
|
||||
data: (info) {
|
||||
bool hasConfig = info.version.major > 4;
|
||||
setState(() {
|
||||
if (hasConfig) {
|
||||
_canSave = !_mapEquals(
|
||||
_enabled,
|
||||
info.config.enabledCapabilities,
|
||||
);
|
||||
} else {
|
||||
_canSave = _interfaces != 0 &&
|
||||
_interfaces !=
|
||||
UsbInterface.forCapabilites(widget
|
||||
.deviceData
|
||||
.info
|
||||
.config
|
||||
.enabledCapabilities[Transport.usb] ??
|
||||
0);
|
||||
}
|
||||
});
|
||||
return Column(
|
||||
children: [
|
||||
hasConfig
|
||||
? _buildCapabilitiesForm(context, ref, info)
|
||||
: _buildModeForm(context, ref, info),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,9 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../core/models.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../widgets/dialog_frame.dart';
|
||||
import '../models.dart';
|
||||
@ -44,17 +46,38 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
|
||||
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) {
|
||||
return buildActions(context, ref).map((e) {
|
||||
final action = e.action;
|
||||
return IconButton(
|
||||
icon: e.icon,
|
||||
tooltip: e.text,
|
||||
onPressed: action != null
|
||||
? () {
|
||||
action(context);
|
||||
}
|
||||
: null,
|
||||
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,
|
||||
iconSize: 22,
|
||||
tooltip: e.text,
|
||||
disabledColor: Colors.white70,
|
||||
onPressed: action != null
|
||||
? () {
|
||||
action(context);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
@ -69,20 +92,62 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
|
||||
}
|
||||
return DialogFrame(
|
||||
child: AlertDialog(
|
||||
title: Text(title),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
actionsPadding: EdgeInsets.zero,
|
||||
title: Center(
|
||||
child: Text(
|
||||
title,
|
||||
overflow: TextOverflow.fade,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(subtitle ?? ''),
|
||||
const SizedBox(height: 8.0),
|
||||
Center(child: FittedBox(child: buildCodeView(ref, big: true))),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
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(
|
||||
children: [
|
||||
if (pinnedCreds.isNotEmpty)
|
||||
const ListTile(
|
||||
ListTile(
|
||||
minVerticalPadding: 16,
|
||||
title: Text(
|
||||
'Pinned',
|
||||
'PINNED',
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
...pinnedCreds.map(
|
||||
@ -106,9 +108,11 @@ class _AccountListState extends ConsumerState<AccountList> {
|
||||
),
|
||||
),
|
||||
if (creds.isNotEmpty)
|
||||
const ListTile(
|
||||
ListTile(
|
||||
minVerticalPadding: 16,
|
||||
title: Text(
|
||||
'Accounts',
|
||||
'ACCOUNTS',
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
...creds.map(
|
||||
|
@ -147,6 +147,16 @@ mixin AccountMixin {
|
||||
final pinned = isPinned(ref);
|
||||
|
||||
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)
|
||||
MenuAction(
|
||||
text: 'Calculate',
|
||||
@ -157,27 +167,19 @@ mixin AccountMixin {
|
||||
}
|
||||
: 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(
|
||||
text: pinned ? 'Unpin account' : 'Pin account',
|
||||
//TODO: Replace this with a custom icon.
|
||||
icon: pinned
|
||||
? CustomPaint(
|
||||
painter: _StrikethroughPainter(
|
||||
Theme.of(context).iconTheme.color ?? Colors.black),
|
||||
child: ClipPath(
|
||||
clipper: _StrikethroughClipper(),
|
||||
child: const Icon(Icons.push_pin_outlined)),
|
||||
)
|
||||
? Builder(builder: (context) {
|
||||
return CustomPaint(
|
||||
painter: _StrikethroughPainter(
|
||||
IconTheme.of(context).color ?? Colors.black),
|
||||
child: ClipPath(
|
||||
clipper: _StrikethroughClipper(),
|
||||
child: const Icon(Icons.push_pin)),
|
||||
);
|
||||
})
|
||||
: const Icon(Icons.push_pin_outlined),
|
||||
action: (context) {
|
||||
ref.read(favoritesProvider.notifier).toggleFavorite(credential.id);
|
||||
@ -194,7 +196,7 @@ mixin AccountMixin {
|
||||
),
|
||||
MenuAction(
|
||||
text: 'Delete account',
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
action: (context) async {
|
||||
await deleteCredential(context, ref);
|
||||
},
|
||||
@ -203,69 +205,59 @@ mixin AccountMixin {
|
||||
}
|
||||
|
||||
@protected
|
||||
Widget buildCodeView(WidgetRef ref, {bool big = false}) {
|
||||
Widget buildCodeView(WidgetRef ref) {
|
||||
final code = getCode(ref);
|
||||
final expired = isExpired(code, ref);
|
||||
return DecoratedBox(
|
||||
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,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: Padding(
|
||||
padding: big
|
||||
? const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0)
|
||||
: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 2.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: code == null
|
||||
? [
|
||||
Icon(
|
||||
credential.oathType == OathType.hotp
|
||||
? Icons.refresh
|
||||
: Icons.touch_app,
|
||||
size: big ? 36 : 18,
|
||||
),
|
||||
Text('', style: TextStyle(fontSize: big ? 32.0 : 22.0)),
|
||||
]
|
||||
: [
|
||||
if (credential.oathType == OathType.totp) ...[
|
||||
...expired
|
||||
? [
|
||||
if (credential.touchRequired) ...[
|
||||
const Icon(Icons.touch_app),
|
||||
const SizedBox(width: 8.0),
|
||||
]
|
||||
]
|
||||
: [
|
||||
SizedBox.square(
|
||||
dimension: big ? 32 : 16,
|
||||
child: CircleTimer(
|
||||
code.validFrom * 1000,
|
||||
code.validTo * 1000,
|
||||
),
|
||||
),
|
||||
return AnimatedSize(
|
||||
alignment: Alignment.centerRight,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: Builder(builder: (context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: code == null
|
||||
? [
|
||||
Icon(
|
||||
credential.oathType == OathType.hotp
|
||||
? Icons.refresh
|
||||
: Icons.touch_app,
|
||||
),
|
||||
const Text(''),
|
||||
]
|
||||
: [
|
||||
if (credential.oathType == OathType.totp) ...[
|
||||
...expired
|
||||
? [
|
||||
if (credential.touchRequired) ...[
|
||||
const Icon(Icons.touch_app),
|
||||
const SizedBox(width: 8.0),
|
||||
],
|
||||
],
|
||||
Opacity(
|
||||
opacity: expired ? 0.4 : 1.0,
|
||||
child: Text(
|
||||
formatCode(code),
|
||||
style: TextStyle(
|
||||
fontSize: big ? 32.0 : 22.0,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
]
|
||||
]
|
||||
: [
|
||||
SizedBox.square(
|
||||
dimension:
|
||||
(IconTheme.of(context).size ?? 18) * 0.8,
|
||||
child: CircleTimer(
|
||||
code.validFrom * 1000,
|
||||
code.validTo * 1000,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
],
|
||||
],
|
||||
Opacity(
|
||||
opacity: expired ? 0.4 : 1.0,
|
||||
child: Text(
|
||||
formatCode(code),
|
||||
style: const TextStyle(
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
//fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -88,56 +88,80 @@ class AccountView extends ConsumerWidget with AccountMixin {
|
||||
items: _buildPopupMenu(context, ref),
|
||||
);
|
||||
},
|
||||
child: ListTile(
|
||||
focusNode: focusNode,
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AccountDialog(credential);
|
||||
},
|
||||
);
|
||||
},
|
||||
onLongPress: () async {
|
||||
if (calculateReady) {
|
||||
await calculateCode(
|
||||
context,
|
||||
ref,
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
final showAvatar = constraints.maxWidth >= 315;
|
||||
return ListTile(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
|
||||
focusNode: focusNode,
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AccountDialog(credential);
|
||||
},
|
||||
);
|
||||
}
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
copyToClipboard(context, ref);
|
||||
},
|
||||
);
|
||||
},
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: darkMode ? Colors.black : Colors.white,
|
||||
backgroundColor: _iconColor(darkMode ? 300 : 400),
|
||||
child: Text(
|
||||
(credential.issuer ?? credential.name)
|
||||
.characters
|
||||
.first
|
||||
.toUpperCase(),
|
||||
style: const TextStyle(fontSize: 18),
|
||||
},
|
||||
onLongPress: () async {
|
||||
if (calculateReady) {
|
||||
await calculateCode(
|
||||
context,
|
||||
ref,
|
||||
);
|
||||
}
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
copyToClipboard(context, ref);
|
||||
},
|
||||
);
|
||||
},
|
||||
leading: showAvatar
|
||||
? CircleAvatar(
|
||||
foregroundColor: darkMode ? Colors.black : Colors.white,
|
||||
backgroundColor: _iconColor(darkMode ? 300 : 400),
|
||||
child: Text(
|
||||
(credential.issuer ?? credential.name)
|
||||
.characters
|
||||
.first
|
||||
.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w300),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
title,
|
||||
overflow: TextOverflow.fade,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
),
|
||||
subtitle: subtitle != null
|
||||
? Text(
|
||||
subtitle!,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
)
|
||||
: null,
|
||||
trailing: buildCodeView(ref),
|
||||
),
|
||||
subtitle: subtitle != null
|
||||
? Text(
|
||||
subtitle!,
|
||||
overflow: TextOverflow.fade,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
)
|
||||
: null,
|
||||
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: [
|
||||
Text(
|
||||
'Account details',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
TextField(
|
||||
key: const Key('issuer'),
|
||||
@ -280,7 +280,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
const Divider(),
|
||||
Text(
|
||||
'Options',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
@ -297,6 +297,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
},
|
||||
),
|
||||
Chip(
|
||||
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||
label: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<OathType>(
|
||||
value: _oathType,
|
||||
@ -319,6 +320,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
),
|
||||
),
|
||||
Chip(
|
||||
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||
label: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<HashAlgorithm>(
|
||||
value: _hashAlgorithm,
|
||||
@ -342,6 +344,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
),
|
||||
if (_oathType == OathType.totp)
|
||||
Chip(
|
||||
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||
label: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
value: int.tryParse(_periodController.text) ??
|
||||
@ -366,6 +369,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
),
|
||||
),
|
||||
Chip(
|
||||
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||
label: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
value: _digits,
|
||||
|
@ -64,7 +64,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
if (widget.state.hasKey) ...[
|
||||
Text(
|
||||
'Current password',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
@ -120,7 +120,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
],
|
||||
Text(
|
||||
'New password',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
TextField(
|
||||
autofocus: !widget.state.hasKey,
|
||||
|
@ -6,10 +6,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/message.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_page.dart';
|
||||
import '../../app/views/graphics.dart';
|
||||
import '../../app/views/message_page.dart';
|
||||
import '../../theme.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'account_list.dart';
|
||||
@ -29,10 +31,9 @@ class OathScreen extends ConsumerWidget {
|
||||
centered: true,
|
||||
child: const AppLoadingScreen(),
|
||||
),
|
||||
error: (error, _) => AppPage(
|
||||
error: (error, _) => AppFailurePage(
|
||||
title: const Text('Authenticator'),
|
||||
centered: true,
|
||||
child: AppFailureScreen('$error'),
|
||||
cause: error,
|
||||
),
|
||||
data: (oathState) => oathState.locked
|
||||
? _LockedView(devicePath, oathState)
|
||||
@ -50,12 +51,42 @@ class _LockedView extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => AppPage(
|
||||
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(
|
||||
children: [
|
||||
const ListTile(title: Text('Unlock')),
|
||||
_UnlockForm(
|
||||
devicePath,
|
||||
oathState,
|
||||
keystore: oathState.keystore,
|
||||
),
|
||||
],
|
||||
@ -76,9 +107,9 @@ class _UnlockedView extends ConsumerWidget {
|
||||
if (isEmpty) {
|
||||
return MessagePage(
|
||||
title: const Text('Authenticator'),
|
||||
graphic: noAccounts,
|
||||
header: 'No accounts',
|
||||
message: 'Follow the instructions on a website to add an account',
|
||||
floatingActionButton: _buildFab(context),
|
||||
actions: _buildActions(context, true),
|
||||
);
|
||||
}
|
||||
|
||||
@ -96,6 +127,7 @@ class _UnlockedView extends ConsumerWidget {
|
||||
return TextFormField(
|
||||
key: const Key('search_accounts'),
|
||||
initialValue: ref.read(searchProvider),
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search accounts',
|
||||
border: InputBorder.none,
|
||||
@ -110,64 +142,64 @@ class _UnlockedView extends ConsumerWidget {
|
||||
);
|
||||
}),
|
||||
),
|
||||
floatingActionButton: _buildFab(context),
|
||||
actions: _buildActions(context, false),
|
||||
child: AccountList(devicePath, oathState),
|
||||
);
|
||||
}
|
||||
|
||||
FloatingActionButton _buildFab(BuildContext context) {
|
||||
final fab = FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.person_add_alt_1),
|
||||
label: const Text('Setup'),
|
||||
onPressed: () {
|
||||
showBottomMenu(context, [
|
||||
MenuAction(
|
||||
text: 'Add account',
|
||||
icon: const Icon(Icons.person_add_alt),
|
||||
action: (context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => OathAddAccountPage(
|
||||
devicePath,
|
||||
openQrScanner: Platform.isAndroid,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
text: oathState.hasKey ? 'Manage password' : 'Set 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_outline),
|
||||
action: (context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(devicePath),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
},
|
||||
);
|
||||
return fab;
|
||||
List<Widget> _buildActions(BuildContext context, bool isEmpty) {
|
||||
return [
|
||||
OutlinedButton.icon(
|
||||
style: isEmpty ? AppTheme.primaryOutlinedButtonStyle(context) : null,
|
||||
label: const Text('Add account'),
|
||||
icon: const Icon(Icons.person_add_alt_1),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => OathAddAccountPage(
|
||||
devicePath,
|
||||
openQrScanner: Platform.isAndroid,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
label: const Text('Options'),
|
||||
icon: const Icon(Icons.tune),
|
||||
onPressed: () {
|
||||
showBottomMenu(context, [
|
||||
MenuAction(
|
||||
text: oathState.hasKey ? 'Manage password' : 'Set 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),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _UnlockForm extends ConsumerStatefulWidget {
|
||||
final DevicePath _devicePath;
|
||||
final OathState _oathState;
|
||||
final KeystoreState keystore;
|
||||
const _UnlockForm(this._devicePath, this._oathState,
|
||||
{required this.keystore});
|
||||
const _UnlockForm(this._devicePath, {required this.keystore});
|
||||
|
||||
@override
|
||||
ConsumerState<_UnlockForm> createState() => _UnlockFormState();
|
||||
@ -224,33 +256,6 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
|
||||
onChanged: (_) => setState(() {}), // Update state on change
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
156
lib/theme.dart
@ -1,8 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const primaryGreen = Color(0xffa8c86c);
|
||||
const primaryGreen = Color(0xffaed581);
|
||||
const accentGreen = Color(0xff9aca3c);
|
||||
const primaryBlue = Color(0xff325f74);
|
||||
const primaryRed = Color(0xffea4335);
|
||||
|
||||
class AppTheme {
|
||||
static ThemeData get lightTheme => ThemeData(
|
||||
@ -14,26 +15,71 @@ class AppTheme {
|
||||
secondary: accentGreen,
|
||||
background: Colors.grey.shade200,
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
iconTheme: IconThemeData(
|
||||
color: Colors.grey.shade400,
|
||||
size: 18.0,
|
||||
),
|
||||
//backgroundColor: Colors.white,
|
||||
toggleableActiveColor: accentGreen,
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0.5,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
toolbarHeight: 48,
|
||||
//shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
|
||||
backgroundColor: Colors.transparent,
|
||||
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(
|
||||
backgroundColor: primaryBlue,
|
||||
),
|
||||
textTheme: TextTheme(
|
||||
bodyText1: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
bodyText2: TextStyle(
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
headline2: TextStyle(
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
fontFamily: 'Roboto',
|
||||
textTheme: const TextTheme(
|
||||
//bodySmall: TextStyle(color: Colors.grey.shade500),
|
||||
//bodyLarge: const TextStyle(color: Colors.white70),
|
||||
//bodyMedium: TextStyle(color: Colors.grey.shade200),
|
||||
//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,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 18),
|
||||
headlineSmall: TextStyle(
|
||||
//color: Colors.grey.shade200,
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 16),
|
||||
),
|
||||
);
|
||||
|
||||
@ -43,23 +89,85 @@ class AppTheme {
|
||||
colorScheme:
|
||||
ColorScheme.fromSwatch(brightness: Brightness.dark).copyWith(
|
||||
primary: primaryGreen,
|
||||
secondary: primaryGreen,
|
||||
onPrimary: Colors.black,
|
||||
secondary: const Color(0xff5d7d90),
|
||||
),
|
||||
iconTheme: const IconThemeData(
|
||||
color: Colors.white70,
|
||||
size: 18.0,
|
||||
),
|
||||
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(
|
||||
foregroundColor: Colors.grey.shade900,
|
||||
backgroundColor: primaryGreen,
|
||||
),
|
||||
fontFamily: 'Roboto',
|
||||
textTheme: TextTheme(
|
||||
bodyText1: TextStyle(
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
bodyText2: TextStyle(
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
headline2: TextStyle(
|
||||
color: Colors.grey.shade100,
|
||||
),
|
||||
bodySmall: TextStyle(color: Colors.grey.shade500),
|
||||
bodyLarge: const TextStyle(color: Colors.white70),
|
||||
bodyMedium: TextStyle(color: Colors.grey.shade200),
|
||||
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,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 18),
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
5
lib/version.dart
Executable file
@ -0,0 +1,5 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// This file is generated by running ./set-version.py <version> <build>
|
||||
|
||||
const String version = '6.0.0-alpha.3';
|
||||
const int build = 59900;
|
@ -59,6 +59,8 @@ class _CircleTimerState extends State<CircleTimer>
|
||||
|
||||
@override
|
||||
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';
|
||||
return DialogFrame(
|
||||
child: AlertDialog(
|
||||
insetPadding: EdgeInsets.zero,
|
||||
title: widget.title,
|
||||
scrollable: true,
|
||||
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 |
17
pubspec.yaml
@ -15,7 +15,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.0.0+1
|
||||
|
||||
# This field is updated by running ./set-version.py <version>
|
||||
# DO NOT MANUALLY EDIT THIS!
|
||||
version: 6.0.0+59900
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
@ -85,7 +88,7 @@ flutter:
|
||||
# - images/a_dot_ham.jpeg
|
||||
assets:
|
||||
- assets/product-images/
|
||||
|
||||
- assets/graphics/
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
@ -112,3 +115,13 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# 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
|
||||
|
131
set-version.py
Executable file
@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
import sys
|
||||
|
||||
"""
|
||||
This script updates version numbers in various files.
|
||||
"""
|
||||
|
||||
version_pattern = r"(\d+)\.(\d+)\.(\d+)(-[^\s+]+)?"
|
||||
lib_version_pattern = rf"const\s+String\s+version\s+=\s+'({version_pattern})';"
|
||||
lib_build_pattern = rf"const\s+int\s+build\s+=\s+(\d+);"
|
||||
|
||||
|
||||
def update_file(fname, func):
|
||||
with open(fname) as f:
|
||||
orig = f.read()
|
||||
|
||||
buf = func(orig)
|
||||
|
||||
if buf != orig:
|
||||
with open(fname, "w") as f:
|
||||
f.write(buf)
|
||||
print("Updated", fname)
|
||||
|
||||
def read_lib_version():
|
||||
with open("lib/version.dart") as f:
|
||||
buf = f.read()
|
||||
|
||||
m = re.search(
|
||||
lib_version_pattern,
|
||||
buf,
|
||||
re.MULTILINE,
|
||||
)
|
||||
version = m.group(1)
|
||||
m = re.search(
|
||||
lib_build_pattern,
|
||||
buf,
|
||||
re.MULTILINE,
|
||||
)
|
||||
build = int(m.group(1))
|
||||
return version, build
|
||||
|
||||
|
||||
def update_lib(buf):
|
||||
buf = re.sub(
|
||||
lib_version_pattern,
|
||||
f"const String version = '{version}';",
|
||||
buf,
|
||||
)
|
||||
|
||||
buf = re.sub(
|
||||
lib_build_pattern,
|
||||
f"const int build = {build};",
|
||||
buf,
|
||||
)
|
||||
|
||||
return buf
|
||||
|
||||
# Handle invocation
|
||||
args = sys.argv[1:]
|
||||
if not args:
|
||||
version, build = read_lib_version()
|
||||
print(f"Using version: {version}, build: {build}...")
|
||||
elif len(args) == 2:
|
||||
version = args[0]
|
||||
if not re.fullmatch(version_pattern, version):
|
||||
print("Version is not a valid semver string!")
|
||||
sys.exit(1)
|
||||
build = int(args[1])
|
||||
print(f"Setting new version: {version}, build: {build}...")
|
||||
update_file("lib/version.dart", update_lib)
|
||||
else:
|
||||
print("Usage: set-version.py <version> <build>")
|
||||
sys.exit(1)
|
||||
|
||||
# x.y.z without trailing part
|
||||
short_version = re.search("(\d+\.\d+\.\d+)", version).group()
|
||||
|
||||
# pubspec.yaml
|
||||
def update_pubspec(buf):
|
||||
return re.sub(
|
||||
r'version:\s+\d+\.\d+\.\d+\+\d+',
|
||||
f'version: {short_version}+{build}',
|
||||
buf,
|
||||
)
|
||||
|
||||
# Windows Runner.rc
|
||||
def update_runner_rc(buf):
|
||||
buf = re.sub(
|
||||
rf'#define VERSION_AS_STRING "{version_pattern}"',
|
||||
f'#define VERSION_AS_STRING "{version}"',
|
||||
buf,
|
||||
)
|
||||
|
||||
version_as_number = short_version.replace(".", ",")
|
||||
buf = re.sub(
|
||||
r"#define VERSION_AS_NUMBER \d+,\d+,\d+",
|
||||
f"#define VERSION_AS_NUMBER {version_as_number}",
|
||||
buf,
|
||||
)
|
||||
return buf
|
||||
|
||||
# Helper version_info
|
||||
def update_helper_version(buf):
|
||||
version_tuple = repr(tuple(int(d) for d in short_version.split(".")) + (0,))
|
||||
buf = re.sub(
|
||||
rf'filevers=\(\d+, \d+, \d+, \d+\)',
|
||||
f'filevers={version_tuple}',
|
||||
buf,
|
||||
)
|
||||
buf = re.sub(
|
||||
rf'prodvers=\(\d+, \d+, \d+, \d+\)',
|
||||
f'prodvers={version_tuple}',
|
||||
buf,
|
||||
)
|
||||
buf = re.sub(
|
||||
rf"'FileVersion', '{version_pattern}'",
|
||||
f"'FileVersion', '{version}'",
|
||||
buf,
|
||||
)
|
||||
buf = re.sub(
|
||||
rf"'ProductVersion', '{version_pattern}'",
|
||||
f"'ProductVersion', '{version}'",
|
||||
buf,
|
||||
)
|
||||
return buf
|
||||
|
||||
|
||||
update_file("pubspec.yaml", update_pubspec)
|
||||
update_file("windows/runner/Runner.rc", update_runner_rc)
|
||||
update_file("helper/version_info.txt", update_helper_version)
|
@ -63,13 +63,13 @@ IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||
#ifdef FLUTTER_BUILD_NUMBER
|
||||
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
|
||||
#else
|
||||
#define VERSION_AS_NUMBER 1,0,0
|
||||
#define VERSION_AS_NUMBER 6,0,0
|
||||
#endif
|
||||
|
||||
#ifdef FLUTTER_BUILD_NAME
|
||||
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
|
||||
#else
|
||||
#define VERSION_AS_STRING "1.0.0"
|
||||
#define VERSION_AS_STRING "6.0.0-alpha.3"
|
||||
#endif
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
|
@ -110,7 +110,7 @@ bool Win32Window::CreateAndShow(const std::wstring& title,
|
||||
// Attempt to create a mutex to enforce single instance.
|
||||
CreateMutex(NULL, TRUE, L"com.yubico.authenticator.mutex");
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
HWND handle=FindWindowA(NULL, "Yubico Authenticator");
|
||||
HWND handle=FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"Yubico Authenticator");
|
||||
WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) };
|
||||
GetWindowPlacement(handle, &place);
|
||||
switch(place.showCmd) {
|
||||
|