mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-26 10:33:15 +03:00
Merge PR #985.
This commit is contained in:
commit
69146be242
@ -15,27 +15,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:yubico_authenticator/android/init.dart';
|
import 'package:yubico_authenticator/android/init.dart';
|
||||||
import 'package:yubico_authenticator/android/keys.dart' as android_keys;
|
import 'package:yubico_authenticator/android/keys.dart' as android_keys;
|
||||||
import 'package:yubico_authenticator/android/qr_scanner/qr_scanner_view.dart';
|
import 'package:yubico_authenticator/android/qr_scanner/qr_scanner_view.dart';
|
||||||
import 'package:yubico_authenticator/android/preferences.dart';
|
|
||||||
import 'package:yubico_authenticator/app/views/device_avatar.dart';
|
import 'package:yubico_authenticator/app/views/device_avatar.dart';
|
||||||
import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
|
import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
|
||||||
|
|
||||||
import '../test_util.dart';
|
import '../test_util.dart';
|
||||||
|
|
||||||
void _setShowBetaDialogPref(bool value) async {
|
|
||||||
SharedPreferences.setMockInitialValues({betaDialogPrefName: value});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> startUp(WidgetTester tester,
|
Future<void> startUp(WidgetTester tester,
|
||||||
[Map<dynamic, dynamic> startUpParams = const {}]) async {
|
[Map<dynamic, dynamic> startUpParams = const {}]) async {
|
||||||
// on Android disable Beta welcome dialog
|
|
||||||
// we need to do it before we pump the app
|
|
||||||
var betaDlgEnabled = startUpParams['dlg.beta.enabled'] ?? false;
|
|
||||||
_setShowBetaDialogPref(betaDlgEnabled);
|
|
||||||
|
|
||||||
await tester.pumpWidget(await initialize());
|
await tester.pumpWidget(await initialize());
|
||||||
|
|
||||||
// only wait for yubikey connection when needed
|
// only wait for yubikey connection when needed
|
||||||
|
@ -16,27 +16,20 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'models.dart';
|
||||||
|
|
||||||
const _prefix = 'android.keys';
|
const _prefix = 'android.keys';
|
||||||
|
|
||||||
const betaDialogView = Key('$_prefix.beta_dialog');
|
|
||||||
|
|
||||||
const nfcTapSetting = Key('$_prefix.nfc_tap');
|
|
||||||
const nfcKeyboardLayoutSetting = Key('$_prefix.nfc_keyboard_layout');
|
|
||||||
const nfcBypassTouchSetting = Key('$_prefix.nfc_bypass_touch');
|
|
||||||
const nfcSilenceSoundsSettings = Key('$_prefix.nfc_silence_sounds');
|
|
||||||
const usbOpenApp = Key('$_prefix.usb_open_app');
|
|
||||||
const themeModeSetting = Key('$_prefix.theme_mode');
|
|
||||||
|
|
||||||
const okButton = Key('$_prefix.ok');
|
const okButton = Key('$_prefix.ok');
|
||||||
const manualEntryButton = Key('$_prefix.manual_entry');
|
const manualEntryButton = Key('$_prefix.manual_entry');
|
||||||
|
|
||||||
const launchTapAction = Key('$_prefix.tap_action_launch');
|
const nfcBypassTouchSetting = Key('$_prefix.nfc_bypass_touch');
|
||||||
const copyTapAction = Key('$_prefix.tap_action_copy');
|
const nfcSilenceSoundsSettings = Key('$_prefix.nfc_silence_sounds');
|
||||||
const bothTapAction = Key('$_prefix.tap_action_both');
|
const usbOpenApp = Key('$_prefix.usb_open_app');
|
||||||
|
|
||||||
const themeModeSystem = Key('$_prefix.theme_mode_system');
|
|
||||||
const themeModeLight = Key('$_prefix.theme_mode_light');
|
|
||||||
const themeModeDark = Key('$_prefix.theme_mode_dark');
|
|
||||||
|
|
||||||
|
const nfcTapSetting = Key('$_prefix.nfc_tap');
|
||||||
|
Key nfcTapOption(NfcTapAction action) =>
|
||||||
|
Key('$_prefix.tap_action.${action.name}');
|
||||||
|
|
||||||
|
const nfcKeyboardLayoutSetting = Key('$_prefix.nfc_keyboard_layout');
|
||||||
Key keyboardLayoutOption(String name) => Key('$_prefix.keyboard_layout.$name');
|
Key keyboardLayoutOption(String name) => Key('$_prefix.keyboard_layout.$name');
|
||||||
|
29
lib/android/preferences.dart → lib/android/models.dart
Normal file → Executable file
29
lib/android/preferences.dart → lib/android/models.dart
Normal file → Executable file
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 Yubico.
|
* Copyright (C) 2023 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -14,12 +14,21 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// shared preferences keys
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
const betaDialogPrefName = 'prefBetaDialogShouldBeShown';
|
|
||||||
const prefNfcOpenApp = 'prefNfcOpenApp';
|
enum NfcTapAction {
|
||||||
const prefNfcBypassTouch = 'prefNfcBypassTouch';
|
launch,
|
||||||
const prefNfcSilenceSounds = 'prefNfcSilenceSounds';
|
copy,
|
||||||
const prefNfcCopyOtp = 'prefNfcCopyOtp';
|
both;
|
||||||
const prefClipKbdLayout = 'prefClipKbdLayout';
|
|
||||||
const prefUsbOpenApp = 'prefUsbOpenApp';
|
String getDescription(AppLocalizations l10n) {
|
||||||
const prefTheme = 'APP_STATE_THEME';
|
switch (this) {
|
||||||
|
case NfcTapAction.launch:
|
||||||
|
return l10n.l_launch_ya;
|
||||||
|
case NfcTapAction.copy:
|
||||||
|
return l10n.l_copy_otp_clipboard;
|
||||||
|
case NfcTapAction.both:
|
||||||
|
return l10n.l_launch_and_copy_otp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,11 +17,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../app/models.dart';
|
import '../app/models.dart';
|
||||||
import '../app/state.dart';
|
import '../app/state.dart';
|
||||||
|
import '../core/state.dart';
|
||||||
import 'app_methods.dart';
|
import 'app_methods.dart';
|
||||||
import 'devices.dart';
|
import 'devices.dart';
|
||||||
|
import 'models.dart';
|
||||||
|
|
||||||
const _contextChannel = MethodChannel('android.state.appContext');
|
const _contextChannel = MethodChannel('android.state.appContext');
|
||||||
|
|
||||||
@ -74,9 +77,8 @@ final androidSdkVersionProvider = Provider<int>((ref) => -1);
|
|||||||
|
|
||||||
final androidNfcSupportProvider = Provider<bool>((ref) => false);
|
final androidNfcSupportProvider = Provider<bool>((ref) => false);
|
||||||
|
|
||||||
final androidNfcStateProvider = StateNotifierProvider<NfcStateNotifier, bool>((ref) =>
|
final androidNfcStateProvider =
|
||||||
NfcStateNotifier()
|
StateNotifierProvider<NfcStateNotifier, bool>((ref) => NfcStateNotifier());
|
||||||
);
|
|
||||||
|
|
||||||
final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) {
|
final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) {
|
||||||
if (ref.read(androidSdkVersionProvider) < 29) {
|
if (ref.read(androidSdkVersionProvider) < 29) {
|
||||||
@ -124,3 +126,114 @@ class AndroidCurrentDeviceNotifier extends CurrentDeviceNotifier {
|
|||||||
state = device;
|
state = device;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final androidNfcTapActionProvider =
|
||||||
|
StateNotifierProvider<NfcTapActionNotifier, NfcTapAction>(
|
||||||
|
(ref) => NfcTapActionNotifier(ref.watch(prefProvider)));
|
||||||
|
|
||||||
|
class NfcTapActionNotifier extends StateNotifier<NfcTapAction> {
|
||||||
|
static const _prefNfcOpenApp = 'prefNfcOpenApp';
|
||||||
|
static const _prefNfcCopyOtp = 'prefNfcCopyOtp';
|
||||||
|
final SharedPreferences _prefs;
|
||||||
|
NfcTapActionNotifier._(this._prefs, super._state);
|
||||||
|
|
||||||
|
factory NfcTapActionNotifier(SharedPreferences prefs) {
|
||||||
|
final launchApp = prefs.getBool(_prefNfcOpenApp) ?? true;
|
||||||
|
final copyOtp = prefs.getBool(_prefNfcCopyOtp) ?? false;
|
||||||
|
final NfcTapAction action;
|
||||||
|
if (launchApp && copyOtp) {
|
||||||
|
action = NfcTapAction.both;
|
||||||
|
} else if (copyOtp) {
|
||||||
|
action = NfcTapAction.copy;
|
||||||
|
} else {
|
||||||
|
// This is the default value if both are false.
|
||||||
|
action = NfcTapAction.launch;
|
||||||
|
}
|
||||||
|
return NfcTapActionNotifier._(prefs, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setTapAction(NfcTapAction value) async {
|
||||||
|
if (state != value) {
|
||||||
|
state = value;
|
||||||
|
await _prefs.setBool(_prefNfcOpenApp, value != NfcTapAction.copy);
|
||||||
|
await _prefs.setBool(_prefNfcCopyOtp, value != NfcTapAction.launch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Get these from Android
|
||||||
|
final androidNfcSupportedKbdLayoutsProvider =
|
||||||
|
Provider<List<String>>((ref) => ['US', 'DE', 'DE-CH']);
|
||||||
|
|
||||||
|
final androidNfcKbdLayoutProvider =
|
||||||
|
StateNotifierProvider<NfcKbdLayoutNotifier, String>(
|
||||||
|
(ref) => NfcKbdLayoutNotifier(ref.watch(prefProvider)));
|
||||||
|
|
||||||
|
class NfcKbdLayoutNotifier extends StateNotifier<String> {
|
||||||
|
static const String _defaultClipKbdLayout = 'US';
|
||||||
|
static const _prefClipKbdLayout = 'prefClipKbdLayout';
|
||||||
|
final SharedPreferences _prefs;
|
||||||
|
NfcKbdLayoutNotifier(this._prefs)
|
||||||
|
: super(_prefs.getString(_prefClipKbdLayout) ?? _defaultClipKbdLayout);
|
||||||
|
|
||||||
|
Future<void> setKeyboardLayout(String value) async {
|
||||||
|
if (state != value) {
|
||||||
|
state = value;
|
||||||
|
await _prefs.setString(_prefClipKbdLayout, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final androidNfcBypassTouchProvider =
|
||||||
|
StateNotifierProvider<NfcBypassTouchNotifier, bool>(
|
||||||
|
(ref) => NfcBypassTouchNotifier(ref.watch(prefProvider)));
|
||||||
|
|
||||||
|
class NfcBypassTouchNotifier extends StateNotifier<bool> {
|
||||||
|
static const _prefNfcBypassTouch = 'prefNfcBypassTouch';
|
||||||
|
final SharedPreferences _prefs;
|
||||||
|
NfcBypassTouchNotifier(this._prefs)
|
||||||
|
: super(_prefs.getBool(_prefNfcBypassTouch) ?? false);
|
||||||
|
|
||||||
|
Future<void> setNfcBypassTouch(bool value) async {
|
||||||
|
if (state != value) {
|
||||||
|
state = value;
|
||||||
|
await _prefs.setBool(_prefNfcBypassTouch, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final androidNfcSilenceSoundsProvider =
|
||||||
|
StateNotifierProvider<NfcSilenceSoundsNotifier, bool>(
|
||||||
|
(ref) => NfcSilenceSoundsNotifier(ref.watch(prefProvider)));
|
||||||
|
|
||||||
|
class NfcSilenceSoundsNotifier extends StateNotifier<bool> {
|
||||||
|
static const _prefNfcSilenceSounds = 'prefNfcSilenceSounds';
|
||||||
|
final SharedPreferences _prefs;
|
||||||
|
NfcSilenceSoundsNotifier(this._prefs)
|
||||||
|
: super(_prefs.getBool(_prefNfcSilenceSounds) ?? false);
|
||||||
|
|
||||||
|
Future<void> setNfcSilenceSounds(bool value) async {
|
||||||
|
if (state != value) {
|
||||||
|
state = value;
|
||||||
|
await _prefs.setBool(_prefNfcSilenceSounds, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final androidUsbLaunchAppProvider =
|
||||||
|
StateNotifierProvider<UsbLaunchAppNotifier, bool>(
|
||||||
|
(ref) => UsbLaunchAppNotifier(ref.watch(prefProvider)));
|
||||||
|
|
||||||
|
class UsbLaunchAppNotifier extends StateNotifier<bool> {
|
||||||
|
static const _prefUsbOpenApp = 'prefUsbOpenApp';
|
||||||
|
final SharedPreferences _prefs;
|
||||||
|
UsbLaunchAppNotifier(this._prefs)
|
||||||
|
: super(_prefs.getBool(_prefUsbOpenApp) ?? false);
|
||||||
|
|
||||||
|
Future<void> setUsbLaunchApp(bool value) async {
|
||||||
|
if (state != value) {
|
||||||
|
state = value;
|
||||||
|
await _prefs.setBool(_prefUsbOpenApp, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,283 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 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_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
import '../../app/state.dart';
|
|
||||||
import '../../core/state.dart';
|
|
||||||
import '../../widgets/list_title.dart';
|
|
||||||
import '../../widgets/responsive_dialog.dart';
|
|
||||||
import '../keys.dart' as keys;
|
|
||||||
import '../preferences.dart';
|
|
||||||
|
|
||||||
// TODO: Get these from Android
|
|
||||||
const List<String> _keyboardLayouts = ['US', 'DE', 'DE-CH'];
|
|
||||||
const String _defaultClipKbdLayout = 'US';
|
|
||||||
|
|
||||||
enum _TapAction {
|
|
||||||
launch,
|
|
||||||
copy,
|
|
||||||
both;
|
|
||||||
|
|
||||||
String getDescription(AppLocalizations l10n) {
|
|
||||||
switch (this) {
|
|
||||||
case _TapAction.launch:
|
|
||||||
return l10n.l_launch_ya;
|
|
||||||
case _TapAction.copy:
|
|
||||||
return l10n.l_copy_otp_clipboard;
|
|
||||||
case _TapAction.both:
|
|
||||||
return l10n.l_launch_and_copy_otp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Key get key {
|
|
||||||
switch (this) {
|
|
||||||
case _TapAction.launch:
|
|
||||||
return keys.launchTapAction;
|
|
||||||
case _TapAction.copy:
|
|
||||||
return keys.copyTapAction;
|
|
||||||
case _TapAction.both:
|
|
||||||
return keys.bothTapAction;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static _TapAction load(SharedPreferences prefs) {
|
|
||||||
final launchApp = prefs.getBool(prefNfcOpenApp) ?? true;
|
|
||||||
final copyOtp = prefs.getBool(prefNfcCopyOtp) ?? false;
|
|
||||||
if (launchApp && copyOtp) {
|
|
||||||
return both;
|
|
||||||
}
|
|
||||||
if (copyOtp) {
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
// This is the default value if both are false.
|
|
||||||
return launch;
|
|
||||||
}
|
|
||||||
|
|
||||||
void save(SharedPreferences prefs) {
|
|
||||||
prefs.setBool(prefNfcOpenApp, this != copy);
|
|
||||||
prefs.setBool(prefNfcCopyOtp, this != launch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on ThemeMode {
|
|
||||||
String getDisplayName(AppLocalizations l10n) {
|
|
||||||
switch (this) {
|
|
||||||
case ThemeMode.system:
|
|
||||||
return l10n.s_system_default;
|
|
||||||
case ThemeMode.light:
|
|
||||||
return l10n.s_light_mode;
|
|
||||||
case ThemeMode.dark:
|
|
||||||
return l10n.s_dark_mode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AndroidSettingsPage extends ConsumerStatefulWidget {
|
|
||||||
const AndroidSettingsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<ConsumerStatefulWidget> createState() =>
|
|
||||||
_AndroidSettingsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
final prefs = ref.watch(prefProvider);
|
|
||||||
|
|
||||||
final tapAction = _TapAction.load(prefs);
|
|
||||||
final clipKbdLayout =
|
|
||||||
prefs.getString(prefClipKbdLayout) ?? _defaultClipKbdLayout;
|
|
||||||
final nfcBypassTouch = prefs.getBool(prefNfcBypassTouch) ?? false;
|
|
||||||
final nfcSilenceSounds = prefs.getBool(prefNfcSilenceSounds) ?? false;
|
|
||||||
final usbOpenApp = prefs.getBool(prefUsbOpenApp) ?? false;
|
|
||||||
final themeMode = ref.watch(themeModeProvider);
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return ResponsiveDialog(
|
|
||||||
title: Text(l10n.s_settings),
|
|
||||||
child: Theme(
|
|
||||||
// Make the headers use the primary color to pop a bit.
|
|
||||||
// Once M3 is implemented this will probably not be needed.
|
|
||||||
data: theme.copyWith(
|
|
||||||
textTheme: theme.textTheme.copyWith(
|
|
||||||
labelLarge: theme.textTheme.labelLarge
|
|
||||||
?.copyWith(color: theme.colorScheme.primary)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
ListTitle(l10n.s_nfc_options),
|
|
||||||
ListTile(
|
|
||||||
title: Text(l10n.l_on_yk_nfc_tap),
|
|
||||||
subtitle: Text(tapAction.getDescription(l10n)),
|
|
||||||
key: keys.nfcTapSetting,
|
|
||||||
onTap: () async {
|
|
||||||
final newTapAction = await _selectTapAction(context, tapAction);
|
|
||||||
setState(() {
|
|
||||||
newTapAction.save(prefs);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text(l10n.l_kbd_layout_for_static),
|
|
||||||
subtitle: Text(clipKbdLayout),
|
|
||||||
key: keys.nfcKeyboardLayoutSetting,
|
|
||||||
enabled: tapAction != _TapAction.launch,
|
|
||||||
onTap: () async {
|
|
||||||
var newValue = await _selectKbdLayout(context, clipKbdLayout);
|
|
||||||
if (newValue != clipKbdLayout) {
|
|
||||||
setState(() {
|
|
||||||
prefs.setString(prefClipKbdLayout, newValue);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SwitchListTile(
|
|
||||||
title: Text(l10n.l_bypass_touch_requirement),
|
|
||||||
subtitle: Text(nfcBypassTouch
|
|
||||||
? l10n.l_bypass_touch_requirement_on
|
|
||||||
: l10n.l_bypass_touch_requirement_off),
|
|
||||||
value: nfcBypassTouch,
|
|
||||||
key: keys.nfcBypassTouchSetting,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
prefs.setBool(prefNfcBypassTouch, value);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
SwitchListTile(
|
|
||||||
title: Text(l10n.s_silence_nfc_sounds),
|
|
||||||
subtitle: Text(nfcSilenceSounds
|
|
||||||
? l10n.l_silence_nfc_sounds_on
|
|
||||||
: l10n.l_silence_nfc_sounds_off),
|
|
||||||
value: nfcSilenceSounds,
|
|
||||||
key: keys.nfcSilenceSoundsSettings,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
prefs.setBool(prefNfcSilenceSounds, value);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
ListTitle(l10n.s_usb_options),
|
|
||||||
SwitchListTile(
|
|
||||||
title: Text(l10n.l_launch_app_on_usb),
|
|
||||||
subtitle: Text(usbOpenApp
|
|
||||||
? l10n.l_launch_app_on_usb_on
|
|
||||||
: l10n.l_launch_app_on_usb_off),
|
|
||||||
value: usbOpenApp,
|
|
||||||
key: keys.usbOpenApp,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
prefs.setBool(prefUsbOpenApp, value);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
ListTitle(l10n.s_appearance),
|
|
||||||
ListTile(
|
|
||||||
title: Text(l10n.s_app_theme),
|
|
||||||
subtitle: Text(themeMode.getDisplayName(l10n)),
|
|
||||||
key: keys.themeModeSetting,
|
|
||||||
onTap: () async {
|
|
||||||
final newMode = await _selectAppearance(
|
|
||||||
ref.read(supportedThemesProvider), context, themeMode);
|
|
||||||
ref.read(themeModeProvider.notifier).setThemeMode(newMode);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<_TapAction> _selectTapAction(
|
|
||||||
BuildContext context, _TapAction tapAction) async =>
|
|
||||||
await showDialog<_TapAction>(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
return SimpleDialog(
|
|
||||||
title: Text(l10n.l_on_yk_nfc_tap),
|
|
||||||
children: _TapAction.values
|
|
||||||
.map(
|
|
||||||
(e) => RadioListTile<_TapAction>(
|
|
||||||
title: Text(e.getDescription(l10n)),
|
|
||||||
key: e.key,
|
|
||||||
value: e,
|
|
||||||
groupValue: tapAction,
|
|
||||||
toggleable: true,
|
|
||||||
onChanged: (mode) {
|
|
||||||
Navigator.pop(context, e);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}) ??
|
|
||||||
_TapAction.launch;
|
|
||||||
|
|
||||||
Future<String> _selectKbdLayout(
|
|
||||||
BuildContext context, String currentKbdLayout) async =>
|
|
||||||
await showDialog<String>(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
return SimpleDialog(
|
|
||||||
title: Text(l10n.s_choose_kbd_layout),
|
|
||||||
children: _keyboardLayouts
|
|
||||||
.map(
|
|
||||||
(e) => RadioListTile<String>(
|
|
||||||
title: Text(e),
|
|
||||||
value: e,
|
|
||||||
key: keys.keyboardLayoutOption(e),
|
|
||||||
toggleable: true,
|
|
||||||
groupValue: currentKbdLayout,
|
|
||||||
onChanged: (mode) {
|
|
||||||
Navigator.pop(context, e);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}) ??
|
|
||||||
_defaultClipKbdLayout;
|
|
||||||
|
|
||||||
Future<ThemeMode> _selectAppearance(List<ThemeMode> supportedThemes,
|
|
||||||
BuildContext context, ThemeMode themeMode) async =>
|
|
||||||
await showDialog<ThemeMode>(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
return SimpleDialog(
|
|
||||||
title: Text(l10n.s_choose_app_theme),
|
|
||||||
children: supportedThemes
|
|
||||||
.map((e) => RadioListTile(
|
|
||||||
title: Text(e.getDisplayName(l10n)),
|
|
||||||
value: e,
|
|
||||||
key: Key('android.keys.theme_mode_${e.name}'),
|
|
||||||
groupValue: themeMode,
|
|
||||||
toggleable: true,
|
|
||||||
onChanged: (mode) {
|
|
||||||
Navigator.pop(context, e);
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}) ??
|
|
||||||
themeMode;
|
|
||||||
}
|
|
187
lib/android/views/settings_views.dart
Executable file
187
lib/android/views/settings_views.dart
Executable file
@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../state.dart';
|
||||||
|
import '../models.dart';
|
||||||
|
import '../keys.dart' as keys;
|
||||||
|
|
||||||
|
class NfcTapActionView extends ConsumerWidget {
|
||||||
|
const NfcTapActionView({super.key});
|
||||||
|
|
||||||
|
Future<NfcTapAction?> _selectTapAction(
|
||||||
|
BuildContext context, NfcTapAction tapAction) async =>
|
||||||
|
await showDialog<NfcTapAction>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
return SimpleDialog(
|
||||||
|
title: Text(l10n.l_on_yk_nfc_tap),
|
||||||
|
children: NfcTapAction.values
|
||||||
|
.map(
|
||||||
|
(e) => RadioListTile<NfcTapAction>(
|
||||||
|
title: Text(e.getDescription(l10n)),
|
||||||
|
key: keys.nfcTapOption(e),
|
||||||
|
value: e,
|
||||||
|
groupValue: tapAction,
|
||||||
|
toggleable: true,
|
||||||
|
onChanged: (mode) {
|
||||||
|
Navigator.pop(context, e);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final tapAction = ref.watch(androidNfcTapActionProvider);
|
||||||
|
return ListTile(
|
||||||
|
title: Text(l10n.l_on_yk_nfc_tap),
|
||||||
|
subtitle: Text(tapAction.getDescription(l10n)),
|
||||||
|
key: keys.nfcTapSetting,
|
||||||
|
onTap: () async {
|
||||||
|
final newTapAction = await _selectTapAction(context, tapAction);
|
||||||
|
if (newTapAction != null) {
|
||||||
|
await ref
|
||||||
|
.read(androidNfcTapActionProvider.notifier)
|
||||||
|
.setTapAction(newTapAction);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NfcKbdLayoutView extends ConsumerWidget {
|
||||||
|
const NfcKbdLayoutView({super.key});
|
||||||
|
|
||||||
|
Future<String?> _selectKbdLayout(BuildContext context, List<String> available,
|
||||||
|
String currentKbdLayout) async =>
|
||||||
|
await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
return SimpleDialog(
|
||||||
|
title: Text(l10n.s_choose_kbd_layout),
|
||||||
|
children: available
|
||||||
|
.map(
|
||||||
|
(e) => RadioListTile<String>(
|
||||||
|
title: Text(e),
|
||||||
|
value: e,
|
||||||
|
key: keys.keyboardLayoutOption(e),
|
||||||
|
toggleable: true,
|
||||||
|
groupValue: currentKbdLayout,
|
||||||
|
onChanged: (mode) {
|
||||||
|
Navigator.pop(context, e);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final tapAction = ref.watch(androidNfcTapActionProvider);
|
||||||
|
final clipKbdLayout = ref.watch(androidNfcKbdLayoutProvider);
|
||||||
|
return ListTile(
|
||||||
|
title: Text(l10n.l_kbd_layout_for_static),
|
||||||
|
subtitle: Text(clipKbdLayout),
|
||||||
|
key: keys.nfcKeyboardLayoutSetting,
|
||||||
|
enabled: tapAction != NfcTapAction.launch,
|
||||||
|
onTap: () async {
|
||||||
|
final newValue = await _selectKbdLayout(
|
||||||
|
context,
|
||||||
|
ref.watch(androidNfcSupportedKbdLayoutsProvider),
|
||||||
|
clipKbdLayout,
|
||||||
|
);
|
||||||
|
if (newValue != null) {
|
||||||
|
await ref
|
||||||
|
.read(androidNfcKbdLayoutProvider.notifier)
|
||||||
|
.setKeyboardLayout(newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NfcBypassTouchView extends ConsumerWidget {
|
||||||
|
const NfcBypassTouchView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final nfcBypassTouch = ref.watch(androidNfcBypassTouchProvider);
|
||||||
|
return SwitchListTile(
|
||||||
|
title: Text(l10n.l_bypass_touch_requirement),
|
||||||
|
subtitle: Text(nfcBypassTouch
|
||||||
|
? l10n.l_bypass_touch_requirement_on
|
||||||
|
: l10n.l_bypass_touch_requirement_off),
|
||||||
|
value: nfcBypassTouch,
|
||||||
|
key: keys.nfcBypassTouchSetting,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref
|
||||||
|
.read(androidNfcBypassTouchProvider.notifier)
|
||||||
|
.setNfcBypassTouch(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NfcSilenceSoundsView extends ConsumerWidget {
|
||||||
|
const NfcSilenceSoundsView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final nfcSilenceSounds = ref.watch(androidNfcSilenceSoundsProvider);
|
||||||
|
return SwitchListTile(
|
||||||
|
title: Text(l10n.s_silence_nfc_sounds),
|
||||||
|
subtitle: Text(nfcSilenceSounds
|
||||||
|
? l10n.l_silence_nfc_sounds_on
|
||||||
|
: l10n.l_silence_nfc_sounds_off),
|
||||||
|
value: nfcSilenceSounds,
|
||||||
|
key: keys.nfcSilenceSoundsSettings,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref
|
||||||
|
.read(androidNfcSilenceSoundsProvider.notifier)
|
||||||
|
.setNfcSilenceSounds(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UsbOpenAppView extends ConsumerWidget {
|
||||||
|
const UsbOpenAppView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final usbOpenApp = ref.watch(androidUsbLaunchAppProvider);
|
||||||
|
return SwitchListTile(
|
||||||
|
title: Text(l10n.l_launch_app_on_usb),
|
||||||
|
subtitle: Text(usbOpenApp
|
||||||
|
? l10n.l_launch_app_on_usb_on
|
||||||
|
: l10n.l_launch_app_on_usb_off),
|
||||||
|
value: usbOpenApp,
|
||||||
|
key: keys.usbOpenApp,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref.read(androidUsbLaunchAppProvider.notifier).setUsbLaunchApp(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -14,15 +14,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
void launchFeedbackUrl() => _launchUrl(Platform.isAndroid
|
import '../core/state.dart';
|
||||||
|
|
||||||
|
void launchFeedbackUrl() => _launchUrl(isAndroid
|
||||||
? 'https://yubi.co/ya-feedback-android'
|
? 'https://yubi.co/ya-feedback-android'
|
||||||
: 'https://yubi.co/ya-feedback-desktop');
|
: 'https://yubi.co/ya-feedback-desktop');
|
||||||
|
|
||||||
void launchHelpUrl() => _launchUrl(Platform.isAndroid
|
void launchHelpUrl() => _launchUrl(isAndroid
|
||||||
? 'https://yubi.co/ya-help-android'
|
? 'https://yubi.co/ya-help-android'
|
||||||
: 'https://yubi.co/ya-help-desktop');
|
: 'https://yubi.co/ya-help-desktop');
|
||||||
|
|
||||||
|
@ -22,14 +22,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import '../about_page.dart';
|
import '../about_page.dart';
|
||||||
import '../android/views/android_settings_page.dart';
|
|
||||||
import '../core/state.dart';
|
import '../core/state.dart';
|
||||||
import '../desktop/state.dart';
|
import '../desktop/state.dart';
|
||||||
import '../oath/keys.dart';
|
import '../oath/keys.dart';
|
||||||
import '../settings_page.dart';
|
|
||||||
import 'message.dart';
|
import 'message.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
import 'state.dart';
|
import 'state.dart';
|
||||||
|
import 'views/settings_page.dart';
|
||||||
|
|
||||||
class OpenIntent extends Intent {
|
class OpenIntent extends Intent {
|
||||||
const OpenIntent();
|
const OpenIntent();
|
||||||
@ -122,9 +121,7 @@ Widget registerGlobalShortcuts(
|
|||||||
if (!Navigator.of(context).canPop()) {
|
if (!Navigator.of(context).canPop()) {
|
||||||
await showBlurDialog(
|
await showBlurDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Platform.isAndroid
|
builder: (context) => const SettingsPage(),
|
||||||
? const AndroidSettingsPage()
|
|
||||||
: const SettingsPage(),
|
|
||||||
routeSettings: const RouteSettings(name: 'settings'),
|
routeSettings: const RouteSettings(name: 'settings'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@ -115,14 +113,13 @@ class _DevicePickerContent extends ConsumerWidget {
|
|||||||
_HeroAvatar(
|
_HeroAvatar(
|
||||||
child: DeviceAvatar(
|
child: DeviceAvatar(
|
||||||
radius: 64,
|
radius: 64,
|
||||||
child: Icon(Platform.isAndroid ? Icons.no_cell : Icons.usb),
|
child: Icon(isAndroid ? Icons.no_cell : Icons.usb),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Center(child: Text(l10n.l_no_yk_present)),
|
title: Center(child: Text(l10n.l_no_yk_present)),
|
||||||
subtitle: Center(
|
subtitle: Center(
|
||||||
child: Text(
|
child: Text(isAndroid ? l10n.l_insert_or_tap_yk : l10n.s_usb)),
|
||||||
Platform.isAndroid ? l10n.l_insert_or_tap_yk : l10n.s_usb)),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -26,3 +26,7 @@ const actionsIconButtonKey = Key('$_prefix.actions_icon_button');
|
|||||||
|
|
||||||
// drawer items
|
// drawer items
|
||||||
const managementAppDrawer = Key('$_prefix.drawer.management');
|
const managementAppDrawer = Key('$_prefix.drawer.management');
|
||||||
|
|
||||||
|
// settings page
|
||||||
|
const themeModeSetting = Key('$_prefix.settings.theme_mode');
|
||||||
|
Key themeModeOption(ThemeMode mode) => Key('$_prefix.theme_mode.${mode.name}');
|
||||||
|
153
lib/app/views/settings_page.dart
Executable file
153
lib/app/views/settings_page.dart
Executable file
@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022-2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../android/views/settings_views.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
|
import '../../widgets/list_title.dart';
|
||||||
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
import 'keys.dart' as keys;
|
||||||
|
|
||||||
|
extension on ThemeMode {
|
||||||
|
String getDisplayName(AppLocalizations l10n) {
|
||||||
|
switch (this) {
|
||||||
|
case ThemeMode.system:
|
||||||
|
return l10n.s_system_default;
|
||||||
|
case ThemeMode.light:
|
||||||
|
return l10n.s_light_mode;
|
||||||
|
case ThemeMode.dark:
|
||||||
|
return l10n.s_dark_mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ThemeModeView extends ConsumerWidget {
|
||||||
|
const _ThemeModeView();
|
||||||
|
|
||||||
|
Future<ThemeMode> _selectAppearance(BuildContext context,
|
||||||
|
List<ThemeMode> supportedThemes, ThemeMode themeMode) async =>
|
||||||
|
await showDialog<ThemeMode>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
return SimpleDialog(
|
||||||
|
title: Text(l10n.s_choose_app_theme),
|
||||||
|
children: supportedThemes
|
||||||
|
.map((e) => RadioListTile(
|
||||||
|
title: Text(e.getDisplayName(l10n)),
|
||||||
|
value: e,
|
||||||
|
key: keys.themeModeOption(e),
|
||||||
|
groupValue: themeMode,
|
||||||
|
toggleable: true,
|
||||||
|
onChanged: (mode) {
|
||||||
|
Navigator.pop(context, e);
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}) ??
|
||||||
|
themeMode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final themeMode = ref.watch(themeModeProvider);
|
||||||
|
return ListTile(
|
||||||
|
title: Text(l10n.s_app_theme),
|
||||||
|
subtitle: Text(themeMode.getDisplayName(l10n)),
|
||||||
|
key: keys.themeModeSetting,
|
||||||
|
onTap: () async {
|
||||||
|
final newMode = await _selectAppearance(
|
||||||
|
context, ref.read(supportedThemesProvider), themeMode);
|
||||||
|
ref.read(themeModeProvider.notifier).setThemeMode(newMode);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CommunityTranslationsView extends ConsumerWidget {
|
||||||
|
const _CommunityTranslationsView();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final enableTranslations = ref.watch(communityTranslationsProvider);
|
||||||
|
return SwitchListTile(
|
||||||
|
title: Text(l10n.l_enable_community_translations),
|
||||||
|
subtitle: Text(l10n.p_community_translations_desc),
|
||||||
|
isThreeLine: true,
|
||||||
|
value: enableTranslations,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref
|
||||||
|
.read(communityTranslationsProvider.notifier)
|
||||||
|
.setEnableCommunityTranslations(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsPage extends ConsumerWidget {
|
||||||
|
const SettingsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final enableTranslations = ref.watch(communityTranslationsProvider);
|
||||||
|
|
||||||
|
return ResponsiveDialog(
|
||||||
|
title: Text(l10n.s_settings),
|
||||||
|
child: Theme(
|
||||||
|
// Make the headers use the primary color to pop a bit.
|
||||||
|
// Once M3 is implemented this will probably not be needed.
|
||||||
|
data: theme.copyWith(
|
||||||
|
textTheme: theme.textTheme.copyWith(
|
||||||
|
labelLarge: theme.textTheme.labelLarge
|
||||||
|
?.copyWith(color: theme.colorScheme.primary)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (isAndroid) ...[
|
||||||
|
ListTitle(l10n.s_nfc_options),
|
||||||
|
const NfcTapActionView(),
|
||||||
|
const NfcKbdLayoutView(),
|
||||||
|
const NfcBypassTouchView(),
|
||||||
|
const NfcSilenceSoundsView(),
|
||||||
|
ListTitle(l10n.s_usb_options),
|
||||||
|
const UsbOpenAppView(),
|
||||||
|
],
|
||||||
|
ListTitle(l10n.s_appearance),
|
||||||
|
const _ThemeModeView(),
|
||||||
|
if (enableTranslations ||
|
||||||
|
basicLocaleListResolution(window.locales, officialLocales) !=
|
||||||
|
basicLocaleListResolution(
|
||||||
|
window.locales, AppLocalizations.supportedLocales)) ...[
|
||||||
|
ListTitle(l10n.s_language),
|
||||||
|
const _CommunityTranslationsView(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -14,14 +14,21 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
bool get isDesktop {
|
||||||
final isAndroid = Platform.isAndroid;
|
return const [
|
||||||
|
TargetPlatform.windows,
|
||||||
|
TargetPlatform.macOS,
|
||||||
|
TargetPlatform.linux
|
||||||
|
].contains(defaultTargetPlatform);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isAndroid {
|
||||||
|
return defaultTargetPlatform == TargetPlatform.android;
|
||||||
|
}
|
||||||
|
|
||||||
// This must be initialized before use, in main.dart.
|
// This must be initialized before use, in main.dart.
|
||||||
final prefProvider = Provider<SharedPreferences>((ref) {
|
final prefProvider = Provider<SharedPreferences>((ref) {
|
||||||
|
@ -56,7 +56,7 @@ final _favoriteAccounts =
|
|||||||
);
|
);
|
||||||
|
|
||||||
final systrayProvider = Provider.autoDispose((ref) {
|
final systrayProvider = Provider.autoDispose((ref) {
|
||||||
final systray = _Systray(ref, ref.watch(l10nProvider));
|
final systray = _Systray(ref);
|
||||||
|
|
||||||
// Keep track of which accounts to show
|
// Keep track of which accounts to show
|
||||||
ref.listen(
|
ref.listen(
|
||||||
@ -64,6 +64,7 @@ final systrayProvider = Provider.autoDispose((ref) {
|
|||||||
(_, next) {
|
(_, next) {
|
||||||
systray._updateCredentials(next);
|
systray._updateCredentials(next);
|
||||||
},
|
},
|
||||||
|
fireImmediately: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Keep track of the shown/hidden state of the app
|
// Keep track of the shown/hidden state of the app
|
||||||
@ -71,6 +72,11 @@ final systrayProvider = Provider.autoDispose((ref) {
|
|||||||
systray._setHidden(hidden);
|
systray._setHidden(hidden);
|
||||||
}, fireImmediately: true);
|
}, fireImmediately: true);
|
||||||
|
|
||||||
|
// Keep track of the locale of the app
|
||||||
|
ref.listen(l10nProvider, (_, l10n) {
|
||||||
|
systray._updateLocale(l10n);
|
||||||
|
});
|
||||||
|
|
||||||
ref.onDispose(systray.dispose);
|
ref.onDispose(systray.dispose);
|
||||||
|
|
||||||
return systray;
|
return systray;
|
||||||
@ -99,20 +105,17 @@ String _getIcon() {
|
|||||||
|
|
||||||
class _Systray extends TrayListener {
|
class _Systray extends TrayListener {
|
||||||
final Ref _ref;
|
final Ref _ref;
|
||||||
final AppLocalizations _l10n;
|
|
||||||
int _lastClick = 0;
|
int _lastClick = 0;
|
||||||
|
AppLocalizations _l10n;
|
||||||
DevicePath _devicePath = DevicePath([]);
|
DevicePath _devicePath = DevicePath([]);
|
||||||
List<OathCredential> _credentials = [];
|
List<OathCredential> _credentials = [];
|
||||||
bool _isHidden = false;
|
bool _isHidden = false;
|
||||||
_Systray(this._ref, this._l10n) {
|
_Systray(this._ref) : _l10n = _ref.read(l10nProvider) {
|
||||||
_init();
|
_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
await trayManager.setIcon(_getIcon(), isTemplate: true);
|
await trayManager.setIcon(_getIcon(), isTemplate: true);
|
||||||
if (!Platform.isLinux) {
|
|
||||||
await trayManager.setToolTip(_l10n.app_name);
|
|
||||||
}
|
|
||||||
await _updateContextMenu();
|
await _updateContextMenu();
|
||||||
|
|
||||||
// Doesn't seem to work on Linux
|
// Doesn't seem to work on Linux
|
||||||
@ -120,9 +123,18 @@ class _Systray extends TrayListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
trayManager.removeListener(this);
|
||||||
trayManager.destroy();
|
trayManager.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateLocale(AppLocalizations l10n) async {
|
||||||
|
_l10n = l10n;
|
||||||
|
if (!Platform.isLinux) {
|
||||||
|
await trayManager.setToolTip(l10n.app_name);
|
||||||
|
}
|
||||||
|
await _updateContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
void _updateCredentials(Pair<DevicePath?, List<OathCredential>> pair) {
|
void _updateCredentials(Pair<DevicePath?, List<OathCredential>> pair) {
|
||||||
if (!listEquals(_credentials, pair.second)) {
|
if (!listEquals(_credentials, pair.second)) {
|
||||||
_devicePath = pair.first ?? _devicePath;
|
_devicePath = pair.first ?? _devicePath;
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -177,7 +176,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
try {
|
try {
|
||||||
if (devicePath == null) {
|
if (devicePath == null) {
|
||||||
assert(Platform.isAndroid, 'devicePath is only optional for Android');
|
assert(isAndroid, 'devicePath is only optional for Android');
|
||||||
await ref
|
await ref
|
||||||
.read(addCredentialToAnyProvider)
|
.read(addCredentialToAnyProvider)
|
||||||
.call(credUri, requireTouch: _touch);
|
.call(credUri, requireTouch: _touch);
|
||||||
@ -334,7 +333,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
final devicePath = deviceNode?.path;
|
final devicePath = deviceNode?.path;
|
||||||
if (devicePath != null) {
|
if (devicePath != null) {
|
||||||
await _doAddCredential(devicePath: devicePath, credUri: cred.toUri());
|
await _doAddCredential(devicePath: devicePath, credUri: cred.toUri());
|
||||||
} else if (Platform.isAndroid) {
|
} else if (isAndroid) {
|
||||||
// Send the credential to Android to be added to the next YubiKey
|
// Send the credential to Android to be added to the next YubiKey
|
||||||
await _doAddCredential(devicePath: null, credUri: cred.toUri());
|
await _doAddCredential(devicePath: null, credUri: cred.toUri());
|
||||||
} else {
|
} else {
|
||||||
|
@ -14,8 +14,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
@ -24,6 +22,7 @@ import 'package:yubico_authenticator/oath/icon_provider/icon_pack_dialog.dart';
|
|||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
import '../../exception/cancellation_exception.dart';
|
import '../../exception/cancellation_exception.dart';
|
||||||
import '../../widgets/list_title.dart';
|
import '../../widgets/list_title.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
@ -61,7 +60,7 @@ Widget oathBuildActions(
|
|||||||
final withContext = ref.read(withContextProvider);
|
final withContext = ref.read(withContextProvider);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
CredentialData? otpauth;
|
CredentialData? otpauth;
|
||||||
if (Platform.isAndroid) {
|
if (isAndroid) {
|
||||||
final scanner = ref.read(qrScannerProvider);
|
final scanner = ref.read(qrScannerProvider);
|
||||||
if (scanner != null) {
|
if (scanner != null) {
|
||||||
try {
|
try {
|
||||||
|
@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 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:flutter/material.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
import 'app/logging.dart';
|
|
||||||
import 'app/state.dart';
|
|
||||||
import 'widgets/list_title.dart';
|
|
||||||
import 'widgets/responsive_dialog.dart';
|
|
||||||
|
|
||||||
final _log = Logger('settings');
|
|
||||||
|
|
||||||
class SettingsPage extends ConsumerWidget {
|
|
||||||
const SettingsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
final themeMode = ref.watch(themeModeProvider);
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final enableTranslations = ref.watch(communityTranslationsProvider);
|
|
||||||
return ResponsiveDialog(
|
|
||||||
title: Text(l10n.s_settings),
|
|
||||||
child: Theme(
|
|
||||||
// Make the headers use the primary color to pop a bit.
|
|
||||||
// Once M3 is implemented this will probably not be needed.
|
|
||||||
data: theme.copyWith(
|
|
||||||
textTheme: theme.textTheme.copyWith(
|
|
||||||
labelLarge: theme.textTheme.labelLarge
|
|
||||||
?.copyWith(color: theme.colorScheme.primary)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
ListTitle(l10n.s_appearance),
|
|
||||||
RadioListTile<ThemeMode>(
|
|
||||||
title: Text(l10n.s_system_default),
|
|
||||||
value: ThemeMode.system,
|
|
||||||
groupValue: themeMode,
|
|
||||||
onChanged: (mode) {
|
|
||||||
ref.read(themeModeProvider.notifier).setThemeMode(mode!);
|
|
||||||
_log.debug('Set theme mode to $mode');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RadioListTile<ThemeMode>(
|
|
||||||
title: Text(l10n.s_light_mode),
|
|
||||||
value: ThemeMode.light,
|
|
||||||
groupValue: themeMode,
|
|
||||||
onChanged: (mode) {
|
|
||||||
ref.read(themeModeProvider.notifier).setThemeMode(mode!);
|
|
||||||
_log.debug('Set theme mode to $mode');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RadioListTile<ThemeMode>(
|
|
||||||
title: Text(l10n.s_dark_mode),
|
|
||||||
value: ThemeMode.dark,
|
|
||||||
groupValue: themeMode,
|
|
||||||
onChanged: (mode) {
|
|
||||||
ref.read(themeModeProvider.notifier).setThemeMode(mode!);
|
|
||||||
_log.debug('Set theme mode to $mode');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (enableTranslations ||
|
|
||||||
basicLocaleListResolution(window.locales, officialLocales) !=
|
|
||||||
basicLocaleListResolution(
|
|
||||||
window.locales, AppLocalizations.supportedLocales)) ...[
|
|
||||||
ListTitle(l10n.s_language),
|
|
||||||
SwitchListTile(
|
|
||||||
title: Text(l10n.l_enable_community_translations),
|
|
||||||
subtitle: Text(l10n.p_community_translations_desc),
|
|
||||||
isThreeLine: true,
|
|
||||||
value: enableTranslations,
|
|
||||||
onChanged: (value) {
|
|
||||||
ref
|
|
||||||
.read(communityTranslationsProvider.notifier)
|
|
||||||
.setEnableCommunityTranslations(value);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,16 +15,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:yubico_authenticator/android/keys.dart' as keys;
|
import 'package:yubico_authenticator/android/models.dart';
|
||||||
import 'package:yubico_authenticator/android/preferences.dart';
|
import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
|
||||||
|
import 'package:yubico_authenticator/android/keys.dart' as android_keys;
|
||||||
import 'package:yubico_authenticator/android/state.dart';
|
import 'package:yubico_authenticator/android/state.dart';
|
||||||
import 'package:yubico_authenticator/android/views/android_settings_page.dart';
|
|
||||||
import 'package:yubico_authenticator/app/state.dart';
|
import 'package:yubico_authenticator/app/state.dart';
|
||||||
|
import 'package:yubico_authenticator/app/views/settings_page.dart';
|
||||||
import 'package:yubico_authenticator/core/state.dart';
|
import 'package:yubico_authenticator/core/state.dart';
|
||||||
|
|
||||||
Widget createMaterialApp({required Widget child}) {
|
Widget createMaterialApp({required Widget child}) {
|
||||||
@ -44,7 +46,7 @@ Widget createMaterialApp({required Widget child}) {
|
|||||||
|
|
||||||
extension _WidgetTesterHelper on WidgetTester {
|
extension _WidgetTesterHelper on WidgetTester {
|
||||||
Future<void> openNfcTapOptionSelection() async {
|
Future<void> openNfcTapOptionSelection() async {
|
||||||
var widget = find.byKey(keys.nfcTapSetting).hitTestable();
|
var widget = find.byKey(android_keys.nfcTapSetting).hitTestable();
|
||||||
expect(widget, findsOneWidget);
|
expect(widget, findsOneWidget);
|
||||||
await tap(widget);
|
await tap(widget);
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
@ -52,28 +54,29 @@ extension _WidgetTesterHelper on WidgetTester {
|
|||||||
|
|
||||||
Future<void> selectLaunchOption() async {
|
Future<void> selectLaunchOption() async {
|
||||||
await openNfcTapOptionSelection();
|
await openNfcTapOptionSelection();
|
||||||
await tap(find.byKey(keys.launchTapAction));
|
await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.launch)));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectCopyOption() async {
|
Future<void> selectCopyOption() async {
|
||||||
await openNfcTapOptionSelection();
|
await openNfcTapOptionSelection();
|
||||||
await tap(find.byKey(keys.copyTapAction));
|
await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.copy)));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectBothOption() async {
|
Future<void> selectBothOption() async {
|
||||||
await openNfcTapOptionSelection();
|
await openNfcTapOptionSelection();
|
||||||
await tap(find.byKey(keys.bothTapAction));
|
await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.both)));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
ListTile keyboardLayoutListTile() =>
|
ListTile keyboardLayoutListTile() =>
|
||||||
find.byKey(keys.nfcKeyboardLayoutSetting).evaluate().single.widget
|
find.byKey(android_keys.nfcKeyboardLayoutSetting).evaluate().single.widget
|
||||||
as ListTile;
|
as ListTile;
|
||||||
|
|
||||||
Future<void> openKeyboardLayoutOptionSelection() async {
|
Future<void> openKeyboardLayoutOptionSelection() async {
|
||||||
var widget = find.byKey(keys.nfcKeyboardLayoutSetting).hitTestable();
|
var widget =
|
||||||
|
find.byKey(android_keys.nfcKeyboardLayoutSetting).hitTestable();
|
||||||
expect(widget, findsOneWidget);
|
expect(widget, findsOneWidget);
|
||||||
await tap(widget);
|
await tap(widget);
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
@ -81,44 +84,45 @@ extension _WidgetTesterHelper on WidgetTester {
|
|||||||
|
|
||||||
Future<void> selectKeyboardLayoutUSOption() async {
|
Future<void> selectKeyboardLayoutUSOption() async {
|
||||||
await openKeyboardLayoutOptionSelection();
|
await openKeyboardLayoutOptionSelection();
|
||||||
await tap(find.byKey(keys.keyboardLayoutOption('US')));
|
await tap(find.byKey(android_keys.keyboardLayoutOption('US')));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectKeyboardLayoutDEOption() async {
|
Future<void> selectKeyboardLayoutDEOption() async {
|
||||||
await openKeyboardLayoutOptionSelection();
|
await openKeyboardLayoutOptionSelection();
|
||||||
await tap(find.byKey(keys.keyboardLayoutOption('DE')));
|
await tap(find.byKey(android_keys.keyboardLayoutOption('DE')));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectKeyboardLayoutDECHOption() async {
|
Future<void> selectKeyboardLayoutDECHOption() async {
|
||||||
await openKeyboardLayoutOptionSelection();
|
await openKeyboardLayoutOptionSelection();
|
||||||
await tap(find.byKey(keys.keyboardLayoutOption('DE-CH')));
|
await tap(find.byKey(android_keys.keyboardLayoutOption('DE-CH')));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tapBypassTouch() async {
|
Future<void> tapBypassTouch() async {
|
||||||
await tap(find.byKey(keys.nfcBypassTouchSetting));
|
await tap(find.byKey(android_keys.nfcBypassTouchSetting));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tapOpenAppOnUsb() async {
|
Future<void> tapOpenAppOnUsb() async {
|
||||||
await ensureVisible(find.byKey(keys.usbOpenApp));
|
await ensureVisible(find.byKey(android_keys.usbOpenApp));
|
||||||
await tap(find.byKey(keys.usbOpenApp));
|
await tap(find.byKey(android_keys.usbOpenApp));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tapSilenceNfcSounds() async {
|
Future<void> tapSilenceNfcSounds() async {
|
||||||
await tap(find.byKey(keys.nfcSilenceSoundsSettings));
|
await tap(find.byKey(android_keys.nfcSilenceSoundsSettings));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
ListTile themeModeListTile() =>
|
ListTile themeModeListTile() =>
|
||||||
find.byKey(keys.themeModeSetting).evaluate().single.widget as ListTile;
|
find.byKey(app_keys.themeModeSetting).evaluate().single.widget
|
||||||
|
as ListTile;
|
||||||
|
|
||||||
Future<void> openAppThemeOptionSelection() async {
|
Future<void> openAppThemeOptionSelection() async {
|
||||||
await ensureVisible(find.byKey(keys.themeModeSetting));
|
await ensureVisible(find.byKey(app_keys.themeModeSetting));
|
||||||
var widget = find.byKey(keys.themeModeSetting).hitTestable();
|
var widget = find.byKey(app_keys.themeModeSetting).hitTestable();
|
||||||
expect(widget, findsOneWidget);
|
expect(widget, findsOneWidget);
|
||||||
await tap(widget);
|
await tap(widget);
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
@ -126,19 +130,19 @@ extension _WidgetTesterHelper on WidgetTester {
|
|||||||
|
|
||||||
Future<void> selectSystemTheme() async {
|
Future<void> selectSystemTheme() async {
|
||||||
await openAppThemeOptionSelection();
|
await openAppThemeOptionSelection();
|
||||||
await tap(find.byKey(keys.themeModeSystem));
|
await tap(find.byKey(app_keys.themeModeOption(ThemeMode.system)));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectLightTheme() async {
|
Future<void> selectLightTheme() async {
|
||||||
await openAppThemeOptionSelection();
|
await openAppThemeOptionSelection();
|
||||||
await tap(find.byKey(keys.themeModeLight));
|
await tap(find.byKey(app_keys.themeModeOption(ThemeMode.light)));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectDarkTheme() async {
|
Future<void> selectDarkTheme() async {
|
||||||
await openAppThemeOptionSelection();
|
await openAppThemeOptionSelection();
|
||||||
await tap(find.byKey(keys.themeModeDark));
|
await tap(find.byKey(app_keys.themeModeOption(ThemeMode.dark)));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,9 +160,12 @@ Widget androidWidget({
|
|||||||
], child: child);
|
], child: child);
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
var widget = createMaterialApp(child: const AndroidSettingsPage());
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||||
|
var widget = createMaterialApp(child: const SettingsPage());
|
||||||
|
|
||||||
testWidgets('NFC Tap options', (WidgetTester tester) async {
|
testWidgets('NFC Tap options', (WidgetTester tester) async {
|
||||||
|
const prefNfcOpenApp = 'prefNfcOpenApp';
|
||||||
|
const prefNfcCopyOtp = 'prefNfcCopyOtp';
|
||||||
SharedPreferences.setMockInitialValues(
|
SharedPreferences.setMockInitialValues(
|
||||||
{prefNfcOpenApp: true, prefNfcCopyOtp: false});
|
{prefNfcOpenApp: true, prefNfcCopyOtp: false});
|
||||||
|
|
||||||
@ -191,6 +198,9 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Static password keyboard layout', (WidgetTester tester) async {
|
testWidgets('Static password keyboard layout', (WidgetTester tester) async {
|
||||||
|
const prefNfcOpenApp = 'prefNfcOpenApp';
|
||||||
|
const prefNfcCopyOtp = 'prefNfcCopyOtp';
|
||||||
|
const prefClipKbdLayout = 'prefClipKbdLayout';
|
||||||
SharedPreferences.setMockInitialValues(
|
SharedPreferences.setMockInitialValues(
|
||||||
{prefNfcOpenApp: true, prefNfcCopyOtp: false, prefClipKbdLayout: 'US'});
|
{prefNfcOpenApp: true, prefNfcCopyOtp: false, prefClipKbdLayout: 'US'});
|
||||||
|
|
||||||
@ -229,6 +239,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Bypass touch req', (WidgetTester tester) async {
|
testWidgets('Bypass touch req', (WidgetTester tester) async {
|
||||||
|
const prefNfcBypassTouch = 'prefNfcBypassTouch';
|
||||||
SharedPreferences.setMockInitialValues({prefNfcBypassTouch: false});
|
SharedPreferences.setMockInitialValues({prefNfcBypassTouch: false});
|
||||||
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
|
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
@ -285,6 +296,7 @@ void main() {
|
|||||||
// no value for theme
|
// no value for theme
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
|
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
|
||||||
|
const prefTheme = 'APP_STATE_THEME';
|
||||||
|
|
||||||
await tester.pumpWidget(androidWidget(
|
await tester.pumpWidget(androidWidget(
|
||||||
sharedPrefs: sharedPrefs,
|
sharedPrefs: sharedPrefs,
|
||||||
@ -303,6 +315,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Open app on USB', (WidgetTester tester) async {
|
testWidgets('Open app on USB', (WidgetTester tester) async {
|
||||||
|
const prefUsbOpenApp = 'prefUsbOpenApp';
|
||||||
SharedPreferences.setMockInitialValues({prefUsbOpenApp: false});
|
SharedPreferences.setMockInitialValues({prefUsbOpenApp: false});
|
||||||
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
|
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
@ -321,6 +334,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Silence NFC sound', (WidgetTester tester) async {
|
testWidgets('Silence NFC sound', (WidgetTester tester) async {
|
||||||
|
const prefNfcSilenceSounds = 'prefNfcSilenceSounds';
|
||||||
SharedPreferences.setMockInitialValues({prefNfcSilenceSounds: false});
|
SharedPreferences.setMockInitialValues({prefNfcSilenceSounds: false});
|
||||||
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
|
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
@ -337,4 +351,6 @@ void main() {
|
|||||||
await tester.tapSilenceNfcSounds();
|
await tester.tapSilenceNfcSounds();
|
||||||
expect(sharedPrefs.getBool(prefNfcSilenceSounds), equals(false));
|
expect(sharedPrefs.getBool(prefNfcSilenceSounds), equals(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user