mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-24 02:33:44 +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
|
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||||
|
|
||||||
enum class OperationContext(val value: Int) {
|
enum class OperationContext(val value: Int) {
|
||||||
Oath(0), Yubikey(1), Invalid(-1);
|
Home(0), Oath(1), Yubikey(2), Invalid(-1);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getByValue(value: Int) = values().firstOrNull { it.value == value } ?: Invalid
|
fun getByValue(value: Int) = values().firstOrNull { it.value == value } ?: Invalid
|
||||||
|
@ -64,8 +64,8 @@ Future<Widget> initialize() async {
|
|||||||
oathStateProvider.overrideWithProvider(androidOathStateProvider.call),
|
oathStateProvider.overrideWithProvider(androidOathStateProvider.call),
|
||||||
credentialListProvider
|
credentialListProvider
|
||||||
.overrideWithProvider(androidCredentialListProvider.call),
|
.overrideWithProvider(androidCredentialListProvider.call),
|
||||||
currentAppProvider.overrideWith(
|
currentAppProvider.overrideWith((ref) => AndroidSubPageNotifier(
|
||||||
(ref) => AndroidSubPageNotifier(ref.watch(supportedAppsProvider))),
|
ref.watch(supportedAppsProvider), ref.watch(prefProvider))),
|
||||||
managementStateProvider.overrideWithProvider(androidManagementState.call),
|
managementStateProvider.overrideWithProvider(androidManagementState.call),
|
||||||
currentDeviceProvider.overrideWith(
|
currentDeviceProvider.overrideWith(
|
||||||
() => AndroidCurrentDeviceNotifier(),
|
() => AndroidCurrentDeviceNotifier(),
|
||||||
@ -95,6 +95,7 @@ Future<Widget> initialize() async {
|
|||||||
..setFeature(features.fido, false)
|
..setFeature(features.fido, false)
|
||||||
..setFeature(features.piv, false)
|
..setFeature(features.piv, false)
|
||||||
..setFeature(features.otp, false)
|
..setFeature(features.otp, false)
|
||||||
|
..setFeature(features.home, false)
|
||||||
..setFeature(features.management, false);
|
..setFeature(features.management, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
class AndroidSubPageNotifier extends CurrentAppNotifier {
|
class AndroidSubPageNotifier extends CurrentAppNotifier {
|
||||||
AndroidSubPageNotifier(super.supportedApps) {
|
AndroidSubPageNotifier(super.supportedApps, super.prefs) {
|
||||||
_handleSubPage(state);
|
_handleSubPage(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,5 +21,6 @@ final fido = root.feature('fido');
|
|||||||
final piv = root.feature('piv');
|
final piv = root.feature('piv');
|
||||||
final otp = root.feature('otp');
|
final otp = root.feature('otp');
|
||||||
final management = root.feature('management');
|
final management = root.feature('management');
|
||||||
|
final home = root.feature('home');
|
||||||
|
|
||||||
final fingerprints = fido.feature('fingerprints');
|
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.freezed.dart';
|
||||||
|
|
||||||
|
part 'models.g.dart';
|
||||||
|
|
||||||
const _listEquality = ListEquality();
|
const _listEquality = ListEquality();
|
||||||
|
|
||||||
enum Availability { enabled, disabled, unsupported }
|
enum Availability { enabled, disabled, unsupported }
|
||||||
|
|
||||||
enum Application {
|
enum Application {
|
||||||
|
home(),
|
||||||
accounts([Capability.oath]),
|
accounts([Capability.oath]),
|
||||||
webauthn([Capability.u2f]),
|
webauthn([Capability.u2f]),
|
||||||
fingerprints([Capability.fido2]),
|
fingerprints([Capability.fido2]),
|
||||||
@ -43,6 +46,7 @@ enum Application {
|
|||||||
const Application([this.capabilities = const []]);
|
const Application([this.capabilities = const []]);
|
||||||
|
|
||||||
String getDisplayName(AppLocalizations l10n) => switch (this) {
|
String getDisplayName(AppLocalizations l10n) => switch (this) {
|
||||||
|
Application.home => l10n.s_home,
|
||||||
Application.accounts => l10n.s_accounts,
|
Application.accounts => l10n.s_accounts,
|
||||||
Application.webauthn => l10n.s_webauthn,
|
Application.webauthn => l10n.s_webauthn,
|
||||||
Application.fingerprints => l10n.s_fingerprints,
|
Application.fingerprints => l10n.s_fingerprints,
|
||||||
@ -155,3 +159,25 @@ class WindowState with _$WindowState {
|
|||||||
@Default(false) bool hidden,
|
@Default(false) bool hidden,
|
||||||
}) = _WindowState;
|
}) = _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 =>
|
_$$WindowStateImplCopyWith<_$WindowStateImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
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:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
@ -27,7 +28,6 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
import '../core/state.dart';
|
import '../core/state.dart';
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
import 'features.dart' as features;
|
import 'features.dart' as features;
|
||||||
import 'key_customization/state.dart';
|
|
||||||
import 'logging.dart';
|
import 'logging.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
|
|
||||||
@ -40,6 +40,7 @@ const officialLocales = [
|
|||||||
|
|
||||||
extension on Application {
|
extension on Application {
|
||||||
Feature get _feature => switch (this) {
|
Feature get _feature => switch (this) {
|
||||||
|
Application.home => features.home,
|
||||||
Application.accounts => features.oath,
|
Application.accounts => features.oath,
|
||||||
Application.webauthn => features.fido,
|
Application.webauthn => features.fido,
|
||||||
Application.passkeys => features.fido,
|
Application.passkeys => features.fido,
|
||||||
@ -202,7 +203,8 @@ abstract class CurrentDeviceNotifier extends Notifier<DeviceNode?> {
|
|||||||
|
|
||||||
final currentAppProvider =
|
final currentAppProvider =
|
||||||
StateNotifierProvider<CurrentAppNotifier, Application>((ref) {
|
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) {
|
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, data) {
|
||||||
notifier._notifyDeviceChanged(data.whenOrNull(data: ((data) => data)));
|
notifier._notifyDeviceChanged(data.whenOrNull(data: ((data) => data)));
|
||||||
}, fireImmediately: true);
|
}, fireImmediately: true);
|
||||||
@ -211,16 +213,37 @@ final currentAppProvider =
|
|||||||
|
|
||||||
class CurrentAppNotifier extends StateNotifier<Application> {
|
class CurrentAppNotifier extends StateNotifier<Application> {
|
||||||
final List<Application> _supportedApps;
|
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) {
|
void setCurrentApp(Application app) {
|
||||||
state = app;
|
state = app;
|
||||||
|
_prefs.setString(_key, app.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _notifyDeviceChanged(YubiKeyData? data) {
|
void _notifyDeviceChanged(YubiKeyData? data) {
|
||||||
if (data == null ||
|
if (data == null) {
|
||||||
state.getAvailability(data) != Availability.unsupported) {
|
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
|
// Keep current app
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -230,6 +253,10 @@ class CurrentAppNotifier extends StateNotifier<Application> {
|
|||||||
orElse: () => _supportedApps.first,
|
orElse: () => _supportedApps.first,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Application _fromName(String? name, List<Application> supportedApps) =>
|
||||||
|
supportedApps.firstWhere((element) => element.name == name,
|
||||||
|
orElse: () => supportedApps.first);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class QrScanner {
|
abstract class QrScanner {
|
||||||
@ -285,3 +312,55 @@ typedef WithContext = Future<T> Function<T>(
|
|||||||
|
|
||||||
final withContextProvider = Provider<WithContext>(
|
final withContextProvider = Provider<WithContext>(
|
||||||
(ref) => ref.watch(contextConsumer.notifier).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(
|
Widget build(BuildContext context) => 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);
|
||||||
@ -160,28 +161,18 @@ class AppPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTitle(BuildContext context) {
|
Widget _buildTitle(BuildContext context) {
|
||||||
return Column(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Wrap(
|
Text(title!,
|
||||||
alignment: WrapAlignment.spaceBetween,
|
style: Theme.of(context).textTheme.displaySmall!.copyWith(
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
color: Theme.of(context).colorScheme.primary.withOpacity(0.9))),
|
||||||
spacing: 2.0,
|
if (capabilities != null)
|
||||||
|
Wrap(
|
||||||
|
spacing: 4.0,
|
||||||
runSpacing: 8.0,
|
runSpacing: 8.0,
|
||||||
children: [
|
children: [...capabilities!.map((c) => CapabilityBadge(c))],
|
||||||
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))],
|
|
||||||
)
|
|
||||||
])
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -189,10 +180,11 @@ class AppPage extends StatelessWidget {
|
|||||||
Widget _buildMainContent(BuildContext context, bool expanded) {
|
Widget _buildMainContent(BuildContext context, bool expanded) {
|
||||||
final actions = actionsBuilder?.call(context, expanded) ?? [];
|
final actions = actionsBuilder?.call(context, expanded) ?? [];
|
||||||
final content = Column(
|
final content = Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (title != null)
|
if (title != null && !centered)
|
||||||
Padding(
|
Padding(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 24.0),
|
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(
|
return SingleChildScrollView(
|
||||||
primary: false,
|
primary: false,
|
||||||
child: SafeArea(
|
child: safeArea,
|
||||||
child: delayedContent
|
|
||||||
? DelayedVisibility(
|
|
||||||
key: GlobalKey(), // Ensure we reset the delay on rebuild
|
|
||||||
delay: const Duration(milliseconds: 400),
|
|
||||||
child: content,
|
|
||||||
)
|
|
||||||
: content,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold _buildScaffold(
|
Scaffold _buildScaffold(
|
||||||
BuildContext context, bool hasDrawer, bool hasRail, bool hasManage) {
|
BuildContext context, bool hasDrawer, bool hasRail, bool hasManage) {
|
||||||
var body = _buildMainContent(context, hasManage);
|
var body = _buildMainContent(context, hasManage);
|
||||||
if (centered) {
|
|
||||||
body = Center(child: body);
|
|
||||||
}
|
|
||||||
if (onFileDropped != null) {
|
if (onFileDropped != null) {
|
||||||
body = FileDropTarget(
|
body = FileDropTarget(
|
||||||
onFileDropped: onFileDropped!,
|
onFileDropped: onFileDropped!,
|
||||||
@ -363,10 +388,10 @@ class AppPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CapabilityBadge extends StatelessWidget {
|
class CapabilityBadge extends StatelessWidget {
|
||||||
final Capability capability;
|
final Capability capability;
|
||||||
|
|
||||||
const _CapabilityBadge(this.capability);
|
const CapabilityBadge(this.capability, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -24,10 +24,10 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import '../../core/models.dart';
|
import '../../core/models.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../../desktop/state.dart';
|
import '../../desktop/state.dart';
|
||||||
|
import '../../home/views/home_message_page.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'elevate_fido_buttons.dart';
|
import 'elevate_fido_buttons.dart';
|
||||||
import 'message_page.dart';
|
|
||||||
|
|
||||||
class DeviceErrorScreen extends ConsumerWidget {
|
class DeviceErrorScreen extends ConsumerWidget {
|
||||||
final DeviceNode node;
|
final DeviceNode node;
|
||||||
@ -40,8 +40,7 @@ class DeviceErrorScreen extends ConsumerWidget {
|
|||||||
if (Platform.isWindows &&
|
if (Platform.isWindows &&
|
||||||
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||||
final currentApp = ref.read(currentAppProvider);
|
final currentApp = ref.read(currentAppProvider);
|
||||||
return MessagePage(
|
return HomeMessagePage(
|
||||||
title: currentApp.getDisplayName(l10n),
|
|
||||||
capabilities: currentApp.capabilities,
|
capabilities: currentApp.capabilities,
|
||||||
header: l10n.l_admin_privileges_required,
|
header: l10n.l_admin_privileges_required,
|
||||||
message: l10n.p_elevated_permissions_required,
|
message: l10n.p_elevated_permissions_required,
|
||||||
@ -52,7 +51,7 @@ class DeviceErrorScreen extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return MessagePage(
|
return HomeMessagePage(
|
||||||
centered: true,
|
centered: true,
|
||||||
graphic: Image.asset(
|
graphic: Image.asset(
|
||||||
'assets/product-images/generic.png',
|
'assets/product-images/generic.png',
|
||||||
@ -70,7 +69,7 @@ class DeviceErrorScreen extends ConsumerWidget {
|
|||||||
return node.map(
|
return node.map(
|
||||||
usbYubiKey: (node) => _buildUsbPid(context, ref, node.pid),
|
usbYubiKey: (node) => _buildUsbPid(context, ref, node.pid),
|
||||||
nfcReader: (node) => switch (error) {
|
nfcReader: (node) => switch (error) {
|
||||||
'unknown-device' => MessagePage(
|
'unknown-device' => HomeMessagePage(
|
||||||
centered: true,
|
centered: true,
|
||||||
graphic: Icon(
|
graphic: Icon(
|
||||||
Symbols.help,
|
Symbols.help,
|
||||||
@ -79,7 +78,7 @@ class DeviceErrorScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
header: l10n.s_unknown_device,
|
header: l10n.s_unknown_device,
|
||||||
),
|
),
|
||||||
_ => MessagePage(
|
_ => HomeMessagePage(
|
||||||
centered: true,
|
centered: true,
|
||||||
graphic: Image.asset(
|
graphic: Image.asset(
|
||||||
'assets/graphics/no-key.png',
|
'assets/graphics/no-key.png',
|
||||||
|
@ -24,18 +24,11 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
import '../../android/state.dart';
|
import '../../android/state.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../../management/models.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 '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'device_avatar.dart';
|
import 'device_avatar.dart';
|
||||||
import 'keys.dart' as keys;
|
import 'keys.dart' as keys;
|
||||||
import 'keys.dart';
|
import 'keys.dart';
|
||||||
import 'reset_dialog.dart';
|
|
||||||
|
|
||||||
final _hiddenDevicesProvider =
|
final _hiddenDevicesProvider =
|
||||||
StateNotifierProvider<_HiddenDevicesNotifier, List<String>>(
|
StateNotifierProvider<_HiddenDevicesNotifier, List<String>>(
|
||||||
@ -276,14 +269,16 @@ class _DeviceRowState extends ConsumerState<_DeviceRow> {
|
|||||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
|
||||||
horizontalTitleGap: 8,
|
horizontalTitleGap: 8,
|
||||||
leading: widget.leading,
|
leading: widget.leading,
|
||||||
trailing: _DeviceMenuButton(
|
trailing: menuItems.isNotEmpty
|
||||||
menuItems: menuItems,
|
? _DeviceMenuButton(
|
||||||
opacity: widget.selected
|
menuItems: menuItems,
|
||||||
? 1.0
|
opacity: widget.selected
|
||||||
: _showContextMenu
|
? 1.0
|
||||||
? 0.3
|
: _showContextMenu
|
||||||
: 0.0,
|
? 0.3
|
||||||
),
|
: 0.0,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
title: Text(
|
title: Text(
|
||||||
widget.title,
|
widget.title,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
@ -339,44 +334,9 @@ class _DeviceRowState extends ConsumerState<_DeviceRow> {
|
|||||||
List<PopupMenuItem> _getMenuItems(
|
List<PopupMenuItem> _getMenuItems(
|
||||||
BuildContext context, WidgetRef ref, DeviceNode? node) {
|
BuildContext context, WidgetRef ref, DeviceNode? node) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final keyCustomizations = ref.watch(keyCustomizationManagerProvider);
|
|
||||||
final hasFeature = ref.watch(featureProvider);
|
|
||||||
final hidden = ref.watch(_hiddenDevicesProvider);
|
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 [
|
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)
|
if (isDesktop && hidden.isNotEmpty)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
enabled: hidden.isNotEmpty,
|
enabled: hidden.isNotEmpty,
|
||||||
@ -403,59 +363,8 @@ class _DeviceRowState extends ConsumerState<_DeviceRow> {
|
|||||||
contentPadding: EdgeInsets.zero,
|
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(
|
_DeviceRow _buildDeviceRow(
|
||||||
|
@ -25,6 +25,7 @@ const noDeviceAvatar = Key('$_prefix.no_device_avatar');
|
|||||||
const actionsIconButtonKey = Key('$_prefix.actions_icon_button');
|
const actionsIconButtonKey = Key('$_prefix.actions_icon_button');
|
||||||
|
|
||||||
// drawer items
|
// drawer items
|
||||||
|
const homeDrawer = Key('$_prefix.drawer.home');
|
||||||
const managementAppDrawer = Key('$_prefix.drawer.management');
|
const managementAppDrawer = Key('$_prefix.drawer.management');
|
||||||
const oathAppDrawer = Key('$_prefix.drawer.oath');
|
const oathAppDrawer = Key('$_prefix.drawer.oath');
|
||||||
const u2fAppDrawer = Key('$_prefix.drawer.fido.webauthn');
|
const u2fAppDrawer = Key('$_prefix.drawer.fido.webauthn');
|
||||||
|
@ -25,6 +25,8 @@ import '../../core/state.dart';
|
|||||||
import '../../fido/views/fingerprints_screen.dart';
|
import '../../fido/views/fingerprints_screen.dart';
|
||||||
import '../../fido/views/passkeys_screen.dart';
|
import '../../fido/views/passkeys_screen.dart';
|
||||||
import '../../fido/views/webauthn_page.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 '../../management/views/management_screen.dart';
|
||||||
import '../../oath/views/oath_screen.dart';
|
import '../../oath/views/oath_screen.dart';
|
||||||
import '../../oath/views/utils.dart';
|
import '../../oath/views/utils.dart';
|
||||||
@ -82,7 +84,7 @@ class MainPage extends ConsumerWidget {
|
|||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
|
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
|
||||||
var isNfcEnabled = ref.watch(androidNfcStateProvider);
|
var isNfcEnabled = ref.watch(androidNfcStateProvider);
|
||||||
return MessagePage(
|
return HomeMessagePage(
|
||||||
centered: true,
|
centered: true,
|
||||||
graphic: noKeyImage,
|
graphic: noKeyImage,
|
||||||
header: hasNfcSupport && isNfcEnabled
|
header: hasNfcSupport && isNfcEnabled
|
||||||
@ -106,7 +108,7 @@ class MainPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return MessagePage(
|
return HomeMessagePage(
|
||||||
centered: true,
|
centered: true,
|
||||||
delayedContent: false,
|
delayedContent: false,
|
||||||
graphic: noKeyImage,
|
graphic: noKeyImage,
|
||||||
@ -120,7 +122,7 @@ class MainPage extends ConsumerWidget {
|
|||||||
final capabilities = app.capabilities;
|
final capabilities = app.capabilities;
|
||||||
if (data.info.supportedCapabilities.isEmpty &&
|
if (data.info.supportedCapabilities.isEmpty &&
|
||||||
data.name == 'Unrecognized device') {
|
data.name == 'Unrecognized device') {
|
||||||
return MessagePage(
|
return HomeMessagePage(
|
||||||
centered: true,
|
centered: true,
|
||||||
graphic: Icon(
|
graphic: Icon(
|
||||||
Symbols.help,
|
Symbols.help,
|
||||||
@ -165,6 +167,7 @@ class MainPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return switch (app) {
|
return switch (app) {
|
||||||
|
Application.home => HomeScreen(data),
|
||||||
Application.accounts => OathScreen(data.node.path),
|
Application.accounts => OathScreen(data.node.path),
|
||||||
Application.webauthn => const WebAuthnScreen(),
|
Application.webauthn => const WebAuthnScreen(),
|
||||||
Application.passkeys => PasskeysScreen(data),
|
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:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
import '../../core/state.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../shortcuts.dart';
|
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'device_picker.dart';
|
import 'device_picker.dart';
|
||||||
import 'keys.dart';
|
import 'keys.dart';
|
||||||
@ -95,6 +95,7 @@ extension on Application {
|
|||||||
Application.slots => Symbols.touch_app,
|
Application.slots => Symbols.touch_app,
|
||||||
Application.certificates => Symbols.approval,
|
Application.certificates => Symbols.approval,
|
||||||
Application.management => Symbols.construction,
|
Application.management => Symbols.construction,
|
||||||
|
Application.home => Symbols.home
|
||||||
};
|
};
|
||||||
|
|
||||||
Key get _key => switch (this) {
|
Key get _key => switch (this) {
|
||||||
@ -105,6 +106,7 @@ extension on Application {
|
|||||||
Application.slots => otpAppDrawer,
|
Application.slots => otpAppDrawer,
|
||||||
Application.certificates => pivAppDrawer,
|
Application.certificates => pivAppDrawer,
|
||||||
Application.management => managementAppDrawer,
|
Application.management => managementAppDrawer,
|
||||||
|
Application.home => homeDrawer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +127,9 @@ class NavigationContent extends ConsumerWidget {
|
|||||||
.where(
|
.where(
|
||||||
(app) => app.getAvailability(data) != Availability.unsupported)
|
(app) => app.getAvailability(data) != Availability.unsupported)
|
||||||
.toList()
|
.toList()
|
||||||
: <Application>[];
|
: !isAndroid // TODO: Remove check when Home is implemented on Android
|
||||||
|
? [Application.home]
|
||||||
|
: <Application>[];
|
||||||
availableApps.remove(Application.management);
|
availableApps.remove(Application.management);
|
||||||
final currentApp = ref.watch(currentAppProvider);
|
final currentApp = ref.watch(currentAppProvider);
|
||||||
|
|
||||||
@ -137,64 +141,36 @@ class NavigationContent extends ConsumerWidget {
|
|||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
child: DevicePickerContent(extended: extended),
|
child: DevicePickerContent(extended: extended),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
AnimatedSize(
|
AnimatedSize(
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (data != null) ...[
|
// Normal YubiKey Applications
|
||||||
// Normal YubiKey Applications
|
...availableApps.map((app) => NavigationItem(
|
||||||
...availableApps.map((app) => NavigationItem(
|
key: app._key,
|
||||||
key: app._key,
|
title: app.getDisplayName(l10n),
|
||||||
title: app.getDisplayName(l10n),
|
leading:
|
||||||
leading: Icon(app._icon,
|
Icon(app._icon, fill: app == currentApp ? 1.0 : 0.0),
|
||||||
fill: app == currentApp ? 1.0 : 0.0),
|
collapsed: !extended,
|
||||||
collapsed: !extended,
|
selected: app == currentApp,
|
||||||
selected: app == currentApp,
|
onTap: data == null && currentApp == Application.home ||
|
||||||
onTap: app.getAvailability(data) == Availability.enabled
|
data != null &&
|
||||||
? () {
|
app.getAvailability(data) ==
|
||||||
ref
|
Availability.enabled
|
||||||
.read(currentAppProvider.notifier)
|
? () {
|
||||||
.setCurrentApp(app);
|
ref
|
||||||
if (shouldPop) {
|
.read(currentAppProvider.notifier)
|
||||||
Navigator.of(context).pop();
|
.setCurrentApp(app);
|
||||||
}
|
if (shouldPop) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
: null,
|
}
|
||||||
)),
|
: null,
|
||||||
const SizedBox(height: 32),
|
)),
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 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_actions": null,
|
||||||
"s_manage": "Verwalten",
|
"s_manage": "Verwalten",
|
||||||
"s_setup": "Einrichten",
|
"s_setup": "Einrichten",
|
||||||
|
"s_device": null,
|
||||||
|
"s_application": null,
|
||||||
"s_settings": "Einstellungen",
|
"s_settings": "Einstellungen",
|
||||||
|
"l_settings_desc": null,
|
||||||
"s_certificates": null,
|
"s_certificates": null,
|
||||||
"s_webauthn": "WebAuthn",
|
"s_webauthn": "WebAuthn",
|
||||||
"s_slots": null,
|
"s_slots": null,
|
||||||
"s_help_and_about": "Hilfe und Über",
|
"s_help_and_about": "Hilfe und Über",
|
||||||
|
"l_help_and_about_desc": null,
|
||||||
"s_help_and_feedback": "Hilfe und Feedback",
|
"s_help_and_feedback": "Hilfe und Feedback",
|
||||||
|
"s_home": null,
|
||||||
"s_send_feedback": "Senden Sie uns Feedback",
|
"s_send_feedback": "Senden Sie uns Feedback",
|
||||||
"s_i_need_help": "Ich brauche Hilfe",
|
"s_i_need_help": "Ich brauche Hilfe",
|
||||||
"s_troubleshooting": "Problembehebung",
|
"s_troubleshooting": "Problembehebung",
|
||||||
@ -128,6 +133,18 @@
|
|||||||
"version": {}
|
"version": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@l_serial_number": {
|
||||||
|
"placeholders": {
|
||||||
|
"serial": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_serial_number": null,
|
||||||
|
"@l_firmware_version": {
|
||||||
|
"placeholders": {
|
||||||
|
"version": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_firmware_version": null,
|
||||||
|
|
||||||
"@_yubikey_interactions": {},
|
"@_yubikey_interactions": {},
|
||||||
"l_insert_yk": "YubiKey anschließen",
|
"l_insert_yk": "YubiKey anschließen",
|
||||||
@ -154,6 +171,8 @@
|
|||||||
"@_app_configuration": {},
|
"@_app_configuration": {},
|
||||||
"s_toggle_applications": "Anwendungen umschalten",
|
"s_toggle_applications": "Anwendungen umschalten",
|
||||||
"s_toggle_interfaces": null,
|
"s_toggle_interfaces": null,
|
||||||
|
"l_toggle_applications_desc": null,
|
||||||
|
"l_toggle_interfaces_desc": null,
|
||||||
"s_reconfiguring_yk": "YubiKey wird neu konfiguriert\u2026",
|
"s_reconfiguring_yk": "YubiKey wird neu konfiguriert\u2026",
|
||||||
"s_config_updated": "Konfiguration aktualisiert",
|
"s_config_updated": "Konfiguration aktualisiert",
|
||||||
"l_config_updated_reinsert": "Konfiguration aktualisiert, entfernen Sie Ihren YubiKey und schließen ihn wieder an",
|
"l_config_updated_reinsert": "Konfiguration aktualisiert, entfernen Sie Ihren YubiKey und schließen ihn wieder an",
|
||||||
@ -645,6 +664,7 @@
|
|||||||
"@_factory_reset": {},
|
"@_factory_reset": {},
|
||||||
"s_reset": "Zurücksetzen",
|
"s_reset": "Zurücksetzen",
|
||||||
"s_factory_reset": "Werkseinstellungen",
|
"s_factory_reset": "Werkseinstellungen",
|
||||||
|
"l_factory_reset_desc": null,
|
||||||
"l_oath_application_reset": "OATH Anwendung zurücksetzen",
|
"l_oath_application_reset": "OATH Anwendung zurücksetzen",
|
||||||
"l_fido_app_reset": "FIDO Anwendung zurückgesetzt",
|
"l_fido_app_reset": "FIDO Anwendung zurückgesetzt",
|
||||||
"l_reset_failed": "Fehler beim Zurücksetzen: {message}",
|
"l_reset_failed": "Fehler beim Zurücksetzen: {message}",
|
||||||
@ -744,7 +764,12 @@
|
|||||||
|
|
||||||
"@_key_customization": {},
|
"@_key_customization": {},
|
||||||
"s_customize_key_action": null,
|
"s_customize_key_action": null,
|
||||||
|
"s_set_label": null,
|
||||||
|
"s_change_label": null,
|
||||||
"s_theme_color": null,
|
"s_theme_color": null,
|
||||||
|
"s_color": null,
|
||||||
|
"p_set_will_add_custom_name": null,
|
||||||
|
"p_rename_will_change_custom_name": null,
|
||||||
|
|
||||||
"@_eof": {}
|
"@_eof": {}
|
||||||
}
|
}
|
||||||
|
@ -61,12 +61,17 @@
|
|||||||
"s_actions": "Actions",
|
"s_actions": "Actions",
|
||||||
"s_manage": "Manage",
|
"s_manage": "Manage",
|
||||||
"s_setup": "Setup",
|
"s_setup": "Setup",
|
||||||
|
"s_device": "Device",
|
||||||
|
"s_application": "Application",
|
||||||
"s_settings": "Settings",
|
"s_settings": "Settings",
|
||||||
|
"l_settings_desc": "Change application preferences",
|
||||||
"s_certificates": "Certificates",
|
"s_certificates": "Certificates",
|
||||||
"s_webauthn": "WebAuthn",
|
"s_webauthn": "WebAuthn",
|
||||||
"s_slots": "Slots",
|
"s_slots": "Slots",
|
||||||
"s_help_and_about": "Help and about",
|
"s_help_and_about": "Help and about",
|
||||||
|
"l_help_and_about_desc": "Troubleshoot and support",
|
||||||
"s_help_and_feedback": "Help and feedback",
|
"s_help_and_feedback": "Help and feedback",
|
||||||
|
"s_home": "Home",
|
||||||
"s_send_feedback": "Send us feedback",
|
"s_send_feedback": "Send us feedback",
|
||||||
"s_i_need_help": "I need help",
|
"s_i_need_help": "I need help",
|
||||||
"s_troubleshooting": "Troubleshooting",
|
"s_troubleshooting": "Troubleshooting",
|
||||||
@ -128,6 +133,18 @@
|
|||||||
"version": {}
|
"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": {},
|
"@_yubikey_interactions": {},
|
||||||
"l_insert_yk": "Insert your YubiKey",
|
"l_insert_yk": "Insert your YubiKey",
|
||||||
@ -154,6 +171,8 @@
|
|||||||
"@_app_configuration": {},
|
"@_app_configuration": {},
|
||||||
"s_toggle_applications": "Toggle applications",
|
"s_toggle_applications": "Toggle applications",
|
||||||
"s_toggle_interfaces": "Toggle interfaces",
|
"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_reconfiguring_yk": "Reconfiguring YubiKey\u2026",
|
||||||
"s_config_updated": "Configuration updated",
|
"s_config_updated": "Configuration updated",
|
||||||
"l_config_updated_reinsert": "Configuration updated, remove and reinsert your YubiKey",
|
"l_config_updated_reinsert": "Configuration updated, remove and reinsert your YubiKey",
|
||||||
@ -645,6 +664,7 @@
|
|||||||
"@_factory_reset": {},
|
"@_factory_reset": {},
|
||||||
"s_reset": "Reset",
|
"s_reset": "Reset",
|
||||||
"s_factory_reset": "Factory reset",
|
"s_factory_reset": "Factory reset",
|
||||||
|
"l_factory_reset_desc": "Restore YubiKey defaults",
|
||||||
"l_oath_application_reset": "OATH application reset",
|
"l_oath_application_reset": "OATH application reset",
|
||||||
"l_fido_app_reset": "FIDO application reset",
|
"l_fido_app_reset": "FIDO application reset",
|
||||||
"l_reset_failed": "Error performing reset: {message}",
|
"l_reset_failed": "Error performing reset: {message}",
|
||||||
@ -744,7 +764,12 @@
|
|||||||
|
|
||||||
"@_key_customization": {},
|
"@_key_customization": {},
|
||||||
"s_customize_key_action": "Set label/color",
|
"s_customize_key_action": "Set label/color",
|
||||||
|
"s_set_label": "Set label",
|
||||||
|
"s_change_label": "Change label",
|
||||||
"s_theme_color": "Theme color",
|
"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": {}
|
"@_eof": {}
|
||||||
}
|
}
|
||||||
|
@ -61,12 +61,17 @@
|
|||||||
"s_actions": "Actions",
|
"s_actions": "Actions",
|
||||||
"s_manage": "Gérer",
|
"s_manage": "Gérer",
|
||||||
"s_setup": "Configuration",
|
"s_setup": "Configuration",
|
||||||
|
"s_device": null,
|
||||||
|
"s_application": null,
|
||||||
"s_settings": "Paramètres",
|
"s_settings": "Paramètres",
|
||||||
|
"l_settings_desc": null,
|
||||||
"s_certificates": "Certificats",
|
"s_certificates": "Certificats",
|
||||||
"s_webauthn": "WebAuthn",
|
"s_webauthn": "WebAuthn",
|
||||||
"s_slots": null,
|
"s_slots": null,
|
||||||
"s_help_and_about": "Aide et à propos",
|
"s_help_and_about": "Aide et à propos",
|
||||||
|
"l_help_and_about_desc": null,
|
||||||
"s_help_and_feedback": "Aide et retours",
|
"s_help_and_feedback": "Aide et retours",
|
||||||
|
"s_home": null,
|
||||||
"s_send_feedback": "Envoyer nous un retour",
|
"s_send_feedback": "Envoyer nous un retour",
|
||||||
"s_i_need_help": "J'ai besoin d'aide",
|
"s_i_need_help": "J'ai besoin d'aide",
|
||||||
"s_troubleshooting": "Dépannage",
|
"s_troubleshooting": "Dépannage",
|
||||||
@ -128,6 +133,18 @@
|
|||||||
"version": {}
|
"version": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@l_serial_number": {
|
||||||
|
"placeholders": {
|
||||||
|
"serial": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_serial_number": null,
|
||||||
|
"@l_firmware_version": {
|
||||||
|
"placeholders": {
|
||||||
|
"version": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_firmware_version": null,
|
||||||
|
|
||||||
"@_yubikey_interactions": {},
|
"@_yubikey_interactions": {},
|
||||||
"l_insert_yk": "Insérez votre YubiKey",
|
"l_insert_yk": "Insérez votre YubiKey",
|
||||||
@ -154,6 +171,8 @@
|
|||||||
"@_app_configuration": {},
|
"@_app_configuration": {},
|
||||||
"s_toggle_applications": "Changer les applications",
|
"s_toggle_applications": "Changer les applications",
|
||||||
"s_toggle_interfaces": null,
|
"s_toggle_interfaces": null,
|
||||||
|
"l_toggle_applications_desc": null,
|
||||||
|
"l_toggle_interfaces_desc": null,
|
||||||
"s_reconfiguring_yk": "Reconfiguration de la YubiKey\u2026",
|
"s_reconfiguring_yk": "Reconfiguration de la YubiKey\u2026",
|
||||||
"s_config_updated": "Configuration mise à jour",
|
"s_config_updated": "Configuration mise à jour",
|
||||||
"l_config_updated_reinsert": "Configuration mise à jour; retirez et réinsérez votre YubiKey",
|
"l_config_updated_reinsert": "Configuration mise à jour; retirez et réinsérez votre YubiKey",
|
||||||
@ -645,6 +664,7 @@
|
|||||||
"@_factory_reset": {},
|
"@_factory_reset": {},
|
||||||
"s_reset": "Réinitialiser",
|
"s_reset": "Réinitialiser",
|
||||||
"s_factory_reset": "Réinitialisation",
|
"s_factory_reset": "Réinitialisation",
|
||||||
|
"l_factory_reset_desc": null,
|
||||||
"l_oath_application_reset": "L'application OATH à été réinitialisée",
|
"l_oath_application_reset": "L'application OATH à été réinitialisée",
|
||||||
"l_fido_app_reset": "L'application FIDO à été réinitialisée",
|
"l_fido_app_reset": "L'application FIDO à été réinitialisée",
|
||||||
"l_reset_failed": "Erreur pendant la réinitialisation: {message}",
|
"l_reset_failed": "Erreur pendant la réinitialisation: {message}",
|
||||||
@ -744,7 +764,12 @@
|
|||||||
|
|
||||||
"@_key_customization": {},
|
"@_key_customization": {},
|
||||||
"s_customize_key_action": null,
|
"s_customize_key_action": null,
|
||||||
|
"s_set_label": null,
|
||||||
|
"s_change_label": null,
|
||||||
"s_theme_color": null,
|
"s_theme_color": null,
|
||||||
|
"s_color": null,
|
||||||
|
"p_set_will_add_custom_name": null,
|
||||||
|
"p_rename_will_change_custom_name": null,
|
||||||
|
|
||||||
"@_eof": {}
|
"@_eof": {}
|
||||||
}
|
}
|
||||||
|
@ -61,12 +61,17 @@
|
|||||||
"s_actions": "アクション",
|
"s_actions": "アクション",
|
||||||
"s_manage": "管理",
|
"s_manage": "管理",
|
||||||
"s_setup": "セットアップ",
|
"s_setup": "セットアップ",
|
||||||
|
"s_device": null,
|
||||||
|
"s_application": null,
|
||||||
"s_settings": "設定",
|
"s_settings": "設定",
|
||||||
|
"l_settings_desc": null,
|
||||||
"s_certificates": "証明書",
|
"s_certificates": "証明書",
|
||||||
"s_webauthn": "WebAuthn",
|
"s_webauthn": "WebAuthn",
|
||||||
"s_slots": null,
|
"s_slots": null,
|
||||||
"s_help_and_about": "ヘルプと概要",
|
"s_help_and_about": "ヘルプと概要",
|
||||||
|
"l_help_and_about_desc": null,
|
||||||
"s_help_and_feedback": "ヘルプとフィードバック",
|
"s_help_and_feedback": "ヘルプとフィードバック",
|
||||||
|
"s_home": null,
|
||||||
"s_send_feedback": "フィードバックの送信",
|
"s_send_feedback": "フィードバックの送信",
|
||||||
"s_i_need_help": "ヘルプが必要",
|
"s_i_need_help": "ヘルプが必要",
|
||||||
"s_troubleshooting": "トラブルシューティング",
|
"s_troubleshooting": "トラブルシューティング",
|
||||||
@ -128,6 +133,18 @@
|
|||||||
"version": {}
|
"version": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@l_serial_number": {
|
||||||
|
"placeholders": {
|
||||||
|
"serial": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_serial_number": null,
|
||||||
|
"@l_firmware_version": {
|
||||||
|
"placeholders": {
|
||||||
|
"version": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_firmware_version": null,
|
||||||
|
|
||||||
"@_yubikey_interactions": {},
|
"@_yubikey_interactions": {},
|
||||||
"l_insert_yk": "YubiKeyを挿入する",
|
"l_insert_yk": "YubiKeyを挿入する",
|
||||||
@ -154,6 +171,8 @@
|
|||||||
"@_app_configuration": {},
|
"@_app_configuration": {},
|
||||||
"s_toggle_applications": "アプリケーションの切替え",
|
"s_toggle_applications": "アプリケーションの切替え",
|
||||||
"s_toggle_interfaces": null,
|
"s_toggle_interfaces": null,
|
||||||
|
"l_toggle_applications_desc": null,
|
||||||
|
"l_toggle_interfaces_desc": null,
|
||||||
"s_reconfiguring_yk": "YubiKeyを再構成しています\u2026",
|
"s_reconfiguring_yk": "YubiKeyを再構成しています\u2026",
|
||||||
"s_config_updated": "構成が更新されました",
|
"s_config_updated": "構成が更新されました",
|
||||||
"l_config_updated_reinsert": "設定が更新されました。YubiKeyを取り外して再挿入してください",
|
"l_config_updated_reinsert": "設定が更新されました。YubiKeyを取り外して再挿入してください",
|
||||||
@ -645,6 +664,7 @@
|
|||||||
"@_factory_reset": {},
|
"@_factory_reset": {},
|
||||||
"s_reset": "リセット",
|
"s_reset": "リセット",
|
||||||
"s_factory_reset": "工場出荷リセット",
|
"s_factory_reset": "工場出荷リセット",
|
||||||
|
"l_factory_reset_desc": null,
|
||||||
"l_oath_application_reset": "OATHアプリケーションのリセット",
|
"l_oath_application_reset": "OATHアプリケーションのリセット",
|
||||||
"l_fido_app_reset": "FIDOアプリケーションのリセット",
|
"l_fido_app_reset": "FIDOアプリケーションのリセット",
|
||||||
"l_reset_failed": "リセット実行中のエラー:{message}",
|
"l_reset_failed": "リセット実行中のエラー:{message}",
|
||||||
@ -744,7 +764,12 @@
|
|||||||
|
|
||||||
"@_key_customization": {},
|
"@_key_customization": {},
|
||||||
"s_customize_key_action": null,
|
"s_customize_key_action": null,
|
||||||
|
"s_set_label": null,
|
||||||
|
"s_change_label": null,
|
||||||
"s_theme_color": null,
|
"s_theme_color": null,
|
||||||
|
"s_color": null,
|
||||||
|
"p_set_will_add_custom_name": null,
|
||||||
|
"p_rename_will_change_custom_name": null,
|
||||||
|
|
||||||
"@_eof": {}
|
"@_eof": {}
|
||||||
}
|
}
|
||||||
|
@ -61,12 +61,17 @@
|
|||||||
"s_actions": "Działania",
|
"s_actions": "Działania",
|
||||||
"s_manage": "Zarządzaj",
|
"s_manage": "Zarządzaj",
|
||||||
"s_setup": "Konfiguruj",
|
"s_setup": "Konfiguruj",
|
||||||
|
"s_device": null,
|
||||||
|
"s_application": null,
|
||||||
"s_settings": "Ustawienia",
|
"s_settings": "Ustawienia",
|
||||||
|
"l_settings_desc": null,
|
||||||
"s_certificates": "Certyfikaty",
|
"s_certificates": "Certyfikaty",
|
||||||
"s_webauthn": "WebAuthn",
|
"s_webauthn": "WebAuthn",
|
||||||
"s_slots": "Sloty",
|
"s_slots": "Sloty",
|
||||||
"s_help_and_about": "Pomoc i informacje",
|
"s_help_and_about": "Pomoc i informacje",
|
||||||
|
"l_help_and_about_desc": null,
|
||||||
"s_help_and_feedback": "Pomoc i opinie",
|
"s_help_and_feedback": "Pomoc i opinie",
|
||||||
|
"s_home": null,
|
||||||
"s_send_feedback": "Prześlij opinię",
|
"s_send_feedback": "Prześlij opinię",
|
||||||
"s_i_need_help": "Pomoc",
|
"s_i_need_help": "Pomoc",
|
||||||
"s_troubleshooting": "Rozwiązywanie problemów",
|
"s_troubleshooting": "Rozwiązywanie problemów",
|
||||||
@ -128,6 +133,18 @@
|
|||||||
"version": {}
|
"version": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@l_serial_number": {
|
||||||
|
"placeholders": {
|
||||||
|
"serial": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_serial_number": null,
|
||||||
|
"@l_firmware_version": {
|
||||||
|
"placeholders": {
|
||||||
|
"version": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_firmware_version": null,
|
||||||
|
|
||||||
"@_yubikey_interactions": {},
|
"@_yubikey_interactions": {},
|
||||||
"l_insert_yk": "Podłącz klucz YubiKey",
|
"l_insert_yk": "Podłącz klucz YubiKey",
|
||||||
@ -154,6 +171,8 @@
|
|||||||
"@_app_configuration": {},
|
"@_app_configuration": {},
|
||||||
"s_toggle_applications": "Przełączanie funkcji",
|
"s_toggle_applications": "Przełączanie funkcji",
|
||||||
"s_toggle_interfaces": "Przełącz interfejsy",
|
"s_toggle_interfaces": "Przełącz interfejsy",
|
||||||
|
"l_toggle_applications_desc": null,
|
||||||
|
"l_toggle_interfaces_desc": null,
|
||||||
"s_reconfiguring_yk": "Rekonfigurowanie YubiKey\u2026",
|
"s_reconfiguring_yk": "Rekonfigurowanie YubiKey\u2026",
|
||||||
"s_config_updated": "Zaktualizowano konfigurację",
|
"s_config_updated": "Zaktualizowano konfigurację",
|
||||||
"l_config_updated_reinsert": "Zaktualizowano konfigurację, podłącz ponownie klucz YubiKey",
|
"l_config_updated_reinsert": "Zaktualizowano konfigurację, podłącz ponownie klucz YubiKey",
|
||||||
@ -645,6 +664,7 @@
|
|||||||
"@_factory_reset": {},
|
"@_factory_reset": {},
|
||||||
"s_reset": "Zresetuj",
|
"s_reset": "Zresetuj",
|
||||||
"s_factory_reset": "Ustawienia fabryczne",
|
"s_factory_reset": "Ustawienia fabryczne",
|
||||||
|
"l_factory_reset_desc": null,
|
||||||
"l_oath_application_reset": "Reset funkcji OATH",
|
"l_oath_application_reset": "Reset funkcji OATH",
|
||||||
"l_fido_app_reset": "Reset funkcji FIDO",
|
"l_fido_app_reset": "Reset funkcji FIDO",
|
||||||
"l_reset_failed": "Błąd podczas resetowania: {message}",
|
"l_reset_failed": "Błąd podczas resetowania: {message}",
|
||||||
@ -744,7 +764,12 @@
|
|||||||
|
|
||||||
"@_key_customization": {},
|
"@_key_customization": {},
|
||||||
"s_customize_key_action": "Dostosuj klucz",
|
"s_customize_key_action": "Dostosuj klucz",
|
||||||
|
"s_set_label": null,
|
||||||
|
"s_change_label": null,
|
||||||
"s_theme_color": "Kolor motywu",
|
"s_theme_color": "Kolor motywu",
|
||||||
|
"s_color": null,
|
||||||
|
"p_set_will_add_custom_name": null,
|
||||||
|
"p_rename_will_change_custom_name": null,
|
||||||
|
|
||||||
"@_eof": {}
|
"@_eof": {}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ class ChoiceFilterChip<T> extends StatefulWidget {
|
|||||||
final void Function(T value)? onChanged;
|
final void Function(T value)? onChanged;
|
||||||
final Widget? avatar;
|
final Widget? avatar;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
|
final bool? disableHover;
|
||||||
const ChoiceFilterChip({
|
const ChoiceFilterChip({
|
||||||
super.key,
|
super.key,
|
||||||
required this.value,
|
required this.value,
|
||||||
@ -37,6 +38,7 @@ class ChoiceFilterChip<T> extends StatefulWidget {
|
|||||||
this.tooltip,
|
this.tooltip,
|
||||||
this.avatar,
|
this.avatar,
|
||||||
this.selected = false,
|
this.selected = false,
|
||||||
|
this.disableHover,
|
||||||
this.labelBuilder,
|
this.labelBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -69,6 +71,8 @@ class _ChoiceFilterChipState<T> extends State<ChoiceFilterChip<T>> {
|
|||||||
color: Theme.of(context).colorScheme.background,
|
color: Theme.of(context).colorScheme.background,
|
||||||
items: widget.items
|
items: widget.items
|
||||||
.map((e) => PopupMenuItem<T>(
|
.map((e) => PopupMenuItem<T>(
|
||||||
|
enabled:
|
||||||
|
widget.disableHover != null ? !widget.disableHover! : true,
|
||||||
value: e,
|
value: e,
|
||||||
height: chipBox.size.height,
|
height: chipBox.size.height,
|
||||||
textStyle: ChipTheme.of(context).labelStyle,
|
textStyle: ChipTheme.of(context).labelStyle,
|
||||||
|
Loading…
Reference in New Issue
Block a user