mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +03:00
Merge PR #1427
This commit is contained in:
commit
8e2b6a1f31
@ -23,7 +23,7 @@ import com.yubico.authenticator.device.Info
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
|
||||
enum class OperationContext(val value: Int) {
|
||||
Oath(0), Yubikey(1), Invalid(-1);
|
||||
Home(0), Oath(1), Yubikey(2), Invalid(-1);
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: Int) = values().firstOrNull { it.value == value } ?: Invalid
|
||||
|
@ -64,8 +64,8 @@ Future<Widget> initialize() async {
|
||||
oathStateProvider.overrideWithProvider(androidOathStateProvider.call),
|
||||
credentialListProvider
|
||||
.overrideWithProvider(androidCredentialListProvider.call),
|
||||
currentAppProvider.overrideWith(
|
||||
(ref) => AndroidSubPageNotifier(ref.watch(supportedAppsProvider))),
|
||||
currentAppProvider.overrideWith((ref) => AndroidSubPageNotifier(
|
||||
ref.watch(supportedAppsProvider), ref.watch(prefProvider))),
|
||||
managementStateProvider.overrideWithProvider(androidManagementState.call),
|
||||
currentDeviceProvider.overrideWith(
|
||||
() => AndroidCurrentDeviceNotifier(),
|
||||
@ -95,6 +95,7 @@ Future<Widget> initialize() async {
|
||||
..setFeature(features.fido, false)
|
||||
..setFeature(features.piv, false)
|
||||
..setFeature(features.otp, false)
|
||||
..setFeature(features.home, false)
|
||||
..setFeature(features.management, false);
|
||||
});
|
||||
|
||||
|
@ -91,7 +91,7 @@ final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) {
|
||||
});
|
||||
|
||||
class AndroidSubPageNotifier extends CurrentAppNotifier {
|
||||
AndroidSubPageNotifier(super.supportedApps) {
|
||||
AndroidSubPageNotifier(super.supportedApps, super.prefs) {
|
||||
_handleSubPage(state);
|
||||
}
|
||||
|
||||
|
@ -21,5 +21,6 @@ final fido = root.feature('fido');
|
||||
final piv = root.feature('piv');
|
||||
final otp = root.feature('otp');
|
||||
final management = root.feature('management');
|
||||
final home = root.feature('home');
|
||||
|
||||
final fingerprints = fido.feature('fingerprints');
|
||||
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 'dart:ui';
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'models.freezed.dart';
|
||||
|
||||
part 'models.g.dart';
|
||||
|
||||
@freezed
|
||||
class KeyCustomization with _$KeyCustomization {
|
||||
factory KeyCustomization({
|
||||
required int serial,
|
||||
@JsonKey(includeIfNull: false) String? name,
|
||||
@JsonKey(includeIfNull: false) @_ColorConverter() Color? color,
|
||||
}) = _KeyCustomization;
|
||||
|
||||
factory KeyCustomization.fromJson(Map<String, dynamic> json) =>
|
||||
_$KeyCustomizationFromJson(json);
|
||||
}
|
||||
|
||||
class _ColorConverter implements JsonConverter<Color?, int?> {
|
||||
const _ColorConverter();
|
||||
|
||||
@override
|
||||
Color? fromJson(int? json) => json != null ? Color(json) : null;
|
||||
|
||||
@override
|
||||
int? toJson(Color? object) => object?.value;
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'models.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
KeyCustomization _$KeyCustomizationFromJson(Map<String, dynamic> json) {
|
||||
return _KeyCustomization.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$KeyCustomization {
|
||||
int get serial => throw _privateConstructorUsedError;
|
||||
@JsonKey(includeIfNull: false)
|
||||
String? get name => throw _privateConstructorUsedError;
|
||||
@JsonKey(includeIfNull: false)
|
||||
@_ColorConverter()
|
||||
Color? get color => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$KeyCustomizationCopyWith<KeyCustomization> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $KeyCustomizationCopyWith<$Res> {
|
||||
factory $KeyCustomizationCopyWith(
|
||||
KeyCustomization value, $Res Function(KeyCustomization) then) =
|
||||
_$KeyCustomizationCopyWithImpl<$Res, KeyCustomization>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int serial,
|
||||
@JsonKey(includeIfNull: false) String? name,
|
||||
@JsonKey(includeIfNull: false) @_ColorConverter() Color? color});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$KeyCustomizationCopyWithImpl<$Res, $Val extends KeyCustomization>
|
||||
implements $KeyCustomizationCopyWith<$Res> {
|
||||
_$KeyCustomizationCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? serial = null,
|
||||
Object? name = freezed,
|
||||
Object? color = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
serial: null == serial
|
||||
? _value.serial
|
||||
: serial // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
name: freezed == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
color: freezed == color
|
||||
? _value.color
|
||||
: color // ignore: cast_nullable_to_non_nullable
|
||||
as Color?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$KeyCustomizationImplCopyWith<$Res>
|
||||
implements $KeyCustomizationCopyWith<$Res> {
|
||||
factory _$$KeyCustomizationImplCopyWith(_$KeyCustomizationImpl value,
|
||||
$Res Function(_$KeyCustomizationImpl) then) =
|
||||
__$$KeyCustomizationImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int serial,
|
||||
@JsonKey(includeIfNull: false) String? name,
|
||||
@JsonKey(includeIfNull: false) @_ColorConverter() Color? color});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$KeyCustomizationImplCopyWithImpl<$Res>
|
||||
extends _$KeyCustomizationCopyWithImpl<$Res, _$KeyCustomizationImpl>
|
||||
implements _$$KeyCustomizationImplCopyWith<$Res> {
|
||||
__$$KeyCustomizationImplCopyWithImpl(_$KeyCustomizationImpl _value,
|
||||
$Res Function(_$KeyCustomizationImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? serial = null,
|
||||
Object? name = freezed,
|
||||
Object? color = freezed,
|
||||
}) {
|
||||
return _then(_$KeyCustomizationImpl(
|
||||
serial: null == serial
|
||||
? _value.serial
|
||||
: serial // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
name: freezed == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
color: freezed == color
|
||||
? _value.color
|
||||
: color // ignore: cast_nullable_to_non_nullable
|
||||
as Color?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$KeyCustomizationImpl implements _KeyCustomization {
|
||||
_$KeyCustomizationImpl(
|
||||
{required this.serial,
|
||||
@JsonKey(includeIfNull: false) this.name,
|
||||
@JsonKey(includeIfNull: false) @_ColorConverter() this.color});
|
||||
|
||||
factory _$KeyCustomizationImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$KeyCustomizationImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int serial;
|
||||
@override
|
||||
@JsonKey(includeIfNull: false)
|
||||
final String? name;
|
||||
@override
|
||||
@JsonKey(includeIfNull: false)
|
||||
@_ColorConverter()
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'KeyCustomization(serial: $serial, name: $name, color: $color)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$KeyCustomizationImpl &&
|
||||
(identical(other.serial, serial) || other.serial == serial) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.color, color) || other.color == color));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, serial, name, color);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$KeyCustomizationImplCopyWith<_$KeyCustomizationImpl> get copyWith =>
|
||||
__$$KeyCustomizationImplCopyWithImpl<_$KeyCustomizationImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$KeyCustomizationImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _KeyCustomization implements KeyCustomization {
|
||||
factory _KeyCustomization(
|
||||
{required final int serial,
|
||||
@JsonKey(includeIfNull: false) final String? name,
|
||||
@JsonKey(includeIfNull: false)
|
||||
@_ColorConverter()
|
||||
final Color? color}) = _$KeyCustomizationImpl;
|
||||
|
||||
factory _KeyCustomization.fromJson(Map<String, dynamic> json) =
|
||||
_$KeyCustomizationImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get serial;
|
||||
@override
|
||||
@JsonKey(includeIfNull: false)
|
||||
String? get name;
|
||||
@override
|
||||
@JsonKey(includeIfNull: false)
|
||||
@_ColorConverter()
|
||||
Color? get color;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$KeyCustomizationImplCopyWith<_$KeyCustomizationImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../core/state.dart';
|
||||
import '../logging.dart';
|
||||
import 'models.dart';
|
||||
|
||||
final keyCustomizationManagerProvider =
|
||||
StateNotifierProvider<KeyCustomizationNotifier, Map<int, KeyCustomization>>(
|
||||
(ref) => KeyCustomizationNotifier(ref.watch(prefProvider)));
|
||||
|
||||
final _log = Logger('key_customization_manager');
|
||||
|
||||
class KeyCustomizationNotifier
|
||||
extends StateNotifier<Map<int, KeyCustomization>> {
|
||||
static const _prefKeyCustomizations = 'KEY_CUSTOMIZATIONS';
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
KeyCustomizationNotifier(this._prefs)
|
||||
: super(_readCustomizations(_prefs.getString(_prefKeyCustomizations)));
|
||||
|
||||
static Map<int, KeyCustomization> _readCustomizations(String? pref) {
|
||||
if (pref == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
final retval = <int, KeyCustomization>{};
|
||||
for (var element in json.decode(pref)) {
|
||||
final keyCustomization = KeyCustomization.fromJson(element);
|
||||
retval[keyCustomization.serial] = keyCustomization;
|
||||
}
|
||||
return retval;
|
||||
} catch (e) {
|
||||
_log.error('Failure reading customizations: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
KeyCustomization? get(int serial) {
|
||||
_log.debug('Getting key customization for $serial');
|
||||
return state[serial];
|
||||
}
|
||||
|
||||
Future<void> set({required int serial, String? name, Color? color}) async {
|
||||
_log.debug('Setting key customization for $serial: $name, $color');
|
||||
if (name == null && color == null) {
|
||||
// remove this customization
|
||||
state = {...state..remove(serial)};
|
||||
} else {
|
||||
state = {
|
||||
...state
|
||||
..[serial] =
|
||||
KeyCustomization(serial: serial, name: name, color: color)
|
||||
};
|
||||
}
|
||||
await _prefs.setString(
|
||||
_prefKeyCustomizations, json.encode(state.values.toList()));
|
||||
}
|
||||
}
|
@ -1,335 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../../android/state.dart';
|
||||
import '../../../core/state.dart';
|
||||
import '../../../management/models.dart';
|
||||
import '../../../theme.dart';
|
||||
import '../../../widgets/app_input_decoration.dart';
|
||||
import '../../../widgets/app_text_form_field.dart';
|
||||
import '../../../widgets/focus_utils.dart';
|
||||
import '../../../widgets/responsive_dialog.dart';
|
||||
import '../../models.dart';
|
||||
import '../../state.dart';
|
||||
import '../../views/device_avatar.dart';
|
||||
import '../../views/keys.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
|
||||
class KeyCustomizationDialog extends ConsumerStatefulWidget {
|
||||
final KeyCustomization initialCustomization;
|
||||
final DeviceNode? node;
|
||||
|
||||
const KeyCustomizationDialog(
|
||||
{super.key, required this.node, required this.initialCustomization});
|
||||
|
||||
@override
|
||||
ConsumerState<KeyCustomizationDialog> createState() =>
|
||||
_KeyCustomizationDialogState();
|
||||
}
|
||||
|
||||
class _KeyCustomizationDialogState
|
||||
extends ConsumerState<KeyCustomizationDialog> {
|
||||
String? _customName;
|
||||
Color? _customColor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_customName = widget.initialCustomization.name;
|
||||
_customColor = widget.initialCustomization.color;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final currentNode = widget.node;
|
||||
final theme = Theme.of(context);
|
||||
final primaryColor = ref.watch(defaultColorProvider);
|
||||
|
||||
final Widget hero;
|
||||
if (currentNode != null) {
|
||||
hero = _CurrentDeviceAvatar(currentNode, _customColor ?? primaryColor);
|
||||
} else {
|
||||
hero = Column(
|
||||
children: [
|
||||
_HeroAvatar(
|
||||
color: _customColor ?? primaryColor,
|
||||
child: DeviceAvatar(
|
||||
radius: 64,
|
||||
child: Icon(isAndroid ? Symbols.contactless_off : Symbols.usb),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Center(child: Text(l10n.l_no_yk_present)),
|
||||
subtitle: Center(
|
||||
child: Text(isAndroid ? l10n.l_insert_or_tap_yk : l10n.s_usb)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final didChange = widget.initialCustomization.name != _customName ||
|
||||
widget.initialCustomization.color != _customColor;
|
||||
|
||||
return Theme(
|
||||
data: AppTheme.getTheme(theme.brightness, _customColor ?? primaryColor),
|
||||
child: ResponsiveDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: didChange ? _submit : null,
|
||||
child: Text(l10n.s_save),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
children: [
|
||||
hero,
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 18, 18, 0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 360),
|
||||
child: AppTextFormField(
|
||||
initialValue: _customName,
|
||||
maxLength: 20,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_label,
|
||||
helperText:
|
||||
'', // Prevents dialog resizing when disabled
|
||||
prefixIcon: const Icon(Symbols.key),
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
final trimmed = value.trim();
|
||||
_customName = trimmed.isEmpty ? null : trimmed;
|
||||
});
|
||||
},
|
||||
onFieldSubmitted: (_) {
|
||||
_submit();
|
||||
},
|
||||
),
|
||||
),
|
||||
Text(l10n.s_theme_color),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 360),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runSpacing: 8,
|
||||
spacing: 16,
|
||||
children: [
|
||||
...[
|
||||
Colors.teal,
|
||||
Colors.cyan,
|
||||
Colors.blueAccent,
|
||||
Colors.deepPurple,
|
||||
Colors.red,
|
||||
Colors.orange,
|
||||
Colors.yellow,
|
||||
// add nice color to devices with dynamic color
|
||||
if (isAndroid &&
|
||||
ref.read(androidSdkVersionProvider) >= 31)
|
||||
Colors.lightGreen
|
||||
].map((e) => _ColorButton(
|
||||
color: e,
|
||||
isSelected: _customColor == e,
|
||||
onPressed: () {
|
||||
_updateColor(e);
|
||||
},
|
||||
)),
|
||||
|
||||
// remove color button
|
||||
RawMaterialButton(
|
||||
onPressed: () => _updateColor(null),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32.0, minHeight: 32.0),
|
||||
fillColor: (isAndroid &&
|
||||
ref.read(androidSdkVersionProvider) >= 31)
|
||||
? theme.colorScheme.onSurface
|
||||
: primaryColor,
|
||||
shape: const CircleBorder(),
|
||||
child: Icon(
|
||||
Symbols.cancel,
|
||||
size: 16,
|
||||
color: _customColor == null
|
||||
? theme.colorScheme.onSurface
|
||||
: theme.colorScheme.surface.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _submit() async {
|
||||
final manager = ref.read(keyCustomizationManagerProvider.notifier);
|
||||
await manager.set(
|
||||
serial: widget.initialCustomization.serial,
|
||||
name: _customName,
|
||||
color: _customColor);
|
||||
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
FocusUtils.unfocus(context);
|
||||
final nav = Navigator.of(context);
|
||||
nav.pop();
|
||||
});
|
||||
}
|
||||
|
||||
void _updateColor(Color? color) {
|
||||
setState(() {
|
||||
_customColor = color;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _getDeviceInfoString(BuildContext context, DeviceInfo info) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final serial = info.serial;
|
||||
return [
|
||||
if (serial != null) l10n.s_sn_serial(serial),
|
||||
if (info.version.isAtLeast(1))
|
||||
l10n.s_fw_version(info.version)
|
||||
else
|
||||
l10n.s_unknown_type,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
List<String> _getDeviceStrings(
|
||||
BuildContext context, WidgetRef ref, DeviceNode node) {
|
||||
final data = ref.watch(currentDeviceDataProvider);
|
||||
|
||||
final messages = node is UsbYubiKeyNode
|
||||
? node.info != null
|
||||
? [node.name, _getDeviceInfoString(context, node.info!)]
|
||||
: <String>[]
|
||||
: data.hasValue
|
||||
? data.value?.node.path == node.path
|
||||
? [
|
||||
data.value!.name,
|
||||
_getDeviceInfoString(context, data.value!.info)
|
||||
]
|
||||
: <String>[]
|
||||
: <String>[];
|
||||
return messages;
|
||||
}
|
||||
|
||||
class _HeroAvatar extends StatelessWidget {
|
||||
final Widget child;
|
||||
final Color color;
|
||||
|
||||
const _HeroAvatar({required this.color, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
color.withOpacity(0.6),
|
||||
color.withOpacity(0.25),
|
||||
(DialogTheme.of(context).backgroundColor ??
|
||||
theme.dialogBackgroundColor)
|
||||
.withOpacity(0),
|
||||
],
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Theme(
|
||||
// Give the avatar a transparent background
|
||||
data: theme.copyWith(
|
||||
colorScheme:
|
||||
theme.colorScheme.copyWith(surfaceVariant: Colors.transparent)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CurrentDeviceAvatar extends ConsumerWidget {
|
||||
final DeviceNode node;
|
||||
final Color color;
|
||||
|
||||
const _CurrentDeviceAvatar(this.node, this.color);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final hero = DeviceAvatar.deviceNode(node, radius: 64);
|
||||
final messages = _getDeviceStrings(context, ref, node);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_HeroAvatar(color: color, child: hero),
|
||||
ListTile(
|
||||
key: deviceInfoListTile,
|
||||
title: Text(messages.removeAt(0), textAlign: TextAlign.center),
|
||||
isThreeLine: messages.length > 1,
|
||||
subtitle: Text(messages.join('\n'), textAlign: TextAlign.center),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ColorButton extends StatefulWidget {
|
||||
final Color? color;
|
||||
final bool isSelected;
|
||||
final Function()? onPressed;
|
||||
|
||||
const _ColorButton({
|
||||
required this.color,
|
||||
required this.isSelected,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ColorButton> createState() => _ColorButtonState();
|
||||
}
|
||||
|
||||
class _ColorButtonState extends State<_ColorButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawMaterialButton(
|
||||
onPressed: widget.onPressed,
|
||||
constraints: const BoxConstraints(minWidth: 32.0, minHeight: 32.0),
|
||||
fillColor: widget.color,
|
||||
shape: const CircleBorder(),
|
||||
child: Icon(
|
||||
Symbols.circle,
|
||||
size: 16,
|
||||
color: widget.isSelected ? Colors.white : Colors.transparent,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -25,11 +25,14 @@ import '../core/state.dart';
|
||||
|
||||
part 'models.freezed.dart';
|
||||
|
||||
part 'models.g.dart';
|
||||
|
||||
const _listEquality = ListEquality();
|
||||
|
||||
enum Availability { enabled, disabled, unsupported }
|
||||
|
||||
enum Application {
|
||||
home(),
|
||||
accounts([Capability.oath]),
|
||||
webauthn([Capability.u2f]),
|
||||
fingerprints([Capability.fido2]),
|
||||
@ -43,6 +46,7 @@ enum Application {
|
||||
const Application([this.capabilities = const []]);
|
||||
|
||||
String getDisplayName(AppLocalizations l10n) => switch (this) {
|
||||
Application.home => l10n.s_home,
|
||||
Application.accounts => l10n.s_accounts,
|
||||
Application.webauthn => l10n.s_webauthn,
|
||||
Application.fingerprints => l10n.s_fingerprints,
|
||||
@ -155,3 +159,25 @@ class WindowState with _$WindowState {
|
||||
@Default(false) bool hidden,
|
||||
}) = _WindowState;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class KeyCustomization with _$KeyCustomization {
|
||||
factory KeyCustomization({
|
||||
required int serial,
|
||||
@JsonKey(includeIfNull: false) String? name,
|
||||
@JsonKey(includeIfNull: false) @_ColorConverter() Color? color,
|
||||
}) = _KeyCustomization;
|
||||
|
||||
factory KeyCustomization.fromJson(Map<String, dynamic> json) =>
|
||||
_$KeyCustomizationFromJson(json);
|
||||
}
|
||||
|
||||
class _ColorConverter implements JsonConverter<Color?, int?> {
|
||||
const _ColorConverter();
|
||||
|
||||
@override
|
||||
Color? fromJson(int? json) => json != null ? Color(json) : null;
|
||||
|
||||
@override
|
||||
int? toJson(Color? object) => object?.value;
|
||||
}
|
||||
|
@ -1084,3 +1084,195 @@ abstract class _WindowState implements WindowState {
|
||||
_$$WindowStateImplCopyWith<_$WindowStateImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
KeyCustomization _$KeyCustomizationFromJson(Map<String, dynamic> json) {
|
||||
return _KeyCustomization.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$KeyCustomization {
|
||||
int get serial => throw _privateConstructorUsedError;
|
||||
@JsonKey(includeIfNull: false)
|
||||
String? get name => throw _privateConstructorUsedError;
|
||||
@JsonKey(includeIfNull: false)
|
||||
@_ColorConverter()
|
||||
Color? get color => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$KeyCustomizationCopyWith<KeyCustomization> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $KeyCustomizationCopyWith<$Res> {
|
||||
factory $KeyCustomizationCopyWith(
|
||||
KeyCustomization value, $Res Function(KeyCustomization) then) =
|
||||
_$KeyCustomizationCopyWithImpl<$Res, KeyCustomization>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int serial,
|
||||
@JsonKey(includeIfNull: false) String? name,
|
||||
@JsonKey(includeIfNull: false) @_ColorConverter() Color? color});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$KeyCustomizationCopyWithImpl<$Res, $Val extends KeyCustomization>
|
||||
implements $KeyCustomizationCopyWith<$Res> {
|
||||
_$KeyCustomizationCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? serial = null,
|
||||
Object? name = freezed,
|
||||
Object? color = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
serial: null == serial
|
||||
? _value.serial
|
||||
: serial // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
name: freezed == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
color: freezed == color
|
||||
? _value.color
|
||||
: color // ignore: cast_nullable_to_non_nullable
|
||||
as Color?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$KeyCustomizationImplCopyWith<$Res>
|
||||
implements $KeyCustomizationCopyWith<$Res> {
|
||||
factory _$$KeyCustomizationImplCopyWith(_$KeyCustomizationImpl value,
|
||||
$Res Function(_$KeyCustomizationImpl) then) =
|
||||
__$$KeyCustomizationImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int serial,
|
||||
@JsonKey(includeIfNull: false) String? name,
|
||||
@JsonKey(includeIfNull: false) @_ColorConverter() Color? color});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$KeyCustomizationImplCopyWithImpl<$Res>
|
||||
extends _$KeyCustomizationCopyWithImpl<$Res, _$KeyCustomizationImpl>
|
||||
implements _$$KeyCustomizationImplCopyWith<$Res> {
|
||||
__$$KeyCustomizationImplCopyWithImpl(_$KeyCustomizationImpl _value,
|
||||
$Res Function(_$KeyCustomizationImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? serial = null,
|
||||
Object? name = freezed,
|
||||
Object? color = freezed,
|
||||
}) {
|
||||
return _then(_$KeyCustomizationImpl(
|
||||
serial: null == serial
|
||||
? _value.serial
|
||||
: serial // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
name: freezed == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
color: freezed == color
|
||||
? _value.color
|
||||
: color // ignore: cast_nullable_to_non_nullable
|
||||
as Color?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$KeyCustomizationImpl implements _KeyCustomization {
|
||||
_$KeyCustomizationImpl(
|
||||
{required this.serial,
|
||||
@JsonKey(includeIfNull: false) this.name,
|
||||
@JsonKey(includeIfNull: false) @_ColorConverter() this.color});
|
||||
|
||||
factory _$KeyCustomizationImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$KeyCustomizationImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int serial;
|
||||
@override
|
||||
@JsonKey(includeIfNull: false)
|
||||
final String? name;
|
||||
@override
|
||||
@JsonKey(includeIfNull: false)
|
||||
@_ColorConverter()
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'KeyCustomization(serial: $serial, name: $name, color: $color)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$KeyCustomizationImpl &&
|
||||
(identical(other.serial, serial) || other.serial == serial) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.color, color) || other.color == color));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, serial, name, color);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$KeyCustomizationImplCopyWith<_$KeyCustomizationImpl> get copyWith =>
|
||||
__$$KeyCustomizationImplCopyWithImpl<_$KeyCustomizationImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$KeyCustomizationImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _KeyCustomization implements KeyCustomization {
|
||||
factory _KeyCustomization(
|
||||
{required final int serial,
|
||||
@JsonKey(includeIfNull: false) final String? name,
|
||||
@JsonKey(includeIfNull: false)
|
||||
@_ColorConverter()
|
||||
final Color? color}) = _$KeyCustomizationImpl;
|
||||
|
||||
factory _KeyCustomization.fromJson(Map<String, dynamic> json) =
|
||||
_$KeyCustomizationImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get serial;
|
||||
@override
|
||||
@JsonKey(includeIfNull: false)
|
||||
String? get name;
|
||||
@override
|
||||
@JsonKey(includeIfNull: false)
|
||||
@_ColorConverter()
|
||||
Color? get color;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$KeyCustomizationImplCopyWith<_$KeyCustomizationImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
@ -27,7 +28,6 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../core/state.dart';
|
||||
import '../theme.dart';
|
||||
import 'features.dart' as features;
|
||||
import 'key_customization/state.dart';
|
||||
import 'logging.dart';
|
||||
import 'models.dart';
|
||||
|
||||
@ -40,6 +40,7 @@ const officialLocales = [
|
||||
|
||||
extension on Application {
|
||||
Feature get _feature => switch (this) {
|
||||
Application.home => features.home,
|
||||
Application.accounts => features.oath,
|
||||
Application.webauthn => features.fido,
|
||||
Application.passkeys => features.fido,
|
||||
@ -202,7 +203,8 @@ abstract class CurrentDeviceNotifier extends Notifier<DeviceNode?> {
|
||||
|
||||
final currentAppProvider =
|
||||
StateNotifierProvider<CurrentAppNotifier, Application>((ref) {
|
||||
final notifier = CurrentAppNotifier(ref.watch(supportedAppsProvider));
|
||||
final notifier = CurrentAppNotifier(
|
||||
ref.watch(supportedAppsProvider), ref.watch(prefProvider));
|
||||
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, data) {
|
||||
notifier._notifyDeviceChanged(data.whenOrNull(data: ((data) => data)));
|
||||
}, fireImmediately: true);
|
||||
@ -211,16 +213,37 @@ final currentAppProvider =
|
||||
|
||||
class CurrentAppNotifier extends StateNotifier<Application> {
|
||||
final List<Application> _supportedApps;
|
||||
static const String _key = 'APP_STATE_LAST_APP';
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
CurrentAppNotifier(this._supportedApps) : super(_supportedApps.first);
|
||||
CurrentAppNotifier(this._supportedApps, this._prefs)
|
||||
: super(_fromName(_prefs.getString(_key), _supportedApps));
|
||||
|
||||
void setCurrentApp(Application app) {
|
||||
state = app;
|
||||
_prefs.setString(_key, app.name);
|
||||
}
|
||||
|
||||
void _notifyDeviceChanged(YubiKeyData? data) {
|
||||
if (data == null ||
|
||||
state.getAvailability(data) != Availability.unsupported) {
|
||||
if (data == null) {
|
||||
state = _supportedApps.first;
|
||||
return;
|
||||
}
|
||||
|
||||
String? lastAppName = _prefs.getString(_key);
|
||||
if (lastAppName != null && lastAppName != state.name) {
|
||||
// Try switching to saved app
|
||||
state = Application.values.firstWhere((app) => app.name == lastAppName);
|
||||
}
|
||||
if (state == Application.passkeys &&
|
||||
state.getAvailability(data) != Availability.enabled) {
|
||||
state = Application.webauthn;
|
||||
}
|
||||
if (state == Application.webauthn &&
|
||||
state.getAvailability(data) != Availability.enabled) {
|
||||
state = Application.passkeys;
|
||||
}
|
||||
if (state.getAvailability(data) != Availability.unsupported) {
|
||||
// Keep current app
|
||||
return;
|
||||
}
|
||||
@ -230,6 +253,10 @@ class CurrentAppNotifier extends StateNotifier<Application> {
|
||||
orElse: () => _supportedApps.first,
|
||||
);
|
||||
}
|
||||
|
||||
static Application _fromName(String? name, List<Application> supportedApps) =>
|
||||
supportedApps.firstWhere((element) => element.name == name,
|
||||
orElse: () => supportedApps.first);
|
||||
}
|
||||
|
||||
abstract class QrScanner {
|
||||
@ -285,3 +312,55 @@ typedef WithContext = Future<T> Function<T>(
|
||||
|
||||
final withContextProvider = Provider<WithContext>(
|
||||
(ref) => ref.watch(contextConsumer.notifier).withContext);
|
||||
|
||||
final keyCustomizationManagerProvider =
|
||||
StateNotifierProvider<KeyCustomizationNotifier, Map<int, KeyCustomization>>(
|
||||
(ref) => KeyCustomizationNotifier(ref.watch(prefProvider)));
|
||||
|
||||
class KeyCustomizationNotifier
|
||||
extends StateNotifier<Map<int, KeyCustomization>> {
|
||||
static const _prefKeyCustomizations = 'KEY_CUSTOMIZATIONS';
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
KeyCustomizationNotifier(this._prefs)
|
||||
: super(_readCustomizations(_prefs.getString(_prefKeyCustomizations)));
|
||||
|
||||
static Map<int, KeyCustomization> _readCustomizations(String? pref) {
|
||||
if (pref == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
final retval = <int, KeyCustomization>{};
|
||||
for (var element in json.decode(pref)) {
|
||||
final keyCustomization = KeyCustomization.fromJson(element);
|
||||
retval[keyCustomization.serial] = keyCustomization;
|
||||
}
|
||||
return retval;
|
||||
} catch (e) {
|
||||
_log.error('Failure reading customizations: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
KeyCustomization? get(int serial) {
|
||||
_log.debug('Getting key customization for $serial');
|
||||
return state[serial];
|
||||
}
|
||||
|
||||
Future<void> set({required int serial, String? name, Color? color}) async {
|
||||
_log.debug('Setting key customization for $serial: $name, $color');
|
||||
if (name == null && color == null) {
|
||||
// remove this customization
|
||||
state = {...state..remove(serial)};
|
||||
} else {
|
||||
state = {
|
||||
...state
|
||||
..[serial] =
|
||||
KeyCustomization(serial: serial, name: name, color: color)
|
||||
};
|
||||
}
|
||||
await _prefs.setString(
|
||||
_prefKeyCustomizations, json.encode(state.values.toList()));
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ class AppPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
|
||||
if (width < 400 ||
|
||||
(isAndroid && width < 600 && width < constraints.maxHeight)) {
|
||||
return _buildScaffold(context, true, false, false);
|
||||
@ -160,28 +161,18 @@ class AppPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildTitle(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 2.0,
|
||||
Text(title!,
|
||||
style: Theme.of(context).textTheme.displaySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.9))),
|
||||
if (capabilities != null)
|
||||
Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
Text(title!,
|
||||
style: Theme.of(context).textTheme.displaySmall!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.9))),
|
||||
if (capabilities != null)
|
||||
Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 8.0,
|
||||
children: [...capabilities!.map((c) => _CapabilityBadge(c))],
|
||||
)
|
||||
])
|
||||
children: [...capabilities!.map((c) => CapabilityBadge(c))],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -189,10 +180,11 @@ class AppPage extends StatelessWidget {
|
||||
Widget _buildMainContent(BuildContext context, bool expanded) {
|
||||
final actions = actionsBuilder?.call(context, expanded) ?? [];
|
||||
final content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null)
|
||||
if (title != null && !centered)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 24.0),
|
||||
@ -226,26 +218,59 @@ class AppPage extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final safeArea = SafeArea(
|
||||
child: delayedContent
|
||||
? DelayedVisibility(
|
||||
key: GlobalKey(), // Ensure we reset the delay on rebuild
|
||||
delay: const Duration(milliseconds: 400),
|
||||
child: content,
|
||||
)
|
||||
: content,
|
||||
);
|
||||
|
||||
if (centered) {
|
||||
return Stack(
|
||||
children: [
|
||||
if (title != null)
|
||||
Positioned.fill(
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16.0, right: 16.0, bottom: 24.0),
|
||||
child: _buildTitle(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
top: title != null ? 68.0 : 0,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: ScrollConfiguration(
|
||||
behavior:
|
||||
ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: safeArea,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
primary: false,
|
||||
child: SafeArea(
|
||||
child: delayedContent
|
||||
? DelayedVisibility(
|
||||
key: GlobalKey(), // Ensure we reset the delay on rebuild
|
||||
delay: const Duration(milliseconds: 400),
|
||||
child: content,
|
||||
)
|
||||
: content,
|
||||
),
|
||||
child: safeArea,
|
||||
);
|
||||
}
|
||||
|
||||
Scaffold _buildScaffold(
|
||||
BuildContext context, bool hasDrawer, bool hasRail, bool hasManage) {
|
||||
var body = _buildMainContent(context, hasManage);
|
||||
if (centered) {
|
||||
body = Center(child: body);
|
||||
}
|
||||
|
||||
if (onFileDropped != null) {
|
||||
body = FileDropTarget(
|
||||
onFileDropped: onFileDropped!,
|
||||
@ -363,10 +388,10 @@ class AppPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _CapabilityBadge extends StatelessWidget {
|
||||
class CapabilityBadge extends StatelessWidget {
|
||||
final Capability capability;
|
||||
|
||||
const _CapabilityBadge(this.capability);
|
||||
const CapabilityBadge(this.capability, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -24,10 +24,10 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../core/models.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../desktop/state.dart';
|
||||
import '../../home/views/home_message_page.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'elevate_fido_buttons.dart';
|
||||
import 'message_page.dart';
|
||||
|
||||
class DeviceErrorScreen extends ConsumerWidget {
|
||||
final DeviceNode node;
|
||||
@ -40,8 +40,7 @@ class DeviceErrorScreen extends ConsumerWidget {
|
||||
if (Platform.isWindows &&
|
||||
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||
final currentApp = ref.read(currentAppProvider);
|
||||
return MessagePage(
|
||||
title: currentApp.getDisplayName(l10n),
|
||||
return HomeMessagePage(
|
||||
capabilities: currentApp.capabilities,
|
||||
header: l10n.l_admin_privileges_required,
|
||||
message: l10n.p_elevated_permissions_required,
|
||||
@ -52,7 +51,7 @@ class DeviceErrorScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
return MessagePage(
|
||||
return HomeMessagePage(
|
||||
centered: true,
|
||||
graphic: Image.asset(
|
||||
'assets/product-images/generic.png',
|
||||
@ -70,7 +69,7 @@ class DeviceErrorScreen extends ConsumerWidget {
|
||||
return node.map(
|
||||
usbYubiKey: (node) => _buildUsbPid(context, ref, node.pid),
|
||||
nfcReader: (node) => switch (error) {
|
||||
'unknown-device' => MessagePage(
|
||||
'unknown-device' => HomeMessagePage(
|
||||
centered: true,
|
||||
graphic: Icon(
|
||||
Symbols.help,
|
||||
@ -79,7 +78,7 @@ class DeviceErrorScreen extends ConsumerWidget {
|
||||
),
|
||||
header: l10n.s_unknown_device,
|
||||
),
|
||||
_ => MessagePage(
|
||||
_ => HomeMessagePage(
|
||||
centered: true,
|
||||
graphic: Image.asset(
|
||||
'assets/graphics/no-key.png',
|
||||
|
@ -24,18 +24,11 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../android/state.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../management/models.dart';
|
||||
import '../../management/views/management_screen.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../key_customization/models.dart';
|
||||
import '../key_customization/state.dart';
|
||||
import '../key_customization/views/key_customization_dialog.dart';
|
||||
import '../message.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'device_avatar.dart';
|
||||
import 'keys.dart' as keys;
|
||||
import 'keys.dart';
|
||||
import 'reset_dialog.dart';
|
||||
|
||||
final _hiddenDevicesProvider =
|
||||
StateNotifierProvider<_HiddenDevicesNotifier, List<String>>(
|
||||
@ -276,14 +269,16 @@ class _DeviceRowState extends ConsumerState<_DeviceRow> {
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
|
||||
horizontalTitleGap: 8,
|
||||
leading: widget.leading,
|
||||
trailing: _DeviceMenuButton(
|
||||
menuItems: menuItems,
|
||||
opacity: widget.selected
|
||||
? 1.0
|
||||
: _showContextMenu
|
||||
? 0.3
|
||||
: 0.0,
|
||||
),
|
||||
trailing: menuItems.isNotEmpty
|
||||
? _DeviceMenuButton(
|
||||
menuItems: menuItems,
|
||||
opacity: widget.selected
|
||||
? 1.0
|
||||
: _showContextMenu
|
||||
? 0.3
|
||||
: 0.0,
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
widget.title,
|
||||
overflow: TextOverflow.fade,
|
||||
@ -339,44 +334,9 @@ class _DeviceRowState extends ConsumerState<_DeviceRow> {
|
||||
List<PopupMenuItem> _getMenuItems(
|
||||
BuildContext context, WidgetRef ref, DeviceNode? node) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final keyCustomizations = ref.watch(keyCustomizationManagerProvider);
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
final hidden = ref.watch(_hiddenDevicesProvider);
|
||||
|
||||
final data = ref.watch(currentDeviceDataProvider).valueOrNull;
|
||||
final managementAvailability =
|
||||
data == null || !hasFeature(features.management)
|
||||
? Availability.unsupported
|
||||
: Application.management.getAvailability(data);
|
||||
|
||||
final serial = node is UsbYubiKeyNode
|
||||
? node.info?.serial
|
||||
: data != null
|
||||
? data.node.path == node?.path && node != null
|
||||
? data.info.serial
|
||||
: null
|
||||
: null;
|
||||
|
||||
return [
|
||||
if (serial != null)
|
||||
PopupMenuItem(
|
||||
enabled: true,
|
||||
onTap: () async {
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
await _showKeyCustomizationDialog(
|
||||
keyCustomizations[serial] ?? KeyCustomization(serial: serial),
|
||||
context,
|
||||
node);
|
||||
});
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(l10n.s_customize_key_action),
|
||||
leading: const Icon(Symbols.palette),
|
||||
key: yubikeyLabelColorMenuButton,
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
enabled: true),
|
||||
),
|
||||
if (isDesktop && hidden.isNotEmpty)
|
||||
PopupMenuItem(
|
||||
enabled: hidden.isNotEmpty,
|
||||
@ -403,59 +363,8 @@ class _DeviceRowState extends ConsumerState<_DeviceRow> {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
if (node == data?.node && managementAvailability == Availability.enabled)
|
||||
PopupMenuItem(
|
||||
onTap: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagementScreen(data),
|
||||
);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(data!.info.version.major > 4
|
||||
? l10n.s_toggle_applications
|
||||
: l10n.s_toggle_interfaces),
|
||||
leading: const Icon(Symbols.construction),
|
||||
key: yubikeyApplicationToggleMenuButton,
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
if (data != null &&
|
||||
node == data.node &&
|
||||
getResetCapabilities(hasFeature).any((c) =>
|
||||
c.value &
|
||||
(data.info.supportedCapabilities[node!.transport] ?? 0) !=
|
||||
0))
|
||||
PopupMenuItem(
|
||||
onTap: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(data),
|
||||
);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(l10n.s_factory_reset),
|
||||
leading: const Icon(Symbols.delete_forever),
|
||||
key: yubikeyFactoryResetMenuButton,
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _showKeyCustomizationDialog(KeyCustomization keyCustomization,
|
||||
BuildContext context, DeviceNode? node) async {
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => KeyCustomizationDialog(
|
||||
node: node,
|
||||
initialCustomization: keyCustomization,
|
||||
),
|
||||
routeSettings: const RouteSettings(name: 'customize'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_DeviceRow _buildDeviceRow(
|
||||
|
@ -25,6 +25,7 @@ const noDeviceAvatar = Key('$_prefix.no_device_avatar');
|
||||
const actionsIconButtonKey = Key('$_prefix.actions_icon_button');
|
||||
|
||||
// drawer items
|
||||
const homeDrawer = Key('$_prefix.drawer.home');
|
||||
const managementAppDrawer = Key('$_prefix.drawer.management');
|
||||
const oathAppDrawer = Key('$_prefix.drawer.oath');
|
||||
const u2fAppDrawer = Key('$_prefix.drawer.fido.webauthn');
|
||||
|
@ -25,6 +25,8 @@ import '../../core/state.dart';
|
||||
import '../../fido/views/fingerprints_screen.dart';
|
||||
import '../../fido/views/passkeys_screen.dart';
|
||||
import '../../fido/views/webauthn_page.dart';
|
||||
import '../../home/views/home_message_page.dart';
|
||||
import '../../home/views/home_screen.dart';
|
||||
import '../../management/views/management_screen.dart';
|
||||
import '../../oath/views/oath_screen.dart';
|
||||
import '../../oath/views/utils.dart';
|
||||
@ -82,7 +84,7 @@ class MainPage extends ConsumerWidget {
|
||||
if (isAndroid) {
|
||||
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
|
||||
var isNfcEnabled = ref.watch(androidNfcStateProvider);
|
||||
return MessagePage(
|
||||
return HomeMessagePage(
|
||||
centered: true,
|
||||
graphic: noKeyImage,
|
||||
header: hasNfcSupport && isNfcEnabled
|
||||
@ -106,7 +108,7 @@ class MainPage extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return MessagePage(
|
||||
return HomeMessagePage(
|
||||
centered: true,
|
||||
delayedContent: false,
|
||||
graphic: noKeyImage,
|
||||
@ -120,7 +122,7 @@ class MainPage extends ConsumerWidget {
|
||||
final capabilities = app.capabilities;
|
||||
if (data.info.supportedCapabilities.isEmpty &&
|
||||
data.name == 'Unrecognized device') {
|
||||
return MessagePage(
|
||||
return HomeMessagePage(
|
||||
centered: true,
|
||||
graphic: Icon(
|
||||
Symbols.help,
|
||||
@ -165,6 +167,7 @@ class MainPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
return switch (app) {
|
||||
Application.home => HomeScreen(data),
|
||||
Application.accounts => OathScreen(data.node.path),
|
||||
Application.webauthn => const WebAuthnScreen(),
|
||||
Application.passkeys => PasskeysScreen(data),
|
||||
|
@ -19,8 +19,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../core/state.dart';
|
||||
import '../models.dart';
|
||||
import '../shortcuts.dart';
|
||||
import '../state.dart';
|
||||
import 'device_picker.dart';
|
||||
import 'keys.dart';
|
||||
@ -95,6 +95,7 @@ extension on Application {
|
||||
Application.slots => Symbols.touch_app,
|
||||
Application.certificates => Symbols.approval,
|
||||
Application.management => Symbols.construction,
|
||||
Application.home => Symbols.home
|
||||
};
|
||||
|
||||
Key get _key => switch (this) {
|
||||
@ -105,6 +106,7 @@ extension on Application {
|
||||
Application.slots => otpAppDrawer,
|
||||
Application.certificates => pivAppDrawer,
|
||||
Application.management => managementAppDrawer,
|
||||
Application.home => homeDrawer,
|
||||
};
|
||||
}
|
||||
|
||||
@ -125,7 +127,9 @@ class NavigationContent extends ConsumerWidget {
|
||||
.where(
|
||||
(app) => app.getAvailability(data) != Availability.unsupported)
|
||||
.toList()
|
||||
: <Application>[];
|
||||
: !isAndroid // TODO: Remove check when Home is implemented on Android
|
||||
? [Application.home]
|
||||
: <Application>[];
|
||||
availableApps.remove(Application.management);
|
||||
final currentApp = ref.watch(currentAppProvider);
|
||||
|
||||
@ -137,64 +141,36 @@ class NavigationContent extends ConsumerWidget {
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: DevicePickerContent(extended: extended),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: Column(
|
||||
children: [
|
||||
if (data != null) ...[
|
||||
// Normal YubiKey Applications
|
||||
...availableApps.map((app) => NavigationItem(
|
||||
key: app._key,
|
||||
title: app.getDisplayName(l10n),
|
||||
leading: Icon(app._icon,
|
||||
fill: app == currentApp ? 1.0 : 0.0),
|
||||
collapsed: !extended,
|
||||
selected: app == currentApp,
|
||||
onTap: app.getAvailability(data) == Availability.enabled
|
||||
? () {
|
||||
ref
|
||||
.read(currentAppProvider.notifier)
|
||||
.setCurrentApp(app);
|
||||
if (shouldPop) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
// Normal YubiKey Applications
|
||||
...availableApps.map((app) => NavigationItem(
|
||||
key: app._key,
|
||||
title: app.getDisplayName(l10n),
|
||||
leading:
|
||||
Icon(app._icon, fill: app == currentApp ? 1.0 : 0.0),
|
||||
collapsed: !extended,
|
||||
selected: app == currentApp,
|
||||
onTap: data == null && currentApp == Application.home ||
|
||||
data != null &&
|
||||
app.getAvailability(data) ==
|
||||
Availability.enabled
|
||||
? () {
|
||||
ref
|
||||
.read(currentAppProvider.notifier)
|
||||
.setCurrentApp(app);
|
||||
if (shouldPop) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
: null,
|
||||
)),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
}
|
||||
: null,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Non-YubiKey pages
|
||||
NavigationItem(
|
||||
leading: const Icon(Symbols.settings),
|
||||
key: settingDrawerIcon,
|
||||
title: l10n.s_settings,
|
||||
collapsed: !extended,
|
||||
onTap: () {
|
||||
if (shouldPop) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
Actions.maybeInvoke(context, const SettingsIntent());
|
||||
},
|
||||
),
|
||||
NavigationItem(
|
||||
leading: const Icon(Symbols.help),
|
||||
key: helpDrawerIcon,
|
||||
title: l10n.s_help_and_about,
|
||||
collapsed: !extended,
|
||||
onTap: () {
|
||||
if (shouldPop) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
Actions.maybeInvoke(context, const AboutIntent());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
82
lib/home/views/home_message_page.dart
Normal file
82
lib/home/views/home_message_page.dart
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/views/message_page.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../management/models.dart';
|
||||
import 'key_actions.dart';
|
||||
|
||||
class HomeMessagePage extends ConsumerWidget {
|
||||
final Widget? graphic;
|
||||
final String? header;
|
||||
final String? message;
|
||||
final String? footnote;
|
||||
final bool delayedContent;
|
||||
final Widget Function(BuildContext context)? actionButtonBuilder;
|
||||
final List<Widget> Function(BuildContext context, bool expanded)?
|
||||
actionsBuilder;
|
||||
final Widget? fileDropOverlay;
|
||||
final Function(File file)? onFileDropped;
|
||||
final List<Capability>? capabilities;
|
||||
final bool keyActionsBadge;
|
||||
final bool centered;
|
||||
|
||||
const HomeMessagePage({
|
||||
super.key,
|
||||
this.graphic,
|
||||
this.header,
|
||||
this.message,
|
||||
this.footnote,
|
||||
this.actionButtonBuilder,
|
||||
this.actionsBuilder,
|
||||
this.fileDropOverlay,
|
||||
this.onFileDropped,
|
||||
this.delayedContent = false,
|
||||
this.keyActionsBadge = false,
|
||||
this.capabilities,
|
||||
this.centered = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
// TODO: Remove Android check when Home is implemented on Android
|
||||
return MessagePage(
|
||||
title: !isAndroid ? l10n.s_home : null,
|
||||
graphic: graphic,
|
||||
header: header,
|
||||
message: message,
|
||||
footnote: footnote,
|
||||
keyActionsBuilder:
|
||||
!isAndroid ? (context) => homeBuildActions(context, null, ref) : null,
|
||||
actionButtonBuilder: actionButtonBuilder,
|
||||
actionsBuilder: actionsBuilder,
|
||||
fileDropOverlay: fileDropOverlay,
|
||||
onFileDropped: onFileDropped,
|
||||
delayedContent: delayedContent,
|
||||
keyActionsBadge: keyActionsBadge,
|
||||
capabilities: capabilities,
|
||||
centered: centered,
|
||||
);
|
||||
}
|
||||
}
|
351
lib/home/views/home_screen.dart
Normal file
351
lib/home/views/home_screen.dart
Normal file
@ -0,0 +1,351 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../android/state.dart';
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/app_page.dart';
|
||||
import '../../core/models.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../management/models.dart';
|
||||
import '../../widgets/choice_filter_chip.dart';
|
||||
import '../../widgets/product_image.dart';
|
||||
import 'key_actions.dart';
|
||||
import 'manage_label_dialog.dart';
|
||||
|
||||
class HomeScreen extends ConsumerWidget {
|
||||
final YubiKeyData deviceData;
|
||||
const HomeScreen(this.deviceData, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
final serial = deviceData.info.serial;
|
||||
final keyCustomization = ref.watch(keyCustomizationManagerProvider)[serial];
|
||||
final enabledCapabilities =
|
||||
deviceData.info.config.enabledCapabilities[deviceData.node.transport] ??
|
||||
0;
|
||||
final primaryColor = ref.watch(defaultColorProvider);
|
||||
|
||||
return AppPage(
|
||||
title: l10n.s_home,
|
||||
keyActionsBuilder: (context) =>
|
||||
homeBuildActions(context, deviceData, ref),
|
||||
builder: (context, expanded) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_DeviceContent(deviceData, keyCustomization),
|
||||
const SizedBox(height: 16.0),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 8,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 8,
|
||||
children: Capability.values
|
||||
.where((c) => enabledCapabilities & c.value != 0)
|
||||
.map((c) => CapabilityBadge(c))
|
||||
.toList(),
|
||||
),
|
||||
if (serial != null) ...[
|
||||
const SizedBox(height: 32.0),
|
||||
_DeviceColor(
|
||||
deviceData: deviceData,
|
||||
initialCustomization: keyCustomization ??
|
||||
KeyCustomization(serial: serial))
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
flex: 6,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 200),
|
||||
child: _HeroAvatar(
|
||||
color: keyCustomization?.color ?? primaryColor,
|
||||
child: ProductImage(
|
||||
name: deviceData.name,
|
||||
formFactor: deviceData.info.formFactor,
|
||||
isNfc: deviceData.info.supportedCapabilities
|
||||
.containsKey(Transport.nfc),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeviceContent extends ConsumerWidget {
|
||||
final YubiKeyData deviceData;
|
||||
final KeyCustomization? initialCustomization;
|
||||
const _DeviceContent(this.deviceData, this.initialCustomization);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
final name = deviceData.name;
|
||||
final serial = deviceData.info.serial;
|
||||
final version = deviceData.info.version;
|
||||
|
||||
final label = initialCustomization?.name;
|
||||
String displayName = label != null ? '$label ($name)' : name;
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
displayName,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (serial != null)
|
||||
Text(
|
||||
l10n.l_serial_number(serial),
|
||||
style: Theme.of(context).textTheme.titleSmall?.apply(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Text(
|
||||
l10n.l_firmware_version(version),
|
||||
style: Theme.of(context).textTheme.titleSmall?.apply(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (serial != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: IconButton(
|
||||
icon: const Icon(Symbols.edit),
|
||||
onPressed: () async {
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
await _showManageLabelDialog(
|
||||
initialCustomization ?? KeyCustomization(serial: serial),
|
||||
context,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showManageLabelDialog(
|
||||
KeyCustomization keyCustomization, BuildContext context) async {
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManageLabelDialog(
|
||||
initialCustomization: keyCustomization,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeviceColor extends ConsumerWidget {
|
||||
final YubiKeyData deviceData;
|
||||
final KeyCustomization initialCustomization;
|
||||
const _DeviceColor(
|
||||
{required this.deviceData, required this.initialCustomization});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
final primaryColor = ref.watch(defaultColorProvider);
|
||||
final defaultColor =
|
||||
(isAndroid && ref.read(androidSdkVersionProvider) >= 31)
|
||||
? theme.colorScheme.onSurface
|
||||
: primaryColor;
|
||||
final customColor = initialCustomization.color;
|
||||
|
||||
return ChoiceFilterChip<Color?>(
|
||||
disableHover: true,
|
||||
value: customColor,
|
||||
items: const [null],
|
||||
selected: customColor != null && customColor != defaultColor,
|
||||
itemBuilder: (e) => Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runSpacing: 8,
|
||||
spacing: 16,
|
||||
children: [
|
||||
...[
|
||||
Colors.teal,
|
||||
Colors.cyan,
|
||||
Colors.blueAccent,
|
||||
Colors.deepPurple,
|
||||
Colors.red,
|
||||
Colors.orange,
|
||||
Colors.yellow,
|
||||
// add nice color to devices with dynamic color
|
||||
if (isAndroid && ref.read(androidSdkVersionProvider) >= 31)
|
||||
Colors.lightGreen
|
||||
].map((e) => _ColorButton(
|
||||
color: e,
|
||||
isSelected: customColor == e,
|
||||
onPressed: () {
|
||||
_updateColor(e, ref);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)),
|
||||
|
||||
// remove color button
|
||||
RawMaterialButton(
|
||||
onPressed: () {
|
||||
_updateColor(null, ref);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
constraints: const BoxConstraints(minWidth: 26.0, minHeight: 26.0),
|
||||
fillColor: (isAndroid && ref.read(androidSdkVersionProvider) >= 31)
|
||||
? theme.colorScheme.onSurface
|
||||
: primaryColor,
|
||||
hoverColor: Colors.black12,
|
||||
shape: const CircleBorder(),
|
||||
child: Icon(
|
||||
Symbols.cancel,
|
||||
size: 16,
|
||||
color: customColor == null
|
||||
? theme.colorScheme.onSurface
|
||||
: theme.colorScheme.surface.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
labelBuilder: (e) => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(minWidth: 22.0, minHeight: 22.0),
|
||||
decoration: BoxDecoration(
|
||||
color: customColor ?? defaultColor, shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Flexible(child: Text(l10n.s_color))
|
||||
],
|
||||
),
|
||||
onChanged: (e) {},
|
||||
);
|
||||
}
|
||||
|
||||
void _updateColor(Color? color, WidgetRef ref) async {
|
||||
final manager = ref.read(keyCustomizationManagerProvider.notifier);
|
||||
await manager.set(
|
||||
serial: initialCustomization.serial,
|
||||
name: initialCustomization.name,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ColorButton extends StatefulWidget {
|
||||
final Color? color;
|
||||
final bool isSelected;
|
||||
final Function()? onPressed;
|
||||
|
||||
const _ColorButton({
|
||||
required this.color,
|
||||
required this.isSelected,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ColorButton> createState() => _ColorButtonState();
|
||||
}
|
||||
|
||||
class _ColorButtonState extends State<_ColorButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawMaterialButton(
|
||||
onPressed: widget.onPressed,
|
||||
constraints: const BoxConstraints(minWidth: 26.0, minHeight: 26.0),
|
||||
fillColor: widget.color,
|
||||
hoverColor: Colors.black12,
|
||||
shape: const CircleBorder(),
|
||||
child: Icon(
|
||||
Symbols.circle,
|
||||
fill: 1,
|
||||
size: 16,
|
||||
color: widget.isSelected ? Colors.white : Colors.transparent,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HeroAvatar extends StatelessWidget {
|
||||
final Widget child;
|
||||
final Color color;
|
||||
|
||||
const _HeroAvatar({required this.color, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
color.withOpacity(0.6),
|
||||
color.withOpacity(0.25),
|
||||
(DialogTheme.of(context).backgroundColor ??
|
||||
theme.dialogBackgroundColor)
|
||||
.withOpacity(0),
|
||||
],
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Theme(
|
||||
// Give the avatar a transparent background
|
||||
data: theme.copyWith(
|
||||
colorScheme:
|
||||
theme.colorScheme.copyWith(surfaceVariant: Colors.transparent)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
113
lib/home/views/key_actions.dart
Normal file
113
lib/home/views/key_actions.dart
Normal file
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../app/features.dart' as features;
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/shortcuts.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../app/views/reset_dialog.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../management/views/management_screen.dart';
|
||||
|
||||
Widget homeBuildActions(
|
||||
BuildContext context, YubiKeyData? deviceData, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
|
||||
final managementAvailability =
|
||||
deviceData == null || !hasFeature(features.management)
|
||||
? Availability.unsupported
|
||||
: Application.management.getAvailability(deviceData);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (deviceData != null)
|
||||
ActionListSection(
|
||||
l10n.s_device,
|
||||
children: [
|
||||
if (managementAvailability == Availability.enabled)
|
||||
ActionListItem(
|
||||
feature: features.management,
|
||||
icon: const Icon(Symbols.construction),
|
||||
actionStyle: ActionStyle.primary,
|
||||
title: deviceData.info.version.major > 4
|
||||
? l10n.s_toggle_applications
|
||||
: l10n.s_toggle_interfaces,
|
||||
subtitle: deviceData.info.version.major > 4
|
||||
? l10n.l_toggle_applications_desc
|
||||
: l10n.l_toggle_interfaces_desc,
|
||||
onTap: (context) async {
|
||||
await ref.read(withContextProvider)(
|
||||
(context) => showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagementScreen(deviceData),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (getResetCapabilities(hasFeature).any((c) =>
|
||||
c.value &
|
||||
(deviceData.info
|
||||
.supportedCapabilities[deviceData.node.transport] ??
|
||||
0) !=
|
||||
0))
|
||||
ActionListItem(
|
||||
icon: const Icon(Symbols.delete_forever),
|
||||
title: l10n.s_factory_reset,
|
||||
subtitle: l10n.l_factory_reset_desc,
|
||||
actionStyle: ActionStyle.primary,
|
||||
onTap: (context) async {
|
||||
await ref.read(withContextProvider)(
|
||||
(context) => showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(deviceData),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
ActionListSection(l10n.s_application, children: [
|
||||
ActionListItem(
|
||||
icon: const Icon(Symbols.settings),
|
||||
title: l10n.s_settings,
|
||||
subtitle: l10n.l_settings_desc,
|
||||
actionStyle: ActionStyle.primary,
|
||||
onTap: (_) {
|
||||
Actions.maybeInvoke(context, const SettingsIntent());
|
||||
},
|
||||
),
|
||||
ActionListItem(
|
||||
icon: const Icon(Symbols.help),
|
||||
title: l10n.s_help_and_about,
|
||||
subtitle: l10n.l_help_and_about_desc,
|
||||
actionStyle: ActionStyle.primary,
|
||||
onTap: (_) {
|
||||
Actions.maybeInvoke(context, const AboutIntent());
|
||||
},
|
||||
)
|
||||
])
|
||||
],
|
||||
);
|
||||
}
|
114
lib/home/views/manage_label_dialog.dart
Normal file
114
lib/home/views/manage_label_dialog.dart
Normal file
@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../widgets/app_input_decoration.dart';
|
||||
import '../../widgets/app_text_form_field.dart';
|
||||
import '../../widgets/focus_utils.dart';
|
||||
import '../../widgets/responsive_dialog.dart';
|
||||
|
||||
class ManageLabelDialog extends ConsumerStatefulWidget {
|
||||
final KeyCustomization initialCustomization;
|
||||
|
||||
const ManageLabelDialog({super.key, required this.initialCustomization});
|
||||
|
||||
@override
|
||||
ConsumerState<ManageLabelDialog> createState() => _ManageLabelDialogState();
|
||||
}
|
||||
|
||||
class _ManageLabelDialogState extends ConsumerState<ManageLabelDialog> {
|
||||
String? _label;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_label = widget.initialCustomization.name;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final initialLabel = widget.initialCustomization.name;
|
||||
final didChange = initialLabel != _label;
|
||||
return ResponsiveDialog(
|
||||
title:
|
||||
Text(initialLabel != null ? l10n.s_change_label : l10n.s_set_label),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: didChange ? _submit : null,
|
||||
child: Text(l10n.s_save),
|
||||
)
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (initialLabel != null) Text(l10n.q_rename_target(initialLabel)),
|
||||
Text(initialLabel == null
|
||||
? l10n.p_set_will_add_custom_name
|
||||
: l10n.p_rename_will_change_custom_name),
|
||||
AppTextFormField(
|
||||
autofocus: true,
|
||||
initialValue: _label,
|
||||
maxLength: 20,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_label,
|
||||
helperText: '',
|
||||
prefixIcon: const Icon(Symbols.key),
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
final trimmed = value.trim();
|
||||
_label = trimmed.isEmpty ? null : trimmed;
|
||||
});
|
||||
},
|
||||
onFieldSubmitted: (_) {
|
||||
_submit();
|
||||
},
|
||||
)
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: e,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _submit() async {
|
||||
final manager = ref.read(keyCustomizationManagerProvider.notifier);
|
||||
await manager.set(
|
||||
serial: widget.initialCustomization.serial,
|
||||
name: _label,
|
||||
color: widget.initialCustomization.color);
|
||||
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
FocusUtils.unfocus(context);
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
}
|
@ -61,12 +61,17 @@
|
||||
"s_actions": null,
|
||||
"s_manage": "Verwalten",
|
||||
"s_setup": "Einrichten",
|
||||
"s_device": null,
|
||||
"s_application": null,
|
||||
"s_settings": "Einstellungen",
|
||||
"l_settings_desc": null,
|
||||
"s_certificates": null,
|
||||
"s_webauthn": "WebAuthn",
|
||||
"s_slots": null,
|
||||
"s_help_and_about": "Hilfe und Über",
|
||||
"l_help_and_about_desc": null,
|
||||
"s_help_and_feedback": "Hilfe und Feedback",
|
||||
"s_home": null,
|
||||
"s_send_feedback": "Senden Sie uns Feedback",
|
||||
"s_i_need_help": "Ich brauche Hilfe",
|
||||
"s_troubleshooting": "Problembehebung",
|
||||
@ -128,6 +133,18 @@
|
||||
"version": {}
|
||||
}
|
||||
},
|
||||
"@l_serial_number": {
|
||||
"placeholders": {
|
||||
"serial": {}
|
||||
}
|
||||
},
|
||||
"l_serial_number": null,
|
||||
"@l_firmware_version": {
|
||||
"placeholders": {
|
||||
"version": {}
|
||||
}
|
||||
},
|
||||
"l_firmware_version": null,
|
||||
|
||||
"@_yubikey_interactions": {},
|
||||
"l_insert_yk": "YubiKey anschließen",
|
||||
@ -154,6 +171,8 @@
|
||||
"@_app_configuration": {},
|
||||
"s_toggle_applications": "Anwendungen umschalten",
|
||||
"s_toggle_interfaces": null,
|
||||
"l_toggle_applications_desc": null,
|
||||
"l_toggle_interfaces_desc": null,
|
||||
"s_reconfiguring_yk": "YubiKey wird neu konfiguriert\u2026",
|
||||
"s_config_updated": "Konfiguration aktualisiert",
|
||||
"l_config_updated_reinsert": "Konfiguration aktualisiert, entfernen Sie Ihren YubiKey und schließen ihn wieder an",
|
||||
@ -645,6 +664,7 @@
|
||||
"@_factory_reset": {},
|
||||
"s_reset": "Zurücksetzen",
|
||||
"s_factory_reset": "Werkseinstellungen",
|
||||
"l_factory_reset_desc": null,
|
||||
"l_oath_application_reset": "OATH Anwendung zurücksetzen",
|
||||
"l_fido_app_reset": "FIDO Anwendung zurückgesetzt",
|
||||
"l_reset_failed": "Fehler beim Zurücksetzen: {message}",
|
||||
@ -744,7 +764,12 @@
|
||||
|
||||
"@_key_customization": {},
|
||||
"s_customize_key_action": null,
|
||||
"s_set_label": null,
|
||||
"s_change_label": null,
|
||||
"s_theme_color": null,
|
||||
"s_color": null,
|
||||
"p_set_will_add_custom_name": null,
|
||||
"p_rename_will_change_custom_name": null,
|
||||
|
||||
"@_eof": {}
|
||||
}
|
||||
|
@ -61,12 +61,17 @@
|
||||
"s_actions": "Actions",
|
||||
"s_manage": "Manage",
|
||||
"s_setup": "Setup",
|
||||
"s_device": "Device",
|
||||
"s_application": "Application",
|
||||
"s_settings": "Settings",
|
||||
"l_settings_desc": "Change application preferences",
|
||||
"s_certificates": "Certificates",
|
||||
"s_webauthn": "WebAuthn",
|
||||
"s_slots": "Slots",
|
||||
"s_help_and_about": "Help and about",
|
||||
"l_help_and_about_desc": "Troubleshoot and support",
|
||||
"s_help_and_feedback": "Help and feedback",
|
||||
"s_home": "Home",
|
||||
"s_send_feedback": "Send us feedback",
|
||||
"s_i_need_help": "I need help",
|
||||
"s_troubleshooting": "Troubleshooting",
|
||||
@ -128,6 +133,18 @@
|
||||
"version": {}
|
||||
}
|
||||
},
|
||||
"@l_serial_number": {
|
||||
"placeholders": {
|
||||
"serial": {}
|
||||
}
|
||||
},
|
||||
"l_serial_number": "Serial number: {serial}",
|
||||
"@l_firmware_version": {
|
||||
"placeholders": {
|
||||
"version": {}
|
||||
}
|
||||
},
|
||||
"l_firmware_version": "Firmware version: {version}",
|
||||
|
||||
"@_yubikey_interactions": {},
|
||||
"l_insert_yk": "Insert your YubiKey",
|
||||
@ -154,6 +171,8 @@
|
||||
"@_app_configuration": {},
|
||||
"s_toggle_applications": "Toggle applications",
|
||||
"s_toggle_interfaces": "Toggle interfaces",
|
||||
"l_toggle_applications_desc": "Enable/disable applications",
|
||||
"l_toggle_interfaces_desc": "Enable/disable interfaces",
|
||||
"s_reconfiguring_yk": "Reconfiguring YubiKey\u2026",
|
||||
"s_config_updated": "Configuration updated",
|
||||
"l_config_updated_reinsert": "Configuration updated, remove and reinsert your YubiKey",
|
||||
@ -645,6 +664,7 @@
|
||||
"@_factory_reset": {},
|
||||
"s_reset": "Reset",
|
||||
"s_factory_reset": "Factory reset",
|
||||
"l_factory_reset_desc": "Restore YubiKey defaults",
|
||||
"l_oath_application_reset": "OATH application reset",
|
||||
"l_fido_app_reset": "FIDO application reset",
|
||||
"l_reset_failed": "Error performing reset: {message}",
|
||||
@ -744,7 +764,12 @@
|
||||
|
||||
"@_key_customization": {},
|
||||
"s_customize_key_action": "Set label/color",
|
||||
"s_set_label": "Set label",
|
||||
"s_change_label": "Change label",
|
||||
"s_theme_color": "Theme color",
|
||||
"s_color": "Color",
|
||||
"p_set_will_add_custom_name": "This will give your YubiKey a custom name.",
|
||||
"p_rename_will_change_custom_name": "This will change the label of your YubiKey.",
|
||||
|
||||
"@_eof": {}
|
||||
}
|
||||
|
@ -61,12 +61,17 @@
|
||||
"s_actions": "Actions",
|
||||
"s_manage": "Gérer",
|
||||
"s_setup": "Configuration",
|
||||
"s_device": null,
|
||||
"s_application": null,
|
||||
"s_settings": "Paramètres",
|
||||
"l_settings_desc": null,
|
||||
"s_certificates": "Certificats",
|
||||
"s_webauthn": "WebAuthn",
|
||||
"s_slots": null,
|
||||
"s_help_and_about": "Aide et à propos",
|
||||
"l_help_and_about_desc": null,
|
||||
"s_help_and_feedback": "Aide et retours",
|
||||
"s_home": null,
|
||||
"s_send_feedback": "Envoyer nous un retour",
|
||||
"s_i_need_help": "J'ai besoin d'aide",
|
||||
"s_troubleshooting": "Dépannage",
|
||||
@ -128,6 +133,18 @@
|
||||
"version": {}
|
||||
}
|
||||
},
|
||||
"@l_serial_number": {
|
||||
"placeholders": {
|
||||
"serial": {}
|
||||
}
|
||||
},
|
||||
"l_serial_number": null,
|
||||
"@l_firmware_version": {
|
||||
"placeholders": {
|
||||
"version": {}
|
||||
}
|
||||
},
|
||||
"l_firmware_version": null,
|
||||
|
||||
"@_yubikey_interactions": {},
|
||||
"l_insert_yk": "Insérez votre YubiKey",
|
||||
@ -154,6 +171,8 @@
|
||||
"@_app_configuration": {},
|
||||
"s_toggle_applications": "Changer les applications",
|
||||
"s_toggle_interfaces": null,
|
||||
"l_toggle_applications_desc": null,
|
||||
"l_toggle_interfaces_desc": null,
|
||||
"s_reconfiguring_yk": "Reconfiguration de la YubiKey\u2026",
|
||||
"s_config_updated": "Configuration mise à jour",
|
||||
"l_config_updated_reinsert": "Configuration mise à jour; retirez et réinsérez votre YubiKey",
|
||||
@ -645,6 +664,7 @@
|
||||
"@_factory_reset": {},
|
||||
"s_reset": "Réinitialiser",
|
||||
"s_factory_reset": "Réinitialisation",
|
||||
"l_factory_reset_desc": null,
|
||||
"l_oath_application_reset": "L'application OATH à été réinitialisée",
|
||||
"l_fido_app_reset": "L'application FIDO à été réinitialisée",
|
||||
"l_reset_failed": "Erreur pendant la réinitialisation: {message}",
|
||||
@ -744,7 +764,12 @@
|
||||
|
||||
"@_key_customization": {},
|
||||
"s_customize_key_action": null,
|
||||
"s_set_label": null,
|
||||
"s_change_label": null,
|
||||
"s_theme_color": null,
|
||||
"s_color": null,
|
||||
"p_set_will_add_custom_name": null,
|
||||
"p_rename_will_change_custom_name": null,
|
||||
|
||||
"@_eof": {}
|
||||
}
|
||||
|
@ -61,12 +61,17 @@
|
||||
"s_actions": "アクション",
|
||||
"s_manage": "管理",
|
||||
"s_setup": "セットアップ",
|
||||
"s_device": null,
|
||||
"s_application": null,
|
||||
"s_settings": "設定",
|
||||
"l_settings_desc": null,
|
||||
"s_certificates": "証明書",
|
||||
"s_webauthn": "WebAuthn",
|
||||
"s_slots": null,
|
||||
"s_help_and_about": "ヘルプと概要",
|
||||
"l_help_and_about_desc": null,
|
||||
"s_help_and_feedback": "ヘルプとフィードバック",
|
||||
"s_home": null,
|
||||
"s_send_feedback": "フィードバックの送信",
|
||||
"s_i_need_help": "ヘルプが必要",
|
||||
"s_troubleshooting": "トラブルシューティング",
|
||||
@ -128,6 +133,18 @@
|
||||
"version": {}
|
||||
}
|
||||
},
|
||||
"@l_serial_number": {
|
||||
"placeholders": {
|
||||
"serial": {}
|
||||
}
|
||||
},
|
||||
"l_serial_number": null,
|
||||
"@l_firmware_version": {
|
||||
"placeholders": {
|
||||
"version": {}
|
||||
}
|
||||
},
|
||||
"l_firmware_version": null,
|
||||
|
||||
"@_yubikey_interactions": {},
|
||||
"l_insert_yk": "YubiKeyを挿入する",
|
||||
@ -154,6 +171,8 @@
|
||||
"@_app_configuration": {},
|
||||
"s_toggle_applications": "アプリケーションの切替え",
|
||||
"s_toggle_interfaces": null,
|
||||
"l_toggle_applications_desc": null,
|
||||
"l_toggle_interfaces_desc": null,
|
||||
"s_reconfiguring_yk": "YubiKeyを再構成しています\u2026",
|
||||
"s_config_updated": "構成が更新されました",
|
||||
"l_config_updated_reinsert": "設定が更新されました。YubiKeyを取り外して再挿入してください",
|
||||
@ -645,6 +664,7 @@
|
||||
"@_factory_reset": {},
|
||||
"s_reset": "リセット",
|
||||
"s_factory_reset": "工場出荷リセット",
|
||||
"l_factory_reset_desc": null,
|
||||
"l_oath_application_reset": "OATHアプリケーションのリセット",
|
||||
"l_fido_app_reset": "FIDOアプリケーションのリセット",
|
||||
"l_reset_failed": "リセット実行中のエラー:{message}",
|
||||
@ -744,7 +764,12 @@
|
||||
|
||||
"@_key_customization": {},
|
||||
"s_customize_key_action": null,
|
||||
"s_set_label": null,
|
||||
"s_change_label": null,
|
||||
"s_theme_color": null,
|
||||
"s_color": null,
|
||||
"p_set_will_add_custom_name": null,
|
||||
"p_rename_will_change_custom_name": null,
|
||||
|
||||
"@_eof": {}
|
||||
}
|
||||
|
@ -61,12 +61,17 @@
|
||||
"s_actions": "Działania",
|
||||
"s_manage": "Zarządzaj",
|
||||
"s_setup": "Konfiguruj",
|
||||
"s_device": null,
|
||||
"s_application": null,
|
||||
"s_settings": "Ustawienia",
|
||||
"l_settings_desc": null,
|
||||
"s_certificates": "Certyfikaty",
|
||||
"s_webauthn": "WebAuthn",
|
||||
"s_slots": "Sloty",
|
||||
"s_help_and_about": "Pomoc i informacje",
|
||||
"l_help_and_about_desc": null,
|
||||
"s_help_and_feedback": "Pomoc i opinie",
|
||||
"s_home": null,
|
||||
"s_send_feedback": "Prześlij opinię",
|
||||
"s_i_need_help": "Pomoc",
|
||||
"s_troubleshooting": "Rozwiązywanie problemów",
|
||||
@ -128,6 +133,18 @@
|
||||
"version": {}
|
||||
}
|
||||
},
|
||||
"@l_serial_number": {
|
||||
"placeholders": {
|
||||
"serial": {}
|
||||
}
|
||||
},
|
||||
"l_serial_number": null,
|
||||
"@l_firmware_version": {
|
||||
"placeholders": {
|
||||
"version": {}
|
||||
}
|
||||
},
|
||||
"l_firmware_version": null,
|
||||
|
||||
"@_yubikey_interactions": {},
|
||||
"l_insert_yk": "Podłącz klucz YubiKey",
|
||||
@ -154,6 +171,8 @@
|
||||
"@_app_configuration": {},
|
||||
"s_toggle_applications": "Przełączanie funkcji",
|
||||
"s_toggle_interfaces": "Przełącz interfejsy",
|
||||
"l_toggle_applications_desc": null,
|
||||
"l_toggle_interfaces_desc": null,
|
||||
"s_reconfiguring_yk": "Rekonfigurowanie YubiKey\u2026",
|
||||
"s_config_updated": "Zaktualizowano konfigurację",
|
||||
"l_config_updated_reinsert": "Zaktualizowano konfigurację, podłącz ponownie klucz YubiKey",
|
||||
@ -645,6 +664,7 @@
|
||||
"@_factory_reset": {},
|
||||
"s_reset": "Zresetuj",
|
||||
"s_factory_reset": "Ustawienia fabryczne",
|
||||
"l_factory_reset_desc": null,
|
||||
"l_oath_application_reset": "Reset funkcji OATH",
|
||||
"l_fido_app_reset": "Reset funkcji FIDO",
|
||||
"l_reset_failed": "Błąd podczas resetowania: {message}",
|
||||
@ -744,7 +764,12 @@
|
||||
|
||||
"@_key_customization": {},
|
||||
"s_customize_key_action": "Dostosuj klucz",
|
||||
"s_set_label": null,
|
||||
"s_change_label": null,
|
||||
"s_theme_color": "Kolor motywu",
|
||||
"s_color": null,
|
||||
"p_set_will_add_custom_name": null,
|
||||
"p_rename_will_change_custom_name": null,
|
||||
|
||||
"@_eof": {}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ class ChoiceFilterChip<T> extends StatefulWidget {
|
||||
final void Function(T value)? onChanged;
|
||||
final Widget? avatar;
|
||||
final bool selected;
|
||||
final bool? disableHover;
|
||||
const ChoiceFilterChip({
|
||||
super.key,
|
||||
required this.value,
|
||||
@ -37,6 +38,7 @@ class ChoiceFilterChip<T> extends StatefulWidget {
|
||||
this.tooltip,
|
||||
this.avatar,
|
||||
this.selected = false,
|
||||
this.disableHover,
|
||||
this.labelBuilder,
|
||||
});
|
||||
|
||||
@ -69,6 +71,8 @@ class _ChoiceFilterChipState<T> extends State<ChoiceFilterChip<T>> {
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
items: widget.items
|
||||
.map((e) => PopupMenuItem<T>(
|
||||
enabled:
|
||||
widget.disableHover != null ? !widget.disableHover! : true,
|
||||
value: e,
|
||||
height: chipBox.size.height,
|
||||
textStyle: ChipTheme.of(context).labelStyle,
|
||||
|
Loading…
Reference in New Issue
Block a user