mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +03:00
Merge branch 'main' into adamve/android_fido_bio
This commit is contained in:
commit
f5d1b21f19
@ -271,6 +271,8 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
|
|
||||||
private fun processYubiKey(device: YubiKeyDevice) {
|
private fun processYubiKey(device: YubiKeyDevice) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
|
||||||
|
if (device is NfcYubiKeyDevice) {
|
||||||
// verify that current context supports connection provided by the YubiKey
|
// verify that current context supports connection provided by the YubiKey
|
||||||
// if not, switch to a context which supports the connection
|
// if not, switch to a context which supports the connection
|
||||||
val supportedApps = DeviceManager.getSupportedContexts(device)
|
val supportedApps = DeviceManager.getSupportedContexts(device)
|
||||||
@ -288,6 +290,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
if (contextManager == null) {
|
if (contextManager == null) {
|
||||||
switchContext(DeviceManager.getPreferredContext(supportedApps))
|
switchContext(DeviceManager.getPreferredContext(supportedApps))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contextManager?.let {
|
contextManager?.let {
|
||||||
try {
|
try {
|
||||||
|
@ -295,7 +295,7 @@ class FidoManager(
|
|||||||
|
|
||||||
fidoViewModel.setSessionState(
|
fidoViewModel.setSessionState(
|
||||||
Session(
|
Session(
|
||||||
fidoSession.cachedInfo,
|
fidoSession.info,
|
||||||
pinStore.hasPin()
|
pinStore.hasPin()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -42,6 +42,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, NoCardException
|
from smartcard.Exceptions import SmartcardException, NoCardException
|
||||||
from smartcard.pcsc.PCSCExceptions import EstablishContextException
|
from smartcard.pcsc.PCSCExceptions import EstablishContextException
|
||||||
|
from smartcard.CardMonitoring import CardObserver, CardMonitor
|
||||||
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
|
||||||
@ -263,9 +264,6 @@ class AbstractDeviceNode(RpcNode):
|
|||||||
|
|
||||||
|
|
||||||
class UsbDeviceNode(AbstractDeviceNode):
|
class UsbDeviceNode(AbstractDeviceNode):
|
||||||
def __init__(self, device, info):
|
|
||||||
super().__init__(device, info)
|
|
||||||
|
|
||||||
def _supports_connection(self, conn_type):
|
def _supports_connection(self, conn_type):
|
||||||
return self._device.pid.supports_connection(conn_type)
|
return self._device.pid.supports_connection(conn_type)
|
||||||
|
|
||||||
@ -308,15 +306,53 @@ class UsbDeviceNode(AbstractDeviceNode):
|
|||||||
raise ConnectionException("fido", e)
|
raise ConnectionException("fido", e)
|
||||||
|
|
||||||
|
|
||||||
|
class _ReaderObserver(CardObserver):
|
||||||
|
def __init__(self, device):
|
||||||
|
self.device = device
|
||||||
|
self.card = None
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
def update(self, observable, actions):
|
||||||
|
added, removed = actions
|
||||||
|
for card in added:
|
||||||
|
if card.reader == self.device.reader.name:
|
||||||
|
if card != self.card:
|
||||||
|
self.card = card
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.card = None
|
||||||
|
self.data = None
|
||||||
|
logger.debug(f"NFC card: {self.card}")
|
||||||
|
|
||||||
|
|
||||||
class ReaderDeviceNode(AbstractDeviceNode):
|
class ReaderDeviceNode(AbstractDeviceNode):
|
||||||
|
def __init__(self, device, info):
|
||||||
|
super().__init__(device, info)
|
||||||
|
self._observer = _ReaderObserver(device)
|
||||||
|
self._monitor = CardMonitor()
|
||||||
|
self._monitor.addObserver(self._observer)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._monitor.deleteObserver(self._observer)
|
||||||
|
super().close()
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
|
if self._observer.data is None:
|
||||||
|
card = self._observer.card
|
||||||
|
if card is None:
|
||||||
|
return dict(present=False, status="no-card")
|
||||||
try:
|
try:
|
||||||
with self._device.open_connection(SmartCardConnection) as conn:
|
with self._device.open_connection(SmartCardConnection) as conn:
|
||||||
return dict(self._read_data(conn), present=True)
|
self._observer.data = dict(self._read_data(conn), present=True)
|
||||||
except NoCardException:
|
except NoCardException:
|
||||||
return dict(present=False, status="no-card")
|
return dict(present=False, status="no-card")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return dict(present=False, status="unknown-device")
|
self._observer.data = dict(present=False, status="unknown-device")
|
||||||
|
return self._observer.data
|
||||||
|
|
||||||
|
@action(closes_child=False)
|
||||||
|
def get(self, params, event, signal):
|
||||||
|
return super().get(params, event, signal)
|
||||||
|
|
||||||
@child
|
@child
|
||||||
def ccid(self):
|
def ccid(self):
|
||||||
|
@ -399,12 +399,41 @@ class SlotNode(RpcNode):
|
|||||||
else None,
|
else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@action(condition=lambda self: self.certificate)
|
@action(condition=lambda self: self.certificate or self.metadata)
|
||||||
def delete(self, params, event, signal):
|
def delete(self, params, event, signal):
|
||||||
|
delete_cert = params.pop("delete_cert", False)
|
||||||
|
delete_key = params.pop("delete_key", False)
|
||||||
|
|
||||||
|
if not delete_cert and not delete_key:
|
||||||
|
raise ValueError("Missing delete option")
|
||||||
|
|
||||||
|
if delete_cert:
|
||||||
self.session.delete_certificate(self.slot)
|
self.session.delete_certificate(self.slot)
|
||||||
self.session.put_object(OBJECT_ID.CHUID, generate_chuid())
|
self.session.put_object(OBJECT_ID.CHUID, generate_chuid())
|
||||||
self._refresh()
|
|
||||||
self.certificate = None
|
self.certificate = None
|
||||||
|
if delete_key:
|
||||||
|
self.session.delete_key(self.slot)
|
||||||
|
self._refresh()
|
||||||
|
return dict()
|
||||||
|
|
||||||
|
@action(condition=lambda self: self.metadata)
|
||||||
|
def move_key(self, params, event, signal):
|
||||||
|
destination = params.pop("destination")
|
||||||
|
overwrite_key = params.pop("overwrite_key")
|
||||||
|
include_certificate = params.pop("include_certificate")
|
||||||
|
|
||||||
|
if include_certificate:
|
||||||
|
source_object = self.session.get_object(OBJECT_ID.from_slot(self.slot))
|
||||||
|
destination = SLOT(int(destination, base=16))
|
||||||
|
if overwrite_key:
|
||||||
|
self.session.delete_key(destination)
|
||||||
|
self.session.move_key(self.slot, destination)
|
||||||
|
if include_certificate:
|
||||||
|
self.session.put_object(OBJECT_ID.from_slot(destination), source_object)
|
||||||
|
self.session.delete_certificate(self.slot)
|
||||||
|
self.session.put_object(OBJECT_ID.CHUID, generate_chuid())
|
||||||
|
self.certificate = None
|
||||||
|
self._refresh()
|
||||||
return dict()
|
return dict()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -24,7 +24,6 @@ import 'package:window_manager/window_manager.dart';
|
|||||||
import '../about_page.dart';
|
import '../about_page.dart';
|
||||||
import '../core/state.dart';
|
import '../core/state.dart';
|
||||||
import '../desktop/state.dart';
|
import '../desktop/state.dart';
|
||||||
import '../oath/keys.dart';
|
|
||||||
import 'message.dart';
|
import 'message.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
import 'state.dart';
|
import 'state.dart';
|
||||||
@ -130,8 +129,8 @@ class GlobalShortcuts extends ConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
SearchIntent: CallbackAction<SearchIntent>(onInvoke: (intent) {
|
SearchIntent: CallbackAction<SearchIntent>(onInvoke: (intent) {
|
||||||
// If the OATH view doesn't have focus, but is shown, find and select the search bar.
|
// If the view doesn't have focus, but is shown, find and select the search bar.
|
||||||
final searchContext = searchAccountsField.currentContext;
|
final searchContext = searchField.currentContext;
|
||||||
if (searchContext != null) {
|
if (searchContext != null) {
|
||||||
if (!Navigator.of(searchContext).canPop()) {
|
if (!Navigator.of(searchContext).canPop()) {
|
||||||
return Actions.maybeInvoke(searchContext, intent);
|
return Actions.maybeInvoke(searchContext, intent);
|
||||||
|
@ -53,7 +53,7 @@ class ActionListItem extends StatelessWidget {
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(48)),
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
subtitle: subtitle != null ? Text(subtitle!) : null,
|
subtitle: subtitle != null ? Text(subtitle!) : null,
|
||||||
leading: Opacity(
|
leading: Opacity(
|
||||||
|
@ -77,7 +77,7 @@ class _AppListItemState<T> extends ConsumerState<AppListItem> {
|
|||||||
item: widget.item,
|
item: widget.item,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(48),
|
||||||
onSecondaryTapDown: buildPopupActions == null
|
onSecondaryTapDown: buildPopupActions == null
|
||||||
? null
|
? null
|
||||||
: (details) {
|
: (details) {
|
||||||
|
@ -14,11 +14,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
|
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../../management/models.dart';
|
import '../../management/models.dart';
|
||||||
@ -30,12 +34,30 @@ import 'fs_dialog.dart';
|
|||||||
import 'keys.dart';
|
import 'keys.dart';
|
||||||
import 'navigation.dart';
|
import 'navigation.dart';
|
||||||
|
|
||||||
// We use global keys here to maintain the NavigatorContent between AppPages.
|
final _navigationProvider = StateNotifierProvider<_NavigationProvider, bool>(
|
||||||
|
(ref) => _NavigationProvider());
|
||||||
|
|
||||||
|
class _NavigationProvider extends StateNotifier<bool> {
|
||||||
|
_NavigationProvider() : super(true);
|
||||||
|
|
||||||
|
void toggleExpanded() {
|
||||||
|
state = !state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use global keys here to maintain the content between AppPages,
|
||||||
|
// and keep track of what has been scrolled under AppBar
|
||||||
final _navKey = GlobalKey();
|
final _navKey = GlobalKey();
|
||||||
final _navExpandedKey = GlobalKey();
|
final _navExpandedKey = GlobalKey();
|
||||||
|
final _sliverTitleGlobalKey = GlobalKey();
|
||||||
|
final _sliverTitleWrapperGlobalKey = GlobalKey();
|
||||||
|
final _headerSliverGlobalKey = GlobalKey();
|
||||||
|
final _detailsViewGlobalKey = GlobalKey();
|
||||||
|
final _mainContentGlobalKey = GlobalKey();
|
||||||
|
|
||||||
class AppPage extends StatelessWidget {
|
class AppPage extends ConsumerStatefulWidget {
|
||||||
final String? title;
|
final String? title;
|
||||||
|
final String? alternativeTitle;
|
||||||
final String? footnote;
|
final String? footnote;
|
||||||
final Widget Function(BuildContext context, bool expanded) builder;
|
final Widget Function(BuildContext context, bool expanded) builder;
|
||||||
final Widget Function(BuildContext context)? detailViewBuilder;
|
final Widget Function(BuildContext context)? detailViewBuilder;
|
||||||
@ -49,9 +71,11 @@ class AppPage extends StatelessWidget {
|
|||||||
final Widget? fileDropOverlay;
|
final Widget? fileDropOverlay;
|
||||||
final Function(File file)? onFileDropped;
|
final Function(File file)? onFileDropped;
|
||||||
final List<Capability>? capabilities;
|
final List<Capability>? capabilities;
|
||||||
const AppPage({
|
final Widget? headerSliver;
|
||||||
super.key,
|
const AppPage(
|
||||||
|
{super.key,
|
||||||
this.title,
|
this.title,
|
||||||
|
this.alternativeTitle,
|
||||||
this.footnote,
|
this.footnote,
|
||||||
required this.builder,
|
required this.builder,
|
||||||
this.centered = false,
|
this.centered = false,
|
||||||
@ -64,14 +88,45 @@ class AppPage extends StatelessWidget {
|
|||||||
this.onFileDropped,
|
this.onFileDropped,
|
||||||
this.delayedContent = false,
|
this.delayedContent = false,
|
||||||
this.keyActionsBadge = false,
|
this.keyActionsBadge = false,
|
||||||
}) : assert(!(onFileDropped != null && fileDropOverlay == null),
|
this.headerSliver})
|
||||||
|
: assert(!(onFileDropped != null && fileDropOverlay == null),
|
||||||
'Declaring onFileDropped requires declaring a fileDropOverlay');
|
'Declaring onFileDropped requires declaring a fileDropOverlay');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => LayoutBuilder(
|
ConsumerState<ConsumerStatefulWidget> createState() => _AppPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppPageState extends ConsumerState<AppPage> {
|
||||||
|
final _VisibilityController _sliverTitleController = _VisibilityController();
|
||||||
|
final _VisibilityController _headerSliverController = _VisibilityController();
|
||||||
|
final _VisibilityController _navController = _VisibilityController();
|
||||||
|
final _VisibilityController _detailsController = _VisibilityController();
|
||||||
|
late _VisibilitiesController _scrolledUnderController;
|
||||||
|
|
||||||
|
final ScrollController _sliverTitleScrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scrolledUnderController = _VisibilitiesController(
|
||||||
|
[_sliverTitleController, _navController, _detailsController]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_sliverTitleController.dispose();
|
||||||
|
_headerSliverController.dispose();
|
||||||
|
_navController.dispose();
|
||||||
|
_detailsController.dispose();
|
||||||
|
_scrolledUnderController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final width = constraints.maxWidth;
|
final width = constraints.maxWidth;
|
||||||
|
|
||||||
if (width < 400 ||
|
if (width < 400 ||
|
||||||
(isAndroid && width < 600 && width < constraints.maxHeight)) {
|
(isAndroid && width < 600 && width < constraints.maxHeight)) {
|
||||||
return _buildScaffold(context, true, false, false);
|
return _buildScaffold(context, true, false, false);
|
||||||
@ -87,34 +142,11 @@ class AppPage extends StatelessWidget {
|
|||||||
if (scaffoldState?.isDrawerOpen == true) {
|
if (scaffoldState?.isDrawerOpen == true) {
|
||||||
scaffoldState?.openEndDrawer();
|
scaffoldState?.openEndDrawer();
|
||||||
}
|
}
|
||||||
return Scaffold(
|
return _buildScaffold(context, false, true, true);
|
||||||
body: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 280,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildLogo(context),
|
|
||||||
NavigationContent(
|
|
||||||
key: _navExpandedKey,
|
|
||||||
shouldPop: false,
|
|
||||||
extended: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: _buildScaffold(context, false, false, true),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildLogo(BuildContext context) {
|
Widget _buildLogo(BuildContext context) {
|
||||||
final color =
|
final color =
|
||||||
@ -160,40 +192,159 @@ class AppPage extends StatelessWidget {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _scrollElement(
|
||||||
|
BuildContext context,
|
||||||
|
ScrollController scrollController,
|
||||||
|
_ScrollDirection direction,
|
||||||
|
_VisibilityController controller,
|
||||||
|
GlobalKey targetKey,
|
||||||
|
GlobalKey? anchorKey) {
|
||||||
|
if (direction != _ScrollDirection.idle) {
|
||||||
|
final currentContext = targetKey.currentContext;
|
||||||
|
if (currentContext == null) return;
|
||||||
|
|
||||||
|
final RenderBox renderBox =
|
||||||
|
currentContext.findRenderObject() as RenderBox;
|
||||||
|
final RenderBox? anchorRenderBox = anchorKey != null
|
||||||
|
? anchorKey.currentContext?.findRenderObject() as RenderBox?
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final anchorHeight = anchorRenderBox != null
|
||||||
|
? anchorRenderBox.size.height
|
||||||
|
: Scaffold.of(context).appBarMaxHeight!;
|
||||||
|
|
||||||
|
final targetHeight = renderBox.size.height;
|
||||||
|
final positionOffset = anchorRenderBox != null
|
||||||
|
? Offset(0, -anchorRenderBox.localToGlobal(Offset.zero).dy)
|
||||||
|
: Offset.zero;
|
||||||
|
|
||||||
|
final position = renderBox.localToGlobal(positionOffset);
|
||||||
|
|
||||||
|
if (direction == _ScrollDirection.up) {
|
||||||
|
var offset = scrollController.position.pixels +
|
||||||
|
(targetHeight - (anchorHeight - position.dy));
|
||||||
|
if (offset > scrollController.position.maxScrollExtent) {
|
||||||
|
offset = scrollController.position.maxScrollExtent;
|
||||||
|
}
|
||||||
|
Timer.run(() {
|
||||||
|
scrollController.animateTo(offset,
|
||||||
|
duration: const Duration(milliseconds: 100), curve: Curves.ease);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var offset =
|
||||||
|
scrollController.position.pixels - (anchorHeight - position.dy);
|
||||||
|
|
||||||
|
if (offset < scrollController.position.minScrollExtent) {
|
||||||
|
offset = scrollController.position.minScrollExtent;
|
||||||
|
}
|
||||||
|
if (controller.visibility != _Visibility.visible) {
|
||||||
|
Timer.run(() {
|
||||||
|
scrollController.animateTo(offset,
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
curve: Curves.ease);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildTitle(BuildContext context) {
|
Widget _buildTitle(BuildContext context) {
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: _sliverTitleController,
|
||||||
|
builder: (context, child) {
|
||||||
|
_scrollElement(
|
||||||
|
context,
|
||||||
|
_sliverTitleScrollController,
|
||||||
|
_sliverTitleController.scrollDirection,
|
||||||
|
_sliverTitleController,
|
||||||
|
_sliverTitleWrapperGlobalKey,
|
||||||
|
null);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(title!,
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
key: _sliverTitleGlobalKey,
|
||||||
|
widget.alternativeTitle ?? widget.title!,
|
||||||
style: Theme.of(context).textTheme.displaySmall!.copyWith(
|
style: Theme.of(context).textTheme.displaySmall!.copyWith(
|
||||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.9))),
|
color: widget.alternativeTitle != null
|
||||||
if (capabilities != null)
|
? Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant
|
||||||
|
.withOpacity(0.4)
|
||||||
|
: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary
|
||||||
|
.withOpacity(0.9),
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.capabilities != null && widget.alternativeTitle == null)
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 4.0,
|
spacing: 4.0,
|
||||||
runSpacing: 8.0,
|
runSpacing: 8.0,
|
||||||
children: [...capabilities!.map((c) => CapabilityBadge(c))],
|
children: [
|
||||||
|
...widget.capabilities!.map((c) => CapabilityBadge(c))
|
||||||
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget? _buildAppBarTitle(
|
||||||
|
BuildContext context, bool hasRail, bool hasManage, bool fullyExpanded) {
|
||||||
|
final showNavigation = ref.watch(_navigationProvider);
|
||||||
|
EdgeInsets padding;
|
||||||
|
if (fullyExpanded) {
|
||||||
|
padding = EdgeInsets.only(left: showNavigation ? 280 : 72, right: 320);
|
||||||
|
} else if (!hasRail && hasManage) {
|
||||||
|
padding = const EdgeInsets.only(right: 320);
|
||||||
|
} else if (hasRail && hasManage) {
|
||||||
|
padding = const EdgeInsets.only(left: 72, right: 320);
|
||||||
|
} else if (hasRail && !hasManage) {
|
||||||
|
padding = const EdgeInsets.only(left: 72);
|
||||||
|
} else {
|
||||||
|
padding = const EdgeInsets.all(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.title != null) {
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: _sliverTitleController,
|
||||||
|
builder: (context, child) {
|
||||||
|
final visible =
|
||||||
|
_sliverTitleController.visibility == _Visibility.scrolledUnder;
|
||||||
|
return Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
opacity: visible ? 1 : 0,
|
||||||
|
child: Text(widget.alternativeTitle ?? widget.title!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMainContent(BuildContext context, bool expanded) {
|
Widget _buildMainContent(BuildContext context, bool expanded) {
|
||||||
final actions = actionsBuilder?.call(context, expanded) ?? [];
|
final actions = widget.actionsBuilder?.call(context, expanded) ?? [];
|
||||||
final content = Column(
|
final content = Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment:
|
crossAxisAlignment: widget.centered
|
||||||
centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
? CrossAxisAlignment.center
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (title != null && !centered)
|
widget.builder(context, expanded),
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 24.0),
|
|
||||||
child: _buildTitle(context),
|
|
||||||
),
|
|
||||||
builder(context, expanded),
|
|
||||||
if (actions.isNotEmpty)
|
if (actions.isNotEmpty)
|
||||||
Align(
|
Align(
|
||||||
alignment: centered ? Alignment.center : Alignment.centerLeft,
|
alignment:
|
||||||
|
widget.centered ? Alignment.center : Alignment.centerLeft,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 16, bottom: 0, left: 16, right: 16),
|
top: 16, bottom: 0, left: 16, right: 16),
|
||||||
@ -204,14 +355,14 @@ class AppPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (footnote != null)
|
if (widget.footnote != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.only(bottom: 16, top: 33, left: 16, right: 16),
|
const EdgeInsets.only(bottom: 16, top: 33, left: 16, right: 16),
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
child: Text(
|
child: Text(
|
||||||
footnote!,
|
widget.footnote!,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -220,7 +371,7 @@ class AppPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final safeArea = SafeArea(
|
final safeArea = SafeArea(
|
||||||
child: delayedContent
|
child: widget.delayedContent
|
||||||
? DelayedVisibility(
|
? DelayedVisibility(
|
||||||
key: GlobalKey(), // Ensure we reset the delay on rebuild
|
key: GlobalKey(), // Ensure we reset the delay on rebuild
|
||||||
delay: const Duration(milliseconds: 400),
|
delay: const Duration(milliseconds: 400),
|
||||||
@ -229,10 +380,9 @@ class AppPage extends StatelessWidget {
|
|||||||
: content,
|
: content,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (centered) {
|
if (widget.centered) {
|
||||||
return Stack(
|
return Stack(children: [
|
||||||
children: [
|
if (widget.title != null)
|
||||||
if (title != null)
|
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
@ -244,24 +394,85 @@ class AppPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
top: title != null ? 68.0 : 0,
|
top: widget.title != null ? 68.0 : 0,
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: ScrollConfiguration(
|
child: ScrollConfiguration(
|
||||||
behavior:
|
behavior:
|
||||||
ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: isAndroid
|
||||||
|
? const ClampingScrollPhysics(
|
||||||
|
parent: AlwaysScrollableScrollPhysics())
|
||||||
|
: null,
|
||||||
child: safeArea,
|
child: safeArea,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (widget.title != null) {
|
||||||
|
return _VisibilityListener(
|
||||||
|
targetKey: _sliverTitleGlobalKey,
|
||||||
|
controller: _sliverTitleController,
|
||||||
|
subTargetKey:
|
||||||
|
widget.headerSliver != null ? _headerSliverGlobalKey : null,
|
||||||
|
subController:
|
||||||
|
widget.headerSliver != null ? _headerSliverController : null,
|
||||||
|
subAnchorKey:
|
||||||
|
widget.headerSliver != null ? _sliverTitleWrapperGlobalKey : null,
|
||||||
|
child: CustomScrollView(
|
||||||
|
physics: isAndroid
|
||||||
|
? const ClampingScrollPhysics(
|
||||||
|
parent: AlwaysScrollableScrollPhysics())
|
||||||
|
: null,
|
||||||
|
controller: _sliverTitleScrollController,
|
||||||
|
key: _mainContentGlobalKey,
|
||||||
|
slivers: [
|
||||||
|
SliverMainAxisGroup(
|
||||||
|
slivers: [
|
||||||
|
SliverPinnedHeader(
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Theme.of(context).colorScheme.background,
|
||||||
|
child: Padding(
|
||||||
|
key: _sliverTitleWrapperGlobalKey,
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 16.0, right: 16.0, bottom: 12.0, top: 4.0),
|
||||||
|
child: _buildTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.headerSliver != null)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: ListenableBuilder(
|
||||||
|
listenable: _headerSliverController,
|
||||||
|
builder: (context, child) {
|
||||||
|
_scrollElement(
|
||||||
|
context,
|
||||||
|
_sliverTitleScrollController,
|
||||||
|
_headerSliverController.scrollDirection,
|
||||||
|
_headerSliverController,
|
||||||
|
_headerSliverGlobalKey,
|
||||||
|
_sliverTitleWrapperGlobalKey);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
key: _headerSliverGlobalKey,
|
||||||
|
child: widget.headerSliver);
|
||||||
|
},
|
||||||
|
))
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(child: safeArea)
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
|
physics: isAndroid
|
||||||
|
? const ClampingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
|
||||||
|
: null,
|
||||||
primary: false,
|
primary: false,
|
||||||
child: safeArea,
|
child: safeArea,
|
||||||
);
|
);
|
||||||
@ -269,12 +480,14 @@ class AppPage extends StatelessWidget {
|
|||||||
|
|
||||||
Scaffold _buildScaffold(
|
Scaffold _buildScaffold(
|
||||||
BuildContext context, bool hasDrawer, bool hasRail, bool hasManage) {
|
BuildContext context, bool hasDrawer, bool hasRail, bool hasManage) {
|
||||||
|
final fullyExpanded = !hasDrawer && hasRail && hasManage;
|
||||||
|
final showNavigation = ref.watch(_navigationProvider);
|
||||||
var body = _buildMainContent(context, hasManage);
|
var body = _buildMainContent(context, hasManage);
|
||||||
|
|
||||||
if (onFileDropped != null) {
|
if (widget.onFileDropped != null) {
|
||||||
body = FileDropTarget(
|
body = FileDropTarget(
|
||||||
onFileDropped: onFileDropped!,
|
onFileDropped: widget.onFileDropped!,
|
||||||
overlay: fileDropOverlay!,
|
overlay: widget.fileDropOverlay!,
|
||||||
child: body,
|
child: body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -282,9 +495,12 @@ class AppPage extends StatelessWidget {
|
|||||||
body = Row(
|
body = Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
if (hasRail)
|
if (hasRail && (!fullyExpanded || !showNavigation))
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 72,
|
width: 72,
|
||||||
|
child: _VisibilityListener(
|
||||||
|
targetKey: _navKey,
|
||||||
|
controller: _navController,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: NavigationContent(
|
child: NavigationContent(
|
||||||
key: _navKey,
|
key: _navKey,
|
||||||
@ -293,6 +509,24 @@ class AppPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
if (fullyExpanded && showNavigation)
|
||||||
|
SizedBox(
|
||||||
|
width: 280,
|
||||||
|
child: _VisibilityListener(
|
||||||
|
controller: _navController,
|
||||||
|
targetKey: _navExpandedKey,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: NavigationContent(
|
||||||
|
key: _navExpandedKey,
|
||||||
|
shouldPop: false,
|
||||||
|
extended: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@ -308,47 +542,85 @@ class AppPage extends StatelessWidget {
|
|||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
if (hasManage &&
|
if (hasManage &&
|
||||||
(detailViewBuilder != null || keyActionsBuilder != null))
|
(widget.detailViewBuilder != null ||
|
||||||
SingleChildScrollView(
|
widget.keyActionsBuilder != null))
|
||||||
|
_VisibilityListener(
|
||||||
|
controller: _detailsController,
|
||||||
|
targetKey: _detailsViewGlobalKey,
|
||||||
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 320,
|
width: 320,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
key: _detailsViewGlobalKey,
|
||||||
children: [
|
children: [
|
||||||
if (detailViewBuilder != null)
|
if (widget.detailViewBuilder != null)
|
||||||
detailViewBuilder!(context),
|
widget.detailViewBuilder!(context),
|
||||||
if (keyActionsBuilder != null)
|
if (widget.keyActionsBuilder != null)
|
||||||
keyActionsBuilder!(context),
|
widget.keyActionsBuilder!(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: scaffoldGlobalKey,
|
key: scaffoldGlobalKey,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1.0),
|
||||||
|
child: ListenableBuilder(
|
||||||
|
listenable: _scrolledUnderController,
|
||||||
|
builder: (context, child) {
|
||||||
|
final visible = _scrolledUnderController.someIsScrolledUnder;
|
||||||
|
return AnimatedOpacity(
|
||||||
|
opacity: visible ? 1 : 0,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
height: 1.0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
scrolledUnderElevation: 0.0,
|
scrolledUnderElevation: 0.0,
|
||||||
leadingWidth: hasRail ? 84 : null,
|
leadingWidth: hasRail ? 84 : null,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
|
title: _buildAppBarTitle(
|
||||||
|
context,
|
||||||
|
hasRail,
|
||||||
|
hasManage,
|
||||||
|
fullyExpanded,
|
||||||
|
),
|
||||||
leading: hasRail
|
leading: hasRail
|
||||||
? const Row(
|
? Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: DrawerButton(),
|
child: DrawerButton(
|
||||||
|
onPressed: fullyExpanded
|
||||||
|
? () {
|
||||||
|
ref
|
||||||
|
.read(_navigationProvider.notifier)
|
||||||
|
.toggleExpanded();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
)),
|
)),
|
||||||
SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
actions: [
|
actions: [
|
||||||
if (actionButtonBuilder == null &&
|
if (widget.actionButtonBuilder == null &&
|
||||||
(keyActionsBuilder != null && !hasManage))
|
(widget.keyActionsBuilder != null && !hasManage))
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 4),
|
padding: const EdgeInsets.only(left: 4),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@ -360,12 +632,12 @@ class AppPage extends StatelessWidget {
|
|||||||
builder: (context) => FsDialog(
|
builder: (context) => FsDialog(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 32),
|
padding: const EdgeInsets.only(top: 32),
|
||||||
child: keyActionsBuilder!(context),
|
child: widget.keyActionsBuilder!(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: keyActionsBadge
|
icon: widget.keyActionsBadge
|
||||||
? const Badge(
|
? const Badge(
|
||||||
child: Icon(Symbols.more_vert),
|
child: Icon(Symbols.more_vert),
|
||||||
)
|
)
|
||||||
@ -375,10 +647,10 @@ class AppPage extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (actionButtonBuilder != null)
|
if (widget.actionButtonBuilder != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 12),
|
padding: const EdgeInsets.only(right: 12),
|
||||||
child: actionButtonBuilder!.call(context),
|
child: widget.actionButtonBuilder!.call(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -408,3 +680,203 @@ class CapabilityBadge extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum _Visibility { visible, topScrolledUnder, halfScrolledUnder, scrolledUnder }
|
||||||
|
|
||||||
|
enum _ScrollDirection { idle, up, down }
|
||||||
|
|
||||||
|
class _VisibilityController with ChangeNotifier {
|
||||||
|
_Visibility _visibility = _Visibility.visible;
|
||||||
|
_ScrollDirection _scrollDirection = _ScrollDirection.idle;
|
||||||
|
|
||||||
|
void setVisibility(_Visibility visibility) {
|
||||||
|
if (visibility != _visibility) {
|
||||||
|
_visibility = visibility;
|
||||||
|
if (_visibility != _Visibility.visible) {
|
||||||
|
_scrollDirection = _ScrollDirection.idle;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyScroll(_ScrollDirection scrollDirection) {
|
||||||
|
if (visibility != _Visibility.scrolledUnder) {
|
||||||
|
_scrollDirection = scrollDirection;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ScrollDirection get scrollDirection => _scrollDirection;
|
||||||
|
_Visibility get visibility => _visibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VisibilitiesController with ChangeNotifier {
|
||||||
|
final List<_VisibilityController> controllers;
|
||||||
|
bool someIsScrolledUnder = false;
|
||||||
|
_VisibilitiesController(this.controllers) {
|
||||||
|
for (var element in controllers) {
|
||||||
|
element.addListener(() {
|
||||||
|
_setScrolledUnder();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setScrolledUnder() {
|
||||||
|
final val =
|
||||||
|
controllers.any((element) => element.visibility != _Visibility.visible);
|
||||||
|
if (val != someIsScrolledUnder) {
|
||||||
|
someIsScrolledUnder = val;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VisibilityListener extends StatefulWidget {
|
||||||
|
final _VisibilityController controller;
|
||||||
|
final Widget child;
|
||||||
|
final GlobalKey targetKey;
|
||||||
|
final _VisibilityController? subController;
|
||||||
|
final GlobalKey? subTargetKey;
|
||||||
|
final GlobalKey? subAnchorKey;
|
||||||
|
const _VisibilityListener(
|
||||||
|
{required this.controller,
|
||||||
|
required this.child,
|
||||||
|
required this.targetKey,
|
||||||
|
this.subController,
|
||||||
|
this.subTargetKey,
|
||||||
|
this.subAnchorKey})
|
||||||
|
: assert(
|
||||||
|
(subController == null &&
|
||||||
|
subTargetKey == null &&
|
||||||
|
subAnchorKey == null) ||
|
||||||
|
(subController != null &&
|
||||||
|
subTargetKey != null &&
|
||||||
|
subAnchorKey != null),
|
||||||
|
'Declaring requires subTargetKey and subAnchorKey, and vice versa',
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_VisibilityListener> createState() => _VisibilityListenerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VisibilityListenerState extends State<_VisibilityListener> {
|
||||||
|
bool disableScroll = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Listener(
|
||||||
|
onPointerDown: (event) {
|
||||||
|
setState(() {
|
||||||
|
disableScroll = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPointerUp: (event) {
|
||||||
|
setState(() {
|
||||||
|
disableScroll = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPointerSignal: (event) {
|
||||||
|
if (event is PointerScrollEvent) {
|
||||||
|
if (!disableScroll) {
|
||||||
|
setState(() {
|
||||||
|
disableScroll = true;
|
||||||
|
});
|
||||||
|
Timer(const Duration(seconds: 1), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
disableScroll = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: NotificationListener(
|
||||||
|
onNotification: (notification) {
|
||||||
|
if (notification is ScrollMetricsNotification ||
|
||||||
|
notification is ScrollUpdateNotification) {
|
||||||
|
_handleScrollUpdate(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification is ScrollEndNotification &&
|
||||||
|
widget.child is CustomScrollView) {
|
||||||
|
// Disable auto scrolling for mouse wheel and scrollbar
|
||||||
|
_handleScrollEnd(context);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
void _handleScrollUpdate(
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
|
widget.controller
|
||||||
|
.setVisibility(_scrolledUnderState(context, widget.targetKey, null));
|
||||||
|
|
||||||
|
if (widget.subController != null) {
|
||||||
|
widget.subController!.setVisibility(_scrolledUnderState(
|
||||||
|
context, widget.subTargetKey!, widget.subAnchorKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleScrollEnd(
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
|
if (!disableScroll) {
|
||||||
|
widget.controller.notifyScroll(_getSrollDirection(
|
||||||
|
_scrolledUnderState(context, widget.targetKey, null)));
|
||||||
|
|
||||||
|
if (widget.subController != null) {
|
||||||
|
widget.subController!.notifyScroll(_getSrollDirection(
|
||||||
|
_scrolledUnderState(
|
||||||
|
context, widget.subTargetKey!, widget.subAnchorKey)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ScrollDirection _getSrollDirection(_Visibility visibility) {
|
||||||
|
if (visibility == _Visibility.halfScrolledUnder) {
|
||||||
|
return _ScrollDirection.up;
|
||||||
|
} else if (visibility == _Visibility.topScrolledUnder) {
|
||||||
|
return _ScrollDirection.down;
|
||||||
|
} else {
|
||||||
|
return _ScrollDirection.idle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_Visibility _scrolledUnderState(
|
||||||
|
BuildContext context,
|
||||||
|
GlobalKey targetKey,
|
||||||
|
GlobalKey? anchorKey,
|
||||||
|
) {
|
||||||
|
final currentContext = targetKey.currentContext;
|
||||||
|
if (currentContext == null) return _Visibility.visible;
|
||||||
|
|
||||||
|
final RenderBox renderBox = currentContext.findRenderObject() as RenderBox;
|
||||||
|
final RenderBox? anchorRenderBox = anchorKey != null
|
||||||
|
? anchorKey.currentContext?.findRenderObject() as RenderBox?
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final anchorHeight = anchorRenderBox != null
|
||||||
|
? anchorRenderBox.size.height
|
||||||
|
: Scaffold.of(context).appBarMaxHeight!;
|
||||||
|
|
||||||
|
final targetHeight = renderBox.size.height;
|
||||||
|
final positionOffset = anchorRenderBox != null
|
||||||
|
? Offset(0, -anchorRenderBox.localToGlobal(Offset.zero).dy)
|
||||||
|
: Offset.zero;
|
||||||
|
|
||||||
|
final position = renderBox.localToGlobal(positionOffset);
|
||||||
|
|
||||||
|
if (anchorHeight - position.dy > targetHeight - 10) {
|
||||||
|
return _Visibility.scrolledUnder;
|
||||||
|
} else if (anchorHeight - position.dy > targetHeight / 2) {
|
||||||
|
return _Visibility.halfScrolledUnder;
|
||||||
|
} else if (anchorHeight - position.dy > 0) {
|
||||||
|
return _Visibility.topScrolledUnder;
|
||||||
|
} else {
|
||||||
|
return _Visibility.visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -131,7 +131,16 @@ class DevicePickerContent extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Column(children: children);
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: !extended && children.length > 1
|
||||||
|
? 13
|
||||||
|
: !extended
|
||||||
|
? 6.5
|
||||||
|
: 0,
|
||||||
|
),
|
||||||
|
child: Column(children: children),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,7 +320,7 @@ class _DeviceRowState extends ConsumerState<_DeviceRow> {
|
|||||||
isDesktop && menuItems.isNotEmpty ? showMenuFn : null,
|
isDesktop && menuItems.isNotEmpty ? showMenuFn : null,
|
||||||
onLongPressStart: isAndroid ? showMenuFn : null,
|
onLongPressStart: isAndroid ? showMenuFn : null,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6.5),
|
padding: const EdgeInsets.only(bottom: 6.5),
|
||||||
child: widget.selected
|
child: widget.selected
|
||||||
? IconButton.filled(
|
? IconButton.filled(
|
||||||
tooltip: isDesktop ? tooltip : null,
|
tooltip: isDesktop ? tooltip : null,
|
||||||
|
@ -18,6 +18,8 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
// global keys
|
// global keys
|
||||||
final scaffoldGlobalKey = GlobalKey<ScaffoldState>();
|
final scaffoldGlobalKey = GlobalKey<ScaffoldState>();
|
||||||
|
// This is global so we can access it from the global Ctrl+F shortcut.
|
||||||
|
final searchField = GlobalKey();
|
||||||
|
|
||||||
const _prefix = 'app.keys';
|
const _prefix = 'app.keys';
|
||||||
const deviceInfoListTile = Key('$_prefix.device_info_list_tile');
|
const deviceInfoListTile = Key('$_prefix.device_info_list_tile');
|
||||||
|
@ -128,7 +128,8 @@ class NavigationContent extends ConsumerWidget {
|
|||||||
final currentSection = ref.watch(currentSectionProvider);
|
final currentSection = ref.watch(currentSectionProvider);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding:
|
||||||
|
const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 8.0, top: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
AnimatedSize(
|
AnimatedSize(
|
||||||
|
@ -31,9 +31,8 @@ import 'state.dart';
|
|||||||
|
|
||||||
const _usbPollDelay = Duration(milliseconds: 500);
|
const _usbPollDelay = Duration(milliseconds: 500);
|
||||||
|
|
||||||
const _nfcPollDelay = Duration(milliseconds: 2500);
|
const _nfcPollReadersDelay = Duration(milliseconds: 2500);
|
||||||
const _nfcAttachPollDelay = Duration(seconds: 1);
|
const _nfcPollCardDelay = Duration(seconds: 1);
|
||||||
const _nfcDetachPollDelay = Duration(seconds: 5);
|
|
||||||
|
|
||||||
final _log = Logger('desktop.devices');
|
final _log = Logger('desktop.devices');
|
||||||
|
|
||||||
@ -197,7 +196,7 @@ class NfcDeviceNotifier extends StateNotifier<List<NfcReaderNode>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_pollTimer = Timer(_nfcPollDelay, _pollReaders);
|
_pollTimer = Timer(_nfcPollReadersDelay, _pollReaders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -260,7 +259,7 @@ class CurrentDeviceDataNotifier extends StateNotifier<AsyncValue<YubiKeyData>> {
|
|||||||
|
|
||||||
void _notifyWindowState(WindowState windowState) {
|
void _notifyWindowState(WindowState windowState) {
|
||||||
if (windowState.active) {
|
if (windowState.active) {
|
||||||
_pollReader();
|
_pollCard();
|
||||||
} else {
|
} else {
|
||||||
_pollTimer?.cancel();
|
_pollTimer?.cancel();
|
||||||
// TODO: Should we clear the key here?
|
// TODO: Should we clear the key here?
|
||||||
@ -276,16 +275,23 @@ class CurrentDeviceDataNotifier extends StateNotifier<AsyncValue<YubiKeyData>> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _pollReader() async {
|
void _pollCard() async {
|
||||||
_pollTimer?.cancel();
|
_pollTimer?.cancel();
|
||||||
final node = _deviceNode!;
|
final node = _deviceNode!;
|
||||||
try {
|
try {
|
||||||
_log.debug('Polling for USB device changes...');
|
_log.debug('Polling for NFC device changes...');
|
||||||
var result = await _rpc?.command('get', node.path.segments);
|
var result = await _rpc?.command('get', node.path.segments);
|
||||||
if (mounted && result != null) {
|
if (mounted && result != null) {
|
||||||
if (result['data']['present']) {
|
if (result['data']['present']) {
|
||||||
state = AsyncValue.data(YubiKeyData(node, result['data']['name'],
|
final oldState = state.valueOrNull;
|
||||||
DeviceInfo.fromJson(result['data']['info'])));
|
final newState = YubiKeyData(node, result['data']['name'],
|
||||||
|
DeviceInfo.fromJson(result['data']['info']));
|
||||||
|
if (oldState != null && oldState != newState) {
|
||||||
|
// Ensure state is cleared
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
} else {
|
||||||
|
state = AsyncValue.data(newState);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
final status = result['data']['status'];
|
final status = result['data']['status'];
|
||||||
// Only update if status is not changed
|
// Only update if status is not changed
|
||||||
@ -298,9 +304,7 @@ class CurrentDeviceDataNotifier extends StateNotifier<AsyncValue<YubiKeyData>> {
|
|||||||
_log.error('Error polling NFC', jsonEncode(e));
|
_log.error('Error polling NFC', jsonEncode(e));
|
||||||
}
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_pollTimer = Timer(
|
_pollTimer = Timer(_nfcPollCardDelay, _pollCard);
|
||||||
state is AsyncData ? _nfcDetachPollDelay : _nfcAttachPollDelay,
|
|
||||||
_pollReader);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -295,7 +295,7 @@ class DesktopCredentialListNotifier extends OathCredentialListNotifier {
|
|||||||
code = OathCode.fromJson(result);
|
code = OathCode.fromJson(result);
|
||||||
}
|
}
|
||||||
_log.debug('Calculate', jsonEncode(code));
|
_log.debug('Calculate', jsonEncode(code));
|
||||||
if (update && mounted) {
|
if (update && mounted && state != null) {
|
||||||
final creds = state!.toList();
|
final creds = state!.toList();
|
||||||
final i = creds.indexWhere((e) => e.credential.id == credential.id);
|
final i = creds.indexWhere((e) => e.credential.id == credential.id);
|
||||||
state = creds..[i] = creds[i].copyWith(code: code);
|
state = creds..[i] = creds[i].copyWith(code: code);
|
||||||
|
@ -72,8 +72,25 @@ class _DesktopPivStateNotifier extends PivStateNotifier {
|
|||||||
ref.invalidate(_sessionProvider(devicePath));
|
ref.invalidate(_sessionProvider(devicePath));
|
||||||
})
|
})
|
||||||
..setErrorHandler('auth-required', (e) async {
|
..setErrorHandler('auth-required', (e) async {
|
||||||
|
try {
|
||||||
|
if (state.valueOrNull?.protectedKey == true) {
|
||||||
|
final String? pin;
|
||||||
|
if (state.valueOrNull?.metadata?.pinMetadata.defaultValue == true) {
|
||||||
|
pin = defaultPin;
|
||||||
|
} else {
|
||||||
|
pin = ref.read(_pinProvider(devicePath));
|
||||||
|
}
|
||||||
|
if (pin != null) {
|
||||||
|
if (await verifyPin(pin) is PinSuccess) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
ref.read(_pinProvider(devicePath).notifier).state = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
final String? mgmtKey;
|
final String? mgmtKey;
|
||||||
if (state.valueOrNull?.metadata?.managementKeyMetadata.defaultValue ==
|
if (state.valueOrNull?.metadata?.managementKeyMetadata
|
||||||
|
.defaultValue ==
|
||||||
true) {
|
true) {
|
||||||
mgmtKey = defaultManagementKey;
|
mgmtKey = defaultManagementKey;
|
||||||
} else {
|
} else {
|
||||||
@ -81,15 +98,16 @@ class _DesktopPivStateNotifier extends PivStateNotifier {
|
|||||||
}
|
}
|
||||||
if (mgmtKey != null) {
|
if (mgmtKey != null) {
|
||||||
if (await authenticate(mgmtKey)) {
|
if (await authenticate(mgmtKey)) {
|
||||||
ref.invalidateSelf();
|
return;
|
||||||
} else {
|
} else {
|
||||||
ref.read(_managementKeyProvider(devicePath).notifier).state = null;
|
ref.read(_managementKeyProvider(devicePath).notifier).state =
|
||||||
ref.invalidateSelf();
|
null;
|
||||||
throw e;
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ref.invalidateSelf();
|
|
||||||
throw e;
|
throw e;
|
||||||
|
} finally {
|
||||||
|
ref.invalidateSelf();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
@ -299,8 +317,24 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> delete(SlotId slot) async {
|
Future<void> delete(SlotId slot, bool deleteCert, bool deleteKey) async {
|
||||||
await _session.command('delete', target: ['slots', slot.hexId]);
|
await _session.command('delete',
|
||||||
|
target: ['slots', slot.hexId],
|
||||||
|
params: {'delete_cert': deleteCert, 'delete_key': deleteKey});
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> moveKey(SlotId source, SlotId destination, bool overwriteKey,
|
||||||
|
bool includeCertificate) async {
|
||||||
|
await _session.command('move_key', target: [
|
||||||
|
'slots',
|
||||||
|
source.hexId
|
||||||
|
], params: {
|
||||||
|
'destination': destination.hexId,
|
||||||
|
'overwrite_key': overwriteKey,
|
||||||
|
'include_certificate': includeCertificate
|
||||||
|
});
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,8 @@ class FidoState with _$FidoState {
|
|||||||
info['options']['credMgmt'] == true ||
|
info['options']['credMgmt'] == true ||
|
||||||
info['options']['credentialMgmtPreview'] == true;
|
info['options']['credentialMgmtPreview'] == true;
|
||||||
|
|
||||||
|
int? get remainingCreds => info['remaining_disc_creds'];
|
||||||
|
|
||||||
bool? get bioEnroll => info['options']['bioEnroll'];
|
bool? get bioEnroll => info['options']['bioEnroll'];
|
||||||
|
|
||||||
bool get alwaysUv => info['options']['alwaysUv'] == true;
|
bool get alwaysUv => info['options']['alwaysUv'] == true;
|
||||||
|
@ -20,6 +20,18 @@ import '../app/models.dart';
|
|||||||
import '../core/state.dart';
|
import '../core/state.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
|
|
||||||
|
final passkeysSearchProvider =
|
||||||
|
StateNotifierProvider<PasskeysSearchNotifier, String>(
|
||||||
|
(ref) => PasskeysSearchNotifier());
|
||||||
|
|
||||||
|
class PasskeysSearchNotifier extends StateNotifier<String> {
|
||||||
|
PasskeysSearchNotifier() : super('');
|
||||||
|
|
||||||
|
void setFilter(String value) {
|
||||||
|
state = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final fidoStateProvider = AsyncNotifierProvider.autoDispose
|
final fidoStateProvider = AsyncNotifierProvider.autoDispose
|
||||||
.family<FidoStateNotifier, FidoState, DevicePath>(
|
.family<FidoStateNotifier, FidoState, DevicePath>(
|
||||||
() => throw UnimplementedError(),
|
() => throw UnimplementedError(),
|
||||||
@ -52,3 +64,23 @@ abstract class FidoCredentialsNotifier
|
|||||||
extends AutoDisposeFamilyAsyncNotifier<List<FidoCredential>, DevicePath> {
|
extends AutoDisposeFamilyAsyncNotifier<List<FidoCredential>, DevicePath> {
|
||||||
Future<void> deleteCredential(FidoCredential credential);
|
Future<void> deleteCredential(FidoCredential credential);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final filteredFidoCredentialsProvider = StateNotifierProvider.autoDispose
|
||||||
|
.family<FilteredFidoCredentialsNotifier, List<FidoCredential>,
|
||||||
|
List<FidoCredential>>(
|
||||||
|
(ref, full) {
|
||||||
|
return FilteredFidoCredentialsNotifier(
|
||||||
|
full, ref.watch(passkeysSearchProvider));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
class FilteredFidoCredentialsNotifier
|
||||||
|
extends StateNotifier<List<FidoCredential>> {
|
||||||
|
final String query;
|
||||||
|
FilteredFidoCredentialsNotifier(List<FidoCredential> full, this.query)
|
||||||
|
: super(full
|
||||||
|
.where((credential) =>
|
||||||
|
credential.rpId.toLowerCase().contains(query.toLowerCase()) ||
|
||||||
|
credential.userName.toLowerCase().contains(query.toLowerCase()))
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
@ -75,7 +75,7 @@ class CredentialDialog extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Icon(Symbols.person, size: 72),
|
const Icon(Symbols.passkey, size: 72),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@ -29,11 +30,14 @@ import '../../app/views/action_list.dart';
|
|||||||
import '../../app/views/app_failure_page.dart';
|
import '../../app/views/app_failure_page.dart';
|
||||||
import '../../app/views/app_list_item.dart';
|
import '../../app/views/app_list_item.dart';
|
||||||
import '../../app/views/app_page.dart';
|
import '../../app/views/app_page.dart';
|
||||||
|
import '../../app/views/keys.dart';
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
import '../../app/views/message_page_not_initialized.dart';
|
import '../../app/views/message_page_not_initialized.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../../exception/no_data_exception.dart';
|
import '../../exception/no_data_exception.dart';
|
||||||
import '../../management/models.dart';
|
import '../../management/models.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
|
import '../../widgets/app_text_form_field.dart';
|
||||||
import '../../widgets/list_title.dart';
|
import '../../widgets/list_title.dart';
|
||||||
import '../features.dart' as features;
|
import '../features.dart' as features;
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
@ -137,7 +141,7 @@ class _FidoLockedPage extends ConsumerWidget {
|
|||||||
: alwaysUv
|
: alwaysUv
|
||||||
? l10n.l_pin_change_required_desc
|
? l10n.l_pin_change_required_desc
|
||||||
: l10n.l_register_sk_on_websites,
|
: l10n.l_register_sk_on_websites,
|
||||||
footnote: isBio ? null : l10n.l_non_passkeys_note,
|
footnote: isBio ? null : l10n.p_non_passkeys_note,
|
||||||
keyActionsBuilder: hasActions ? _buildActions : null,
|
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||||
keyActionsBadge: passkeysShowActionsNotifier(state),
|
keyActionsBadge: passkeysShowActionsNotifier(state),
|
||||||
);
|
);
|
||||||
@ -149,7 +153,7 @@ class _FidoLockedPage extends ConsumerWidget {
|
|||||||
capabilities: const [Capability.fido2],
|
capabilities: const [Capability.fido2],
|
||||||
header: l10n.l_ready_to_use,
|
header: l10n.l_ready_to_use,
|
||||||
message: l10n.l_register_sk_on_websites,
|
message: l10n.l_register_sk_on_websites,
|
||||||
footnote: l10n.l_non_passkeys_note,
|
footnote: l10n.p_non_passkeys_note,
|
||||||
keyActionsBuilder: hasActions ? _buildActions : null,
|
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||||
keyActionsBadge: passkeysShowActionsNotifier(state),
|
keyActionsBadge: passkeysShowActionsNotifier(state),
|
||||||
);
|
);
|
||||||
@ -206,8 +210,30 @@ class _FidoUnlockedPage extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
||||||
|
late FocusNode searchFocus;
|
||||||
|
late TextEditingController searchController;
|
||||||
FidoCredential? _selected;
|
FidoCredential? _selected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
searchFocus = FocusNode();
|
||||||
|
searchController =
|
||||||
|
TextEditingController(text: ref.read(passkeysSearchProvider));
|
||||||
|
searchFocus.addListener(_onFocusChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
searchFocus.dispose();
|
||||||
|
searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFocusChange() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
@ -222,7 +248,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
|||||||
capabilities: const [Capability.fido2],
|
capabilities: const [Capability.fido2],
|
||||||
header: l10n.l_no_discoverable_accounts,
|
header: l10n.l_no_discoverable_accounts,
|
||||||
message: l10n.l_register_sk_on_websites,
|
message: l10n.l_register_sk_on_websites,
|
||||||
footnote: l10n.l_non_passkeys_note,
|
footnote: l10n.p_non_passkeys_note,
|
||||||
keyActionsBuilder: hasActions
|
keyActionsBuilder: hasActions
|
||||||
? (context) =>
|
? (context) =>
|
||||||
passkeysBuildActions(context, widget.node, widget.state)
|
passkeysBuildActions(context, widget.node, widget.state)
|
||||||
@ -236,6 +262,12 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
|||||||
return _buildLoadingPage(context);
|
return _buildLoadingPage(context);
|
||||||
}
|
}
|
||||||
final credentials = data.value;
|
final credentials = data.value;
|
||||||
|
final filteredCredentials =
|
||||||
|
ref.watch(filteredFidoCredentialsProvider(credentials.toList()));
|
||||||
|
|
||||||
|
final remainingCreds = widget.state.remainingCreds;
|
||||||
|
final maxCreds =
|
||||||
|
remainingCreds != null ? remainingCreds + credentials.length : 25;
|
||||||
|
|
||||||
if (credentials.isEmpty) {
|
if (credentials.isEmpty) {
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
@ -265,14 +297,22 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
|||||||
passkeysBuildActions(context, widget.node, widget.state)
|
passkeysBuildActions(context, widget.node, widget.state)
|
||||||
: null,
|
: null,
|
||||||
keyActionsBadge: passkeysShowActionsNotifier(widget.state),
|
keyActionsBadge: passkeysShowActionsNotifier(widget.state),
|
||||||
footnote: l10n.l_non_passkeys_note,
|
footnote: l10n.p_non_passkeys_note,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final credential = _selected;
|
final credential = _selected;
|
||||||
|
final searchText = searchController.text;
|
||||||
return FidoActions(
|
return FidoActions(
|
||||||
devicePath: widget.node.path,
|
devicePath: widget.node.path,
|
||||||
actions: (context) => {
|
actions: (context) => {
|
||||||
|
SearchIntent: CallbackAction<SearchIntent>(onInvoke: (_) {
|
||||||
|
searchController.selection = TextSelection(
|
||||||
|
baseOffset: 0, extentOffset: searchController.text.length);
|
||||||
|
searchFocus.unfocus();
|
||||||
|
Timer.run(() => searchFocus.requestFocus());
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
EscapeIntent: CallbackAction<EscapeIntent>(onInvoke: (intent) {
|
EscapeIntent: CallbackAction<EscapeIntent>(onInvoke: (intent) {
|
||||||
if (_selected != null) {
|
if (_selected != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -307,8 +347,84 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
|||||||
},
|
},
|
||||||
builder: (context) => AppPage(
|
builder: (context) => AppPage(
|
||||||
title: l10n.s_passkeys,
|
title: l10n.s_passkeys,
|
||||||
|
alternativeTitle:
|
||||||
|
searchText != '' ? l10n.l_results_for(searchText) : null,
|
||||||
capabilities: const [Capability.fido2],
|
capabilities: const [Capability.fido2],
|
||||||
footnote: l10n.l_non_passkeys_note,
|
footnote:
|
||||||
|
'${l10n.p_passkeys_used(credentials.length, maxCreds)} ${l10n.p_non_passkeys_note}',
|
||||||
|
headerSliver: Focus(
|
||||||
|
canRequestFocus: false,
|
||||||
|
onKeyEvent: (node, event) {
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||||
|
node.focusInDirection(TraversalDirection.down);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
|
searchController.clear();
|
||||||
|
ref.read(passkeysSearchProvider.notifier).setFilter('');
|
||||||
|
node.unfocus();
|
||||||
|
setState(() {});
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
},
|
||||||
|
child: Builder(builder: (context) {
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
return Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: AppTextFormField(
|
||||||
|
key: searchField,
|
||||||
|
controller: searchController,
|
||||||
|
focusNode: searchFocus,
|
||||||
|
// Use the default style, but with a smaller font size:
|
||||||
|
style: textTheme.titleMedium
|
||||||
|
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
|
||||||
|
decoration: AppInputDecoration(
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(48),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
width: 0,
|
||||||
|
style: searchFocus.hasFocus
|
||||||
|
? BorderStyle.solid
|
||||||
|
: BorderStyle.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.all(16),
|
||||||
|
fillColor: Theme.of(context).hoverColor,
|
||||||
|
filled: true,
|
||||||
|
hintText: l10n.s_search_passkeys,
|
||||||
|
isDense: true,
|
||||||
|
prefixIcon: const Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(start: 8.0),
|
||||||
|
child: Icon(Icons.search_outlined),
|
||||||
|
),
|
||||||
|
suffixIcon: searchController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
iconSize: 16,
|
||||||
|
onPressed: () {
|
||||||
|
searchController.clear();
|
||||||
|
ref
|
||||||
|
.read(passkeysSearchProvider.notifier)
|
||||||
|
.setFilter('');
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
ref.read(passkeysSearchProvider.notifier).setFilter(value);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
onFieldSubmitted: (value) {
|
||||||
|
Focus.of(context).focusInDirection(TraversalDirection.down);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
detailViewBuilder: credential != null
|
detailViewBuilder: credential != null
|
||||||
? (context) => Column(
|
? (context) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@ -347,7 +463,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Icon(Symbols.person, size: 72),
|
const Icon(Symbols.passkey, size: 72),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -390,15 +506,19 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
|||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: credentials
|
children: [
|
||||||
.map(
|
if (filteredCredentials.isEmpty)
|
||||||
|
Center(
|
||||||
|
child: Text(l10n.s_no_passkeys),
|
||||||
|
),
|
||||||
|
...filteredCredentials.map(
|
||||||
(cred) => _CredentialListItem(
|
(cred) => _CredentialListItem(
|
||||||
cred,
|
cred,
|
||||||
expanded: expanded,
|
expanded: expanded,
|
||||||
selected: _selected == cred,
|
selected: _selected == cred,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
.toList(),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -423,13 +543,14 @@ class _CredentialListItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
return AppListItem(
|
return AppListItem(
|
||||||
credential,
|
credential,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
foregroundColor: colorScheme.onSecondary,
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
backgroundColor: colorScheme.secondary,
|
||||||
child: const Icon(Symbols.person),
|
child: const Icon(Symbols.passkey),
|
||||||
),
|
),
|
||||||
title: credential.userName,
|
title: credential.userName,
|
||||||
subtitle: credential.rpId,
|
subtitle: credential.rpId,
|
||||||
|
@ -14,21 +14,39 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
import '../../management/models.dart';
|
import '../../management/models.dart';
|
||||||
|
|
||||||
class WebAuthnScreen extends StatelessWidget {
|
class WebAuthnScreen extends StatefulWidget {
|
||||||
const WebAuthnScreen({super.key});
|
const WebAuthnScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WebAuthnScreen> createState() => _WebAuthnScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WebAuthnScreenState extends State<WebAuthnScreen> {
|
||||||
|
bool hide = true;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
// We need this to avoid unwanted app switch animation
|
||||||
|
if (hide) {
|
||||||
|
Timer.run(() {
|
||||||
|
setState(() {
|
||||||
|
hide = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
title: l10n.s_security_key,
|
title: hide ? null : l10n.s_security_key,
|
||||||
capabilities: const [Capability.u2f],
|
capabilities: const [Capability.u2f],
|
||||||
|
delayedContent: hide,
|
||||||
header: l10n.l_ready_to_use,
|
header: l10n.l_ready_to_use,
|
||||||
message: l10n.l_register_sk_on_websites,
|
message: l10n.l_register_sk_on_websites,
|
||||||
);
|
);
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@ -32,32 +34,49 @@ import '../../widgets/product_image.dart';
|
|||||||
import 'key_actions.dart';
|
import 'key_actions.dart';
|
||||||
import 'manage_label_dialog.dart';
|
import 'manage_label_dialog.dart';
|
||||||
|
|
||||||
class HomeScreen extends ConsumerWidget {
|
class HomeScreen extends ConsumerStatefulWidget {
|
||||||
final YubiKeyData deviceData;
|
final YubiKeyData deviceData;
|
||||||
const HomeScreen(this.deviceData, {super.key});
|
const HomeScreen(this.deviceData, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<ConsumerStatefulWidget> createState() => _HomeScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||||
|
bool hide = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
final serial = deviceData.info.serial;
|
final serial = widget.deviceData.info.serial;
|
||||||
final keyCustomization = ref.watch(keyCustomizationManagerProvider)[serial];
|
final keyCustomization = ref.watch(keyCustomizationManagerProvider)[serial];
|
||||||
final enabledCapabilities =
|
final enabledCapabilities = widget.deviceData.info.config
|
||||||
deviceData.info.config.enabledCapabilities[deviceData.node.transport] ??
|
.enabledCapabilities[widget.deviceData.node.transport] ??
|
||||||
0;
|
0;
|
||||||
final primaryColor = ref.watch(defaultColorProvider);
|
final primaryColor = ref.watch(defaultColorProvider);
|
||||||
|
|
||||||
|
// We need this to avoid unwanted app switch animation
|
||||||
|
if (hide) {
|
||||||
|
Timer.run(() {
|
||||||
|
setState(() {
|
||||||
|
hide = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return AppPage(
|
return AppPage(
|
||||||
title: l10n.s_home,
|
title: hide ? null : l10n.s_home,
|
||||||
|
delayedContent: hide,
|
||||||
keyActionsBuilder: (context) =>
|
keyActionsBuilder: (context) =>
|
||||||
homeBuildActions(context, deviceData, ref),
|
homeBuildActions(context, widget.deviceData, ref),
|
||||||
builder: (context, expanded) {
|
builder: (context, expanded) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_DeviceContent(deviceData, keyCustomization),
|
_DeviceContent(widget.deviceData, keyCustomization),
|
||||||
const SizedBox(height: 16.0),
|
const SizedBox(height: 16.0),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -79,7 +98,7 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
if (serial != null) ...[
|
if (serial != null) ...[
|
||||||
const SizedBox(height: 32.0),
|
const SizedBox(height: 32.0),
|
||||||
_DeviceColor(
|
_DeviceColor(
|
||||||
deviceData: deviceData,
|
deviceData: widget.deviceData,
|
||||||
initialCustomization: keyCustomization ??
|
initialCustomization: keyCustomization ??
|
||||||
KeyCustomization(serial: serial))
|
KeyCustomization(serial: serial))
|
||||||
]
|
]
|
||||||
@ -93,9 +112,9 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
child: _HeroAvatar(
|
child: _HeroAvatar(
|
||||||
color: keyCustomization?.color ?? primaryColor,
|
color: keyCustomization?.color ?? primaryColor,
|
||||||
child: ProductImage(
|
child: ProductImage(
|
||||||
name: deviceData.name,
|
name: widget.deviceData.name,
|
||||||
formFactor: deviceData.info.formFactor,
|
formFactor: widget.deviceData.info.formFactor,
|
||||||
isNfc: deviceData.info.supportedCapabilities
|
isNfc: widget.deviceData.info.supportedCapabilities
|
||||||
.containsKey(Transport.nfc),
|
.containsKey(Transport.nfc),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"s_cancel": "Abbrechen",
|
"s_cancel": "Abbrechen",
|
||||||
"s_close": "Schließen",
|
"s_close": "Schließen",
|
||||||
"s_delete": "Löschen",
|
"s_delete": "Löschen",
|
||||||
|
"s_move": null,
|
||||||
"s_quit": "Beenden",
|
"s_quit": "Beenden",
|
||||||
"s_status": null,
|
"s_status": null,
|
||||||
"s_unlock": "Entsperren",
|
"s_unlock": "Entsperren",
|
||||||
@ -352,6 +353,12 @@
|
|||||||
},
|
},
|
||||||
"s_accounts": "Konten",
|
"s_accounts": "Konten",
|
||||||
"s_no_accounts": "Keine Konten",
|
"s_no_accounts": "Keine Konten",
|
||||||
|
"l_results_for": null,
|
||||||
|
"@l_results_for": {
|
||||||
|
"placeholders": {
|
||||||
|
"query": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"l_authenticator_get_started": null,
|
"l_authenticator_get_started": null,
|
||||||
"l_no_accounts_desc": null,
|
"l_no_accounts_desc": null,
|
||||||
"s_add_account": "Konto hinzufügen",
|
"s_add_account": "Konto hinzufügen",
|
||||||
@ -419,15 +426,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"s_passkeys": null,
|
"s_passkeys": null,
|
||||||
|
"s_no_passkeys": null,
|
||||||
"l_ready_to_use": "Bereit zur Verwendung",
|
"l_ready_to_use": "Bereit zur Verwendung",
|
||||||
"l_register_sk_on_websites": "Als Sicherheitsschlüssel auf Webseiten registrieren",
|
"l_register_sk_on_websites": "Als Sicherheitsschlüssel auf Webseiten registrieren",
|
||||||
"l_no_discoverable_accounts": "Keine erkennbaren Konten",
|
"l_no_discoverable_accounts": "Keine erkennbaren Konten",
|
||||||
"l_non_passkeys_note": null,
|
"p_non_passkeys_note": null,
|
||||||
"s_delete_passkey": null,
|
"s_delete_passkey": null,
|
||||||
"l_delete_passkey_desc": null,
|
"l_delete_passkey_desc": null,
|
||||||
"s_passkey_deleted": null,
|
"s_passkey_deleted": null,
|
||||||
"p_warning_delete_passkey": null,
|
"p_warning_delete_passkey": null,
|
||||||
|
"s_search_passkeys": null,
|
||||||
|
"p_passkeys_used": null,
|
||||||
|
"@p_passkeys_used": {
|
||||||
|
"placeholders": {
|
||||||
|
"used": {},
|
||||||
|
"max": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@_fingerprints": {},
|
"@_fingerprints": {},
|
||||||
"l_fingerprint": "Fingerabdruck: {label}",
|
"l_fingerprint": "Fingerabdruck: {label}",
|
||||||
"@l_fingerprint": {
|
"@l_fingerprint": {
|
||||||
@ -505,6 +520,12 @@
|
|||||||
"l_unsupported_key_type": null,
|
"l_unsupported_key_type": null,
|
||||||
"l_delete_certificate": null,
|
"l_delete_certificate": null,
|
||||||
"l_delete_certificate_desc": null,
|
"l_delete_certificate_desc": null,
|
||||||
|
"l_delete_key": null,
|
||||||
|
"l_delete_key_desc": null,
|
||||||
|
"l_delete_certificate_or_key": null,
|
||||||
|
"l_delete_certificate_or_key_desc": null,
|
||||||
|
"l_move_key": null,
|
||||||
|
"l_move_key_desc": null,
|
||||||
"s_issuer": null,
|
"s_issuer": null,
|
||||||
"s_serial": null,
|
"s_serial": null,
|
||||||
"s_certificate_fingerprint": null,
|
"s_certificate_fingerprint": null,
|
||||||
@ -522,14 +543,53 @@
|
|||||||
},
|
},
|
||||||
"l_generating_private_key": null,
|
"l_generating_private_key": null,
|
||||||
"s_private_key_generated": null,
|
"s_private_key_generated": null,
|
||||||
|
"p_select_what_to_delete": null,
|
||||||
"p_warning_delete_certificate": null,
|
"p_warning_delete_certificate": null,
|
||||||
|
"p_warning_delete_key": null,
|
||||||
|
"p_warning_delete_certificate_and_key": null,
|
||||||
"q_delete_certificate_confirm": null,
|
"q_delete_certificate_confirm": null,
|
||||||
"@q_delete_certificate_confirm": {
|
"@q_delete_certificate_confirm": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"slot": {}
|
"slot": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"q_delete_key_confirm": null,
|
||||||
|
"@q_delete_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_delete_certificate_and_key_confirm": null,
|
||||||
|
"@q_delete_certificate_and_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"l_certificate_deleted": null,
|
"l_certificate_deleted": null,
|
||||||
|
"l_key_deleted": null,
|
||||||
|
"l_certificate_and_key_deleted": null,
|
||||||
|
"l_include_certificate": null,
|
||||||
|
"l_select_destination_slot": null,
|
||||||
|
"q_move_key_confirm": null,
|
||||||
|
"@q_move_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_move_key_to_slot_confirm": null,
|
||||||
|
"@q_move_key_to_slot_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {},
|
||||||
|
"to_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_move_key_and_certificate_to_slot_confirm": null,
|
||||||
|
"@q_move_key_and_certificate_to_slot_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {},
|
||||||
|
"to_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"p_password_protected_file": null,
|
"p_password_protected_file": null,
|
||||||
"p_import_items_desc": null,
|
"p_import_items_desc": null,
|
||||||
"@p_import_items_desc": {
|
"@p_import_items_desc": {
|
||||||
@ -537,6 +597,8 @@
|
|||||||
"slot": {}
|
"slot": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"l_key_moved": null,
|
||||||
|
"l_key_and_certificate_moved": null,
|
||||||
"p_subject_desc": null,
|
"p_subject_desc": null,
|
||||||
"l_rfc4514_invalid": null,
|
"l_rfc4514_invalid": null,
|
||||||
"rfc4514_examples": null,
|
"rfc4514_examples": null,
|
||||||
@ -560,6 +622,12 @@
|
|||||||
"hexid": {}
|
"hexid": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"s_retired_slot_display_name": null,
|
||||||
|
"@s_retired_slot_display_name": {
|
||||||
|
"placeholders": {
|
||||||
|
"hexid": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"s_slot_9a": null,
|
"s_slot_9a": null,
|
||||||
"s_slot_9c": null,
|
"s_slot_9c": null,
|
||||||
"s_slot_9d": null,
|
"s_slot_9d": null,
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"s_cancel": "Cancel",
|
"s_cancel": "Cancel",
|
||||||
"s_close": "Close",
|
"s_close": "Close",
|
||||||
"s_delete": "Delete",
|
"s_delete": "Delete",
|
||||||
|
"s_move": "Move",
|
||||||
"s_quit": "Quit",
|
"s_quit": "Quit",
|
||||||
"s_status": "Status",
|
"s_status": "Status",
|
||||||
"s_unlock": "Unlock",
|
"s_unlock": "Unlock",
|
||||||
@ -352,6 +353,12 @@
|
|||||||
},
|
},
|
||||||
"s_accounts": "Accounts",
|
"s_accounts": "Accounts",
|
||||||
"s_no_accounts": "No accounts",
|
"s_no_accounts": "No accounts",
|
||||||
|
"l_results_for": "Results for \"{query}\"",
|
||||||
|
"@l_results_for": {
|
||||||
|
"placeholders": {
|
||||||
|
"query": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"l_authenticator_get_started": "Get started with OTP accounts",
|
"l_authenticator_get_started": "Get started with OTP accounts",
|
||||||
"l_no_accounts_desc": "Add accounts to your YubiKey from any service provider supporting OATH TOTP/HOTP",
|
"l_no_accounts_desc": "Add accounts to your YubiKey from any service provider supporting OATH TOTP/HOTP",
|
||||||
"s_add_account": "Add account",
|
"s_add_account": "Add account",
|
||||||
@ -419,15 +426,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"s_passkeys": "Passkeys",
|
"s_passkeys": "Passkeys",
|
||||||
|
"s_no_passkeys": "No passkeys",
|
||||||
"l_ready_to_use": "Ready to use",
|
"l_ready_to_use": "Ready to use",
|
||||||
"l_register_sk_on_websites": "Register as a Security Key on websites",
|
"l_register_sk_on_websites": "Register as a Security Key on websites",
|
||||||
"l_no_discoverable_accounts": "No passkeys stored",
|
"l_no_discoverable_accounts": "No passkeys stored",
|
||||||
"l_non_passkeys_note": "Non-passkey credentials may exist, but can not be listed",
|
"p_non_passkeys_note": "Non-passkey credentials may exist, but can not be listed.",
|
||||||
"s_delete_passkey": "Delete passkey",
|
"s_delete_passkey": "Delete passkey",
|
||||||
"l_delete_passkey_desc": "Remove the passkey from the YubiKey",
|
"l_delete_passkey_desc": "Remove the passkey from the YubiKey",
|
||||||
"s_passkey_deleted": "Passkey deleted",
|
"s_passkey_deleted": "Passkey deleted",
|
||||||
"p_warning_delete_passkey": "This will delete the passkey from your YubiKey.",
|
"p_warning_delete_passkey": "This will delete the passkey from your YubiKey.",
|
||||||
|
"s_search_passkeys": "Search passkeys",
|
||||||
|
"p_passkeys_used": "{used} of {max} passkeys used.",
|
||||||
|
"@p_passkeys_used": {
|
||||||
|
"placeholders": {
|
||||||
|
"used": {},
|
||||||
|
"max": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@_fingerprints": {},
|
"@_fingerprints": {},
|
||||||
"l_fingerprint": "Fingerprint: {label}",
|
"l_fingerprint": "Fingerprint: {label}",
|
||||||
"@l_fingerprint": {
|
"@l_fingerprint": {
|
||||||
@ -505,6 +520,12 @@
|
|||||||
"l_unsupported_key_type": "Unsupported key type",
|
"l_unsupported_key_type": "Unsupported key type",
|
||||||
"l_delete_certificate": "Delete certificate",
|
"l_delete_certificate": "Delete certificate",
|
||||||
"l_delete_certificate_desc": "Remove the certificate from your YubiKey",
|
"l_delete_certificate_desc": "Remove the certificate from your YubiKey",
|
||||||
|
"l_delete_key": "Delete key",
|
||||||
|
"l_delete_key_desc": "Remove the key from your YubiKey",
|
||||||
|
"l_delete_certificate_or_key": "Delete certificate/key",
|
||||||
|
"l_delete_certificate_or_key_desc": "Remove the certificate or key from your YubiKey",
|
||||||
|
"l_move_key": "Move key",
|
||||||
|
"l_move_key_desc": "Move a key from one PIV slot into another",
|
||||||
"s_issuer": "Issuer",
|
"s_issuer": "Issuer",
|
||||||
"s_serial": "Serial",
|
"s_serial": "Serial",
|
||||||
"s_certificate_fingerprint": "Fingerprint",
|
"s_certificate_fingerprint": "Fingerprint",
|
||||||
@ -522,14 +543,53 @@
|
|||||||
},
|
},
|
||||||
"l_generating_private_key": "Generating private key\u2026",
|
"l_generating_private_key": "Generating private key\u2026",
|
||||||
"s_private_key_generated": "Private key generated",
|
"s_private_key_generated": "Private key generated",
|
||||||
|
"p_select_what_to_delete": "Select what to delete from the slot.",
|
||||||
"p_warning_delete_certificate": "Warning! This action will delete the certificate from your YubiKey.",
|
"p_warning_delete_certificate": "Warning! This action will delete the certificate from your YubiKey.",
|
||||||
|
"p_warning_delete_key": "Warning! This action will delete the private key from your YubiKey.",
|
||||||
|
"p_warning_delete_certificate_and_key": "Warning! This action will delete the certificate and private key from your YubiKey.",
|
||||||
"q_delete_certificate_confirm": "Delete the certificate in PIV slot {slot}?",
|
"q_delete_certificate_confirm": "Delete the certificate in PIV slot {slot}?",
|
||||||
"@q_delete_certificate_confirm": {
|
"@q_delete_certificate_confirm": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"slot": {}
|
"slot": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"q_delete_key_confirm": "Delete the private key in PIV slot {slot}?",
|
||||||
|
"@q_delete_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_delete_certificate_and_key_confirm": "Delete the certificate and private key in PIV slot {slot}?",
|
||||||
|
"@q_delete_certificate_and_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"l_certificate_deleted": "Certificate deleted",
|
"l_certificate_deleted": "Certificate deleted",
|
||||||
|
"l_key_deleted": "Key deleted",
|
||||||
|
"l_certificate_and_key_deleted": "Certificate and key deleted",
|
||||||
|
"l_include_certificate": "Include certificate",
|
||||||
|
"l_select_destination_slot": "Select destination slot",
|
||||||
|
"q_move_key_confirm": "Move the private key in PIV slot {from_slot}?",
|
||||||
|
"@q_move_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_move_key_to_slot_confirm": "Move the private key in PIV slot {from_slot} to slot {to_slot}?",
|
||||||
|
"@q_move_key_to_slot_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {},
|
||||||
|
"to_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_move_key_and_certificate_to_slot_confirm": "Move the private key and certificate in PIV slot {from_slot} to slot {to_slot}?",
|
||||||
|
"@q_move_key_and_certificate_to_slot_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {},
|
||||||
|
"to_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"p_password_protected_file": "The selected file is password protected. Enter the password to proceed.",
|
"p_password_protected_file": "The selected file is password protected. Enter the password to proceed.",
|
||||||
"p_import_items_desc": "The following item(s) will be imported into PIV slot {slot}.",
|
"p_import_items_desc": "The following item(s) will be imported into PIV slot {slot}.",
|
||||||
"@p_import_items_desc": {
|
"@p_import_items_desc": {
|
||||||
@ -537,6 +597,8 @@
|
|||||||
"slot": {}
|
"slot": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"l_key_moved": "Key moved",
|
||||||
|
"l_key_and_certificate_moved": "Key and certificate moved",
|
||||||
"p_subject_desc": "A distinguished name (DN) formatted in accordance to the RFC 4514 specification.",
|
"p_subject_desc": "A distinguished name (DN) formatted in accordance to the RFC 4514 specification.",
|
||||||
"l_rfc4514_invalid": "Invalid RFC 4514 format",
|
"l_rfc4514_invalid": "Invalid RFC 4514 format",
|
||||||
"rfc4514_examples": "Examples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net",
|
"rfc4514_examples": "Examples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net",
|
||||||
@ -560,6 +622,12 @@
|
|||||||
"hexid": {}
|
"hexid": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"s_retired_slot_display_name": "Retired Key Management ({hexid})",
|
||||||
|
"@s_retired_slot_display_name": {
|
||||||
|
"placeholders": {
|
||||||
|
"hexid": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"s_slot_9a": "Authentication",
|
"s_slot_9a": "Authentication",
|
||||||
"s_slot_9c": "Digital Signature",
|
"s_slot_9c": "Digital Signature",
|
||||||
"s_slot_9d": "Key Management",
|
"s_slot_9d": "Key Management",
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"s_cancel": "Annuler",
|
"s_cancel": "Annuler",
|
||||||
"s_close": "Fermer",
|
"s_close": "Fermer",
|
||||||
"s_delete": "Supprimer",
|
"s_delete": "Supprimer",
|
||||||
|
"s_move": null,
|
||||||
"s_quit": "Quitter",
|
"s_quit": "Quitter",
|
||||||
"s_status": null,
|
"s_status": null,
|
||||||
"s_unlock": "Déverrouiller",
|
"s_unlock": "Déverrouiller",
|
||||||
@ -352,6 +353,12 @@
|
|||||||
},
|
},
|
||||||
"s_accounts": "Comptes",
|
"s_accounts": "Comptes",
|
||||||
"s_no_accounts": "Aucun compte",
|
"s_no_accounts": "Aucun compte",
|
||||||
|
"l_results_for": null,
|
||||||
|
"@l_results_for": {
|
||||||
|
"placeholders": {
|
||||||
|
"query": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"l_authenticator_get_started": null,
|
"l_authenticator_get_started": null,
|
||||||
"l_no_accounts_desc": null,
|
"l_no_accounts_desc": null,
|
||||||
"s_add_account": "Ajouter un compte",
|
"s_add_account": "Ajouter un compte",
|
||||||
@ -419,15 +426,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"s_passkeys": "Passkeys",
|
"s_passkeys": "Passkeys",
|
||||||
|
"s_no_passkeys": null,
|
||||||
"l_ready_to_use": "Prêt à l'emploi",
|
"l_ready_to_use": "Prêt à l'emploi",
|
||||||
"l_register_sk_on_websites": "Enregistrer comme clé de sécurité sur les sites internet",
|
"l_register_sk_on_websites": "Enregistrer comme clé de sécurité sur les sites internet",
|
||||||
"l_no_discoverable_accounts": "Aucune Passkey détectée",
|
"l_no_discoverable_accounts": "Aucune Passkey détectée",
|
||||||
"l_non_passkeys_note": null,
|
"p_non_passkeys_note": null,
|
||||||
"s_delete_passkey": "Supprimer une Passkey",
|
"s_delete_passkey": "Supprimer une Passkey",
|
||||||
"l_delete_passkey_desc": "Supprimer la Passkey de votre YubiKey",
|
"l_delete_passkey_desc": "Supprimer la Passkey de votre YubiKey",
|
||||||
"s_passkey_deleted": "Passkey supprimée",
|
"s_passkey_deleted": "Passkey supprimée",
|
||||||
"p_warning_delete_passkey": "Cette action supprimera cette Passkey de votre YubiKey.",
|
"p_warning_delete_passkey": "Cette action supprimera cette Passkey de votre YubiKey.",
|
||||||
|
"s_search_passkeys": null,
|
||||||
|
"p_passkeys_used": null,
|
||||||
|
"@p_passkeys_used": {
|
||||||
|
"placeholders": {
|
||||||
|
"used": {},
|
||||||
|
"max": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@_fingerprints": {},
|
"@_fingerprints": {},
|
||||||
"l_fingerprint": "Empreinte: {label}",
|
"l_fingerprint": "Empreinte: {label}",
|
||||||
"@l_fingerprint": {
|
"@l_fingerprint": {
|
||||||
@ -505,6 +520,12 @@
|
|||||||
"l_unsupported_key_type": null,
|
"l_unsupported_key_type": null,
|
||||||
"l_delete_certificate": "Supprimer un certificat",
|
"l_delete_certificate": "Supprimer un certificat",
|
||||||
"l_delete_certificate_desc": "Supprimer un certificat de votre YubiKey",
|
"l_delete_certificate_desc": "Supprimer un certificat de votre YubiKey",
|
||||||
|
"l_delete_key": null,
|
||||||
|
"l_delete_key_desc": null,
|
||||||
|
"l_delete_certificate_or_key": null,
|
||||||
|
"l_delete_certificate_or_key_desc": null,
|
||||||
|
"l_move_key": null,
|
||||||
|
"l_move_key_desc": null,
|
||||||
"s_issuer": "Émetteur",
|
"s_issuer": "Émetteur",
|
||||||
"s_serial": "Série",
|
"s_serial": "Série",
|
||||||
"s_certificate_fingerprint": "Empreinte digitale",
|
"s_certificate_fingerprint": "Empreinte digitale",
|
||||||
@ -522,14 +543,53 @@
|
|||||||
},
|
},
|
||||||
"l_generating_private_key": "Génération d'une clé privée\u2026",
|
"l_generating_private_key": "Génération d'une clé privée\u2026",
|
||||||
"s_private_key_generated": "Clé privée générée",
|
"s_private_key_generated": "Clé privée générée",
|
||||||
|
"p_select_what_to_delete": null,
|
||||||
"p_warning_delete_certificate": "Attention! Cette action supprimera le certificat de votre YubiKey.",
|
"p_warning_delete_certificate": "Attention! Cette action supprimera le certificat de votre YubiKey.",
|
||||||
|
"p_warning_delete_key": null,
|
||||||
|
"p_warning_delete_certificate_and_key": null,
|
||||||
"q_delete_certificate_confirm": "Supprimer le certficat du slot PIV {slot}?",
|
"q_delete_certificate_confirm": "Supprimer le certficat du slot PIV {slot}?",
|
||||||
"@q_delete_certificate_confirm": {
|
"@q_delete_certificate_confirm": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"slot": {}
|
"slot": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"q_delete_key_confirm": null,
|
||||||
|
"@q_delete_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_delete_certificate_and_key_confirm": null,
|
||||||
|
"@q_delete_certificate_and_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"l_certificate_deleted": "Certificat supprimé",
|
"l_certificate_deleted": "Certificat supprimé",
|
||||||
|
"l_key_deleted": null,
|
||||||
|
"l_certificate_and_key_deleted": null,
|
||||||
|
"l_include_certificate": null,
|
||||||
|
"l_select_destination_slot": null,
|
||||||
|
"q_move_key_confirm": null,
|
||||||
|
"@q_move_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_move_key_to_slot_confirm": null,
|
||||||
|
"@q_move_key_to_slot_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {},
|
||||||
|
"to_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_move_key_and_certificate_to_slot_confirm": null,
|
||||||
|
"@q_move_key_and_certificate_to_slot_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {},
|
||||||
|
"to_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"p_password_protected_file": "Le fichier sélectionné est protégé par un mot de passe. Enterez le mot de passe pour continuer.",
|
"p_password_protected_file": "Le fichier sélectionné est protégé par un mot de passe. Enterez le mot de passe pour continuer.",
|
||||||
"p_import_items_desc": "Les éléments suivants seront importés dans le slot PIV {slot}.",
|
"p_import_items_desc": "Les éléments suivants seront importés dans le slot PIV {slot}.",
|
||||||
"@p_import_items_desc": {
|
"@p_import_items_desc": {
|
||||||
@ -537,6 +597,8 @@
|
|||||||
"slot": {}
|
"slot": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"l_key_moved": null,
|
||||||
|
"l_key_and_certificate_moved": null,
|
||||||
"p_subject_desc": "Un nom distinctif (DN) formaté conformément à la spécification RFC 4514.",
|
"p_subject_desc": "Un nom distinctif (DN) formaté conformément à la spécification RFC 4514.",
|
||||||
"l_rfc4514_invalid": "Format RFC 4514 invalide",
|
"l_rfc4514_invalid": "Format RFC 4514 invalide",
|
||||||
"rfc4514_examples": "Exemples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net",
|
"rfc4514_examples": "Exemples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net",
|
||||||
@ -560,6 +622,12 @@
|
|||||||
"hexid": {}
|
"hexid": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"s_retired_slot_display_name": null,
|
||||||
|
"@s_retired_slot_display_name": {
|
||||||
|
"placeholders": {
|
||||||
|
"hexid": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"s_slot_9a": "Authentification",
|
"s_slot_9a": "Authentification",
|
||||||
"s_slot_9c": "Signature digitale",
|
"s_slot_9c": "Signature digitale",
|
||||||
"s_slot_9d": "Gestion des clés",
|
"s_slot_9d": "Gestion des clés",
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"s_cancel": "キャンセル",
|
"s_cancel": "キャンセル",
|
||||||
"s_close": "閉じる",
|
"s_close": "閉じる",
|
||||||
"s_delete": "消去",
|
"s_delete": "消去",
|
||||||
|
"s_move": null,
|
||||||
"s_quit": "終了",
|
"s_quit": "終了",
|
||||||
"s_status": null,
|
"s_status": null,
|
||||||
"s_unlock": "ロック解除",
|
"s_unlock": "ロック解除",
|
||||||
@ -352,6 +353,12 @@
|
|||||||
},
|
},
|
||||||
"s_accounts": "アカウント",
|
"s_accounts": "アカウント",
|
||||||
"s_no_accounts": "アカウントがありません",
|
"s_no_accounts": "アカウントがありません",
|
||||||
|
"l_results_for": null,
|
||||||
|
"@l_results_for": {
|
||||||
|
"placeholders": {
|
||||||
|
"query": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"l_authenticator_get_started": null,
|
"l_authenticator_get_started": null,
|
||||||
"l_no_accounts_desc": null,
|
"l_no_accounts_desc": null,
|
||||||
"s_add_account": "アカウントの追加",
|
"s_add_account": "アカウントの追加",
|
||||||
@ -419,15 +426,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"s_passkeys": "パスキー",
|
"s_passkeys": "パスキー",
|
||||||
|
"s_no_passkeys": null,
|
||||||
"l_ready_to_use": "すぐに使用可能",
|
"l_ready_to_use": "すぐに使用可能",
|
||||||
"l_register_sk_on_websites": "Webサイトにセキュリティキーとして登録する",
|
"l_register_sk_on_websites": "Webサイトにセキュリティキーとして登録する",
|
||||||
"l_no_discoverable_accounts": "パスキーは保存されていません",
|
"l_no_discoverable_accounts": "パスキーは保存されていません",
|
||||||
"l_non_passkeys_note": null,
|
"p_non_passkeys_note": null,
|
||||||
"s_delete_passkey": "パスキーを削除",
|
"s_delete_passkey": "パスキーを削除",
|
||||||
"l_delete_passkey_desc": "YubiKeyからパスキーの削除",
|
"l_delete_passkey_desc": "YubiKeyからパスキーの削除",
|
||||||
"s_passkey_deleted": "パスキーが削除されました",
|
"s_passkey_deleted": "パスキーが削除されました",
|
||||||
"p_warning_delete_passkey": "これにより、YubiKeyからパスキーが削除されます",
|
"p_warning_delete_passkey": "これにより、YubiKeyからパスキーが削除されます",
|
||||||
|
"s_search_passkeys": null,
|
||||||
|
"p_passkeys_used": null,
|
||||||
|
"@p_passkeys_used": {
|
||||||
|
"placeholders": {
|
||||||
|
"used": {},
|
||||||
|
"max": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@_fingerprints": {},
|
"@_fingerprints": {},
|
||||||
"l_fingerprint": "指紋:{label}",
|
"l_fingerprint": "指紋:{label}",
|
||||||
"@l_fingerprint": {
|
"@l_fingerprint": {
|
||||||
@ -505,6 +520,12 @@
|
|||||||
"l_unsupported_key_type": null,
|
"l_unsupported_key_type": null,
|
||||||
"l_delete_certificate": "証明書を削除",
|
"l_delete_certificate": "証明書を削除",
|
||||||
"l_delete_certificate_desc": "YubiKeyか証明書の削除",
|
"l_delete_certificate_desc": "YubiKeyか証明書の削除",
|
||||||
|
"l_delete_key": null,
|
||||||
|
"l_delete_key_desc": null,
|
||||||
|
"l_delete_certificate_or_key": null,
|
||||||
|
"l_delete_certificate_or_key_desc": null,
|
||||||
|
"l_move_key": null,
|
||||||
|
"l_move_key_desc": null,
|
||||||
"s_issuer": "発行者",
|
"s_issuer": "発行者",
|
||||||
"s_serial": "シリアル番号",
|
"s_serial": "シリアル番号",
|
||||||
"s_certificate_fingerprint": "指紋",
|
"s_certificate_fingerprint": "指紋",
|
||||||
@ -522,14 +543,53 @@
|
|||||||
},
|
},
|
||||||
"l_generating_private_key": "秘密鍵を生成しています\u2026",
|
"l_generating_private_key": "秘密鍵を生成しています\u2026",
|
||||||
"s_private_key_generated": "秘密鍵を生成しました",
|
"s_private_key_generated": "秘密鍵を生成しました",
|
||||||
|
"p_select_what_to_delete": null,
|
||||||
"p_warning_delete_certificate": "警告!この操作によってYubiKeyから証明書が削除されます",
|
"p_warning_delete_certificate": "警告!この操作によってYubiKeyから証明書が削除されます",
|
||||||
|
"p_warning_delete_key": null,
|
||||||
|
"p_warning_delete_certificate_and_key": null,
|
||||||
"q_delete_certificate_confirm": "PIVスロット{slot}の証明書を削除しますか?",
|
"q_delete_certificate_confirm": "PIVスロット{slot}の証明書を削除しますか?",
|
||||||
"@q_delete_certificate_confirm": {
|
"@q_delete_certificate_confirm": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"slot": {}
|
"slot": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"q_delete_key_confirm": null,
|
||||||
|
"@q_delete_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_delete_certificate_and_key_confirm": null,
|
||||||
|
"@q_delete_certificate_and_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"l_certificate_deleted": "証明書が削除されました",
|
"l_certificate_deleted": "証明書が削除されました",
|
||||||
|
"l_key_deleted": null,
|
||||||
|
"l_certificate_and_key_deleted": null,
|
||||||
|
"l_include_certificate": null,
|
||||||
|
"l_select_destination_slot": null,
|
||||||
|
"q_move_key_confirm": null,
|
||||||
|
"@q_move_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_move_key_to_slot_confirm": null,
|
||||||
|
"@q_move_key_to_slot_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {},
|
||||||
|
"to_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_move_key_and_certificate_to_slot_confirm": null,
|
||||||
|
"@q_move_key_and_certificate_to_slot_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {},
|
||||||
|
"to_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"p_password_protected_file": "選択したファイルはパスワードで保護されています。パスワードを入力して続行します",
|
"p_password_protected_file": "選択したファイルはパスワードで保護されています。パスワードを入力して続行します",
|
||||||
"p_import_items_desc": "次のアイテムはPIVスロット{slot}にインポートされます",
|
"p_import_items_desc": "次のアイテムはPIVスロット{slot}にインポートされます",
|
||||||
"@p_import_items_desc": {
|
"@p_import_items_desc": {
|
||||||
@ -537,6 +597,8 @@
|
|||||||
"slot": {}
|
"slot": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"l_key_moved": null,
|
||||||
|
"l_key_and_certificate_moved": null,
|
||||||
"p_subject_desc": "RFC 4514フォーマットの識別名識別名 (DN)",
|
"p_subject_desc": "RFC 4514フォーマットの識別名識別名 (DN)",
|
||||||
"l_rfc4514_invalid": "無効な RFC 4514 形式です",
|
"l_rfc4514_invalid": "無効な RFC 4514 形式です",
|
||||||
"rfc4514_examples": "例:\nCN=Example Name\nCN=jsmith,DC=example,DC=net",
|
"rfc4514_examples": "例:\nCN=Example Name\nCN=jsmith,DC=example,DC=net",
|
||||||
@ -560,6 +622,12 @@
|
|||||||
"hexid": {}
|
"hexid": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"s_retired_slot_display_name": null,
|
||||||
|
"@s_retired_slot_display_name": {
|
||||||
|
"placeholders": {
|
||||||
|
"hexid": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"s_slot_9a": "認証",
|
"s_slot_9a": "認証",
|
||||||
"s_slot_9c": "デジタル署名",
|
"s_slot_9c": "デジタル署名",
|
||||||
"s_slot_9d": "鍵の管理",
|
"s_slot_9d": "鍵の管理",
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"s_cancel": "Anuluj",
|
"s_cancel": "Anuluj",
|
||||||
"s_close": "Zamknij",
|
"s_close": "Zamknij",
|
||||||
"s_delete": "Usuń",
|
"s_delete": "Usuń",
|
||||||
|
"s_move": null,
|
||||||
"s_quit": "Wyjdź",
|
"s_quit": "Wyjdź",
|
||||||
"s_status": "Status",
|
"s_status": "Status",
|
||||||
"s_unlock": "Odblokuj",
|
"s_unlock": "Odblokuj",
|
||||||
@ -352,6 +353,12 @@
|
|||||||
},
|
},
|
||||||
"s_accounts": "Konta",
|
"s_accounts": "Konta",
|
||||||
"s_no_accounts": "Brak kont",
|
"s_no_accounts": "Brak kont",
|
||||||
|
"l_results_for": null,
|
||||||
|
"@l_results_for": {
|
||||||
|
"placeholders": {
|
||||||
|
"query": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"l_authenticator_get_started": "Rozpocznij korzystanie z kont OTP",
|
"l_authenticator_get_started": "Rozpocznij korzystanie z kont OTP",
|
||||||
"l_no_accounts_desc": "Dodaj konta do swojego klucza YubiKey od dowolnego dostawcy usług obsługującego OATH TOTP/HOTP",
|
"l_no_accounts_desc": "Dodaj konta do swojego klucza YubiKey od dowolnego dostawcy usług obsługującego OATH TOTP/HOTP",
|
||||||
"s_add_account": "Dodaj konto",
|
"s_add_account": "Dodaj konto",
|
||||||
@ -419,15 +426,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"s_passkeys": "Klucze dostępu",
|
"s_passkeys": "Klucze dostępu",
|
||||||
|
"s_no_passkeys": null,
|
||||||
"l_ready_to_use": "Gotowe do użycia",
|
"l_ready_to_use": "Gotowe do użycia",
|
||||||
"l_register_sk_on_websites": "Zarejestruj jako klucz bezpieczeństwa na stronach internetowych",
|
"l_register_sk_on_websites": "Zarejestruj jako klucz bezpieczeństwa na stronach internetowych",
|
||||||
"l_no_discoverable_accounts": "Nie wykryto kont",
|
"l_no_discoverable_accounts": "Nie wykryto kont",
|
||||||
"l_non_passkeys_note": "Mogą istnieć inne dane uwierzytelniające, ale nie mogą być wyświetlane",
|
"p_non_passkeys_note": null,
|
||||||
"s_delete_passkey": "Usuń klucz dostępu",
|
"s_delete_passkey": "Usuń klucz dostępu",
|
||||||
"l_delete_passkey_desc": "Usuń klucz dostępu z klucza YubiKey",
|
"l_delete_passkey_desc": "Usuń klucz dostępu z klucza YubiKey",
|
||||||
"s_passkey_deleted": "Usunięto klucz dostępu",
|
"s_passkey_deleted": "Usunięto klucz dostępu",
|
||||||
"p_warning_delete_passkey": "Spowoduje to usunięcie klucza dostępu z klucza YubiKey.",
|
"p_warning_delete_passkey": "Spowoduje to usunięcie klucza dostępu z klucza YubiKey.",
|
||||||
|
"s_search_passkeys": null,
|
||||||
|
"p_passkeys_used": null,
|
||||||
|
"@p_passkeys_used": {
|
||||||
|
"placeholders": {
|
||||||
|
"used": {},
|
||||||
|
"max": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@_fingerprints": {},
|
"@_fingerprints": {},
|
||||||
"l_fingerprint": "Odcisk palca: {label}",
|
"l_fingerprint": "Odcisk palca: {label}",
|
||||||
"@l_fingerprint": {
|
"@l_fingerprint": {
|
||||||
@ -505,6 +520,12 @@
|
|||||||
"l_unsupported_key_type": null,
|
"l_unsupported_key_type": null,
|
||||||
"l_delete_certificate": "Usuń certyfikat",
|
"l_delete_certificate": "Usuń certyfikat",
|
||||||
"l_delete_certificate_desc": "Usuń certyfikat z klucza YubiKey",
|
"l_delete_certificate_desc": "Usuń certyfikat z klucza YubiKey",
|
||||||
|
"l_delete_key": null,
|
||||||
|
"l_delete_key_desc": null,
|
||||||
|
"l_delete_certificate_or_key": null,
|
||||||
|
"l_delete_certificate_or_key_desc": null,
|
||||||
|
"l_move_key": null,
|
||||||
|
"l_move_key_desc": null,
|
||||||
"s_issuer": "Wydawca",
|
"s_issuer": "Wydawca",
|
||||||
"s_serial": "Nr. seryjny",
|
"s_serial": "Nr. seryjny",
|
||||||
"s_certificate_fingerprint": "Odcisk palca",
|
"s_certificate_fingerprint": "Odcisk palca",
|
||||||
@ -522,14 +543,53 @@
|
|||||||
},
|
},
|
||||||
"l_generating_private_key": "Generowanie prywatnego klucza\u2026",
|
"l_generating_private_key": "Generowanie prywatnego klucza\u2026",
|
||||||
"s_private_key_generated": "Wygenerowano klucz prywatny",
|
"s_private_key_generated": "Wygenerowano klucz prywatny",
|
||||||
|
"p_select_what_to_delete": null,
|
||||||
"p_warning_delete_certificate": "Uwaga! Ta czynność spowoduje usunięcie certyfikatu z klucza YubiKey.",
|
"p_warning_delete_certificate": "Uwaga! Ta czynność spowoduje usunięcie certyfikatu z klucza YubiKey.",
|
||||||
|
"p_warning_delete_key": null,
|
||||||
|
"p_warning_delete_certificate_and_key": null,
|
||||||
"q_delete_certificate_confirm": "Usunąć certyfikat ze slotu PIV {slot}?",
|
"q_delete_certificate_confirm": "Usunąć certyfikat ze slotu PIV {slot}?",
|
||||||
"@q_delete_certificate_confirm": {
|
"@q_delete_certificate_confirm": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"slot": {}
|
"slot": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"q_delete_key_confirm": null,
|
||||||
|
"@q_delete_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_delete_certificate_and_key_confirm": null,
|
||||||
|
"@q_delete_certificate_and_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"l_certificate_deleted": "Certyfikat został usunięty",
|
"l_certificate_deleted": "Certyfikat został usunięty",
|
||||||
|
"l_key_deleted": null,
|
||||||
|
"l_certificate_and_key_deleted": null,
|
||||||
|
"l_include_certificate": null,
|
||||||
|
"l_select_destination_slot": null,
|
||||||
|
"q_move_key_confirm": null,
|
||||||
|
"@q_move_key_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_move_key_to_slot_confirm": null,
|
||||||
|
"@q_move_key_to_slot_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {},
|
||||||
|
"to_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"q_move_key_and_certificate_to_slot_confirm": null,
|
||||||
|
"@q_move_key_and_certificate_to_slot_confirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"from_slot": {},
|
||||||
|
"to_slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"p_password_protected_file": "Wybrany plik jest chroniony hasłem. Wprowadź je, aby kontynuować.",
|
"p_password_protected_file": "Wybrany plik jest chroniony hasłem. Wprowadź je, aby kontynuować.",
|
||||||
"p_import_items_desc": "Następujące elementy zostaną zaimportowane do slotu PIV {slot}.",
|
"p_import_items_desc": "Następujące elementy zostaną zaimportowane do slotu PIV {slot}.",
|
||||||
"@p_import_items_desc": {
|
"@p_import_items_desc": {
|
||||||
@ -537,6 +597,8 @@
|
|||||||
"slot": {}
|
"slot": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"l_key_moved": null,
|
||||||
|
"l_key_and_certificate_moved": null,
|
||||||
"p_subject_desc": "Nazwa wyróżniająca (DN) sformatowana zgodnie ze specyfikacją RFC 4514.",
|
"p_subject_desc": "Nazwa wyróżniająca (DN) sformatowana zgodnie ze specyfikacją RFC 4514.",
|
||||||
"l_rfc4514_invalid": "Nieprawidłowy format RFC 4514",
|
"l_rfc4514_invalid": "Nieprawidłowy format RFC 4514",
|
||||||
"rfc4514_examples": "Przykłady:\nCN=Przykładowa Nazwa\nCN=jkowalski,DC=przyklad,DC=pl",
|
"rfc4514_examples": "Przykłady:\nCN=Przykładowa Nazwa\nCN=jkowalski,DC=przyklad,DC=pl",
|
||||||
@ -560,6 +622,12 @@
|
|||||||
"hexid": {}
|
"hexid": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"s_retired_slot_display_name": null,
|
||||||
|
"@s_retired_slot_display_name": {
|
||||||
|
"placeholders": {
|
||||||
|
"hexid": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"s_slot_9a": "Uwierzytelnienie",
|
"s_slot_9a": "Uwierzytelnienie",
|
||||||
"s_slot_9c": "Cyfrowy podpis",
|
"s_slot_9c": "Cyfrowy podpis",
|
||||||
"s_slot_9d": "Menedżer kluczy",
|
"s_slot_9d": "Menedżer kluczy",
|
||||||
|
@ -20,9 +20,6 @@ const _prefix = 'oath.keys';
|
|||||||
const _keyAction = '$_prefix.actions';
|
const _keyAction = '$_prefix.actions';
|
||||||
const _accountAction = '$_prefix.account.actions';
|
const _accountAction = '$_prefix.account.actions';
|
||||||
|
|
||||||
// This is global so we can access it from the global Ctrl+F shortcut.
|
|
||||||
final searchAccountsField = GlobalKey();
|
|
||||||
|
|
||||||
// Key actions
|
// Key actions
|
||||||
const setOrManagePasswordAction =
|
const setOrManagePasswordAction =
|
||||||
Key('$_keyAction.action.set_or_manage_password');
|
Key('$_keyAction.action.set_or_manage_password');
|
||||||
|
@ -26,11 +26,12 @@ import '../app/state.dart';
|
|||||||
import '../core/state.dart';
|
import '../core/state.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
|
|
||||||
final searchProvider =
|
final accountsSearchProvider =
|
||||||
StateNotifierProvider<SearchNotifier, String>((ref) => SearchNotifier());
|
StateNotifierProvider<AccountsSearchNotifier, String>(
|
||||||
|
(ref) => AccountsSearchNotifier());
|
||||||
|
|
||||||
class SearchNotifier extends StateNotifier<String> {
|
class AccountsSearchNotifier extends StateNotifier<String> {
|
||||||
SearchNotifier() : super('');
|
AccountsSearchNotifier() : super('');
|
||||||
|
|
||||||
void setFilter(String value) {
|
void setFilter(String value) {
|
||||||
state = value;
|
state = value;
|
||||||
@ -184,7 +185,7 @@ class FavoritesNotifier extends StateNotifier<List<String>> {
|
|||||||
final filteredCredentialsProvider = StateNotifierProvider.autoDispose
|
final filteredCredentialsProvider = StateNotifierProvider.autoDispose
|
||||||
.family<FilteredCredentialsNotifier, List<OathPair>, List<OathPair>>(
|
.family<FilteredCredentialsNotifier, List<OathPair>, List<OathPair>>(
|
||||||
(ref, full) {
|
(ref, full) {
|
||||||
return FilteredCredentialsNotifier(full, ref.watch(searchProvider));
|
return FilteredCredentialsNotifier(full, ref.watch(accountsSearchProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> {
|
class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> {
|
||||||
|
@ -57,9 +57,11 @@ class AccountList extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (pinnedCreds.isNotEmpty && creds.isNotEmpty)
|
if (pinnedCreds.isNotEmpty && creds.isNotEmpty)
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Divider(),
|
child: Divider(
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
...creds.map(
|
...creds.map(
|
||||||
(entry) => AccountView(
|
(entry) => AccountView(
|
||||||
|
@ -30,6 +30,7 @@ import '../../app/state.dart';
|
|||||||
import '../../app/views/action_list.dart';
|
import '../../app/views/action_list.dart';
|
||||||
import '../../app/views/app_failure_page.dart';
|
import '../../app/views/app_failure_page.dart';
|
||||||
import '../../app/views/app_page.dart';
|
import '../../app/views/app_page.dart';
|
||||||
|
import '../../app/views/keys.dart';
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
import '../../app/views/message_page_not_initialized.dart';
|
import '../../app/views/message_page_not_initialized.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
@ -121,7 +122,8 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
searchFocus = FocusNode();
|
searchFocus = FocusNode();
|
||||||
searchController = TextEditingController(text: ref.read(searchProvider));
|
searchController =
|
||||||
|
TextEditingController(text: ref.read(accountsSearchProvider));
|
||||||
searchFocus.addListener(_onFocusChange);
|
searchFocus.addListener(_onFocusChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,6 +146,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
|||||||
.select((value) => value?.length));
|
.select((value) => value?.length));
|
||||||
final hasFeature = ref.watch(featureProvider);
|
final hasFeature = ref.watch(featureProvider);
|
||||||
final hasActions = hasFeature(features.actions);
|
final hasActions = hasFeature(features.actions);
|
||||||
|
final searchText = searchController.text;
|
||||||
|
|
||||||
Future<void> onFileDropped(File file) async {
|
Future<void> onFileDropped(File file) async {
|
||||||
final qrScanner = ref.read(qrScannerProvider);
|
final qrScanner = ref.read(qrScannerProvider);
|
||||||
@ -210,7 +213,8 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
|||||||
SearchIntent: CallbackAction<SearchIntent>(onInvoke: (_) {
|
SearchIntent: CallbackAction<SearchIntent>(onInvoke: (_) {
|
||||||
searchController.selection = TextSelection(
|
searchController.selection = TextSelection(
|
||||||
baseOffset: 0, extentOffset: searchController.text.length);
|
baseOffset: 0, extentOffset: searchController.text.length);
|
||||||
searchFocus.requestFocus();
|
searchFocus.unfocus();
|
||||||
|
Timer.run(() => searchFocus.requestFocus());
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
EscapeIntent: CallbackAction<EscapeIntent>(onInvoke: (intent) {
|
EscapeIntent: CallbackAction<EscapeIntent>(onInvoke: (intent) {
|
||||||
@ -261,6 +265,8 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
|||||||
},
|
},
|
||||||
builder: (context) => AppPage(
|
builder: (context) => AppPage(
|
||||||
title: l10n.s_accounts,
|
title: l10n.s_accounts,
|
||||||
|
alternativeTitle:
|
||||||
|
searchText != '' ? l10n.l_results_for(searchText) : null,
|
||||||
capabilities: const [Capability.oath],
|
capabilities: const [Capability.oath],
|
||||||
keyActionsBuilder: hasActions
|
keyActionsBuilder: hasActions
|
||||||
? (context) => oathBuildActions(
|
? (context) => oathBuildActions(
|
||||||
@ -350,6 +356,79 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
headerSliver: Focus(
|
||||||
|
canRequestFocus: false,
|
||||||
|
onKeyEvent: (node, event) {
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||||
|
node.focusInDirection(TraversalDirection.down);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
|
searchController.clear();
|
||||||
|
ref.read(accountsSearchProvider.notifier).setFilter('');
|
||||||
|
node.unfocus();
|
||||||
|
setState(() {});
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
},
|
||||||
|
child: Builder(builder: (context) {
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
return Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: AppTextFormField(
|
||||||
|
key: searchField,
|
||||||
|
controller: searchController,
|
||||||
|
focusNode: searchFocus,
|
||||||
|
// Use the default style, but with a smaller font size:
|
||||||
|
style: textTheme.titleMedium
|
||||||
|
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
|
||||||
|
decoration: AppInputDecoration(
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(48),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
width: 0,
|
||||||
|
style: searchFocus.hasFocus
|
||||||
|
? BorderStyle.solid
|
||||||
|
: BorderStyle.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.all(16),
|
||||||
|
fillColor: Theme.of(context).hoverColor,
|
||||||
|
filled: true,
|
||||||
|
hintText: l10n.s_search_accounts,
|
||||||
|
isDense: true,
|
||||||
|
prefixIcon: const Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(start: 8.0),
|
||||||
|
child: Icon(Icons.search_outlined),
|
||||||
|
),
|
||||||
|
suffixIcon: searchController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
iconSize: 16,
|
||||||
|
onPressed: () {
|
||||||
|
searchController.clear();
|
||||||
|
ref
|
||||||
|
.read(accountsSearchProvider.notifier)
|
||||||
|
.setFilter('');
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
ref.read(accountsSearchProvider.notifier).setFilter(value);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
onFieldSubmitted: (value) {
|
||||||
|
Focus.of(context).focusInDirection(TraversalDirection.down);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
builder: (context, expanded) {
|
builder: (context, expanded) {
|
||||||
// De-select if window is resized to be non-expanded.
|
// De-select if window is resized to be non-expanded.
|
||||||
if (!expanded && _selected != null) {
|
if (!expanded && _selected != null) {
|
||||||
@ -373,80 +452,6 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
|||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Focus(
|
|
||||||
canRequestFocus: false,
|
|
||||||
onKeyEvent: (node, event) {
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
|
||||||
node.focusInDirection(TraversalDirection.down);
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
|
||||||
searchController.clear();
|
|
||||||
ref.read(searchProvider.notifier).setFilter('');
|
|
||||||
node.unfocus();
|
|
||||||
setState(() {});
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
},
|
|
||||||
child: Builder(builder: (context) {
|
|
||||||
final textTheme = Theme.of(context).textTheme;
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.0, vertical: 8.0),
|
|
||||||
child: AppTextFormField(
|
|
||||||
key: keys.searchAccountsField,
|
|
||||||
controller: searchController,
|
|
||||||
focusNode: searchFocus,
|
|
||||||
// Use the default style, but with a smaller font size:
|
|
||||||
style: textTheme.titleMedium?.copyWith(
|
|
||||||
fontSize: textTheme.titleSmall?.fontSize),
|
|
||||||
decoration: AppInputDecoration(
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(32),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
width: 0,
|
|
||||||
style: searchFocus.hasFocus
|
|
||||||
? BorderStyle.solid
|
|
||||||
: BorderStyle.none,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.all(16),
|
|
||||||
fillColor: Theme.of(context).hoverColor,
|
|
||||||
filled: true,
|
|
||||||
hintText: l10n.s_search_accounts,
|
|
||||||
isDense: true,
|
|
||||||
prefixIcon: const Padding(
|
|
||||||
padding: EdgeInsetsDirectional.only(start: 8.0),
|
|
||||||
child: Icon(Symbols.search),
|
|
||||||
),
|
|
||||||
suffixIcon: searchController.text.isNotEmpty
|
|
||||||
? IconButton(
|
|
||||||
icon: const Icon(Symbols.clear),
|
|
||||||
iconSize: 16,
|
|
||||||
onPressed: () {
|
|
||||||
searchController.clear();
|
|
||||||
ref
|
|
||||||
.read(searchProvider.notifier)
|
|
||||||
.setFilter('');
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
ref.read(searchProvider.notifier).setFilter(value);
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
textInputAction: TextInputAction.next,
|
|
||||||
onFieldSubmitted: (value) {
|
|
||||||
Focus.of(context)
|
|
||||||
.focusInDirection(TraversalDirection.down);
|
|
||||||
},
|
|
||||||
).init(),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
return AccountList(
|
return AccountList(
|
||||||
|
@ -29,3 +29,4 @@ final slotsGenerate = slots.feature('generate');
|
|||||||
final slotsImport = slots.feature('import');
|
final slotsImport = slots.feature('import');
|
||||||
final slotsExport = slots.feature('export');
|
final slotsExport = slots.feature('export');
|
||||||
final slotsDelete = slots.feature('delete');
|
final slotsDelete = slots.feature('delete');
|
||||||
|
final slotsMove = slots.feature('move');
|
||||||
|
@ -32,6 +32,7 @@ const generateAction = Key('$_slotAction.generate');
|
|||||||
const importAction = Key('$_slotAction.import');
|
const importAction = Key('$_slotAction.import');
|
||||||
const exportAction = Key('$_slotAction.export');
|
const exportAction = Key('$_slotAction.export');
|
||||||
const deleteAction = Key('$_slotAction.delete');
|
const deleteAction = Key('$_slotAction.delete');
|
||||||
|
const moveAction = Key('$_slotAction.move');
|
||||||
|
|
||||||
const saveButton = Key('$_prefix.save');
|
const saveButton = Key('$_prefix.save');
|
||||||
const deleteButton = Key('$_prefix.delete');
|
const deleteButton = Key('$_prefix.delete');
|
||||||
@ -50,11 +51,51 @@ const meatballButton9a = Key('$_prefix.9a.meatball.button');
|
|||||||
const meatballButton9c = Key('$_prefix.9c.meatball.button');
|
const meatballButton9c = Key('$_prefix.9c.meatball.button');
|
||||||
const meatballButton9d = Key('$_prefix.9d.meatball.button');
|
const meatballButton9d = Key('$_prefix.9d.meatball.button');
|
||||||
const meatballButton9e = Key('$_prefix.9e.meatball.button');
|
const meatballButton9e = Key('$_prefix.9e.meatball.button');
|
||||||
|
const meatballButton82 = Key('$_prefix.82.meatball.button');
|
||||||
|
const meatballButton83 = Key('$_prefix.83.meatball.button');
|
||||||
|
const meatballButton84 = Key('$_prefix.84.meatball.button');
|
||||||
|
const meatballButton85 = Key('$_prefix.85.meatball.button');
|
||||||
|
const meatballButton86 = Key('$_prefix.86.meatball.button');
|
||||||
|
const meatballButton87 = Key('$_prefix.87.meatball.button');
|
||||||
|
const meatballButton88 = Key('$_prefix.88.meatball.button');
|
||||||
|
const meatballButton89 = Key('$_prefix.89.meatball.button');
|
||||||
|
const meatballButton8a = Key('$_prefix.8a.meatball.button');
|
||||||
|
const meatballButton8b = Key('$_prefix.8b.meatball.button');
|
||||||
|
const meatballButton8c = Key('$_prefix.8c.meatball.button');
|
||||||
|
const meatballButton8d = Key('$_prefix.8d.meatball.button');
|
||||||
|
const meatballButton8e = Key('$_prefix.8e.meatball.button');
|
||||||
|
const meatballButton8f = Key('$_prefix.8f.meatball.button');
|
||||||
|
const meatballButton90 = Key('$_prefix.90.meatball.button');
|
||||||
|
const meatballButton91 = Key('$_prefix.91.meatball.button');
|
||||||
|
const meatballButton92 = Key('$_prefix.92.meatball.button');
|
||||||
|
const meatballButton93 = Key('$_prefix.93.meatball.button');
|
||||||
|
const meatballButton94 = Key('$_prefix.94.meatball.button');
|
||||||
|
const meatballButton95 = Key('$_prefix.95.meatball.button');
|
||||||
|
|
||||||
const appListItem9a = Key('$_prefix.9a.applistitem');
|
const appListItem9a = Key('$_prefix.9a.applistitem');
|
||||||
const appListItem9c = Key('$_prefix.9c.applistitem');
|
const appListItem9c = Key('$_prefix.9c.applistitem');
|
||||||
const appListItem9d = Key('$_prefix.9d.applistitem');
|
const appListItem9d = Key('$_prefix.9d.applistitem');
|
||||||
const appListItem9e = Key('$_prefix.9e.applistitem');
|
const appListItem9e = Key('$_prefix.9e.applistitem');
|
||||||
|
const appListItem82 = Key('$_prefix.82.applistitem');
|
||||||
|
const appListItem83 = Key('$_prefix.83.applistitem');
|
||||||
|
const appListItem84 = Key('$_prefix.84.applistitem');
|
||||||
|
const appListItem85 = Key('$_prefix.85.applistitem');
|
||||||
|
const appListItem86 = Key('$_prefix.86.applistitem');
|
||||||
|
const appListItem87 = Key('$_prefix.87.applistitem');
|
||||||
|
const appListItem88 = Key('$_prefix.88.applistitem');
|
||||||
|
const appListItem89 = Key('$_prefix.89.applistitem');
|
||||||
|
const appListItem8a = Key('$_prefix.8a.applistitem');
|
||||||
|
const appListItem8b = Key('$_prefix.8b.applistitem');
|
||||||
|
const appListItem8c = Key('$_prefix.8c.applistitem');
|
||||||
|
const appListItem8d = Key('$_prefix.8d.applistitem');
|
||||||
|
const appListItem8e = Key('$_prefix.8e.applistitem');
|
||||||
|
const appListItem8f = Key('$_prefix.8f.applistitem');
|
||||||
|
const appListItem90 = Key('$_prefix.90.applistitem');
|
||||||
|
const appListItem91 = Key('$_prefix.91.applistitem');
|
||||||
|
const appListItem92 = Key('$_prefix.92.applistitem');
|
||||||
|
const appListItem93 = Key('$_prefix.93.applistitem');
|
||||||
|
const appListItem94 = Key('$_prefix.94.applistitem');
|
||||||
|
const appListItem95 = Key('$_prefix.95.applistitem');
|
||||||
|
|
||||||
// SlotMetadata body keys
|
// SlotMetadata body keys
|
||||||
const slotMetadataKeyType = Key('$_prefix.slotMetadata.keyType');
|
const slotMetadataKeyType = Key('$_prefix.slotMetadata.keyType');
|
||||||
|
@ -47,10 +47,31 @@ enum SlotId {
|
|||||||
authentication(0x9a),
|
authentication(0x9a),
|
||||||
signature(0x9c),
|
signature(0x9c),
|
||||||
keyManagement(0x9d),
|
keyManagement(0x9d),
|
||||||
cardAuth(0x9e);
|
cardAuth(0x9e),
|
||||||
|
retired1(0x82, true),
|
||||||
|
retired2(0x83, true),
|
||||||
|
retired3(0x84, true),
|
||||||
|
retired4(0x85, true),
|
||||||
|
retired5(0x86, true),
|
||||||
|
retired6(0x87, true),
|
||||||
|
retired7(0x88, true),
|
||||||
|
retired8(0x89, true),
|
||||||
|
retired9(0x8a, true),
|
||||||
|
retired10(0x8b, true),
|
||||||
|
retired11(0x8c, true),
|
||||||
|
retired12(0x8d, true),
|
||||||
|
retired13(0x8e, true),
|
||||||
|
retired14(0x8f, true),
|
||||||
|
retired15(0x90, true),
|
||||||
|
retired16(0x91, true),
|
||||||
|
retired17(0x92, true),
|
||||||
|
retired18(0x93, true),
|
||||||
|
retired19(0x94, true),
|
||||||
|
retired20(0x95, true);
|
||||||
|
|
||||||
final int id;
|
final int id;
|
||||||
const SlotId(this.id);
|
final bool isRetired;
|
||||||
|
const SlotId(this.id, [this.isRetired = false]);
|
||||||
|
|
||||||
String get hexId => id.toRadixString(16).padLeft(2, '0');
|
String get hexId => id.toRadixString(16).padLeft(2, '0');
|
||||||
|
|
||||||
@ -61,6 +82,7 @@ enum SlotId {
|
|||||||
SlotId.signature => nameFor(l10n.s_slot_9c),
|
SlotId.signature => nameFor(l10n.s_slot_9c),
|
||||||
SlotId.keyManagement => nameFor(l10n.s_slot_9d),
|
SlotId.keyManagement => nameFor(l10n.s_slot_9d),
|
||||||
SlotId.cardAuth => nameFor(l10n.s_slot_9e),
|
SlotId.cardAuth => nameFor(l10n.s_slot_9e),
|
||||||
|
_ => l10n.s_retired_slot_display_name(hexId)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,8 +208,8 @@ class PinMetadata with _$PinMetadata {
|
|||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class PinVerificationStatus with _$PinVerificationStatus {
|
class PinVerificationStatus with _$PinVerificationStatus {
|
||||||
const factory PinVerificationStatus.success() = _PinSuccess;
|
const factory PinVerificationStatus.success() = PinSuccess;
|
||||||
factory PinVerificationStatus.failure(int attemptsRemaining) = _PinFailure;
|
factory PinVerificationStatus.failure(int attemptsRemaining) = PinFailure;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
@ -211,20 +211,20 @@ mixin _$PinVerificationStatus {
|
|||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult map<TResult extends Object?>({
|
TResult map<TResult extends Object?>({
|
||||||
required TResult Function(_PinSuccess value) success,
|
required TResult Function(PinSuccess value) success,
|
||||||
required TResult Function(_PinFailure value) failure,
|
required TResult Function(PinFailure value) failure,
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? mapOrNull<TResult extends Object?>({
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
TResult? Function(_PinSuccess value)? success,
|
TResult? Function(PinSuccess value)? success,
|
||||||
TResult? Function(_PinFailure value)? failure,
|
TResult? Function(PinFailure value)? failure,
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeMap<TResult extends Object?>({
|
TResult maybeMap<TResult extends Object?>({
|
||||||
TResult Function(_PinSuccess value)? success,
|
TResult Function(PinSuccess value)? success,
|
||||||
TResult Function(_PinFailure value)? failure,
|
TResult Function(PinFailure value)? failure,
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@ -267,7 +267,7 @@ class __$$PinSuccessImplCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
||||||
class _$PinSuccessImpl implements _PinSuccess {
|
class _$PinSuccessImpl implements PinSuccess {
|
||||||
const _$PinSuccessImpl();
|
const _$PinSuccessImpl();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -318,8 +318,8 @@ class _$PinSuccessImpl implements _PinSuccess {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult map<TResult extends Object?>({
|
TResult map<TResult extends Object?>({
|
||||||
required TResult Function(_PinSuccess value) success,
|
required TResult Function(PinSuccess value) success,
|
||||||
required TResult Function(_PinFailure value) failure,
|
required TResult Function(PinFailure value) failure,
|
||||||
}) {
|
}) {
|
||||||
return success(this);
|
return success(this);
|
||||||
}
|
}
|
||||||
@ -327,8 +327,8 @@ class _$PinSuccessImpl implements _PinSuccess {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? mapOrNull<TResult extends Object?>({
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
TResult? Function(_PinSuccess value)? success,
|
TResult? Function(PinSuccess value)? success,
|
||||||
TResult? Function(_PinFailure value)? failure,
|
TResult? Function(PinFailure value)? failure,
|
||||||
}) {
|
}) {
|
||||||
return success?.call(this);
|
return success?.call(this);
|
||||||
}
|
}
|
||||||
@ -336,8 +336,8 @@ class _$PinSuccessImpl implements _PinSuccess {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeMap<TResult extends Object?>({
|
TResult maybeMap<TResult extends Object?>({
|
||||||
TResult Function(_PinSuccess value)? success,
|
TResult Function(PinSuccess value)? success,
|
||||||
TResult Function(_PinFailure value)? failure,
|
TResult Function(PinFailure value)? failure,
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) {
|
}) {
|
||||||
if (success != null) {
|
if (success != null) {
|
||||||
@ -347,8 +347,8 @@ class _$PinSuccessImpl implements _PinSuccess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _PinSuccess implements PinVerificationStatus {
|
abstract class PinSuccess implements PinVerificationStatus {
|
||||||
const factory _PinSuccess() = _$PinSuccessImpl;
|
const factory PinSuccess() = _$PinSuccessImpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -384,7 +384,7 @@ class __$$PinFailureImplCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
||||||
class _$PinFailureImpl implements _PinFailure {
|
class _$PinFailureImpl implements PinFailure {
|
||||||
_$PinFailureImpl(this.attemptsRemaining);
|
_$PinFailureImpl(this.attemptsRemaining);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -447,8 +447,8 @@ class _$PinFailureImpl implements _PinFailure {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult map<TResult extends Object?>({
|
TResult map<TResult extends Object?>({
|
||||||
required TResult Function(_PinSuccess value) success,
|
required TResult Function(PinSuccess value) success,
|
||||||
required TResult Function(_PinFailure value) failure,
|
required TResult Function(PinFailure value) failure,
|
||||||
}) {
|
}) {
|
||||||
return failure(this);
|
return failure(this);
|
||||||
}
|
}
|
||||||
@ -456,8 +456,8 @@ class _$PinFailureImpl implements _PinFailure {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? mapOrNull<TResult extends Object?>({
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
TResult? Function(_PinSuccess value)? success,
|
TResult? Function(PinSuccess value)? success,
|
||||||
TResult? Function(_PinFailure value)? failure,
|
TResult? Function(PinFailure value)? failure,
|
||||||
}) {
|
}) {
|
||||||
return failure?.call(this);
|
return failure?.call(this);
|
||||||
}
|
}
|
||||||
@ -465,8 +465,8 @@ class _$PinFailureImpl implements _PinFailure {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeMap<TResult extends Object?>({
|
TResult maybeMap<TResult extends Object?>({
|
||||||
TResult Function(_PinSuccess value)? success,
|
TResult Function(PinSuccess value)? success,
|
||||||
TResult Function(_PinFailure value)? failure,
|
TResult Function(PinFailure value)? failure,
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) {
|
}) {
|
||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
@ -476,8 +476,8 @@ class _$PinFailureImpl implements _PinFailure {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _PinFailure implements PinVerificationStatus {
|
abstract class PinFailure implements PinVerificationStatus {
|
||||||
factory _PinFailure(final int attemptsRemaining) = _$PinFailureImpl;
|
factory PinFailure(final int attemptsRemaining) = _$PinFailureImpl;
|
||||||
|
|
||||||
int get attemptsRemaining;
|
int get attemptsRemaining;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
|
@ -20,6 +20,18 @@ import '../app/models.dart';
|
|||||||
import '../core/state.dart';
|
import '../core/state.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
|
|
||||||
|
final passkeysSearchProvider =
|
||||||
|
StateNotifierProvider<PasskeysSearchNotifier, String>(
|
||||||
|
(ref) => PasskeysSearchNotifier());
|
||||||
|
|
||||||
|
class PasskeysSearchNotifier extends StateNotifier<String> {
|
||||||
|
PasskeysSearchNotifier() : super('');
|
||||||
|
|
||||||
|
void setFilter(String value) {
|
||||||
|
state = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final pivStateProvider = AsyncNotifierProvider.autoDispose
|
final pivStateProvider = AsyncNotifierProvider.autoDispose
|
||||||
.family<PivStateNotifier, PivState, DevicePath>(
|
.family<PivStateNotifier, PivState, DevicePath>(
|
||||||
() => throw UnimplementedError(),
|
() => throw UnimplementedError(),
|
||||||
@ -66,5 +78,7 @@ abstract class PivSlotsNotifier
|
|||||||
PinPolicy pinPolicy = PinPolicy.dfault,
|
PinPolicy pinPolicy = PinPolicy.dfault,
|
||||||
TouchPolicy touchPolicy = TouchPolicy.dfault,
|
TouchPolicy touchPolicy = TouchPolicy.dfault,
|
||||||
});
|
});
|
||||||
Future<void> delete(SlotId slot);
|
Future<void> delete(SlotId slot, bool deleteCert, bool deleteKey);
|
||||||
|
Future<void> moveKey(SlotId source, SlotId destination, bool overwriteKey,
|
||||||
|
bool includeCertificate);
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ import 'authentication_dialog.dart';
|
|||||||
import 'delete_certificate_dialog.dart';
|
import 'delete_certificate_dialog.dart';
|
||||||
import 'generate_key_dialog.dart';
|
import 'generate_key_dialog.dart';
|
||||||
import 'import_file_dialog.dart';
|
import 'import_file_dialog.dart';
|
||||||
|
import 'move_key_dialog.dart';
|
||||||
import 'pin_dialog.dart';
|
import 'pin_dialog.dart';
|
||||||
|
|
||||||
class GenerateIntent extends Intent {
|
class GenerateIntent extends Intent {
|
||||||
@ -52,15 +53,19 @@ class ExportIntent extends Intent {
|
|||||||
const ExportIntent(this.slot);
|
const ExportIntent(this.slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MoveIntent extends Intent {
|
||||||
|
final PivSlot slot;
|
||||||
|
const MoveIntent(this.slot);
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> _authIfNeeded(BuildContext context, WidgetRef ref,
|
Future<bool> _authIfNeeded(BuildContext context, WidgetRef ref,
|
||||||
DevicePath devicePath, PivState pivState) async {
|
DevicePath devicePath, PivState pivState) async {
|
||||||
if (pivState.needsAuth) {
|
if (pivState.needsAuth) {
|
||||||
if (pivState.protectedKey &&
|
if (pivState.protectedKey &&
|
||||||
pivState.metadata?.pinMetadata.defaultValue == true) {
|
pivState.metadata?.pinMetadata.defaultValue == true) {
|
||||||
final status = await ref
|
return await ref
|
||||||
.read(pivStateProvider(devicePath).notifier)
|
.read(pivStateProvider(devicePath).notifier)
|
||||||
.verifyPin(defaultPin);
|
.verifyPin(defaultPin) is PinSuccess;
|
||||||
return status.when(success: () => true, failure: (_) => false);
|
|
||||||
}
|
}
|
||||||
return await showBlurDialog(
|
return await showBlurDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -108,11 +113,9 @@ class PivActions extends ConsumerWidget {
|
|||||||
if (!pivState.protectedKey) {
|
if (!pivState.protectedKey) {
|
||||||
bool verified;
|
bool verified;
|
||||||
if (pivState.metadata?.pinMetadata.defaultValue == true) {
|
if (pivState.metadata?.pinMetadata.defaultValue == true) {
|
||||||
final status = await ref
|
verified = await ref
|
||||||
.read(pivStateProvider(devicePath).notifier)
|
.read(pivStateProvider(devicePath).notifier)
|
||||||
.verifyPin(defaultPin);
|
.verifyPin(defaultPin) is PinSuccess;
|
||||||
verified =
|
|
||||||
status.when(success: () => true, failure: (_) => false);
|
|
||||||
} else {
|
} else {
|
||||||
verified = await withContext((context) async =>
|
verified = await withContext((context) async =>
|
||||||
await showBlurDialog(
|
await showBlurDialog(
|
||||||
@ -266,12 +269,32 @@ class PivActions extends ConsumerWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) => DeleteCertificateDialog(
|
builder: (context) => DeleteCertificateDialog(
|
||||||
devicePath,
|
devicePath,
|
||||||
|
pivState,
|
||||||
intent.target,
|
intent.target,
|
||||||
),
|
),
|
||||||
) ??
|
) ??
|
||||||
false);
|
false);
|
||||||
return deleted;
|
return deleted;
|
||||||
}),
|
}),
|
||||||
|
if (hasFeature(features.slotsMove))
|
||||||
|
MoveIntent: CallbackAction<MoveIntent>(onInvoke: (intent) async {
|
||||||
|
if (!await withContext((context) =>
|
||||||
|
_authIfNeeded(context, ref, devicePath, pivState))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool? moved = await withContext((context) async =>
|
||||||
|
await showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => MoveKeyDialog(
|
||||||
|
devicePath,
|
||||||
|
pivState,
|
||||||
|
intent.slot,
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
false);
|
||||||
|
return moved;
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
child: Builder(
|
child: Builder(
|
||||||
// Builder to ensure new scope for actions, they can invoke parent actions
|
// Builder to ensure new scope for actions, they can invoke parent actions
|
||||||
@ -286,10 +309,13 @@ class PivActions extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ActionItem> buildSlotActions(PivSlot slot, AppLocalizations l10n) {
|
List<ActionItem> buildSlotActions(
|
||||||
|
PivState pivState, PivSlot slot, AppLocalizations l10n) {
|
||||||
final hasCert = slot.certInfo != null;
|
final hasCert = slot.certInfo != null;
|
||||||
final hasKey = slot.metadata != null;
|
final hasKey = slot.metadata != null;
|
||||||
|
final canDeleteOrMoveKey = hasKey && pivState.version.isAtLeast(5, 7);
|
||||||
return [
|
return [
|
||||||
|
if (!slot.slot.isRetired) ...[
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.generateAction,
|
key: keys.generateAction,
|
||||||
feature: features.slotsGenerate,
|
feature: features.slotsGenerate,
|
||||||
@ -307,6 +333,7 @@ List<ActionItem> buildSlotActions(PivSlot slot, AppLocalizations l10n) {
|
|||||||
subtitle: l10n.l_import_desc,
|
subtitle: l10n.l_import_desc,
|
||||||
intent: ImportIntent(slot),
|
intent: ImportIntent(slot),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
if (hasCert) ...[
|
if (hasCert) ...[
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.exportAction,
|
key: keys.exportAction,
|
||||||
@ -316,15 +343,6 @@ List<ActionItem> buildSlotActions(PivSlot slot, AppLocalizations l10n) {
|
|||||||
subtitle: l10n.l_export_certificate_desc,
|
subtitle: l10n.l_export_certificate_desc,
|
||||||
intent: ExportIntent(slot),
|
intent: ExportIntent(slot),
|
||||||
),
|
),
|
||||||
ActionItem(
|
|
||||||
key: keys.deleteAction,
|
|
||||||
feature: features.slotsDelete,
|
|
||||||
actionStyle: ActionStyle.error,
|
|
||||||
icon: const Icon(Symbols.delete),
|
|
||||||
title: l10n.l_delete_certificate,
|
|
||||||
subtitle: l10n.l_delete_certificate_desc,
|
|
||||||
intent: DeleteIntent(slot),
|
|
||||||
),
|
|
||||||
] else if (hasKey) ...[
|
] else if (hasKey) ...[
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.exportAction,
|
key: keys.exportAction,
|
||||||
@ -335,5 +353,33 @@ List<ActionItem> buildSlotActions(PivSlot slot, AppLocalizations l10n) {
|
|||||||
intent: ExportIntent(slot),
|
intent: ExportIntent(slot),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
if (canDeleteOrMoveKey)
|
||||||
|
ActionItem(
|
||||||
|
key: keys.moveAction,
|
||||||
|
feature: features.slotsMove,
|
||||||
|
actionStyle: ActionStyle.error,
|
||||||
|
icon: const Icon(Symbols.move_item),
|
||||||
|
title: l10n.l_move_key,
|
||||||
|
subtitle: l10n.l_move_key_desc,
|
||||||
|
intent: MoveIntent(slot),
|
||||||
|
),
|
||||||
|
if (hasCert || canDeleteOrMoveKey)
|
||||||
|
ActionItem(
|
||||||
|
key: keys.deleteAction,
|
||||||
|
feature: features.slotsDelete,
|
||||||
|
actionStyle: ActionStyle.error,
|
||||||
|
icon: const Icon(Symbols.delete),
|
||||||
|
title: hasCert && canDeleteOrMoveKey
|
||||||
|
? l10n.l_delete_certificate_or_key
|
||||||
|
: hasCert
|
||||||
|
? l10n.l_delete_certificate
|
||||||
|
: l10n.l_delete_key,
|
||||||
|
subtitle: hasCert && canDeleteOrMoveKey
|
||||||
|
? l10n.l_delete_certificate_or_key_desc
|
||||||
|
: hasCert
|
||||||
|
? l10n.l_delete_certificate_desc
|
||||||
|
: l10n.l_delete_key_desc,
|
||||||
|
intent: DeleteIntent(slot),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -27,34 +27,75 @@ import '../keys.dart' as keys;
|
|||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
|
|
||||||
class DeleteCertificateDialog extends ConsumerWidget {
|
class DeleteCertificateDialog extends ConsumerStatefulWidget {
|
||||||
final DevicePath devicePath;
|
final DevicePath devicePath;
|
||||||
|
final PivState pivState;
|
||||||
final PivSlot pivSlot;
|
final PivSlot pivSlot;
|
||||||
const DeleteCertificateDialog(this.devicePath, this.pivSlot, {super.key});
|
const DeleteCertificateDialog(this.devicePath, this.pivState, this.pivSlot,
|
||||||
|
{super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||||
|
_DeleteCertificateDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeleteCertificateDialogState
|
||||||
|
extends ConsumerState<DeleteCertificateDialog> {
|
||||||
|
late bool _deleteCertificate;
|
||||||
|
late bool _deleteKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_deleteCertificate = widget.pivSlot.certInfo != null;
|
||||||
|
_deleteKey = widget.pivSlot.metadata != null &&
|
||||||
|
widget.pivState.version.isAtLeast(5, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final canDeleteCertificate = widget.pivSlot.certInfo != null;
|
||||||
|
final canDeleteKey = widget.pivSlot.metadata != null &&
|
||||||
|
widget.pivState.version.isAtLeast(5, 7);
|
||||||
|
|
||||||
return ResponsiveDialog(
|
return ResponsiveDialog(
|
||||||
title: Text(l10n.l_delete_certificate),
|
title: Text(canDeleteKey && canDeleteCertificate
|
||||||
|
? l10n.l_delete_certificate_or_key
|
||||||
|
: canDeleteCertificate
|
||||||
|
? l10n.l_delete_certificate
|
||||||
|
: l10n.l_delete_key),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
key: keys.deleteButton,
|
key: keys.deleteButton,
|
||||||
onPressed: () async {
|
onPressed: _deleteKey || _deleteCertificate
|
||||||
|
? () async {
|
||||||
try {
|
try {
|
||||||
await ref
|
await ref
|
||||||
.read(pivSlotsProvider(devicePath).notifier)
|
.read(pivSlotsProvider(widget.devicePath).notifier)
|
||||||
.delete(pivSlot.slot);
|
.delete(widget.pivSlot.slot, _deleteCertificate,
|
||||||
|
_deleteKey);
|
||||||
|
|
||||||
await ref.read(withContextProvider)(
|
await ref.read(withContextProvider)(
|
||||||
(context) async {
|
(context) async {
|
||||||
|
String message;
|
||||||
|
if (_deleteCertificate && _deleteKey) {
|
||||||
|
message = l10n.l_certificate_and_key_deleted;
|
||||||
|
} else if (_deleteCertificate) {
|
||||||
|
message = l10n.l_certificate_deleted;
|
||||||
|
} else {
|
||||||
|
message = l10n.l_key_deleted;
|
||||||
|
}
|
||||||
|
|
||||||
Navigator.of(context).pop(true);
|
Navigator.of(context).pop(true);
|
||||||
showMessage(context, l10n.l_certificate_deleted);
|
showMessage(context, message);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} on CancellationException catch (_) {
|
} on CancellationException catch (_) {
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
child: Text(l10n.s_delete),
|
child: Text(l10n.s_delete),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -63,9 +104,55 @@ class DeleteCertificateDialog extends ConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(l10n.p_warning_delete_certificate),
|
if (_deleteCertificate || _deleteKey) ...[
|
||||||
Text(l10n.q_delete_certificate_confirm(
|
Text(
|
||||||
pivSlot.slot.getDisplayName(l10n))),
|
_deleteCertificate && _deleteKey
|
||||||
|
? l10n.p_warning_delete_certificate_and_key
|
||||||
|
: _deleteCertificate
|
||||||
|
? l10n.p_warning_delete_certificate
|
||||||
|
: l10n.p_warning_delete_key,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.copyWith(fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
Text(_deleteCertificate && _deleteKey
|
||||||
|
? l10n.q_delete_certificate_and_key_confirm(
|
||||||
|
widget.pivSlot.slot.getDisplayName(l10n))
|
||||||
|
: _deleteCertificate
|
||||||
|
? l10n.q_delete_certificate_confirm(
|
||||||
|
widget.pivSlot.slot.getDisplayName(l10n))
|
||||||
|
: l10n.q_delete_key_confirm(
|
||||||
|
widget.pivSlot.slot.getDisplayName(l10n)))
|
||||||
|
],
|
||||||
|
if (!_deleteCertificate && !_deleteKey)
|
||||||
|
Text(l10n.p_select_what_to_delete),
|
||||||
|
if (canDeleteKey && canDeleteCertificate)
|
||||||
|
Wrap(
|
||||||
|
spacing: 4.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: [
|
||||||
|
if (canDeleteCertificate)
|
||||||
|
FilterChip(
|
||||||
|
label: Text(l10n.s_certificate),
|
||||||
|
selected: _deleteCertificate,
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
_deleteCertificate = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (canDeleteKey)
|
||||||
|
FilterChip(
|
||||||
|
label: Text(l10n.s_private_key),
|
||||||
|
selected: _deleteKey,
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
_deleteKey = value;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
.map((e) => Padding(
|
.map((e) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
162
lib/piv/views/move_key_dialog.dart
Normal file
162
lib/piv/views/move_key_dialog.dart
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/message.dart';
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../app/state.dart';
|
||||||
|
import '../../exception/cancellation_exception.dart';
|
||||||
|
import '../../widgets/choice_filter_chip.dart';
|
||||||
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../keys.dart' as keys;
|
||||||
|
import '../models.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
import 'overwrite_confirm_dialog.dart';
|
||||||
|
|
||||||
|
class MoveKeyDialog extends ConsumerStatefulWidget {
|
||||||
|
final DevicePath devicePath;
|
||||||
|
final PivState pivState;
|
||||||
|
final PivSlot pivSlot;
|
||||||
|
const MoveKeyDialog(this.devicePath, this.pivState, this.pivSlot,
|
||||||
|
{super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConsumerStatefulWidget> createState() => _MoveKeyDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MoveKeyDialogState extends ConsumerState<MoveKeyDialog> {
|
||||||
|
SlotId? _destination;
|
||||||
|
late bool _includeCertificate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_includeCertificate = widget.pivSlot.certInfo != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
return ResponsiveDialog(
|
||||||
|
title: Text(l10n.l_move_key),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
key: keys.deleteButton,
|
||||||
|
onPressed: _destination != null
|
||||||
|
? () async {
|
||||||
|
try {
|
||||||
|
final pivSlots =
|
||||||
|
ref.read(pivSlotsProvider(widget.devicePath)).asData;
|
||||||
|
if (pivSlots != null) {
|
||||||
|
final destination = pivSlots.value.firstWhere(
|
||||||
|
(element) => element.slot == _destination);
|
||||||
|
|
||||||
|
if (!await confirmOverwrite(context, destination,
|
||||||
|
writeKey: true, writeCert: _includeCertificate)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(pivSlotsProvider(widget.devicePath).notifier)
|
||||||
|
.moveKey(
|
||||||
|
widget.pivSlot.slot,
|
||||||
|
destination.slot,
|
||||||
|
destination.metadata != null,
|
||||||
|
_includeCertificate);
|
||||||
|
|
||||||
|
await ref.read(withContextProvider)(
|
||||||
|
(context) async {
|
||||||
|
String message;
|
||||||
|
if (_includeCertificate) {
|
||||||
|
message = l10n.l_key_and_certificate_moved;
|
||||||
|
} else {
|
||||||
|
message = l10n.l_key_moved;
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
showMessage(context, message);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on CancellationException catch (_) {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Text(l10n.s_move),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(_destination == null
|
||||||
|
? l10n.q_move_key_confirm(
|
||||||
|
widget.pivSlot.slot.getDisplayName(l10n))
|
||||||
|
: widget.pivSlot.certInfo != null && _includeCertificate
|
||||||
|
? l10n.q_move_key_and_certificate_to_slot_confirm(
|
||||||
|
widget.pivSlot.slot.getDisplayName(l10n),
|
||||||
|
_destination!.getDisplayName(l10n))
|
||||||
|
: l10n.q_move_key_to_slot_confirm(
|
||||||
|
widget.pivSlot.slot.getDisplayName(l10n),
|
||||||
|
_destination!.getDisplayName(l10n))),
|
||||||
|
Wrap(
|
||||||
|
spacing: 4.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: [
|
||||||
|
ChoiceFilterChip<SlotId?>(
|
||||||
|
menuConstraints: const BoxConstraints(maxHeight: 200),
|
||||||
|
value: _destination,
|
||||||
|
items: SlotId.values
|
||||||
|
.where((element) => element != widget.pivSlot.slot)
|
||||||
|
.toList(),
|
||||||
|
labelBuilder: (value) => Text(_destination == null
|
||||||
|
? l10n.l_select_destination_slot
|
||||||
|
: _destination!.getDisplayName(l10n)),
|
||||||
|
itemBuilder: (value) => Text(value!.getDisplayName(l10n)),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_destination = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (widget.pivSlot.certInfo != null)
|
||||||
|
FilterChip(
|
||||||
|
label: Text(l10n.l_include_certificate),
|
||||||
|
selected: _includeCertificate,
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
_includeCertificate = value;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: e,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -72,6 +72,16 @@ class _PivScreenState extends ConsumerState<PivScreen> {
|
|||||||
final selected = _selected != null
|
final selected = _selected != null
|
||||||
? pivSlots?.value.firstWhere((e) => e.slot == _selected)
|
? pivSlots?.value.firstWhere((e) => e.slot == _selected)
|
||||||
: null;
|
: null;
|
||||||
|
final normalSlots = pivSlots?.value
|
||||||
|
.where((element) => !element.slot.isRetired)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
final shownRetiredSlots = pivSlots?.value
|
||||||
|
.where((element) =>
|
||||||
|
element.slot.isRetired &&
|
||||||
|
(element.certInfo != null && element.metadata != null))
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final textTheme = theme.textTheme;
|
final textTheme = theme.textTheme;
|
||||||
// This is what ListTile uses for subtitle
|
// This is what ListTile uses for subtitle
|
||||||
@ -150,7 +160,8 @@ class _PivScreenState extends ConsumerState<PivScreen> {
|
|||||||
ActionListSection.fromMenuActions(
|
ActionListSection.fromMenuActions(
|
||||||
context,
|
context,
|
||||||
l10n.s_actions,
|
l10n.s_actions,
|
||||||
actions: buildSlotActions(selected, l10n),
|
actions:
|
||||||
|
buildSlotActions(pivState, selected, l10n),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -183,14 +194,22 @@ class _PivScreenState extends ConsumerState<PivScreen> {
|
|||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (pivSlots?.hasValue == true)
|
...normalSlots.map(
|
||||||
...pivSlots!.value.map(
|
|
||||||
(e) => _CertificateListItem(
|
(e) => _CertificateListItem(
|
||||||
|
pivState,
|
||||||
e,
|
e,
|
||||||
expanded: expanded,
|
expanded: expanded,
|
||||||
selected: e == selected,
|
selected: e == selected,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
...shownRetiredSlots.map(
|
||||||
|
(e) => _CertificateListItem(
|
||||||
|
pivState,
|
||||||
|
e,
|
||||||
|
expanded: expanded,
|
||||||
|
selected: e == selected,
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -204,11 +223,12 @@ class _PivScreenState extends ConsumerState<PivScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CertificateListItem extends ConsumerWidget {
|
class _CertificateListItem extends ConsumerWidget {
|
||||||
|
final PivState pivState;
|
||||||
final PivSlot pivSlot;
|
final PivSlot pivSlot;
|
||||||
final bool expanded;
|
final bool expanded;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
|
|
||||||
const _CertificateListItem(this.pivSlot,
|
const _CertificateListItem(this.pivState, this.pivSlot,
|
||||||
{required this.expanded, required this.selected});
|
{required this.expanded, required this.selected});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -226,7 +246,7 @@ class _CertificateListItem extends ConsumerWidget {
|
|||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
foregroundColor: colorScheme.onSecondary,
|
foregroundColor: colorScheme.onSecondary,
|
||||||
backgroundColor: colorScheme.secondary,
|
backgroundColor: colorScheme.secondary,
|
||||||
child: const Icon(Symbols.badge),
|
child: Icon(slot.isRetired ? Symbols.manage_history : Symbols.badge),
|
||||||
),
|
),
|
||||||
title: slot.getDisplayName(l10n),
|
title: slot.getDisplayName(l10n),
|
||||||
subtitle: certInfo != null
|
subtitle: certInfo != null
|
||||||
@ -245,7 +265,7 @@ class _CertificateListItem extends ConsumerWidget {
|
|||||||
tapIntent: isDesktop && !expanded ? null : OpenIntent(pivSlot),
|
tapIntent: isDesktop && !expanded ? null : OpenIntent(pivSlot),
|
||||||
doubleTapIntent: isDesktop && !expanded ? OpenIntent(pivSlot) : null,
|
doubleTapIntent: isDesktop && !expanded ? OpenIntent(pivSlot) : null,
|
||||||
buildPopupActions: hasFeature(features.slots)
|
buildPopupActions: hasFeature(features.slots)
|
||||||
? (context) => buildSlotActions(pivSlot, l10n)
|
? (context) => buildSlotActions(pivState, pivSlot, l10n)
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -255,12 +275,52 @@ class _CertificateListItem extends ConsumerWidget {
|
|||||||
SlotId.signature => meatballButton9c,
|
SlotId.signature => meatballButton9c,
|
||||||
SlotId.keyManagement => meatballButton9d,
|
SlotId.keyManagement => meatballButton9d,
|
||||||
SlotId.cardAuth => meatballButton9e,
|
SlotId.cardAuth => meatballButton9e,
|
||||||
|
SlotId.retired1 => meatballButton82,
|
||||||
|
SlotId.retired2 => meatballButton83,
|
||||||
|
SlotId.retired3 => meatballButton84,
|
||||||
|
SlotId.retired4 => meatballButton85,
|
||||||
|
SlotId.retired5 => meatballButton86,
|
||||||
|
SlotId.retired6 => meatballButton87,
|
||||||
|
SlotId.retired7 => meatballButton88,
|
||||||
|
SlotId.retired8 => meatballButton89,
|
||||||
|
SlotId.retired9 => meatballButton8a,
|
||||||
|
SlotId.retired10 => meatballButton8b,
|
||||||
|
SlotId.retired11 => meatballButton8c,
|
||||||
|
SlotId.retired12 => meatballButton8d,
|
||||||
|
SlotId.retired13 => meatballButton8e,
|
||||||
|
SlotId.retired14 => meatballButton8f,
|
||||||
|
SlotId.retired15 => meatballButton90,
|
||||||
|
SlotId.retired16 => meatballButton91,
|
||||||
|
SlotId.retired17 => meatballButton92,
|
||||||
|
SlotId.retired18 => meatballButton93,
|
||||||
|
SlotId.retired19 => meatballButton94,
|
||||||
|
SlotId.retired20 => meatballButton95
|
||||||
};
|
};
|
||||||
|
|
||||||
Key _getAppListItemKey(SlotId slotId) => switch (slotId) {
|
Key _getAppListItemKey(SlotId slotId) => switch (slotId) {
|
||||||
SlotId.authentication => appListItem9a,
|
SlotId.authentication => appListItem9a,
|
||||||
SlotId.signature => appListItem9c,
|
SlotId.signature => appListItem9c,
|
||||||
SlotId.keyManagement => appListItem9d,
|
SlotId.keyManagement => appListItem9d,
|
||||||
SlotId.cardAuth => appListItem9e
|
SlotId.cardAuth => appListItem9e,
|
||||||
|
SlotId.retired1 => appListItem82,
|
||||||
|
SlotId.retired2 => appListItem83,
|
||||||
|
SlotId.retired3 => appListItem84,
|
||||||
|
SlotId.retired4 => appListItem85,
|
||||||
|
SlotId.retired5 => appListItem86,
|
||||||
|
SlotId.retired6 => appListItem87,
|
||||||
|
SlotId.retired7 => appListItem88,
|
||||||
|
SlotId.retired8 => appListItem89,
|
||||||
|
SlotId.retired9 => appListItem8a,
|
||||||
|
SlotId.retired10 => appListItem8b,
|
||||||
|
SlotId.retired11 => appListItem8c,
|
||||||
|
SlotId.retired12 => appListItem8d,
|
||||||
|
SlotId.retired13 => appListItem8e,
|
||||||
|
SlotId.retired14 => appListItem8f,
|
||||||
|
SlotId.retired15 => appListItem90,
|
||||||
|
SlotId.retired16 => appListItem91,
|
||||||
|
SlotId.retired17 => appListItem92,
|
||||||
|
SlotId.retired18 => appListItem93,
|
||||||
|
SlotId.retired19 => appListItem94,
|
||||||
|
SlotId.retired20 => appListItem95
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ class SlotDialog extends ConsumerWidget {
|
|||||||
ActionListSection.fromMenuActions(
|
ActionListSection.fromMenuActions(
|
||||||
context,
|
context,
|
||||||
l10n.s_actions,
|
l10n.s_actions,
|
||||||
actions: buildSlotActions(slotData, l10n),
|
actions: buildSlotActions(pivState, slotData, l10n),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -29,6 +29,7 @@ class ChoiceFilterChip<T> extends StatefulWidget {
|
|||||||
final Widget? avatar;
|
final Widget? avatar;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
final bool? disableHover;
|
final bool? disableHover;
|
||||||
|
final BoxConstraints? menuConstraints;
|
||||||
const ChoiceFilterChip({
|
const ChoiceFilterChip({
|
||||||
super.key,
|
super.key,
|
||||||
required this.value,
|
required this.value,
|
||||||
@ -40,6 +41,7 @@ class ChoiceFilterChip<T> extends StatefulWidget {
|
|||||||
this.selected = false,
|
this.selected = false,
|
||||||
this.disableHover,
|
this.disableHover,
|
||||||
this.labelBuilder,
|
this.labelBuilder,
|
||||||
|
this.menuConstraints,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -63,6 +65,7 @@ class _ChoiceFilterChipState<T> extends State<ChoiceFilterChip<T>> {
|
|||||||
Offset.zero & overlay.size,
|
Offset.zero & overlay.size,
|
||||||
);
|
);
|
||||||
return await showMenu(
|
return await showMenu(
|
||||||
|
constraints: widget.menuConstraints,
|
||||||
context: context,
|
context: context,
|
||||||
position: position,
|
position: position,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
|
@ -815,6 +815,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
version: "0.0.99"
|
||||||
|
sliver_tools:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sliver_tools
|
||||||
|
sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.12"
|
||||||
source_gen:
|
source_gen:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -71,6 +71,7 @@ dependencies:
|
|||||||
base32: ^2.1.3
|
base32: ^2.1.3
|
||||||
convert: ^3.1.1
|
convert: ^3.1.1
|
||||||
material_symbols_icons: ^4.2719.1
|
material_symbols_icons: ^4.2719.1
|
||||||
|
sliver_tools: ^0.2.12
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
integration_test:
|
integration_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user