mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 10:11:52 +03:00
Externalize strings.
This commit is contained in:
parent
64e2d1bc51
commit
6bf231dc7d
@ -40,8 +40,9 @@ class AboutPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return ResponsiveDialog(
|
return ResponsiveDialog(
|
||||||
title: Text(AppLocalizations.of(context)!.general_about),
|
title: Text(l10n.general_about),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -51,7 +52,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 24.0),
|
padding: const EdgeInsets.only(top: 24.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Yubico Authenticator',
|
l10n.general_app_name,
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -62,7 +63,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context)!.general_terms_of_use,
|
l10n.general_terms_of_use,
|
||||||
style:
|
style:
|
||||||
const TextStyle(decoration: TextDecoration.underline),
|
const TextStyle(decoration: TextDecoration.underline),
|
||||||
),
|
),
|
||||||
@ -72,7 +73,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context)!.general_privacy_policy,
|
l10n.general_privacy_policy,
|
||||||
style:
|
style:
|
||||||
const TextStyle(decoration: TextDecoration.underline),
|
const TextStyle(decoration: TextDecoration.underline),
|
||||||
),
|
),
|
||||||
@ -84,7 +85,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context)!.general_open_src_licenses,
|
l10n.general_open_src_licenses,
|
||||||
style: const TextStyle(decoration: TextDecoration.underline),
|
style: const TextStyle(decoration: TextDecoration.underline),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -103,7 +104,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context)!.general_help_and_feedback,
|
l10n.general_help_and_feedback,
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -112,7 +113,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context)!.general_send_feedback,
|
l10n.general_send_feedback,
|
||||||
style:
|
style:
|
||||||
const TextStyle(decoration: TextDecoration.underline),
|
const TextStyle(decoration: TextDecoration.underline),
|
||||||
),
|
),
|
||||||
@ -122,7 +123,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context)!.general_i_need_help,
|
l10n.general_i_need_help,
|
||||||
style:
|
style:
|
||||||
const TextStyle(decoration: TextDecoration.underline),
|
const TextStyle(decoration: TextDecoration.underline),
|
||||||
),
|
),
|
||||||
@ -139,7 +140,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context)!.general_troubleshooting,
|
l10n.general_troubleshooting,
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -150,8 +151,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
const SizedBox(height: 12.0),
|
const SizedBox(height: 12.0),
|
||||||
ActionChip(
|
ActionChip(
|
||||||
avatar: const Icon(Icons.bug_report_outlined),
|
avatar: const Icon(Icons.bug_report_outlined),
|
||||||
label:
|
label: Text(l10n.general_run_diagnostics),
|
||||||
Text(AppLocalizations.of(context)!.general_run_diagnostics),
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
_log.info('Running diagnostics...');
|
_log.info('Running diagnostics...');
|
||||||
final response = await ref
|
final response = await ref
|
||||||
@ -169,10 +169,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
await ref.read(clipboardProvider).setText(text);
|
await ref.read(clipboardProvider).setText(text);
|
||||||
await ref.read(withContextProvider)(
|
await ref.read(withContextProvider)(
|
||||||
(context) async {
|
(context) async {
|
||||||
showMessage(
|
showMessage(context, l10n.general_diagnostics_copied);
|
||||||
context,
|
|
||||||
AppLocalizations.of(context)!
|
|
||||||
.general_diagnostics_copied);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -183,8 +180,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
if (isAndroid) ...[
|
if (isAndroid) ...[
|
||||||
const SizedBox(height: 12.0),
|
const SizedBox(height: 12.0),
|
||||||
FilterChip(
|
FilterChip(
|
||||||
label: Text(
|
label: Text(l10n.general_allow_screenshots),
|
||||||
AppLocalizations.of(context)!.general_allow_screenshots),
|
|
||||||
selected: ref.watch(androidAllowScreenshotsProvider),
|
selected: ref.watch(androidAllowScreenshotsProvider),
|
||||||
onSelected: (value) async {
|
onSelected: (value) async {
|
||||||
ref
|
ref
|
||||||
@ -205,6 +201,7 @@ class LoggingPanel extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final logLevel = ref.watch(logLevelProvider);
|
final logLevel = ref.watch(logLevelProvider);
|
||||||
return Wrap(
|
return Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
@ -220,7 +217,7 @@ class LoggingPanel extends ConsumerWidget {
|
|||||||
items: Levels.LEVELS,
|
items: Levels.LEVELS,
|
||||||
selected: logLevel != Level.INFO,
|
selected: logLevel != Level.INFO,
|
||||||
labelBuilder: (value) => Text(
|
labelBuilder: (value) => Text(
|
||||||
'${AppLocalizations.of(context)!.general_log_level}: ${value.name[0]}${value.name.substring(1).toLowerCase()}'),
|
'${l10n.general_log_level}: ${value.name[0]}${value.name.substring(1).toLowerCase()}'),
|
||||||
itemBuilder: (value) =>
|
itemBuilder: (value) =>
|
||||||
Text('${value.name[0]}${value.name.substring(1).toLowerCase()}'),
|
Text('${value.name[0]}${value.name.substring(1).toLowerCase()}'),
|
||||||
onChanged: (level) {
|
onChanged: (level) {
|
||||||
@ -230,7 +227,7 @@ class LoggingPanel extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
ActionChip(
|
ActionChip(
|
||||||
avatar: const Icon(Icons.copy),
|
avatar: const Icon(Icons.copy),
|
||||||
label: Text(AppLocalizations.of(context)!.general_copy_log),
|
label: Text(l10n.general_copy_log),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
_log.info('Copying log to clipboard ($version)...');
|
_log.info('Copying log to clipboard ($version)...');
|
||||||
final logs = await ref.read(logLevelProvider.notifier).getLogs();
|
final logs = await ref.read(logLevelProvider.notifier).getLogs();
|
||||||
@ -239,8 +236,7 @@ class LoggingPanel extends ConsumerWidget {
|
|||||||
if (!clipboard.platformGivesFeedback()) {
|
if (!clipboard.platformGivesFeedback()) {
|
||||||
await ref.read(withContextProvider)(
|
await ref.read(withContextProvider)(
|
||||||
(context) async {
|
(context) async {
|
||||||
showMessage(context,
|
showMessage(context, l10n.general_log_copied);
|
||||||
AppLocalizations.of(context)!.general_log_copied);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import 'dart:convert';
|
|||||||
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:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import '../../app/logging.dart';
|
import '../../app/logging.dart';
|
||||||
@ -189,12 +190,15 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
|
|||||||
if (_isUsbAttached) {
|
if (_isUsbAttached) {
|
||||||
void triggerTouchPrompt() async {
|
void triggerTouchPrompt() async {
|
||||||
controller = await _withContext(
|
controller = await _withContext(
|
||||||
(context) async => promptUserInteraction(
|
(context) async {
|
||||||
context,
|
final l10n = AppLocalizations.of(context)!;
|
||||||
icon: const Icon(Icons.touch_app),
|
return promptUserInteraction(
|
||||||
title: 'Touch Required',
|
context,
|
||||||
description: 'Touch the button on your YubiKey now.',
|
icon: const Icon(Icons.touch_app),
|
||||||
),
|
title: l10n.oath_touch_required,
|
||||||
|
description: l10n.oath_touch_now,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'qr_scanner_scan_status.dart';
|
import 'qr_scanner_scan_status.dart';
|
||||||
import 'qr_scanner_util.dart';
|
import 'qr_scanner_util.dart';
|
||||||
@ -32,7 +33,8 @@ class QRScannerPermissionsUI extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var scannerAreaWidth = getScannerAreaWidth(screenSize);
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final scannerAreaWidth = getScannerAreaWidth(screenSize);
|
||||||
|
|
||||||
return Stack(children: [
|
return Stack(children: [
|
||||||
/// instruction text under the scanner area
|
/// instruction text under the scanner area
|
||||||
@ -42,11 +44,11 @@ class QRScannerPermissionsUI extends StatelessWidget {
|
|||||||
screenSize.height - scannerAreaWidth / 2.0 + 8.0),
|
screenSize.height - scannerAreaWidth / 2.0 + 8.0),
|
||||||
width: screenSize.width,
|
width: screenSize.width,
|
||||||
height: screenSize.height),
|
height: screenSize.height),
|
||||||
child: const Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 36),
|
padding: const EdgeInsets.symmetric(horizontal: 36),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Yubico Authenticator needs Camera permissions for scanning QR codes.',
|
l10n.androidQrScanner_need_camera_permission,
|
||||||
style: TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
@ -63,32 +65,36 @@ class QRScannerPermissionsUI extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'Have account info?',
|
l10n.androidQrScanner_have_account_info,
|
||||||
textScaleFactor: 0.7,
|
textScaleFactor: 0.7,
|
||||||
style: TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop('');
|
Navigator.of(context).pop('');
|
||||||
},
|
},
|
||||||
child: const Text('Enter manually',
|
child: Text(
|
||||||
style: TextStyle(color: Colors.white))),
|
l10n.androidQrScanner_enter_manually,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'Would like to scan?',
|
l10n.androidQrScanner_want_to_scan,
|
||||||
textScaleFactor: 0.7,
|
textScaleFactor: 0.7,
|
||||||
style: TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onPermissionRequest();
|
onPermissionRequest();
|
||||||
},
|
},
|
||||||
child: const Text('Review permissions',
|
child: Text(
|
||||||
style: TextStyle(color: Colors.white))),
|
l10n.androidQrScanner_review_permissions,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
import 'qr_scanner_scan_status.dart';
|
import 'qr_scanner_scan_status.dart';
|
||||||
@ -32,7 +33,8 @@ class QRScannerUI extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var scannerAreaWidth = getScannerAreaWidth(screenSize);
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final scannerAreaWidth = getScannerAreaWidth(screenSize);
|
||||||
|
|
||||||
return Stack(children: [
|
return Stack(children: [
|
||||||
/// instruction text under the scanner area
|
/// instruction text under the scanner area
|
||||||
@ -44,8 +46,8 @@ class QRScannerUI extends StatelessWidget {
|
|||||||
height: screenSize.height),
|
height: screenSize.height),
|
||||||
child: Text(
|
child: Text(
|
||||||
status != ScanStatus.error
|
status != ScanStatus.error
|
||||||
? 'Point your camera at a QR code to scan it'
|
? l10n.androidQrScanner_point_and_scan
|
||||||
: 'Invalid QR code',
|
: l10n.androidQrScanner_invalid_code,
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
@ -60,18 +62,20 @@ class QRScannerUI extends StatelessWidget {
|
|||||||
height: screenSize.height),
|
height: screenSize.height),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'No QR code?',
|
l10n.androidQrScanner_no_code,
|
||||||
textScaleFactor: 0.7,
|
textScaleFactor: 0.7,
|
||||||
style: TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop('');
|
Navigator.of(context).pop('');
|
||||||
},
|
},
|
||||||
key: keys.manualEntryButton,
|
key: keys.manualEntryButton,
|
||||||
child: const Text('Enter manually',
|
child: Text(
|
||||||
style: TextStyle(color: Colors.white))),
|
l10n.androidQrScanner_enter_manually,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:qrscanner_zxing/qrscanner_zxing_view.dart';
|
import 'package:qrscanner_zxing/qrscanner_zxing_view.dart';
|
||||||
|
|
||||||
import '../../oath/models.dart';
|
import '../../oath/models.dart';
|
||||||
@ -63,7 +64,6 @@ class _QrScannerViewState extends State<QrScannerView> {
|
|||||||
_status = ScanStatus.scanning;
|
_status = ScanStatus.scanning;
|
||||||
|
|
||||||
_zxingViewKey.currentState?.resumeScanning();
|
_zxingViewKey.currentState?.resumeScanning();
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,15 +106,16 @@ class _QrScannerViewState extends State<QrScannerView> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final screenSize = MediaQuery.of(context).size;
|
final screenSize = MediaQuery.of(context).size;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
extendBody: true,
|
extendBody: true,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Add account',
|
l10n.oath_add_account,
|
||||||
style: TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
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';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
@ -34,14 +35,14 @@ enum _TapAction {
|
|||||||
copy,
|
copy,
|
||||||
both;
|
both;
|
||||||
|
|
||||||
String get description {
|
String getDescription(AppLocalizations l10n) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case _TapAction.launch:
|
case _TapAction.launch:
|
||||||
return 'Launch Yubico Authenticator';
|
return l10n.androidSettings_launch_app;
|
||||||
case _TapAction.copy:
|
case _TapAction.copy:
|
||||||
return 'Copy OTP to clipboard';
|
return l10n.androidSettings_copy_otp;
|
||||||
case _TapAction.both:
|
case _TapAction.both:
|
||||||
return 'Launch app and copy OTP';
|
return l10n.androidSettings_launch_and_copy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,14 +77,14 @@ enum _TapAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension on ThemeMode {
|
extension on ThemeMode {
|
||||||
String get displayName {
|
String getDisplayName(AppLocalizations l10n) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case ThemeMode.system:
|
case ThemeMode.system:
|
||||||
return 'System default';
|
return l10n.general_system_default;
|
||||||
case ThemeMode.light:
|
case ThemeMode.light:
|
||||||
return 'Light theme';
|
return l10n.general_light_mode;
|
||||||
case ThemeMode.dark:
|
case ThemeMode.dark:
|
||||||
return 'Dark theme';
|
return l10n.general_dark_mode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,6 +100,7 @@ class AndroidSettingsPage extends ConsumerStatefulWidget {
|
|||||||
class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final prefs = ref.watch(prefProvider);
|
final prefs = ref.watch(prefProvider);
|
||||||
|
|
||||||
final tapAction = _TapAction.load(prefs);
|
final tapAction = _TapAction.load(prefs);
|
||||||
@ -112,7 +114,7 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return ResponsiveDialog(
|
return ResponsiveDialog(
|
||||||
title: const Text('Settings'),
|
title: Text(l10n.general_settings),
|
||||||
child: Theme(
|
child: Theme(
|
||||||
// Make the headers use the primary color to pop a bit.
|
// Make the headers use the primary color to pop a bit.
|
||||||
// Once M3 is implemented this will probably not be needed.
|
// Once M3 is implemented this will probably not be needed.
|
||||||
@ -125,10 +127,10 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const ListTitle('NFC options'),
|
ListTitle(l10n.androidSettings_nfc_options),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('On YubiKey NFC tap'),
|
title: Text(l10n.androidSettings_nfc_on_tap),
|
||||||
subtitle: Text(tapAction.description),
|
subtitle: Text(tapAction.getDescription(l10n)),
|
||||||
key: keys.nfcTapSetting,
|
key: keys.nfcTapSetting,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final newTapAction = await _selectTapAction(context, tapAction);
|
final newTapAction = await _selectTapAction(context, tapAction);
|
||||||
@ -138,7 +140,7 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Keyboard Layout (for static password)'),
|
title: Text(l10n.androidSettings_keyboard_layout),
|
||||||
subtitle: Text(clipKbdLayout),
|
subtitle: Text(clipKbdLayout),
|
||||||
key: keys.nfcKeyboardLayoutSetting,
|
key: keys.nfcKeyboardLayoutSetting,
|
||||||
enabled: tapAction != _TapAction.launch,
|
enabled: tapAction != _TapAction.launch,
|
||||||
@ -152,12 +154,10 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: const Text('Bypass touch requirement'),
|
title: Text(l10n.androidSettings_bypass_touch),
|
||||||
subtitle: nfcBypassTouch
|
subtitle: Text(nfcBypassTouch
|
||||||
? const Text(
|
? l10n.androidSettings_bypass_touch_on
|
||||||
'Accounts that require touch are automatically shown over NFC')
|
: l10n.androidSettings_bypass_touch_off),
|
||||||
: const Text(
|
|
||||||
'Accounts that require touch need an additional tap over NFC'),
|
|
||||||
value: nfcBypassTouch,
|
value: nfcBypassTouch,
|
||||||
key: keys.nfcBypassTouchSetting,
|
key: keys.nfcBypassTouchSetting,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -166,11 +166,10 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: const Text('Silence NFC sounds'),
|
title: Text(l10n.androidSettings_silence_nfc),
|
||||||
subtitle: nfcSilenceSounds
|
subtitle: Text(nfcSilenceSounds
|
||||||
? const Text(
|
? l10n.androidSettings_silence_nfc_on
|
||||||
'No sounds will be played on NFC tap')
|
: l10n.androidSettings_silence_nfc_off),
|
||||||
: const Text('Sound will play on NFC tap'),
|
|
||||||
value: nfcSilenceSounds,
|
value: nfcSilenceSounds,
|
||||||
key: keys.nfcSilenceSoundsSettings,
|
key: keys.nfcSilenceSoundsSettings,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -178,13 +177,12 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
|||||||
prefs.setBool(prefNfcSilenceSounds, value);
|
prefs.setBool(prefNfcSilenceSounds, value);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
const ListTitle('USB options'),
|
ListTitle(l10n.androidSettings_usb_options),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: const Text('Launch when YubiKey is connected'),
|
title: Text(l10n.androidSettings_usb_launch),
|
||||||
subtitle: usbOpenApp
|
subtitle: Text(usbOpenApp
|
||||||
? const Text(
|
? l10n.androidSettings_usb_launch_on
|
||||||
'This prevents other apps from using the YubiKey over USB')
|
: l10n.androidSettings_usb_launch_off),
|
||||||
: const Text('Other apps can use the YubiKey over USB'),
|
|
||||||
value: usbOpenApp,
|
value: usbOpenApp,
|
||||||
key: keys.usbOpenApp,
|
key: keys.usbOpenApp,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -192,10 +190,10 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
|||||||
prefs.setBool(prefUsbOpenApp, value);
|
prefs.setBool(prefUsbOpenApp, value);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
const ListTitle('Appearance'),
|
ListTitle(l10n.general_appearance),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('App theme'),
|
title: Text(l10n.androidSettings_app_theme),
|
||||||
subtitle: Text(themeMode.displayName),
|
subtitle: Text(themeMode.getDisplayName(l10n)),
|
||||||
key: keys.themeModeSetting,
|
key: keys.themeModeSetting,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final newMode = await _selectAppearance(
|
final newMode = await _selectAppearance(
|
||||||
@ -214,12 +212,13 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
|||||||
await showDialog<_TapAction>(
|
await showDialog<_TapAction>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return SimpleDialog(
|
return SimpleDialog(
|
||||||
title: const Text('On YubiKey NFC tap'),
|
title: Text(l10n.androidSettings_nfc_on_tap),
|
||||||
children: _TapAction.values
|
children: _TapAction.values
|
||||||
.map(
|
.map(
|
||||||
(e) => RadioListTile<_TapAction>(
|
(e) => RadioListTile<_TapAction>(
|
||||||
title: Text(e.description),
|
title: Text(e.getDescription(l10n)),
|
||||||
key: e.key,
|
key: e.key,
|
||||||
value: e,
|
value: e,
|
||||||
groupValue: tapAction,
|
groupValue: tapAction,
|
||||||
@ -238,8 +237,9 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
|||||||
await showDialog<String>(
|
await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return SimpleDialog(
|
return SimpleDialog(
|
||||||
title: const Text('Choose keyboard layout'),
|
title: Text(l10n.androidSettings_choose_keyboard_layout),
|
||||||
children: _keyboardLayouts
|
children: _keyboardLayouts
|
||||||
.map(
|
.map(
|
||||||
(e) => RadioListTile<String>(
|
(e) => RadioListTile<String>(
|
||||||
@ -262,11 +262,12 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
|||||||
await showDialog<ThemeMode>(
|
await showDialog<ThemeMode>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return SimpleDialog(
|
return SimpleDialog(
|
||||||
title: const Text('Choose app theme'),
|
title: Text(l10n.androidSettings_choose_app_theme),
|
||||||
children: supportedThemes
|
children: supportedThemes
|
||||||
.map((e) => RadioListTile(
|
.map((e) => RadioListTile(
|
||||||
title: Text(e.displayName),
|
title: Text(e.getDisplayName(l10n)),
|
||||||
value: e,
|
value: e,
|
||||||
key: Key('android.keys.theme_mode_${e.name}'),
|
key: Key('android.keys.theme_mode_${e.name}'),
|
||||||
groupValue: themeMode,
|
groupValue: themeMode,
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import '../../management/models.dart';
|
import '../../management/models.dart';
|
||||||
import '../core/models.dart';
|
import '../core/models.dart';
|
||||||
@ -28,16 +29,15 @@ const _listEquality = ListEquality();
|
|||||||
enum Availability { enabled, disabled, unsupported }
|
enum Availability { enabled, disabled, unsupported }
|
||||||
|
|
||||||
enum Application {
|
enum Application {
|
||||||
oath('Authenticator'),
|
oath,
|
||||||
fido('WebAuthn'),
|
fido,
|
||||||
otp('One-Time Passwords'),
|
otp,
|
||||||
piv('Certificates'),
|
piv,
|
||||||
openpgp('OpenPGP'),
|
openpgp,
|
||||||
hsmauth('YubiHSM Auth'),
|
hsmauth,
|
||||||
management('Toggle Applications');
|
management;
|
||||||
|
|
||||||
final String displayName;
|
const Application();
|
||||||
const Application(this.displayName);
|
|
||||||
|
|
||||||
bool _inCapabilities(int capabilities) {
|
bool _inCapabilities(int capabilities) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
@ -59,6 +59,17 @@ enum Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getDisplayName(AppLocalizations l10n) {
|
||||||
|
switch (this) {
|
||||||
|
case Application.oath:
|
||||||
|
return l10n.oath_authenticator;
|
||||||
|
case Application.fido:
|
||||||
|
return l10n.fido_webauthn;
|
||||||
|
default:
|
||||||
|
return name.substring(0, 1).toUpperCase() + name.substring(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Availability getAvailability(YubiKeyData data) {
|
Availability getAvailability(YubiKeyData data) {
|
||||||
if (this == Application.management) {
|
if (this == Application.management) {
|
||||||
final version = data.info.version;
|
final version = data.info.version;
|
||||||
|
@ -34,10 +34,11 @@ class AppFailurePage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final reason = cause;
|
final reason = cause;
|
||||||
|
|
||||||
Widget? graphic = const Icon(Icons.error);
|
Widget? graphic = const Icon(Icons.error);
|
||||||
String? header = 'An error has occured';
|
String? header = l10n.appFailurePage_error_occured;
|
||||||
String? message = reason.toString();
|
String? message = reason.toString();
|
||||||
List<Widget> actions = [];
|
List<Widget> actions = [];
|
||||||
|
|
||||||
@ -45,13 +46,13 @@ class AppFailurePage extends ConsumerWidget {
|
|||||||
if (reason.status == 'connection-error') {
|
if (reason.status == 'connection-error') {
|
||||||
switch (reason.body['connection']) {
|
switch (reason.body['connection']) {
|
||||||
case 'ccid':
|
case 'ccid':
|
||||||
header = 'Failed to open smart card connection';
|
header = l10n.appFailurePage_ccid_failed;
|
||||||
if (Platform.isMacOS) {
|
if (Platform.isMacOS) {
|
||||||
message = 'Try to remove and re-insert your YubiKey.';
|
message = l10n.appFailurePage_msg_reinsert;
|
||||||
} else if (Platform.isLinux) {
|
} else if (Platform.isLinux) {
|
||||||
message = 'Make sure pcscd is installed and running.';
|
message = l10n.appFailurePage_pcscd_unavailable;
|
||||||
} else {
|
} else {
|
||||||
message = 'Make sure your smart card service is functioning.';
|
message = l10n.appFailurePage_ccid_unavailable;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'fido':
|
case 'fido':
|
||||||
@ -59,17 +60,14 @@ class AppFailurePage extends ConsumerWidget {
|
|||||||
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||||
graphic = noPermission;
|
graphic = noPermission;
|
||||||
header = null;
|
header = null;
|
||||||
message = AppLocalizations.of(context)!.appFailurePage_txt_info;
|
message = l10n.appFailurePage_txt_info;
|
||||||
actions = [
|
actions = [
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
label: Text(
|
label: Text(l10n.appFailurePage_btn_unlock),
|
||||||
AppLocalizations.of(context)!.appFailurePage_btn_unlock),
|
|
||||||
icon: const Icon(Icons.lock_open),
|
icon: const Icon(Icons.lock_open),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final closeMessage = showMessage(
|
final closeMessage = showMessage(
|
||||||
context,
|
context, l10n.appFailurePage_msg_permission,
|
||||||
AppLocalizations.of(context)!
|
|
||||||
.appFailurePage_msg_permission,
|
|
||||||
duration: const Duration(seconds: 30));
|
duration: const Duration(seconds: 30));
|
||||||
try {
|
try {
|
||||||
if (await ref.read(rpcProvider).requireValue.elevate()) {
|
if (await ref.read(rpcProvider).requireValue.elevate()) {
|
||||||
@ -77,7 +75,10 @@ class AppFailurePage extends ConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
await ref.read(withContextProvider)(
|
await ref.read(withContextProvider)(
|
||||||
(context) async {
|
(context) async {
|
||||||
showMessage(context, 'Permission denied');
|
showMessage(
|
||||||
|
context,
|
||||||
|
l10n.general_permission_denied,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -90,8 +91,8 @@ class AppFailurePage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
header = 'Failed to open connection';
|
header = l10n.appFailurePage_failed_connection;
|
||||||
message = 'Try to remove and re-insert your YubiKey.';
|
message = l10n.appFailurePage_msg_reinsert;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ 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 '../../core/models.dart';
|
import '../../core/models.dart';
|
||||||
import '../../desktop/state.dart';
|
import '../../desktop/state.dart';
|
||||||
@ -34,26 +35,27 @@ class DeviceErrorScreen extends ConsumerWidget {
|
|||||||
const DeviceErrorScreen(this.node, {this.error, super.key});
|
const DeviceErrorScreen(this.node, {this.error, super.key});
|
||||||
|
|
||||||
Widget _buildUsbPid(BuildContext context, WidgetRef ref, UsbPid pid) {
|
Widget _buildUsbPid(BuildContext context, WidgetRef ref, UsbPid pid) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
if (pid.usbInterfaces == UsbInterface.fido.value) {
|
if (pid.usbInterfaces == UsbInterface.fido.value) {
|
||||||
if (Platform.isWindows &&
|
if (Platform.isWindows &&
|
||||||
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
graphic: noPermission,
|
graphic: noPermission,
|
||||||
message: 'Managing this device requires elevated privileges.',
|
message: l10n.general_elevated_permissions_required,
|
||||||
actions: [
|
actions: [
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
label: const Text('Unlock'),
|
label: Text(l10n.appFailurePage_btn_unlock),
|
||||||
icon: const Icon(Icons.lock_open),
|
icon: const Icon(Icons.lock_open),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final closeMessage = showMessage(
|
final closeMessage = showMessage(
|
||||||
context, 'Elevating permissions...',
|
context, l10n.appFailurePage_msg_permission,
|
||||||
duration: const Duration(seconds: 30));
|
duration: const Duration(seconds: 30));
|
||||||
try {
|
try {
|
||||||
if (await ref.read(rpcProvider).requireValue.elevate()) {
|
if (await ref.read(rpcProvider).requireValue.elevate()) {
|
||||||
ref.invalidate(rpcProvider);
|
ref.invalidate(rpcProvider);
|
||||||
} else {
|
} else {
|
||||||
await ref.read(withContextProvider)((context) async =>
|
await ref.read(withContextProvider)((context) async =>
|
||||||
showMessage(context, 'Permission denied'));
|
showMessage(context, l10n.general_permission_denied));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
closeMessage();
|
closeMessage();
|
||||||
@ -64,24 +66,25 @@ class DeviceErrorScreen extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return const MessagePage(
|
return MessagePage(
|
||||||
graphic: DeviceAvatar(child: Icon(Icons.usb_off)),
|
graphic: const DeviceAvatar(child: Icon(Icons.usb_off)),
|
||||||
message: 'This YubiKey cannot be accessed',
|
message: l10n.general_yubikey_no_access,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return node.map(
|
return node.map(
|
||||||
usbYubiKey: (node) => _buildUsbPid(context, ref, node.pid),
|
usbYubiKey: (node) => _buildUsbPid(context, ref, node.pid),
|
||||||
nfcReader: (node) {
|
nfcReader: (node) {
|
||||||
final String message;
|
final String message;
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case 'unknown-device':
|
case 'unknown-device':
|
||||||
message = 'Unrecognized device';
|
message = l10n.devicePicker_unknown_device;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
message = 'Place your YubiKey on the NFC reader';
|
message = l10n.general_place_on_nfc_reader;
|
||||||
}
|
}
|
||||||
return MessagePage(message: message);
|
return MessagePage(message: message);
|
||||||
},
|
},
|
||||||
|
@ -71,6 +71,7 @@ class MainPageDrawer extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final supportedApps = ref.watch(supportedAppsProvider);
|
final supportedApps = ref.watch(supportedAppsProvider);
|
||||||
final data = ref.watch(currentDeviceDataProvider).valueOrNull;
|
final data = ref.watch(currentDeviceDataProvider).valueOrNull;
|
||||||
final color =
|
final color =
|
||||||
@ -132,7 +133,7 @@ class MainPageDrawer extends ConsumerWidget {
|
|||||||
if (data != null) ...[
|
if (data != null) ...[
|
||||||
// Normal YubiKey Applications
|
// Normal YubiKey Applications
|
||||||
...availableApps.map((app) => NavigationDrawerDestination(
|
...availableApps.map((app) => NavigationDrawerDestination(
|
||||||
label: Text(app.displayName),
|
label: Text(app.getDisplayName(l10n)),
|
||||||
icon: Icon(app._icon),
|
icon: Icon(app._icon),
|
||||||
selectedIcon: Icon(app._filledIcon),
|
selectedIcon: Icon(app._filledIcon),
|
||||||
)),
|
)),
|
||||||
@ -141,7 +142,7 @@ class MainPageDrawer extends ConsumerWidget {
|
|||||||
NavigationDrawerDestination(
|
NavigationDrawerDestination(
|
||||||
key: managementAppDrawer,
|
key: managementAppDrawer,
|
||||||
label: Text(
|
label: Text(
|
||||||
AppLocalizations.of(context)!.mainDrawer_txt_applications,
|
l10n.mainDrawer_txt_applications,
|
||||||
),
|
),
|
||||||
icon: Icon(Application.management._icon),
|
icon: Icon(Application.management._icon),
|
||||||
selectedIcon: Icon(Application.management._filledIcon),
|
selectedIcon: Icon(Application.management._filledIcon),
|
||||||
@ -151,11 +152,11 @@ class MainPageDrawer extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
// Non-YubiKey pages
|
// Non-YubiKey pages
|
||||||
NavigationDrawerDestination(
|
NavigationDrawerDestination(
|
||||||
label: Text(AppLocalizations.of(context)!.mainDrawer_txt_settings),
|
label: Text(l10n.mainDrawer_txt_settings),
|
||||||
icon: const Icon(Icons.settings_outlined),
|
icon: const Icon(Icons.settings_outlined),
|
||||||
),
|
),
|
||||||
NavigationDrawerDestination(
|
NavigationDrawerDestination(
|
||||||
label: Text(AppLocalizations.of(context)!.mainDrawer_txt_help),
|
label: Text(l10n.mainDrawer_txt_help),
|
||||||
icon: const Icon(Icons.help_outline),
|
icon: const Icon(Icons.help_outline),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -16,16 +16,17 @@
|
|||||||
|
|
||||||
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:yubico_authenticator/android/app_methods.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:yubico_authenticator/android/state.dart';
|
|
||||||
import 'package:yubico_authenticator/widgets/custom_icons.dart';
|
|
||||||
|
|
||||||
|
import '../../android/app_methods.dart';
|
||||||
|
import '../../android/state.dart';
|
||||||
import '../../exception/cancellation_exception.dart';
|
import '../../exception/cancellation_exception.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../../fido/views/fido_screen.dart';
|
import '../../fido/views/fido_screen.dart';
|
||||||
import '../../oath/models.dart';
|
import '../../oath/models.dart';
|
||||||
import '../../oath/views/add_account_page.dart';
|
import '../../oath/views/add_account_page.dart';
|
||||||
import '../../oath/views/oath_screen.dart';
|
import '../../oath/views/oath_screen.dart';
|
||||||
|
import '../../widgets/custom_icons.dart';
|
||||||
import '../message.dart';
|
import '../message.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
@ -37,6 +38,7 @@ class MainPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
ref.listen<Function(BuildContext)?>(
|
ref.listen<Function(BuildContext)?>(
|
||||||
contextConsumer,
|
contextConsumer,
|
||||||
(previous, next) {
|
(previous, next) {
|
||||||
@ -45,7 +47,8 @@ class MainPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
isNfcEnabled().then((value) => ref.read(androidNfcStateProvider.notifier).setNfcEnabled(value));
|
isNfcEnabled().then((value) =>
|
||||||
|
ref.read(androidNfcStateProvider.notifier).setNfcEnabled(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the current device changes, we need to pop any open dialogs.
|
// If the current device changes, we need to pop any open dialogs.
|
||||||
@ -75,18 +78,17 @@ class MainPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
if (deviceNode == null) {
|
if (deviceNode == null) {
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
|
|
||||||
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
|
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
|
||||||
var isNfcEnabled = ref.watch(androidNfcStateProvider);
|
var isNfcEnabled = ref.watch(androidNfcStateProvider);
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
graphic: noKeyImage,
|
graphic: noKeyImage,
|
||||||
message: hasNfcSupport && isNfcEnabled
|
message: hasNfcSupport && isNfcEnabled
|
||||||
? 'Tap or insert your YubiKey'
|
? l10n.devicePicker_insert_or_tap
|
||||||
: 'Insert your YubiKey',
|
: l10n.devicePicker_insert_yubikey,
|
||||||
actions: [
|
actions: [
|
||||||
if (hasNfcSupport && !isNfcEnabled)
|
if (hasNfcSupport && !isNfcEnabled)
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
label: const Text('Enable NFC'),
|
label: Text(l10n.general_enable_nfc),
|
||||||
icon: nfcIcon,
|
icon: nfcIcon,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await openNfcSettings();
|
await openNfcSettings();
|
||||||
@ -94,7 +96,7 @@ class MainPage extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
actionButtonBuilder: (context) => IconButton(
|
actionButtonBuilder: (context) => IconButton(
|
||||||
icon: const Icon(Icons.person_add_alt_1),
|
icon: const Icon(Icons.person_add_alt_1),
|
||||||
tooltip: 'Add account',
|
tooltip: l10n.oath_add_account,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
CredentialData? otpauth;
|
CredentialData? otpauth;
|
||||||
final scanner = ref.read(qrScannerProvider);
|
final scanner = ref.read(qrScannerProvider);
|
||||||
@ -130,7 +132,7 @@ class MainPage extends ConsumerWidget {
|
|||||||
return MessagePage(
|
return MessagePage(
|
||||||
delayedContent: true,
|
delayedContent: true,
|
||||||
graphic: noKeyImage,
|
graphic: noKeyImage,
|
||||||
message: 'Insert your YubiKey',
|
message: l10n.devicePicker_insert_yubikey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -139,21 +141,19 @@ class MainPage extends ConsumerWidget {
|
|||||||
final app = ref.watch(currentAppProvider);
|
final app = ref.watch(currentAppProvider);
|
||||||
if (data.info.supportedCapabilities.isEmpty &&
|
if (data.info.supportedCapabilities.isEmpty &&
|
||||||
data.name == 'Unrecognized device') {
|
data.name == 'Unrecognized device') {
|
||||||
return const MessagePage(
|
return MessagePage(
|
||||||
header: 'Device not recognized',
|
header: l10n.mainPage_not_recognized,
|
||||||
);
|
);
|
||||||
} else if (app.getAvailability(data) ==
|
} else if (app.getAvailability(data) ==
|
||||||
Availability.unsupported) {
|
Availability.unsupported) {
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
header: 'Application not supported',
|
header: l10n.mainPage_app_not_supported,
|
||||||
message:
|
message: l10n.mainPage_app_not_supported_on_yubikey(app.name),
|
||||||
'The used YubiKey does not support \'${app.name}\' application',
|
|
||||||
);
|
);
|
||||||
} else if (app.getAvailability(data) != Availability.enabled) {
|
} else if (app.getAvailability(data) != Availability.enabled) {
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
header: 'Application disabled',
|
header: l10n.mainPage_app_not_enabled,
|
||||||
message:
|
message: l10n.mainPage_app_not_enabled_desc(app.name),
|
||||||
'Enable the \'${app.name}\' application on your YubiKey to access',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,9 +163,9 @@ class MainPage extends ConsumerWidget {
|
|||||||
case Application.fido:
|
case Application.fido:
|
||||||
return FidoScreen(data);
|
return FidoScreen(data);
|
||||||
default:
|
default:
|
||||||
return const MessagePage(
|
return MessagePage(
|
||||||
header: 'Not supported',
|
header: l10n.mainPage_app_not_supported,
|
||||||
message: 'This application is not supported',
|
message: l10n.mainPage_app_not_supported_desc,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
"oath_fail_add_account": "Failed adding account",
|
"oath_fail_add_account": "Failed adding account",
|
||||||
"oath_add_account": "Add account",
|
"oath_add_account": "Add account",
|
||||||
"oath_save": "Save",
|
"oath_save": "Save",
|
||||||
"oath_no_qr_code": "No QR code found",
|
|
||||||
"oath_duplicate_name": "This name already exists for the Issuer",
|
"oath_duplicate_name": "This name already exists for the Issuer",
|
||||||
"oath_issuer_optional": "Issuer (optional)",
|
"oath_issuer_optional": "Issuer (optional)",
|
||||||
"oath_account_name": "Account name",
|
"oath_account_name": "Account name",
|
||||||
@ -139,12 +138,20 @@
|
|||||||
"general_allow_screenshots": "Allow screenshots",
|
"general_allow_screenshots": "Allow screenshots",
|
||||||
"general_usb": "USB",
|
"general_usb": "USB",
|
||||||
"general_nfc": "NFC",
|
"general_nfc": "NFC",
|
||||||
|
"general_enable_nfc": "Enable NFC",
|
||||||
|
"general_place_on_nfc_reader": "Place your YubiKey on the NFC reader",
|
||||||
"general_setup": "Setup",
|
"general_setup": "Setup",
|
||||||
"general_manage": "Manage",
|
"general_manage": "Manage",
|
||||||
"general_configure_yubikey": "Configure YubiKey",
|
"general_configure_yubikey": "Configure YubiKey",
|
||||||
"general_show_window": "Show window",
|
"general_show_window": "Show window",
|
||||||
"general_hide_window": "Hide window",
|
"general_hide_window": "Hide window",
|
||||||
"general_quit": "Quit",
|
"general_quit": "Quit",
|
||||||
|
"general_character_count": "Character count",
|
||||||
|
"general_please_wait": "Please wait\u2026",
|
||||||
|
"general_unsupported_yubikey": "Unsupported YubiKey",
|
||||||
|
"general_yubikey_no_access": "This YubiKey cannot be accessed",
|
||||||
|
"general_permission_denied": "Permission denied",
|
||||||
|
"general_elevated_permissions_required": "Managing this device requires elevated privileges.",
|
||||||
|
|
||||||
"fido_press_fingerprint_begin": "Press your finger against the YubiKey to begin.",
|
"fido_press_fingerprint_begin": "Press your finger against the YubiKey to begin.",
|
||||||
"fido_keep_touching_yubikey": "Keep touching your YubiKey repeatedly\u2026",
|
"fido_keep_touching_yubikey": "Keep touching your YubiKey repeatedly\u2026",
|
||||||
@ -253,22 +260,39 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
"mainPage_not_recognized": "Device not recognized",
|
||||||
|
"mainPage_app_not_supported": "Application not supported",
|
||||||
|
"mainPage_app_not_supported_on_yubikey": "The used YubiKey does not support '${app}' application",
|
||||||
|
"@mainPage_app_not_supported_on_yubikey" : {
|
||||||
|
"placeholders": {
|
||||||
|
"app": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mainPage_app_not_supported_desc": "This application is not supported",
|
||||||
|
"mainPage_app_not_enabled": "Application disabled",
|
||||||
|
"mainPage_app_not_enabled_desc": "Enable the '{app}' application on your YubiKey to access",
|
||||||
|
"@mainPage_app_not_enabled_desc" : {
|
||||||
|
"placeholders": {
|
||||||
|
"app": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"appFailurePage_btn_unlock": "Unlock",
|
"appFailurePage_btn_unlock": "Unlock",
|
||||||
"appFailurePage_txt_info": "WebAuthn management requires elevated privileges.",
|
"appFailurePage_txt_info": "WebAuthn management requires elevated privileges.",
|
||||||
"appFailurePage_msg_permission": "Elevating permissions\u2026",
|
"appFailurePage_msg_permission": "Elevating permissions\u2026",
|
||||||
|
"appFailurePage_error_occured": "An error has occured",
|
||||||
|
"appFailurePage_failed_connection": "Failed to open connection",
|
||||||
|
"appFailurePage_msg_reinsert": "Try to remove and re-insert your YubiKey.",
|
||||||
|
"appFailurePage_ccid_failed": "Failed to open smart card connection",
|
||||||
|
"appFailurePage_ccid_unavailable": "Make sure your smart card service is functioning.",
|
||||||
|
"appFailurePage_pcscd_unavailable": "Make sure pcscd is installed and running.",
|
||||||
|
|
||||||
"mainDrawer_txt_applications": "Toggle applications",
|
"mainDrawer_txt_applications": "Toggle applications",
|
||||||
"mainDrawer_txt_settings": "Settings",
|
"mainDrawer_txt_settings": "Settings",
|
||||||
"mainDrawer_txt_help": "Help and about",
|
"mainDrawer_txt_help": "Help and about",
|
||||||
|
|
||||||
"devicePicker_no_yubikey": "No YubiKey present",
|
"devicePicker_no_yubikey": "No YubiKey present",
|
||||||
|
"devicePicker_insert_yubikey": "Insert your YubiKey",
|
||||||
"devicePicker_insert_or_tap": "Insert or tap a YubiKey",
|
"devicePicker_insert_or_tap": "Insert or tap a YubiKey",
|
||||||
"devicePicker_select_to_scan": "Select to scan",
|
"devicePicker_select_to_scan": "Select to scan",
|
||||||
"devicePicker_inaccessible": "Device inaccessible",
|
"devicePicker_inaccessible": "Device inaccessible",
|
||||||
@ -297,5 +321,36 @@
|
|||||||
"label": {}
|
"label": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"systray_no_pinned": "No pinned accounts"
|
"systray_no_pinned": "No pinned accounts",
|
||||||
|
|
||||||
|
"androidSettings_launch_app": "Launch Yubico Authenticator",
|
||||||
|
"androidSettings_copy_otp": "Copy OTP to clipboard",
|
||||||
|
"androidSettings_launch_and_copy": "Launch app and copy OTP",
|
||||||
|
"androidSettings_nfc_options": "NFC options",
|
||||||
|
"androidSettings_nfc_on_tap": "On YubiKey NFC tap",
|
||||||
|
"androidSettings_keyboard_layout": "Keyboard layout (for static password)",
|
||||||
|
"androidSettings_choose_keyboard_layout": "Choose keyboard layout",
|
||||||
|
"androidSettings_bypass_touch": "Bypass touch requirement",
|
||||||
|
"androidSettings_bypass_touch_on": "Accounts that require touch are automatically shown over NFC",
|
||||||
|
"androidSettings_bypass_touch_off": "Accounts that require touch need an additional tap over NFC",
|
||||||
|
"androidSettings_silence_nfc": "Silence NFC sounds",
|
||||||
|
"androidSettings_silence_nfc_on": "No sounds will be played on NFC tap",
|
||||||
|
"androidSettings_silence_nfc_off": "Sound will play on NFC tap",
|
||||||
|
"androidSettings_usb_options": "USB options",
|
||||||
|
"androidSettings_usb_launch": "Launch when YubiKey is connected",
|
||||||
|
"androidSettings_usb_launch_on": "This prevents other apps from using the YubiKey over USB",
|
||||||
|
"androidSettings_usb_launch_off": "Other apps can use the YubiKey over USB",
|
||||||
|
"androidSettings_app_theme": "App theme",
|
||||||
|
"androidSettings_choose_app_theme": "Choose app theme",
|
||||||
|
|
||||||
|
"androidQrScanner_point_and_scan": "Point your camera at a QR code to scan it",
|
||||||
|
"androidQrScanner_invalid_code": "Invalid QR code",
|
||||||
|
"androidQrScanner_no_code": "No QR code?",
|
||||||
|
"androidQrScanner_enter_manually": "Enter manually",
|
||||||
|
"androidQrScanner_need_camera_permission": "Yubico Authenticator needs Camera permissions for scanning QR codes.",
|
||||||
|
"androidQrScanner_have_account_info": "Have account info?",
|
||||||
|
"androidQrScanner_want_to_scan": "Would like to scan?",
|
||||||
|
"androidQrScanner_review_permissions": "Review permissions",
|
||||||
|
|
||||||
|
"@_EOF": {}
|
||||||
}
|
}
|
@ -113,10 +113,10 @@ class _CapabilitiesForm extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (usbCapabilities != 0) ...[
|
if (usbCapabilities != 0) ...[
|
||||||
const ListTile(
|
ListTile(
|
||||||
leading: Icon(Icons.usb),
|
leading: const Icon(Icons.usb),
|
||||||
title: Text('USB'),
|
title: Text(AppLocalizations.of(context)!.general_usb),
|
||||||
contentPadding: EdgeInsets.only(bottom: 8),
|
contentPadding: const EdgeInsets.only(bottom: 8),
|
||||||
horizontalTitleGap: 0,
|
horizontalTitleGap: 0,
|
||||||
),
|
),
|
||||||
_CapabilityForm(
|
_CapabilityForm(
|
||||||
@ -136,7 +136,7 @@ class _CapabilitiesForm extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: nfcIcon,
|
leading: nfcIcon,
|
||||||
title: const Text('NFC'),
|
title: Text(AppLocalizations.of(context)!.general_nfc),
|
||||||
contentPadding: const EdgeInsets.only(bottom: 8),
|
contentPadding: const EdgeInsets.only(bottom: 8),
|
||||||
horizontalTitleGap: 0,
|
horizontalTitleGap: 0,
|
||||||
),
|
),
|
||||||
|
@ -18,6 +18,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
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 '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/shortcuts.dart';
|
import '../../app/shortcuts.dart';
|
||||||
@ -37,13 +38,16 @@ class AccountDialog extends ConsumerWidget {
|
|||||||
const AccountDialog(this.credential, {super.key});
|
const AccountDialog(this.credential, {super.key});
|
||||||
|
|
||||||
List<Widget> _buildActions(BuildContext context, AccountHelper helper) {
|
List<Widget> _buildActions(BuildContext context, AccountHelper helper) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final actions = helper.buildActions();
|
final actions = helper.buildActions();
|
||||||
|
|
||||||
final theme =
|
final theme =
|
||||||
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
|
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
|
||||||
|
|
||||||
final copy = actions.firstWhere(((e) => e.text.startsWith('Copy')));
|
final copy =
|
||||||
final delete = actions.firstWhere(((e) => e.text.startsWith('Delete')));
|
actions.firstWhere(((e) => e.text == l10n.oath_copy_to_clipboard));
|
||||||
|
final delete =
|
||||||
|
actions.firstWhere(((e) => e.text == l10n.oath_delete_account));
|
||||||
final colors = {
|
final colors = {
|
||||||
copy: Pair(theme.primary, theme.onPrimary),
|
copy: Pair(theme.primary, theme.onPrimary),
|
||||||
delete: Pair(theme.error, theme.onError),
|
delete: Pair(theme.error, theme.onError),
|
||||||
@ -51,7 +55,7 @@ class AccountDialog extends ConsumerWidget {
|
|||||||
|
|
||||||
// If we can't copy, but can calculate, highlight that button instead
|
// If we can't copy, but can calculate, highlight that button instead
|
||||||
if (copy.intent == null) {
|
if (copy.intent == null) {
|
||||||
final calculates = actions.where(((e) => e.text.startsWith('Calculate')));
|
final calculates = actions.where(((e) => e.text == l10n.oath_calculate));
|
||||||
if (calculates.isNotEmpty) {
|
if (calculates.isNotEmpty) {
|
||||||
colors[calculates.first] = Pair(theme.primary, theme.onPrimary);
|
colors[calculates.first] = Pair(theme.primary, theme.onPrimary);
|
||||||
}
|
}
|
||||||
|
@ -62,25 +62,23 @@ class AccountHelper {
|
|||||||
final ready = expired || credential.oathType == OathType.hotp;
|
final ready = expired || credential.oathType == OathType.hotp;
|
||||||
final pinned = _ref.watch(favoritesProvider).contains(credential.id);
|
final pinned = _ref.watch(favoritesProvider).contains(credential.id);
|
||||||
|
|
||||||
final appLocalizations = AppLocalizations.of(_context)!;
|
final l10n = AppLocalizations.of(_context)!;
|
||||||
final shortcut = Platform.isMacOS ? '\u2318 C' : 'Ctrl+C';
|
final shortcut = Platform.isMacOS ? '\u2318 C' : 'Ctrl+C';
|
||||||
return [
|
return [
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: appLocalizations.oath_copy_to_clipboard,
|
text: l10n.oath_copy_to_clipboard,
|
||||||
icon: const Icon(Icons.copy),
|
icon: const Icon(Icons.copy),
|
||||||
intent: code == null || expired ? null : const CopyIntent(),
|
intent: code == null || expired ? null : const CopyIntent(),
|
||||||
trailing: shortcut,
|
trailing: shortcut,
|
||||||
),
|
),
|
||||||
if (manual)
|
if (manual)
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: appLocalizations.oath_calculate,
|
text: l10n.oath_calculate,
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
intent: ready ? const CalculateIntent() : null,
|
intent: ready ? const CalculateIntent() : null,
|
||||||
),
|
),
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: pinned
|
text: pinned ? l10n.oath_unpin_account : l10n.oath_pin_account,
|
||||||
? appLocalizations.oath_unpin_account
|
|
||||||
: appLocalizations.oath_pin_account,
|
|
||||||
icon: pinned
|
icon: pinned
|
||||||
? pushPinStrokeIcon
|
? pushPinStrokeIcon
|
||||||
: const Icon(Icons.push_pin_outlined),
|
: const Icon(Icons.push_pin_outlined),
|
||||||
@ -89,11 +87,11 @@ class AccountHelper {
|
|||||||
if (data.info.version.isAtLeast(5, 3))
|
if (data.info.version.isAtLeast(5, 3))
|
||||||
MenuAction(
|
MenuAction(
|
||||||
icon: const Icon(Icons.edit_outlined),
|
icon: const Icon(Icons.edit_outlined),
|
||||||
text: appLocalizations.oath_rename_account,
|
text: l10n.oath_rename_account,
|
||||||
intent: const EditIntent(),
|
intent: const EditIntent(),
|
||||||
),
|
),
|
||||||
MenuAction(
|
MenuAction(
|
||||||
text: appLocalizations.oath_delete_account,
|
text: l10n.oath_delete_account,
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
intent: const DeleteIntent(),
|
intent: const DeleteIntent(),
|
||||||
),
|
),
|
||||||
|
@ -185,7 +185,6 @@ class _AccountViewState extends ConsumerState<AccountView> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_lastTap = 0;
|
_lastTap = 0;
|
||||||
});
|
});
|
||||||
//triggerCopy();
|
|
||||||
Actions.maybeInvoke(context, const CopyIntent());
|
Actions.maybeInvoke(context, const CopyIntent());
|
||||||
} else {
|
} else {
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
|
@ -108,6 +108,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_scanQrCode(QrScanner qrScanner) async {
|
_scanQrCode(QrScanner qrScanner) async {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
try {
|
try {
|
||||||
setState(() {
|
setState(() {
|
||||||
// If we have a previous scan result stored, clear it
|
// If we have a previous scan result stored, clear it
|
||||||
@ -125,7 +126,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
final otpauth = await qrScanner.scanQr();
|
final otpauth = await qrScanner.scanQr();
|
||||||
if (otpauth == null) {
|
if (otpauth == null) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
showMessage(context, AppLocalizations.of(context)!.oath_no_qr_code);
|
showMessage(context, l10n.oath_no_qr_code);
|
||||||
setState(() {
|
setState(() {
|
||||||
_qrState = _QrScanState.failed;
|
_qrState = _QrScanState.failed;
|
||||||
});
|
});
|
||||||
@ -145,7 +146,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
if (e is! CancellationException) {
|
if (e is! CancellationException) {
|
||||||
showMessage(
|
showMessage(
|
||||||
context,
|
context,
|
||||||
'${AppLocalizations.of(context)!.oath_failed_reading_qr}: $errorMessage',
|
'${l10n.oath_failed_reading_qr}: $errorMessage',
|
||||||
duration: const Duration(seconds: 4),
|
duration: const Duration(seconds: 4),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -173,6 +174,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
|
|
||||||
Future<void> _doAddCredential(
|
Future<void> _doAddCredential(
|
||||||
{DevicePath? devicePath, required Uri credUri}) async {
|
{DevicePath? devicePath, required Uri credUri}) async {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
try {
|
try {
|
||||||
if (devicePath == null) {
|
if (devicePath == null) {
|
||||||
assert(Platform.isAndroid, 'devicePath is only optional for Android');
|
assert(Platform.isAndroid, 'devicePath is only optional for Android');
|
||||||
@ -186,8 +188,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
}
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
showMessage(
|
showMessage(context, l10n.oath_success_add_account);
|
||||||
context, AppLocalizations.of(context)!.oath_success_add_account);
|
|
||||||
} on CancellationException catch (_) {
|
} on CancellationException catch (_) {
|
||||||
// ignored
|
// ignored
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -203,7 +204,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
}
|
}
|
||||||
showMessage(
|
showMessage(
|
||||||
context,
|
context,
|
||||||
'${AppLocalizations.of(context)!.oath_fail_add_account}: $errorMessage',
|
'${l10n.oath_fail_add_account}: $errorMessage',
|
||||||
duration: const Duration(seconds: 4),
|
duration: const Duration(seconds: 4),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -211,6 +212,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final deviceNode = ref.watch(currentDeviceProvider);
|
final deviceNode = ref.watch(currentDeviceProvider);
|
||||||
if (widget.devicePath != null && widget.devicePath != deviceNode?.path) {
|
if (widget.devicePath != null && widget.devicePath != deviceNode?.path) {
|
||||||
// If the dialog was started for a specific device and it was
|
// If the dialog was started for a specific device and it was
|
||||||
@ -233,7 +235,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final otpauthUri = _otpauthUri;
|
final otpauthUri = _otpauthUri;
|
||||||
_promptController?.updateContent(title: 'Insert YubiKey');
|
_promptController?.updateContent(title: l10n.devicePicker_insert_yubikey);
|
||||||
if (otpauthUri != null && deviceNode != null) {
|
if (otpauthUri != null && deviceNode != null) {
|
||||||
final deviceData = ref.watch(currentDeviceDataProvider);
|
final deviceData = ref.watch(currentDeviceDataProvider);
|
||||||
deviceData.when(data: (data) {
|
deviceData.when(data: (data) {
|
||||||
@ -242,7 +244,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
0) !=
|
0) !=
|
||||||
0) {
|
0) {
|
||||||
if (oathState == null) {
|
if (oathState == null) {
|
||||||
_promptController?.updateContent(title: 'Please wait...');
|
_promptController?.updateContent(title: l10n.general_please_wait);
|
||||||
} else if (oathState.locked) {
|
} else if (oathState.locked) {
|
||||||
_promptController?.close();
|
_promptController?.close();
|
||||||
} else {
|
} else {
|
||||||
@ -254,12 +256,14 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_promptController?.updateContent(title: 'Unsupported YubiKey');
|
_promptController?.updateContent(
|
||||||
|
title: l10n.general_unsupported_yubikey);
|
||||||
}
|
}
|
||||||
}, error: (error, _) {
|
}, error: (error, _) {
|
||||||
_promptController?.updateContent(title: 'Unsupported YubiKey');
|
_promptController?.updateContent(
|
||||||
|
title: l10n.general_unsupported_yubikey);
|
||||||
}, loading: () {
|
}, loading: () {
|
||||||
_promptController?.updateContent(title: 'Please wait...');
|
_promptController?.updateContent(title: l10n.general_please_wait);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,8 +344,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
_otpauthUri = cred.toUri();
|
_otpauthUri = cred.toUri();
|
||||||
_promptController = promptUserInteraction(
|
_promptController = promptUserInteraction(
|
||||||
context,
|
context,
|
||||||
title: 'Insert YubiKey',
|
title: l10n.devicePicker_insert_yubikey,
|
||||||
description: 'Add account',
|
description: l10n.oath_add_account,
|
||||||
icon: const Icon(Icons.usb),
|
icon: const Icon(Icons.usb),
|
||||||
onCancel: () {
|
onCancel: () {
|
||||||
_otpauthUri = null;
|
_otpauthUri = null;
|
||||||
@ -356,12 +360,11 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ResponsiveDialog(
|
return ResponsiveDialog(
|
||||||
title: Text(AppLocalizations.of(context)!.oath_add_account),
|
title: Text(l10n.oath_add_account),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: isValid ? submit : null,
|
onPressed: isValid ? submit : null,
|
||||||
child: Text(AppLocalizations.of(context)!.oath_save,
|
child: Text(l10n.oath_save, key: keys.saveButton),
|
||||||
key: keys.saveButton),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: FileDropTarget(
|
child: FileDropTarget(
|
||||||
@ -371,8 +374,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
final otpauth = await qrScanner.scanQr(b64Image);
|
final otpauth = await qrScanner.scanQr(b64Image);
|
||||||
if (otpauth == null) {
|
if (otpauth == null) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
showMessage(
|
showMessage(context, l10n.oath_no_qr_code);
|
||||||
context, AppLocalizations.of(context)!.oath_no_qr_code);
|
|
||||||
} else {
|
} else {
|
||||||
final data = CredentialData.fromUri(Uri.parse(otpauth));
|
final data = CredentialData.fromUri(Uri.parse(otpauth));
|
||||||
_loadCredentialData(data);
|
_loadCredentialData(data);
|
||||||
@ -402,8 +404,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
buildCounter: buildByteCounterFor(issuerText),
|
buildCounter: buildByteCounterFor(issuerText),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText:
|
labelText: l10n.oath_issuer_optional,
|
||||||
AppLocalizations.of(context)!.oath_issuer_optional,
|
|
||||||
helperText:
|
helperText:
|
||||||
'', // Prevents dialog resizing when disabled
|
'', // Prevents dialog resizing when disabled
|
||||||
prefixIcon: const Icon(Icons.business_outlined),
|
prefixIcon: const Icon(Icons.business_outlined),
|
||||||
@ -411,8 +412,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
? '' // needs empty string to render as error
|
? '' // needs empty string to render as error
|
||||||
: issuerNoColon
|
: issuerNoColon
|
||||||
? null
|
? null
|
||||||
: AppLocalizations.of(context)!
|
: l10n.oath_invalid_character_issuer,
|
||||||
.oath_invalid_character_issuer,
|
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -433,16 +433,14 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
labelText:
|
labelText: l10n.oath_account_name,
|
||||||
AppLocalizations.of(context)!.oath_account_name,
|
|
||||||
helperText:
|
helperText:
|
||||||
'', // Prevents dialog resizing when disabled
|
'', // Prevents dialog resizing when disabled
|
||||||
errorText: (byteLength(nameText) > nameMaxLength)
|
errorText: (byteLength(nameText) > nameMaxLength)
|
||||||
? '' // needs empty string to render as error
|
? '' // needs empty string to render as error
|
||||||
: isUnique
|
: isUnique
|
||||||
? null
|
? null
|
||||||
: AppLocalizations.of(context)!
|
: l10n.oath_duplicate_name,
|
||||||
.oath_duplicate_name,
|
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -478,11 +476,9 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
),
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
prefixIcon: const Icon(Icons.key_outlined),
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
labelText:
|
labelText: l10n.oath_secret_key,
|
||||||
AppLocalizations.of(context)!.oath_secret_key,
|
|
||||||
errorText: _validateSecretLength && !secretLengthValid
|
errorText: _validateSecretLength && !secretLengthValid
|
||||||
? AppLocalizations.of(context)!
|
? l10n.oath_invalid_length
|
||||||
.oath_invalid_length
|
|
||||||
: null),
|
: null),
|
||||||
readOnly: _qrState == _QrScanState.success,
|
readOnly: _qrState == _QrScanState.success,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
@ -507,10 +503,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
: const CircularProgressIndicator(
|
: const CircularProgressIndicator(
|
||||||
strokeWidth: 2.0),
|
strokeWidth: 2.0),
|
||||||
label: _qrState == _QrScanState.success
|
label: _qrState == _QrScanState.success
|
||||||
? Text(AppLocalizations.of(context)!
|
? Text(l10n.oath_scanned_qr)
|
||||||
.oath_scanned_qr)
|
: Text(l10n.oath_scan_qr),
|
||||||
: Text(
|
|
||||||
AppLocalizations.of(context)!.oath_scan_qr),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_scanQrCode(qrScanner);
|
_scanQrCode(qrScanner);
|
||||||
}),
|
}),
|
||||||
@ -523,8 +517,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
children: [
|
children: [
|
||||||
if (oathState?.version.isAtLeast(4, 2) ?? true)
|
if (oathState?.version.isAtLeast(4, 2) ?? true)
|
||||||
FilterChip(
|
FilterChip(
|
||||||
label: Text(AppLocalizations.of(context)!
|
label: Text(l10n.oath_require_touch),
|
||||||
.oath_require_touch),
|
|
||||||
selected: _touch,
|
selected: _touch,
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -565,8 +558,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
defaultPeriod,
|
defaultPeriod,
|
||||||
selected: int.tryParse(_periodController.text) !=
|
selected: int.tryParse(_periodController.text) !=
|
||||||
defaultPeriod,
|
defaultPeriod,
|
||||||
itemBuilder: ((value) => Text(
|
itemBuilder: ((value) =>
|
||||||
'$value ${AppLocalizations.of(context)!.oath_sec}')),
|
Text('$value ${l10n.oath_sec}')),
|
||||||
onChanged: _qrState != _QrScanState.success
|
onChanged: _qrState != _QrScanState.success
|
||||||
? (period) {
|
? (period) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -579,8 +572,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
items: _digitsValues,
|
items: _digitsValues,
|
||||||
value: _digits,
|
value: _digits,
|
||||||
selected: _digits != defaultDigits,
|
selected: _digits != defaultDigits,
|
||||||
itemBuilder: (value) => Text(
|
itemBuilder: (value) =>
|
||||||
'$value ${AppLocalizations.of(context)!.oath_digits}'),
|
Text('$value ${l10n.oath_digits}'),
|
||||||
onChanged: _qrState != _QrScanState.success
|
onChanged: _qrState != _QrScanState.success
|
||||||
? (digits) {
|
? (digits) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -18,6 +18,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
/// Get the number of bytes used by a String when encoded to UTF-8.
|
/// Get the number of bytes used by a String when encoded to UTF-8.
|
||||||
int byteLength(String value) => utf8.encode(value).length;
|
int byteLength(String value) => utf8.encode(value).length;
|
||||||
@ -37,7 +38,7 @@ InputCounterWidgetBuilder buildByteCounterFor(String currentValue) =>
|
|||||||
return Text(
|
return Text(
|
||||||
maxLength != null ? '${byteLength(currentValue)}/$maxLength' : '',
|
maxLength != null ? '${byteLength(currentValue)}/$maxLength' : '',
|
||||||
style: style,
|
style: style,
|
||||||
semanticsLabel: 'Character count',
|
semanticsLabel: AppLocalizations.of(context)!.general_character_count,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user