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:shared_preferences/shared_preferences.dart';
|
||||
import 'package:yubico_authenticator/android/init.dart';
|
||||
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/preferences.dart';
|
||||
import 'package:yubico_authenticator/app/views/device_avatar.dart';
|
||||
import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
|
||||
|
||||
import '../test_util.dart';
|
||||
|
||||
void _setShowBetaDialogPref(bool value) async {
|
||||
SharedPreferences.setMockInitialValues({betaDialogPrefName: value});
|
||||
}
|
||||
|
||||
Future<void> startUp(WidgetTester tester,
|
||||
[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());
|
||||
|
||||
// only wait for yubikey connection when needed
|
||||
|
@ -16,27 +16,20 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'models.dart';
|
||||
|
||||
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 manualEntryButton = Key('$_prefix.manual_entry');
|
||||
|
||||
const launchTapAction = Key('$_prefix.tap_action_launch');
|
||||
const copyTapAction = Key('$_prefix.tap_action_copy');
|
||||
const bothTapAction = Key('$_prefix.tap_action_both');
|
||||
|
||||
const themeModeSystem = Key('$_prefix.theme_mode_system');
|
||||
const themeModeLight = Key('$_prefix.theme_mode_light');
|
||||
const themeModeDark = Key('$_prefix.theme_mode_dark');
|
||||
const nfcBypassTouchSetting = Key('$_prefix.nfc_bypass_touch');
|
||||
const nfcSilenceSoundsSettings = Key('$_prefix.nfc_silence_sounds');
|
||||
const usbOpenApp = Key('$_prefix.usb_open_app');
|
||||
|
||||
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');
|
||||
|
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,12 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// shared preferences keys
|
||||
const betaDialogPrefName = 'prefBetaDialogShouldBeShown';
|
||||
const prefNfcOpenApp = 'prefNfcOpenApp';
|
||||
const prefNfcBypassTouch = 'prefNfcBypassTouch';
|
||||
const prefNfcSilenceSounds = 'prefNfcSilenceSounds';
|
||||
const prefNfcCopyOtp = 'prefNfcCopyOtp';
|
||||
const prefClipKbdLayout = 'prefClipKbdLayout';
|
||||
const prefUsbOpenApp = 'prefUsbOpenApp';
|
||||
const prefTheme = 'APP_STATE_THEME';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
enum NfcTapAction {
|
||||
launch,
|
||||
copy,
|
||||
both;
|
||||
|
||||
String getDescription(AppLocalizations l10n) {
|
||||
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/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../app/models.dart';
|
||||
import '../app/state.dart';
|
||||
import '../core/state.dart';
|
||||
import 'app_methods.dart';
|
||||
import 'devices.dart';
|
||||
import 'models.dart';
|
||||
|
||||
const _contextChannel = MethodChannel('android.state.appContext');
|
||||
|
||||
@ -74,9 +77,8 @@ final androidSdkVersionProvider = Provider<int>((ref) => -1);
|
||||
|
||||
final androidNfcSupportProvider = Provider<bool>((ref) => false);
|
||||
|
||||
final androidNfcStateProvider = StateNotifierProvider<NfcStateNotifier, bool>((ref) =>
|
||||
NfcStateNotifier()
|
||||
);
|
||||
final androidNfcStateProvider =
|
||||
StateNotifierProvider<NfcStateNotifier, bool>((ref) => NfcStateNotifier());
|
||||
|
||||
final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) {
|
||||
if (ref.read(androidSdkVersionProvider) < 29) {
|
||||
@ -124,3 +126,114 @@ class AndroidCurrentDeviceNotifier extends CurrentDeviceNotifier {
|
||||
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.
|
||||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
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-desktop');
|
||||
|
||||
void launchHelpUrl() => _launchUrl(Platform.isAndroid
|
||||
void launchHelpUrl() => _launchUrl(isAndroid
|
||||
? 'https://yubi.co/ya-help-android'
|
||||
: '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 '../about_page.dart';
|
||||
import '../android/views/android_settings_page.dart';
|
||||
import '../core/state.dart';
|
||||
import '../desktop/state.dart';
|
||||
import '../oath/keys.dart';
|
||||
import '../settings_page.dart';
|
||||
import 'message.dart';
|
||||
import 'models.dart';
|
||||
import 'state.dart';
|
||||
import 'views/settings_page.dart';
|
||||
|
||||
class OpenIntent extends Intent {
|
||||
const OpenIntent();
|
||||
@ -122,9 +121,7 @@ Widget registerGlobalShortcuts(
|
||||
if (!Navigator.of(context).canPop()) {
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => Platform.isAndroid
|
||||
? const AndroidSettingsPage()
|
||||
: const SettingsPage(),
|
||||
builder: (context) => const SettingsPage(),
|
||||
routeSettings: const RouteSettings(name: 'settings'),
|
||||
);
|
||||
}
|
||||
|
@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -115,14 +113,13 @@ class _DevicePickerContent extends ConsumerWidget {
|
||||
_HeroAvatar(
|
||||
child: DeviceAvatar(
|
||||
radius: 64,
|
||||
child: Icon(Platform.isAndroid ? Icons.no_cell : Icons.usb),
|
||||
child: Icon(isAndroid ? Icons.no_cell : Icons.usb),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Center(child: Text(l10n.l_no_yk_present)),
|
||||
subtitle: Center(
|
||||
child: Text(
|
||||
Platform.isAndroid ? l10n.l_insert_or_tap_yk : l10n.s_usb)),
|
||||
child: Text(isAndroid ? l10n.l_insert_or_tap_yk : l10n.s_usb)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -26,3 +26,7 @@ const actionsIconButtonKey = Key('$_prefix.actions_icon_button');
|
||||
|
||||
// drawer items
|
||||
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.
|
||||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||
final isAndroid = Platform.isAndroid;
|
||||
bool get isDesktop {
|
||||
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.
|
||||
final prefProvider = Provider<SharedPreferences>((ref) {
|
||||
|
@ -56,7 +56,7 @@ final _favoriteAccounts =
|
||||
);
|
||||
|
||||
final systrayProvider = Provider.autoDispose((ref) {
|
||||
final systray = _Systray(ref, ref.watch(l10nProvider));
|
||||
final systray = _Systray(ref);
|
||||
|
||||
// Keep track of which accounts to show
|
||||
ref.listen(
|
||||
@ -64,6 +64,7 @@ final systrayProvider = Provider.autoDispose((ref) {
|
||||
(_, next) {
|
||||
systray._updateCredentials(next);
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
|
||||
// Keep track of the shown/hidden state of the app
|
||||
@ -71,6 +72,11 @@ final systrayProvider = Provider.autoDispose((ref) {
|
||||
systray._setHidden(hidden);
|
||||
}, fireImmediately: true);
|
||||
|
||||
// Keep track of the locale of the app
|
||||
ref.listen(l10nProvider, (_, l10n) {
|
||||
systray._updateLocale(l10n);
|
||||
});
|
||||
|
||||
ref.onDispose(systray.dispose);
|
||||
|
||||
return systray;
|
||||
@ -99,20 +105,17 @@ String _getIcon() {
|
||||
|
||||
class _Systray extends TrayListener {
|
||||
final Ref _ref;
|
||||
final AppLocalizations _l10n;
|
||||
int _lastClick = 0;
|
||||
AppLocalizations _l10n;
|
||||
DevicePath _devicePath = DevicePath([]);
|
||||
List<OathCredential> _credentials = [];
|
||||
bool _isHidden = false;
|
||||
_Systray(this._ref, this._l10n) {
|
||||
_Systray(this._ref) : _l10n = _ref.read(l10nProvider) {
|
||||
_init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
await trayManager.setIcon(_getIcon(), isTemplate: true);
|
||||
if (!Platform.isLinux) {
|
||||
await trayManager.setToolTip(_l10n.app_name);
|
||||
}
|
||||
await _updateContextMenu();
|
||||
|
||||
// Doesn't seem to work on Linux
|
||||
@ -120,9 +123,18 @@ class _Systray extends TrayListener {
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
trayManager.removeListener(this);
|
||||
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) {
|
||||
if (!listEquals(_credentials, pair.second)) {
|
||||
_devicePath = pair.first ?? _devicePath;
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@ -177,7 +176,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
try {
|
||||
if (devicePath == null) {
|
||||
assert(Platform.isAndroid, 'devicePath is only optional for Android');
|
||||
assert(isAndroid, 'devicePath is only optional for Android');
|
||||
await ref
|
||||
.read(addCredentialToAnyProvider)
|
||||
.call(credUri, requireTouch: _touch);
|
||||
@ -334,7 +333,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
final devicePath = deviceNode?.path;
|
||||
if (devicePath != null) {
|
||||
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
|
||||
await _doAddCredential(devicePath: null, credUri: cred.toUri());
|
||||
} else {
|
||||
|
@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../exception/cancellation_exception.dart';
|
||||
import '../../widgets/list_title.dart';
|
||||
import '../models.dart';
|
||||
@ -61,7 +60,7 @@ Widget oathBuildActions(
|
||||
final withContext = ref.read(withContextProvider);
|
||||
Navigator.of(context).pop();
|
||||
CredentialData? otpauth;
|
||||
if (Platform.isAndroid) {
|
||||
if (isAndroid) {
|
||||
final scanner = ref.read(qrScannerProvider);
|
||||
if (scanner != null) {
|
||||
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/foundation.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:yubico_authenticator/android/keys.dart' as keys;
|
||||
import 'package:yubico_authenticator/android/preferences.dart';
|
||||
import 'package:yubico_authenticator/android/models.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/views/android_settings_page.dart';
|
||||
import 'package:yubico_authenticator/app/state.dart';
|
||||
import 'package:yubico_authenticator/app/views/settings_page.dart';
|
||||
import 'package:yubico_authenticator/core/state.dart';
|
||||
|
||||
Widget createMaterialApp({required Widget child}) {
|
||||
@ -44,7 +46,7 @@ Widget createMaterialApp({required Widget child}) {
|
||||
|
||||
extension _WidgetTesterHelper on WidgetTester {
|
||||
Future<void> openNfcTapOptionSelection() async {
|
||||
var widget = find.byKey(keys.nfcTapSetting).hitTestable();
|
||||
var widget = find.byKey(android_keys.nfcTapSetting).hitTestable();
|
||||
expect(widget, findsOneWidget);
|
||||
await tap(widget);
|
||||
await pumpAndSettle();
|
||||
@ -52,28 +54,29 @@ extension _WidgetTesterHelper on WidgetTester {
|
||||
|
||||
Future<void> selectLaunchOption() async {
|
||||
await openNfcTapOptionSelection();
|
||||
await tap(find.byKey(keys.launchTapAction));
|
||||
await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.launch)));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> selectCopyOption() async {
|
||||
await openNfcTapOptionSelection();
|
||||
await tap(find.byKey(keys.copyTapAction));
|
||||
await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.copy)));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> selectBothOption() async {
|
||||
await openNfcTapOptionSelection();
|
||||
await tap(find.byKey(keys.bothTapAction));
|
||||
await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.both)));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
ListTile keyboardLayoutListTile() =>
|
||||
find.byKey(keys.nfcKeyboardLayoutSetting).evaluate().single.widget
|
||||
find.byKey(android_keys.nfcKeyboardLayoutSetting).evaluate().single.widget
|
||||
as ListTile;
|
||||
|
||||
Future<void> openKeyboardLayoutOptionSelection() async {
|
||||
var widget = find.byKey(keys.nfcKeyboardLayoutSetting).hitTestable();
|
||||
var widget =
|
||||
find.byKey(android_keys.nfcKeyboardLayoutSetting).hitTestable();
|
||||
expect(widget, findsOneWidget);
|
||||
await tap(widget);
|
||||
await pumpAndSettle();
|
||||
@ -81,44 +84,45 @@ extension _WidgetTesterHelper on WidgetTester {
|
||||
|
||||
Future<void> selectKeyboardLayoutUSOption() async {
|
||||
await openKeyboardLayoutOptionSelection();
|
||||
await tap(find.byKey(keys.keyboardLayoutOption('US')));
|
||||
await tap(find.byKey(android_keys.keyboardLayoutOption('US')));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> selectKeyboardLayoutDEOption() async {
|
||||
await openKeyboardLayoutOptionSelection();
|
||||
await tap(find.byKey(keys.keyboardLayoutOption('DE')));
|
||||
await tap(find.byKey(android_keys.keyboardLayoutOption('DE')));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> selectKeyboardLayoutDECHOption() async {
|
||||
await openKeyboardLayoutOptionSelection();
|
||||
await tap(find.byKey(keys.keyboardLayoutOption('DE-CH')));
|
||||
await tap(find.byKey(android_keys.keyboardLayoutOption('DE-CH')));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapBypassTouch() async {
|
||||
await tap(find.byKey(keys.nfcBypassTouchSetting));
|
||||
await tap(find.byKey(android_keys.nfcBypassTouchSetting));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapOpenAppOnUsb() async {
|
||||
await ensureVisible(find.byKey(keys.usbOpenApp));
|
||||
await tap(find.byKey(keys.usbOpenApp));
|
||||
await ensureVisible(find.byKey(android_keys.usbOpenApp));
|
||||
await tap(find.byKey(android_keys.usbOpenApp));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapSilenceNfcSounds() async {
|
||||
await tap(find.byKey(keys.nfcSilenceSoundsSettings));
|
||||
await tap(find.byKey(android_keys.nfcSilenceSoundsSettings));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
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 {
|
||||
await ensureVisible(find.byKey(keys.themeModeSetting));
|
||||
var widget = find.byKey(keys.themeModeSetting).hitTestable();
|
||||
await ensureVisible(find.byKey(app_keys.themeModeSetting));
|
||||
var widget = find.byKey(app_keys.themeModeSetting).hitTestable();
|
||||
expect(widget, findsOneWidget);
|
||||
await tap(widget);
|
||||
await pumpAndSettle();
|
||||
@ -126,19 +130,19 @@ extension _WidgetTesterHelper on WidgetTester {
|
||||
|
||||
Future<void> selectSystemTheme() async {
|
||||
await openAppThemeOptionSelection();
|
||||
await tap(find.byKey(keys.themeModeSystem));
|
||||
await tap(find.byKey(app_keys.themeModeOption(ThemeMode.system)));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> selectLightTheme() async {
|
||||
await openAppThemeOptionSelection();
|
||||
await tap(find.byKey(keys.themeModeLight));
|
||||
await tap(find.byKey(app_keys.themeModeOption(ThemeMode.light)));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> selectDarkTheme() async {
|
||||
await openAppThemeOptionSelection();
|
||||
await tap(find.byKey(keys.themeModeDark));
|
||||
await tap(find.byKey(app_keys.themeModeOption(ThemeMode.dark)));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
@ -156,9 +160,12 @@ Widget androidWidget({
|
||||
], child: child);
|
||||
|
||||
void main() {
|
||||
var widget = createMaterialApp(child: const AndroidSettingsPage());
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
var widget = createMaterialApp(child: const SettingsPage());
|
||||
|
||||
testWidgets('NFC Tap options', (WidgetTester tester) async {
|
||||
const prefNfcOpenApp = 'prefNfcOpenApp';
|
||||
const prefNfcCopyOtp = 'prefNfcCopyOtp';
|
||||
SharedPreferences.setMockInitialValues(
|
||||
{prefNfcOpenApp: true, prefNfcCopyOtp: false});
|
||||
|
||||
@ -191,6 +198,9 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Static password keyboard layout', (WidgetTester tester) async {
|
||||
const prefNfcOpenApp = 'prefNfcOpenApp';
|
||||
const prefNfcCopyOtp = 'prefNfcCopyOtp';
|
||||
const prefClipKbdLayout = 'prefClipKbdLayout';
|
||||
SharedPreferences.setMockInitialValues(
|
||||
{prefNfcOpenApp: true, prefNfcCopyOtp: false, prefClipKbdLayout: 'US'});
|
||||
|
||||
@ -229,6 +239,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Bypass touch req', (WidgetTester tester) async {
|
||||
const prefNfcBypassTouch = 'prefNfcBypassTouch';
|
||||
SharedPreferences.setMockInitialValues({prefNfcBypassTouch: false});
|
||||
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
|
||||
|
||||
@ -285,6 +296,7 @@ void main() {
|
||||
// no value for theme
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
|
||||
const prefTheme = 'APP_STATE_THEME';
|
||||
|
||||
await tester.pumpWidget(androidWidget(
|
||||
sharedPrefs: sharedPrefs,
|
||||
@ -303,6 +315,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Open app on USB', (WidgetTester tester) async {
|
||||
const prefUsbOpenApp = 'prefUsbOpenApp';
|
||||
SharedPreferences.setMockInitialValues({prefUsbOpenApp: false});
|
||||
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
|
||||
|
||||
@ -321,6 +334,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Silence NFC sound', (WidgetTester tester) async {
|
||||
const prefNfcSilenceSounds = 'prefNfcSilenceSounds';
|
||||
SharedPreferences.setMockInitialValues({prefNfcSilenceSounds: false});
|
||||
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
|
||||
|
||||
@ -337,4 +351,6 @@ void main() {
|
||||
await tester.tapSilenceNfcSounds();
|
||||
expect(sharedPrefs.getBool(prefNfcSilenceSounds), equals(false));
|
||||
});
|
||||
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user