mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-27 04:03:16 +03:00
Merge PR #980.
This commit is contained in:
commit
96781f7140
35
.github/workflows/check-strings.yml
vendored
Normal file
35
.github/workflows/check-strings.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
name: Check strings
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
strings:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
FLUTTER: '3.7.5'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Ensure main locale is correct
|
||||
run: python check_strings.py lib/l10n/app_en.arb
|
||||
|
||||
- name: Check remaining locales
|
||||
run: |
|
||||
find lib/l10n/ -name "app_en.arb" -prune -o -name "*.arb" -print0 | xargs -r -n 1 -0 ./check_strings.py >> $GITHUB_STEP_SUMMARY || echo "::warning::Problems in locales"
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: ${{ env.FLUTTER }}
|
||||
|
||||
- name: Check missing strings
|
||||
run: |
|
||||
flutter gen-l10n
|
||||
if [[ "$(cat missing_l10n_strings.json)" != "{}" ]]; then
|
||||
echo "::notice::Strings missing in translations"
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Missing strings:" >> $GITHUB_STEP_SUMMARY
|
||||
cat missing_l10n_strings.json >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -42,3 +42,6 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Report of untranslated strings
|
||||
/missing_l10n_strings.json
|
113
check_strings.py
Executable file
113
check_strings.py
Executable file
@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# 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 sys
|
||||
import json
|
||||
|
||||
errors = []
|
||||
|
||||
|
||||
def check_duplicate_keys(pairs):
|
||||
seen = set()
|
||||
for d in [k for k, v in pairs if k in seen or seen.add(k)]:
|
||||
errors.append(f"Duplicate key: {d}")
|
||||
return dict(pairs)
|
||||
|
||||
|
||||
def check_duplicate_values(strings):
|
||||
seen = {}
|
||||
for k, v in strings.items():
|
||||
if isinstance(v, str):
|
||||
if v in seen:
|
||||
errors.append(
|
||||
f"Duplicate value in key: {k} (originally in {seen[v]}): {v}"
|
||||
)
|
||||
else:
|
||||
seen[v] = k
|
||||
|
||||
|
||||
def check_prefixes(k, v, s_max_words, s_max_len):
|
||||
errs = []
|
||||
if k.startswith("s_"):
|
||||
if len(v) > s_max_len:
|
||||
errs.append(f"Too long ({len(v)} chars)")
|
||||
if len(v.split()) > s_max_words:
|
||||
errs.append(f"Too many words ({len(v.split())})")
|
||||
if k.startswith("l_") or k.startswith("s_"):
|
||||
if v.endswith("."):
|
||||
errs.append("Ends with '.'")
|
||||
if ". " in v:
|
||||
errs.append("Spans multiple sentences")
|
||||
elif k.startswith("p_"):
|
||||
if v[-1] not in ".!":
|
||||
errs.append("Doesn't end in punctuation")
|
||||
elif k.startswith("q_"):
|
||||
if not v.endswith("?"):
|
||||
errs.append("Doesn't end in '?'")
|
||||
return errs
|
||||
|
||||
|
||||
def check_misc(k, v):
|
||||
errs = []
|
||||
if "..." in v:
|
||||
errs.append("'...' should be replaced with '\\u2026'")
|
||||
if v[0].upper() != v[0]:
|
||||
errs.append("Starts with lowercase letter")
|
||||
return errs
|
||||
|
||||
|
||||
def lint_strings(strings, rules):
|
||||
for k, v in strings.items():
|
||||
errs = []
|
||||
errs.extend(
|
||||
check_prefixes(
|
||||
k,
|
||||
v,
|
||||
rules.get("s_max_words", 4),
|
||||
rules.get("s_max_len", 32),
|
||||
)
|
||||
)
|
||||
errs.extend(check_misc(k, v))
|
||||
if errs:
|
||||
errors.append(f'Errors in {k}: "{v}"')
|
||||
errors.extend([f" {e}" for e in errs])
|
||||
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
print("USAGE: check_strings.py <ARB_FILE>")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
target = sys.argv[1]
|
||||
with open(target) as f:
|
||||
values = json.load(f, object_pairs_hook=check_duplicate_keys)
|
||||
|
||||
strings = {k: v for k, v in values.items() if not k.startswith("@")}
|
||||
|
||||
print(target, f"- checking {len(strings)} strings")
|
||||
lint_strings(strings, strings.get("@_lint_rules", {}))
|
||||
check_duplicate_values(strings)
|
||||
|
||||
if errors:
|
||||
print()
|
||||
print(target, "HAS ERRORS:")
|
||||
for e in errors:
|
||||
print(e)
|
||||
print()
|
||||
sys.exit(1)
|
||||
|
||||
print(target, "OK")
|
@ -1,3 +1,4 @@
|
||||
arb-dir: lib/l10n
|
||||
template-arb-file: app_en.arb
|
||||
output-localization-file: app_localizations.dart
|
||||
output-localization-file: app_localizations.dart
|
||||
untranslated-messages-file: missing_l10n_strings.json
|
@ -40,8 +40,9 @@ class AboutPage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ResponsiveDialog(
|
||||
title: Text(AppLocalizations.of(context)!.general_about),
|
||||
title: Text(l10n.s_about),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
child: Column(
|
||||
@ -51,7 +52,7 @@ class AboutPage extends ConsumerWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0),
|
||||
child: Text(
|
||||
'Yubico Authenticator',
|
||||
l10n.app_name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
@ -62,7 +63,7 @@ class AboutPage extends ConsumerWidget {
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.general_terms_of_use,
|
||||
l10n.s_terms_of_use,
|
||||
style:
|
||||
const TextStyle(decoration: TextDecoration.underline),
|
||||
),
|
||||
@ -72,7 +73,7 @@ class AboutPage extends ConsumerWidget {
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.general_privacy_policy,
|
||||
l10n.s_privacy_policy,
|
||||
style:
|
||||
const TextStyle(decoration: TextDecoration.underline),
|
||||
),
|
||||
@ -84,7 +85,7 @@ class AboutPage extends ConsumerWidget {
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.general_open_src_licenses,
|
||||
l10n.s_open_src_licenses,
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
),
|
||||
onPressed: () {
|
||||
@ -103,7 +104,7 @@ class AboutPage extends ConsumerWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.general_help_and_feedback,
|
||||
l10n.s_help_and_feedback,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
@ -112,7 +113,7 @@ class AboutPage extends ConsumerWidget {
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.general_send_feedback,
|
||||
l10n.s_send_feedback,
|
||||
style:
|
||||
const TextStyle(decoration: TextDecoration.underline),
|
||||
),
|
||||
@ -122,7 +123,7 @@ class AboutPage extends ConsumerWidget {
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.general_i_need_help,
|
||||
l10n.s_i_need_help,
|
||||
style:
|
||||
const TextStyle(decoration: TextDecoration.underline),
|
||||
),
|
||||
@ -139,7 +140,7 @@ class AboutPage extends ConsumerWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.general_troubleshooting,
|
||||
l10n.s_troubleshooting,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
@ -150,8 +151,7 @@ class AboutPage extends ConsumerWidget {
|
||||
const SizedBox(height: 12.0),
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.bug_report_outlined),
|
||||
label:
|
||||
Text(AppLocalizations.of(context)!.general_run_diagnostics),
|
||||
label: Text(l10n.s_run_diagnostics),
|
||||
onPressed: () async {
|
||||
_log.info('Running diagnostics...');
|
||||
final response = await ref
|
||||
@ -169,10 +169,7 @@ class AboutPage extends ConsumerWidget {
|
||||
await ref.read(clipboardProvider).setText(text);
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!
|
||||
.general_diagnostics_copied);
|
||||
showMessage(context, l10n.l_diagnostics_copied);
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -183,8 +180,7 @@ class AboutPage extends ConsumerWidget {
|
||||
if (isAndroid) ...[
|
||||
const SizedBox(height: 12.0),
|
||||
FilterChip(
|
||||
label: Text(
|
||||
AppLocalizations.of(context)!.general_allow_screenshots),
|
||||
label: Text(l10n.s_allow_screenshots),
|
||||
selected: ref.watch(androidAllowScreenshotsProvider),
|
||||
onSelected: (value) async {
|
||||
ref
|
||||
@ -205,6 +201,7 @@ class LoggingPanel extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final logLevel = ref.watch(logLevelProvider);
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
@ -219,8 +216,8 @@ class LoggingPanel extends ConsumerWidget {
|
||||
value: logLevel,
|
||||
items: Levels.LEVELS,
|
||||
selected: logLevel != Level.INFO,
|
||||
labelBuilder: (value) => Text(
|
||||
'${AppLocalizations.of(context)!.general_log_level}: ${value.name[0]}${value.name.substring(1).toLowerCase()}'),
|
||||
labelBuilder: (value) => Text(l10n.s_log_level(
|
||||
value.name[0] + value.name.substring(1).toLowerCase())),
|
||||
itemBuilder: (value) =>
|
||||
Text('${value.name[0]}${value.name.substring(1).toLowerCase()}'),
|
||||
onChanged: (level) {
|
||||
@ -230,7 +227,7 @@ class LoggingPanel extends ConsumerWidget {
|
||||
),
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.copy),
|
||||
label: Text(AppLocalizations.of(context)!.general_copy_log),
|
||||
label: Text(l10n.s_copy_log),
|
||||
onPressed: () async {
|
||||
_log.info('Copying log to clipboard ($version)...');
|
||||
final logs = await ref.read(logLevelProvider.notifier).getLogs();
|
||||
@ -239,8 +236,7 @@ class LoggingPanel extends ConsumerWidget {
|
||||
if (!clipboard.platformGivesFeedback()) {
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
showMessage(context,
|
||||
AppLocalizations.of(context)!.general_log_copied);
|
||||
showMessage(context, l10n.l_log_copied);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../app/logging.dart';
|
||||
@ -189,12 +190,15 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
|
||||
if (_isUsbAttached) {
|
||||
void triggerTouchPrompt() async {
|
||||
controller = await _withContext(
|
||||
(context) async => promptUserInteraction(
|
||||
context,
|
||||
icon: const Icon(Icons.touch_app),
|
||||
title: 'Touch Required',
|
||||
description: 'Touch the button on your YubiKey now.',
|
||||
),
|
||||
(context) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return promptUserInteraction(
|
||||
context,
|
||||
icon: const Icon(Icons.touch_app),
|
||||
title: l10n.s_touch_required,
|
||||
description: l10n.l_touch_button_now,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import 'qr_scanner_scan_status.dart';
|
||||
import 'qr_scanner_util.dart';
|
||||
@ -32,7 +33,8 @@ class QRScannerPermissionsUI extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var scannerAreaWidth = getScannerAreaWidth(screenSize);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final scannerAreaWidth = getScannerAreaWidth(screenSize);
|
||||
|
||||
return Stack(children: [
|
||||
/// instruction text under the scanner area
|
||||
@ -42,11 +44,11 @@ class QRScannerPermissionsUI extends StatelessWidget {
|
||||
screenSize.height - scannerAreaWidth / 2.0 + 8.0),
|
||||
width: screenSize.width,
|
||||
height: screenSize.height),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 36),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 36),
|
||||
child: Text(
|
||||
'Yubico Authenticator needs Camera permissions for scanning QR codes.',
|
||||
style: TextStyle(color: Colors.white),
|
||||
l10n.p_need_camera_permission,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)),
|
||||
@ -63,32 +65,36 @@ class QRScannerPermissionsUI extends StatelessWidget {
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Have account info?',
|
||||
Text(
|
||||
l10n.q_have_account_info,
|
||||
textScaleFactor: 0.7,
|
||||
style: TextStyle(color: Colors.white),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop('');
|
||||
},
|
||||
child: const Text('Enter manually',
|
||||
style: TextStyle(color: Colors.white))),
|
||||
child: Text(
|
||||
l10n.s_enter_manually,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Would like to scan?',
|
||||
Text(
|
||||
l10n.q_want_to_scan,
|
||||
textScaleFactor: 0.7,
|
||||
style: TextStyle(color: Colors.white),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
onPermissionRequest();
|
||||
},
|
||||
child: const Text('Review permissions',
|
||||
style: TextStyle(color: Colors.white))),
|
||||
child: Text(
|
||||
l10n.s_review_permissions,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)),
|
||||
],
|
||||
)
|
||||
]),
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../keys.dart' as keys;
|
||||
import 'qr_scanner_scan_status.dart';
|
||||
@ -32,7 +33,8 @@ class QRScannerUI extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var scannerAreaWidth = getScannerAreaWidth(screenSize);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final scannerAreaWidth = getScannerAreaWidth(screenSize);
|
||||
|
||||
return Stack(children: [
|
||||
/// instruction text under the scanner area
|
||||
@ -44,8 +46,8 @@ class QRScannerUI extends StatelessWidget {
|
||||
height: screenSize.height),
|
||||
child: Text(
|
||||
status != ScanStatus.error
|
||||
? 'Point your camera at a QR code to scan it'
|
||||
: 'Invalid QR code',
|
||||
? l10n.l_point_camera_scan
|
||||
: l10n.l_invalid_qr,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@ -60,18 +62,20 @@ class QRScannerUI extends StatelessWidget {
|
||||
height: screenSize.height),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'No QR code?',
|
||||
Text(
|
||||
l10n.q_no_qr,
|
||||
textScaleFactor: 0.7,
|
||||
style: TextStyle(color: Colors.white),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop('');
|
||||
},
|
||||
key: keys.manualEntryButton,
|
||||
child: const Text('Enter manually',
|
||||
style: TextStyle(color: Colors.white))),
|
||||
child: Text(
|
||||
l10n.s_enter_manually,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:qrscanner_zxing/qrscanner_zxing_view.dart';
|
||||
|
||||
import '../../oath/models.dart';
|
||||
@ -63,7 +64,6 @@ class _QrScannerViewState extends State<QrScannerView> {
|
||||
_status = ScanStatus.scanning;
|
||||
|
||||
_zxingViewKey.currentState?.resumeScanning();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@ -106,15 +106,16 @@ class _QrScannerViewState extends State<QrScannerView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
extendBodyBehindAppBar: true,
|
||||
extendBody: true,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Add account',
|
||||
style: TextStyle(color: Colors.white),
|
||||
title: Text(
|
||||
l10n.s_add_account,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
|
@ -17,6 +17,7 @@
|
||||
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';
|
||||
@ -34,14 +35,14 @@ enum _TapAction {
|
||||
copy,
|
||||
both;
|
||||
|
||||
String get description {
|
||||
String getDescription(AppLocalizations l10n) {
|
||||
switch (this) {
|
||||
case _TapAction.launch:
|
||||
return 'Launch Yubico Authenticator';
|
||||
return l10n.l_launch_ya;
|
||||
case _TapAction.copy:
|
||||
return 'Copy OTP to clipboard';
|
||||
return l10n.l_copy_otp_clipboard;
|
||||
case _TapAction.both:
|
||||
return 'Launch app and copy OTP';
|
||||
return l10n.l_launch_and_copy_otp;
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,14 +77,14 @@ enum _TapAction {
|
||||
}
|
||||
|
||||
extension on ThemeMode {
|
||||
String get displayName {
|
||||
String getDisplayName(AppLocalizations l10n) {
|
||||
switch (this) {
|
||||
case ThemeMode.system:
|
||||
return 'System default';
|
||||
return l10n.s_system_default;
|
||||
case ThemeMode.light:
|
||||
return 'Light theme';
|
||||
return l10n.s_light_mode;
|
||||
case ThemeMode.dark:
|
||||
return 'Dark theme';
|
||||
return l10n.s_dark_mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -99,6 +100,7 @@ class AndroidSettingsPage extends ConsumerStatefulWidget {
|
||||
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);
|
||||
@ -112,7 +114,7 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Settings'),
|
||||
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.
|
||||
@ -125,10 +127,10 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ListTitle('NFC options'),
|
||||
ListTitle(l10n.s_nfc_options),
|
||||
ListTile(
|
||||
title: const Text('On YubiKey NFC tap'),
|
||||
subtitle: Text(tapAction.description),
|
||||
title: Text(l10n.l_on_yk_nfc_tap),
|
||||
subtitle: Text(tapAction.getDescription(l10n)),
|
||||
key: keys.nfcTapSetting,
|
||||
onTap: () async {
|
||||
final newTapAction = await _selectTapAction(context, tapAction);
|
||||
@ -138,7 +140,7 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Keyboard Layout (for static password)'),
|
||||
title: Text(l10n.l_kbd_layout_for_static),
|
||||
subtitle: Text(clipKbdLayout),
|
||||
key: keys.nfcKeyboardLayoutSetting,
|
||||
enabled: tapAction != _TapAction.launch,
|
||||
@ -152,12 +154,10 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Bypass touch requirement'),
|
||||
subtitle: nfcBypassTouch
|
||||
? const Text(
|
||||
'Accounts that require touch are automatically shown over NFC')
|
||||
: const Text(
|
||||
'Accounts that require touch need an additional tap over NFC'),
|
||||
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) {
|
||||
@ -166,11 +166,10 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
});
|
||||
}),
|
||||
SwitchListTile(
|
||||
title: const Text('Silence NFC sounds'),
|
||||
subtitle: nfcSilenceSounds
|
||||
? const Text(
|
||||
'No sounds will be played on NFC tap')
|
||||
: const Text('Sound will play on NFC tap'),
|
||||
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) {
|
||||
@ -178,13 +177,12 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
prefs.setBool(prefNfcSilenceSounds, value);
|
||||
});
|
||||
}),
|
||||
const ListTitle('USB options'),
|
||||
ListTitle(l10n.s_usb_options),
|
||||
SwitchListTile(
|
||||
title: const Text('Launch when YubiKey is connected'),
|
||||
subtitle: usbOpenApp
|
||||
? const Text(
|
||||
'This prevents other apps from using the YubiKey over USB')
|
||||
: const Text('Other apps can use the YubiKey over USB'),
|
||||
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) {
|
||||
@ -192,10 +190,10 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
prefs.setBool(prefUsbOpenApp, value);
|
||||
});
|
||||
}),
|
||||
const ListTitle('Appearance'),
|
||||
ListTitle(l10n.s_appearance),
|
||||
ListTile(
|
||||
title: const Text('App theme'),
|
||||
subtitle: Text(themeMode.displayName),
|
||||
title: Text(l10n.s_app_theme),
|
||||
subtitle: Text(themeMode.getDisplayName(l10n)),
|
||||
key: keys.themeModeSetting,
|
||||
onTap: () async {
|
||||
final newMode = await _selectAppearance(
|
||||
@ -214,12 +212,13 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
await showDialog<_TapAction>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return SimpleDialog(
|
||||
title: const Text('On YubiKey NFC tap'),
|
||||
title: Text(l10n.l_on_yk_nfc_tap),
|
||||
children: _TapAction.values
|
||||
.map(
|
||||
(e) => RadioListTile<_TapAction>(
|
||||
title: Text(e.description),
|
||||
title: Text(e.getDescription(l10n)),
|
||||
key: e.key,
|
||||
value: e,
|
||||
groupValue: tapAction,
|
||||
@ -238,8 +237,9 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
await showDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return SimpleDialog(
|
||||
title: const Text('Choose keyboard layout'),
|
||||
title: Text(l10n.s_choose_kbd_layout),
|
||||
children: _keyboardLayouts
|
||||
.map(
|
||||
(e) => RadioListTile<String>(
|
||||
@ -262,11 +262,12 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
await showDialog<ThemeMode>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return SimpleDialog(
|
||||
title: const Text('Choose app theme'),
|
||||
title: Text(l10n.s_choose_app_theme),
|
||||
children: supportedThemes
|
||||
.map((e) => RadioListTile(
|
||||
title: Text(e.displayName),
|
||||
title: Text(e.getDisplayName(l10n)),
|
||||
value: e,
|
||||
key: Key('android.keys.theme_mode_${e.name}'),
|
||||
groupValue: themeMode,
|
||||
|
@ -33,23 +33,24 @@ class YubicoAuthenticatorApp extends ConsumerWidget {
|
||||
return registerGlobalShortcuts(
|
||||
ref: ref,
|
||||
child: LogWarningOverlay(
|
||||
child: MaterialApp(
|
||||
title: 'Yubico Authenticator',
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: ref.watch(themeModeProvider),
|
||||
home: page,
|
||||
debugShowCheckedModeBanner: false,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale('en', ''), // English, no country code
|
||||
],
|
||||
),
|
||||
child: Consumer(builder: (context, ref, _) {
|
||||
return MaterialApp(
|
||||
title: ref.watch(l10nProvider).app_name,
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: ref.watch(themeModeProvider),
|
||||
home: page,
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: ref.watch(currentLocaleProvider),
|
||||
supportedLocales: ref.watch(supportedLocalesProvider),
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../../management/models.dart';
|
||||
import '../core/models.dart';
|
||||
@ -28,16 +29,15 @@ const _listEquality = ListEquality();
|
||||
enum Availability { enabled, disabled, unsupported }
|
||||
|
||||
enum Application {
|
||||
oath('Authenticator'),
|
||||
fido('WebAuthn'),
|
||||
otp('One-Time Passwords'),
|
||||
piv('Certificates'),
|
||||
openpgp('OpenPGP'),
|
||||
hsmauth('YubiHSM Auth'),
|
||||
management('Toggle Applications');
|
||||
oath,
|
||||
fido,
|
||||
otp,
|
||||
piv,
|
||||
openpgp,
|
||||
hsmauth,
|
||||
management;
|
||||
|
||||
final String displayName;
|
||||
const Application(this.displayName);
|
||||
const Application();
|
||||
|
||||
bool _inCapabilities(int capabilities) {
|
||||
switch (this) {
|
||||
@ -59,6 +59,17 @@ enum Application {
|
||||
}
|
||||
}
|
||||
|
||||
String getDisplayName(AppLocalizations l10n) {
|
||||
switch (this) {
|
||||
case Application.oath:
|
||||
return l10n.s_authenticator;
|
||||
case Application.fido:
|
||||
return l10n.s_webauthn;
|
||||
default:
|
||||
return name.substring(0, 1).toUpperCase() + name.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
Availability getAvailability(YubiKeyData data) {
|
||||
if (this == Application.management) {
|
||||
final version = data.info.version;
|
||||
|
@ -29,6 +29,11 @@ import 'models.dart';
|
||||
|
||||
final _log = Logger('app.state');
|
||||
|
||||
// Officially supported translations
|
||||
const officialLocales = [
|
||||
Locale('en', ''),
|
||||
];
|
||||
|
||||
// Override this to alter the set of supported apps.
|
||||
final supportedAppsProvider =
|
||||
Provider<List<Application>>((ref) => Application.values);
|
||||
@ -42,32 +47,37 @@ final supportedThemesProvider = StateProvider<List<ThemeMode>>(
|
||||
(ref) => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
final _l10nProvider = StateNotifierProvider<_L10nNotifier, AppLocalizations>(
|
||||
(ref) => _L10nNotifier());
|
||||
final communityTranslationsProvider =
|
||||
StateNotifierProvider<CommunityTranslationsNotifier, bool>(
|
||||
(ref) => CommunityTranslationsNotifier(ref.watch(prefProvider)));
|
||||
|
||||
final l10nProvider = Provider<AppLocalizations>(
|
||||
(ref) => ref.watch(_l10nProvider),
|
||||
);
|
||||
class CommunityTranslationsNotifier extends StateNotifier<bool> {
|
||||
static const String _key = 'APP_STATE_ENABLE_COMMUNITY_TRANSLATIONS';
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
class _L10nNotifier extends StateNotifier<AppLocalizations>
|
||||
with WidgetsBindingObserver {
|
||||
_L10nNotifier() : super(lookupAppLocalizations(window.locale)) {
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
CommunityTranslationsNotifier(this._prefs)
|
||||
: super(_prefs.getBool(_key) == true);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
void didChangeLocales(List<Locale>? locales) {
|
||||
state = lookupAppLocalizations(window.locale);
|
||||
void setEnableCommunityTranslations(bool value) {
|
||||
state = value;
|
||||
_prefs.setBool(_key, value);
|
||||
}
|
||||
}
|
||||
|
||||
final supportedLocalesProvider = Provider<List<Locale>>((ref) =>
|
||||
ref.watch(communityTranslationsProvider)
|
||||
? AppLocalizations.supportedLocales
|
||||
: officialLocales);
|
||||
|
||||
final currentLocaleProvider = Provider<Locale>(
|
||||
(ref) => basicLocaleListResolution(
|
||||
window.locales, ref.watch(supportedLocalesProvider)),
|
||||
);
|
||||
|
||||
final l10nProvider = Provider<AppLocalizations>(
|
||||
(ref) => lookupAppLocalizations(ref.watch(currentLocaleProvider)),
|
||||
);
|
||||
|
||||
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
|
||||
(ref) => ThemeModeNotifier(
|
||||
ref.watch(prefProvider), ref.read(supportedThemesProvider)),
|
||||
|
@ -34,10 +34,11 @@ class AppFailurePage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final reason = cause;
|
||||
|
||||
Widget? graphic = const Icon(Icons.error);
|
||||
String? header = 'An error has occured';
|
||||
String? header = l10n.l_error_occured;
|
||||
String? message = reason.toString();
|
||||
List<Widget> actions = [];
|
||||
|
||||
@ -45,13 +46,13 @@ class AppFailurePage extends ConsumerWidget {
|
||||
if (reason.status == 'connection-error') {
|
||||
switch (reason.body['connection']) {
|
||||
case 'ccid':
|
||||
header = 'Failed to open smart card connection';
|
||||
header = l10n.l_ccid_connection_failed;
|
||||
if (Platform.isMacOS) {
|
||||
message = 'Try to remove and re-insert your YubiKey.';
|
||||
message = l10n.p_try_reinsert_yk;
|
||||
} else if (Platform.isLinux) {
|
||||
message = 'Make sure pcscd is installed and running.';
|
||||
message = l10n.p_pcscd_unavailable;
|
||||
} else {
|
||||
message = 'Make sure your smart card service is functioning.';
|
||||
message = l10n.p_ccid_service_unavailable;
|
||||
}
|
||||
break;
|
||||
case 'fido':
|
||||
@ -59,17 +60,14 @@ class AppFailurePage extends ConsumerWidget {
|
||||
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||
graphic = noPermission;
|
||||
header = null;
|
||||
message = AppLocalizations.of(context)!.appFailurePage_txt_info;
|
||||
message = l10n.p_webauthn_elevated_permissions_required;
|
||||
actions = [
|
||||
ElevatedButton.icon(
|
||||
label: Text(
|
||||
AppLocalizations.of(context)!.appFailurePage_btn_unlock),
|
||||
label: Text(l10n.s_unlock),
|
||||
icon: const Icon(Icons.lock_open),
|
||||
onPressed: () async {
|
||||
final closeMessage = showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!
|
||||
.appFailurePage_msg_permission,
|
||||
context, l10n.l_elevating_permissions,
|
||||
duration: const Duration(seconds: 30));
|
||||
try {
|
||||
if (await ref.read(rpcProvider).requireValue.elevate()) {
|
||||
@ -77,7 +75,10 @@ class AppFailurePage extends ConsumerWidget {
|
||||
} else {
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
showMessage(context, 'Permission denied');
|
||||
showMessage(
|
||||
context,
|
||||
l10n.s_permission_denied,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -90,8 +91,8 @@ class AppFailurePage extends ConsumerWidget {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
header = 'Failed to open connection';
|
||||
message = 'Try to remove and re-insert your YubiKey.';
|
||||
header = l10n.l_open_connection_failed;
|
||||
message = l10n.p_try_reinsert_yk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -129,8 +129,7 @@ class AppPage extends StatelessWidget {
|
||||
},
|
||||
icon: const Icon(Icons.tune),
|
||||
iconSize: 24,
|
||||
tooltip:
|
||||
AppLocalizations.of(context)!.general_configure_yubikey,
|
||||
tooltip: AppLocalizations.of(context)!.s_configure_yk,
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
),
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../message.dart';
|
||||
import 'device_avatar.dart';
|
||||
@ -44,7 +45,7 @@ class DeviceButton extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return IconButton(
|
||||
tooltip: 'Select YubiKey',
|
||||
tooltip: AppLocalizations.of(context)!.s_select_yk,
|
||||
icon: _CircledDeviceAvatar(radius),
|
||||
onPressed: () async {
|
||||
await showBlurDialog(
|
||||
|
@ -18,6 +18,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../../core/models.dart';
|
||||
import '../../desktop/state.dart';
|
||||
@ -34,26 +35,27 @@ class DeviceErrorScreen extends ConsumerWidget {
|
||||
const DeviceErrorScreen(this.node, {this.error, super.key});
|
||||
|
||||
Widget _buildUsbPid(BuildContext context, WidgetRef ref, UsbPid pid) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
if (pid.usbInterfaces == UsbInterface.fido.value) {
|
||||
if (Platform.isWindows &&
|
||||
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||
return MessagePage(
|
||||
graphic: noPermission,
|
||||
message: 'Managing this device requires elevated privileges.',
|
||||
message: l10n.p_elevated_permissions_required,
|
||||
actions: [
|
||||
ElevatedButton.icon(
|
||||
label: const Text('Unlock'),
|
||||
label: Text(l10n.s_unlock),
|
||||
icon: const Icon(Icons.lock_open),
|
||||
onPressed: () async {
|
||||
final closeMessage = showMessage(
|
||||
context, 'Elevating permissions...',
|
||||
context, l10n.l_elevating_permissions,
|
||||
duration: const Duration(seconds: 30));
|
||||
try {
|
||||
if (await ref.read(rpcProvider).requireValue.elevate()) {
|
||||
ref.invalidate(rpcProvider);
|
||||
} else {
|
||||
await ref.read(withContextProvider)((context) async =>
|
||||
showMessage(context, 'Permission denied'));
|
||||
showMessage(context, l10n.s_permission_denied));
|
||||
}
|
||||
} finally {
|
||||
closeMessage();
|
||||
@ -64,24 +66,25 @@ class DeviceErrorScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
return const MessagePage(
|
||||
graphic: DeviceAvatar(child: Icon(Icons.usb_off)),
|
||||
message: 'This YubiKey cannot be accessed',
|
||||
return MessagePage(
|
||||
graphic: const DeviceAvatar(child: Icon(Icons.usb_off)),
|
||||
message: l10n.l_yk_no_access,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return node.map(
|
||||
usbYubiKey: (node) => _buildUsbPid(context, ref, node.pid),
|
||||
nfcReader: (node) {
|
||||
final String message;
|
||||
switch (error) {
|
||||
case 'unknown-device':
|
||||
message = 'Unrecognized device';
|
||||
message = l10n.s_unknown_device;
|
||||
break;
|
||||
default:
|
||||
message = 'Place your YubiKey on the NFC reader';
|
||||
message = l10n.l_place_on_nfc_reader;
|
||||
}
|
||||
return MessagePage(message: message);
|
||||
},
|
||||
|
@ -92,6 +92,7 @@ class _DevicePickerContent extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hidden = ref.watch(_hiddenDevicesProvider);
|
||||
final devices = ref
|
||||
.watch(attachedDevicesProvider)
|
||||
@ -118,13 +119,10 @@ class _DevicePickerContent extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.devicePicker_no_yubikey)),
|
||||
title: Center(child: Text(l10n.l_no_yk_present)),
|
||||
subtitle: Center(
|
||||
child: Text(Platform.isAndroid
|
||||
? AppLocalizations.of(context)!.devicePicker_insert_or_tap
|
||||
: AppLocalizations.of(context)!.general_usb)),
|
||||
child: Text(
|
||||
Platform.isAndroid ? l10n.l_insert_or_tap_yk : l10n.s_usb)),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -138,8 +136,8 @@ class _DevicePickerContent extends ConsumerWidget {
|
||||
padding: EdgeInsets.symmetric(horizontal: 4),
|
||||
child: DeviceAvatar(child: Icon(Icons.usb)),
|
||||
),
|
||||
title: Text(AppLocalizations.of(context)!.general_usb),
|
||||
subtitle: Text(AppLocalizations.of(context)!.devicePicker_no_yubikey),
|
||||
title: Text(l10n.s_usb),
|
||||
subtitle: Text(l10n.l_no_yk_present),
|
||||
onTap: () {
|
||||
ref.read(currentDeviceProvider.notifier).setCurrentDevice(null);
|
||||
},
|
||||
@ -170,8 +168,7 @@ class _DevicePickerContent extends ConsumerWidget {
|
||||
ref.read(_hiddenDevicesProvider.notifier).showAll();
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(AppLocalizations.of(context)!
|
||||
.devicePicker_show_hidden),
|
||||
title: Text(l10n.s_show_hidden_devices),
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@ -195,36 +192,33 @@ class _DevicePickerContent extends ConsumerWidget {
|
||||
}
|
||||
|
||||
String _getDeviceInfoString(BuildContext context, DeviceInfo info) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final serial = info.serial;
|
||||
return [
|
||||
if (serial != null) AppLocalizations.of(context)!.devicePicker_sn(serial),
|
||||
if (serial != null) l10n.s_sn_serial(serial),
|
||||
if (info.version.isAtLeast(1))
|
||||
AppLocalizations.of(context)!.devicePicker_fw(info.version)
|
||||
l10n.s_fw_version(info.version)
|
||||
else
|
||||
AppLocalizations.of(context)!.devicePicker_unknown_type,
|
||||
l10n.s_unknown_type,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
List<String> _getDeviceStrings(
|
||||
BuildContext context, DeviceNode node, AsyncValue<YubiKeyData> data) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final messages = data.whenOrNull(
|
||||
data: (data) => [data.name, _getDeviceInfoString(context, data.info)],
|
||||
error: (error, _) {
|
||||
switch (error) {
|
||||
case 'device-inaccessible':
|
||||
return [
|
||||
node.name,
|
||||
AppLocalizations.of(context)!.devicePicker_inaccessible
|
||||
];
|
||||
return [node.name, l10n.s_yk_inaccessible];
|
||||
case 'unknown-device':
|
||||
return [
|
||||
AppLocalizations.of(context)!.devicePicker_unknown_device
|
||||
];
|
||||
return [l10n.s_unknown_device];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
) ??
|
||||
[AppLocalizations.of(context)!.devicePicker_no_yubikey];
|
||||
[l10n.l_no_yk_present];
|
||||
|
||||
// Add the NFC reader name, unless it's already included (as device name, like on Android)
|
||||
if (node is NfcReaderNode && !messages.contains(node.name)) {
|
||||
@ -302,6 +296,7 @@ class _DeviceRow extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
@ -311,10 +306,9 @@ class _DeviceRow extends ConsumerWidget {
|
||||
subtitle: Text(
|
||||
node.when(
|
||||
usbYubiKey: (_, __, ___, info) => info == null
|
||||
? AppLocalizations.of(context)!.devicePicker_inaccessible
|
||||
? l10n.s_yk_inaccessible
|
||||
: _getDeviceInfoString(context, info),
|
||||
nfcReader: (_, __) =>
|
||||
AppLocalizations.of(context)!.devicePicker_select_to_scan,
|
||||
nfcReader: (_, __) => l10n.s_select_to_scan,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
@ -331,6 +325,7 @@ class _NfcDeviceRow extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hidden = ref.watch(_hiddenDevicesProvider);
|
||||
return GestureDetector(
|
||||
onSecondaryTapDown: (details) {
|
||||
@ -349,8 +344,7 @@ class _NfcDeviceRow extends ConsumerWidget {
|
||||
ref.read(_hiddenDevicesProvider.notifier).showAll();
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.devicePicker_show_hidden),
|
||||
title: Text(l10n.s_show_hidden_devices),
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
enabled: hidden.isNotEmpty,
|
||||
@ -361,8 +355,7 @@ class _NfcDeviceRow extends ConsumerWidget {
|
||||
ref.read(_hiddenDevicesProvider.notifier).hideDevice(node.path);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.devicePicker_hide_device),
|
||||
title: Text(l10n.s_hide_device),
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
|
@ -71,6 +71,7 @@ class MainPageDrawer extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final supportedApps = ref.watch(supportedAppsProvider);
|
||||
final data = ref.watch(currentDeviceDataProvider).valueOrNull;
|
||||
final color =
|
||||
@ -132,7 +133,7 @@ class MainPageDrawer extends ConsumerWidget {
|
||||
if (data != null) ...[
|
||||
// Normal YubiKey Applications
|
||||
...availableApps.map((app) => NavigationDrawerDestination(
|
||||
label: Text(app.displayName),
|
||||
label: Text(app.getDisplayName(l10n)),
|
||||
icon: Icon(app._icon),
|
||||
selectedIcon: Icon(app._filledIcon),
|
||||
)),
|
||||
@ -141,7 +142,7 @@ class MainPageDrawer extends ConsumerWidget {
|
||||
NavigationDrawerDestination(
|
||||
key: managementAppDrawer,
|
||||
label: Text(
|
||||
AppLocalizations.of(context)!.mainDrawer_txt_applications,
|
||||
l10n.s_toggle_applications,
|
||||
),
|
||||
icon: Icon(Application.management._icon),
|
||||
selectedIcon: Icon(Application.management._filledIcon),
|
||||
@ -151,11 +152,11 @@ class MainPageDrawer extends ConsumerWidget {
|
||||
],
|
||||
// Non-YubiKey pages
|
||||
NavigationDrawerDestination(
|
||||
label: Text(AppLocalizations.of(context)!.mainDrawer_txt_settings),
|
||||
label: Text(l10n.s_settings),
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
),
|
||||
NavigationDrawerDestination(
|
||||
label: Text(AppLocalizations.of(context)!.mainDrawer_txt_help),
|
||||
label: Text(l10n.s_help_and_about),
|
||||
icon: const Icon(Icons.help_outline),
|
||||
),
|
||||
],
|
||||
|
@ -16,16 +16,17 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:yubico_authenticator/android/app_methods.dart';
|
||||
import 'package:yubico_authenticator/android/state.dart';
|
||||
import 'package:yubico_authenticator/widgets/custom_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../../android/app_methods.dart';
|
||||
import '../../android/state.dart';
|
||||
import '../../exception/cancellation_exception.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../fido/views/fido_screen.dart';
|
||||
import '../../oath/models.dart';
|
||||
import '../../oath/views/add_account_page.dart';
|
||||
import '../../oath/views/oath_screen.dart';
|
||||
import '../../widgets/custom_icons.dart';
|
||||
import '../message.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
@ -37,6 +38,7 @@ class MainPage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ref.listen<Function(BuildContext)?>(
|
||||
contextConsumer,
|
||||
(previous, next) {
|
||||
@ -45,7 +47,8 @@ class MainPage extends ConsumerWidget {
|
||||
);
|
||||
|
||||
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.
|
||||
@ -75,18 +78,17 @@ class MainPage extends ConsumerWidget {
|
||||
);
|
||||
if (deviceNode == null) {
|
||||
if (isAndroid) {
|
||||
|
||||
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
|
||||
var isNfcEnabled = ref.watch(androidNfcStateProvider);
|
||||
return MessagePage(
|
||||
graphic: noKeyImage,
|
||||
message: hasNfcSupport && isNfcEnabled
|
||||
? 'Tap or insert your YubiKey'
|
||||
: 'Insert your YubiKey',
|
||||
? l10n.l_insert_or_tap_yk
|
||||
: l10n.l_insert_yk,
|
||||
actions: [
|
||||
if (hasNfcSupport && !isNfcEnabled)
|
||||
ElevatedButton.icon(
|
||||
label: const Text('Enable NFC'),
|
||||
label: Text(l10n.s_enable_nfc),
|
||||
icon: nfcIcon,
|
||||
onPressed: () async {
|
||||
await openNfcSettings();
|
||||
@ -94,7 +96,7 @@ class MainPage extends ConsumerWidget {
|
||||
],
|
||||
actionButtonBuilder: (context) => IconButton(
|
||||
icon: const Icon(Icons.person_add_alt_1),
|
||||
tooltip: 'Add account',
|
||||
tooltip: l10n.s_add_account,
|
||||
onPressed: () async {
|
||||
CredentialData? otpauth;
|
||||
final scanner = ref.read(qrScannerProvider);
|
||||
@ -130,7 +132,7 @@ class MainPage extends ConsumerWidget {
|
||||
return MessagePage(
|
||||
delayedContent: true,
|
||||
graphic: noKeyImage,
|
||||
message: 'Insert your YubiKey',
|
||||
message: l10n.l_insert_yk,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@ -139,21 +141,19 @@ class MainPage extends ConsumerWidget {
|
||||
final app = ref.watch(currentAppProvider);
|
||||
if (data.info.supportedCapabilities.isEmpty &&
|
||||
data.name == 'Unrecognized device') {
|
||||
return const MessagePage(
|
||||
header: 'Device not recognized',
|
||||
return MessagePage(
|
||||
header: l10n.s_yk_not_recognized,
|
||||
);
|
||||
} else if (app.getAvailability(data) ==
|
||||
Availability.unsupported) {
|
||||
return MessagePage(
|
||||
header: 'Application not supported',
|
||||
message:
|
||||
'The used YubiKey does not support \'${app.name}\' application',
|
||||
header: l10n.s_app_not_supported,
|
||||
message: l10n.l_app_not_supported_on_yk(app.name),
|
||||
);
|
||||
} else if (app.getAvailability(data) != Availability.enabled) {
|
||||
return MessagePage(
|
||||
header: 'Application disabled',
|
||||
message:
|
||||
'Enable the \'${app.name}\' application on your YubiKey to access',
|
||||
header: l10n.s_app_disabled,
|
||||
message: l10n.l_app_disabled_desc(app.name),
|
||||
);
|
||||
}
|
||||
|
||||
@ -163,9 +163,9 @@ class MainPage extends ConsumerWidget {
|
||||
case Application.fido:
|
||||
return FidoScreen(data);
|
||||
default:
|
||||
return const MessagePage(
|
||||
header: 'Not supported',
|
||||
message: 'This application is not supported',
|
||||
return MessagePage(
|
||||
header: l10n.s_app_not_supported,
|
||||
message: l10n.l_app_not_supported_desc,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -278,13 +278,14 @@ class _HelperWaiterState extends ConsumerState<_HelperWaiter> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (slow) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return MessagePage(
|
||||
graphic: const CircularProgressIndicator(),
|
||||
message: 'The Helper process isn\'t responding',
|
||||
message: l10n.l_helper_not_responding,
|
||||
actions: [
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.copy),
|
||||
label: Text(AppLocalizations.of(context)!.general_copy_log),
|
||||
label: Text(l10n.s_copy_log),
|
||||
onPressed: () async {
|
||||
_log.info('Copying log to clipboard ($version)...');
|
||||
final logs = await ref.read(logLevelProvider.notifier).getLogs();
|
||||
@ -295,7 +296,7 @@ class _HelperWaiterState extends ConsumerState<_HelperWaiter> {
|
||||
(context) async {
|
||||
showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!.general_log_copied,
|
||||
l10n.l_log_copied,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -271,8 +271,8 @@ class DesktopCredentialListNotifier extends OathCredentialListNotifier {
|
||||
return promptUserInteraction(
|
||||
context,
|
||||
icon: const Icon(Icons.touch_app),
|
||||
title: l10n.oath_touch_required,
|
||||
description: l10n.oath_touch_now,
|
||||
title: l10n.s_touch_required,
|
||||
description: l10n.l_touch_button_now,
|
||||
headless: headless,
|
||||
);
|
||||
},
|
||||
|
@ -111,7 +111,7 @@ class _Systray extends TrayListener {
|
||||
Future<void> _init() async {
|
||||
await trayManager.setIcon(_getIcon(), isTemplate: true);
|
||||
if (!Platform.isLinux) {
|
||||
await trayManager.setToolTip(_l10n.general_app_name);
|
||||
await trayManager.setToolTip(_l10n.app_name);
|
||||
}
|
||||
await _updateContextMenu();
|
||||
|
||||
@ -176,8 +176,8 @@ class _Systray extends TrayListener {
|
||||
.read(clipboardProvider)
|
||||
.setText(code.value, isSensitive: true);
|
||||
final notification = LocalNotification(
|
||||
title: _l10n.systray_oath_copied,
|
||||
body: _l10n.systray_oath_copied_to_clipboard(label),
|
||||
title: _l10n.s_code_copied,
|
||||
body: _l10n.p_target_copied_clipboard(label),
|
||||
silent: true,
|
||||
);
|
||||
await notification.show();
|
||||
@ -190,14 +190,12 @@ class _Systray extends TrayListener {
|
||||
),
|
||||
if (_credentials.isEmpty)
|
||||
MenuItem(
|
||||
label: _l10n.systray_no_pinned,
|
||||
label: _l10n.s_no_pinned_accounts,
|
||||
disabled: true,
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
label: _isHidden
|
||||
? _l10n.general_show_window
|
||||
: _l10n.general_hide_window,
|
||||
label: _isHidden ? _l10n.s_show_window : _l10n.s_hide_window,
|
||||
onClick: (_) {
|
||||
_ref
|
||||
.read(desktopWindowStateProvider.notifier)
|
||||
@ -206,7 +204,7 @@ class _Systray extends TrayListener {
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
label: _l10n.general_quit,
|
||||
label: _l10n.s_quit,
|
||||
onClick: (_) {
|
||||
_ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
|
@ -25,7 +25,7 @@ class ErrorPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.general_application_error),
|
||||
title: Text(AppLocalizations.of(context)!.s_application_error),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
|
@ -127,25 +127,26 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
}
|
||||
|
||||
String _getMessage() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
if (_samples == 0) {
|
||||
return AppLocalizations.of(context)!.fido_press_fingerprint_begin;
|
||||
return l10n.p_press_fingerprint_begin;
|
||||
}
|
||||
if (_fingerprint == null) {
|
||||
return AppLocalizations.of(context)!.fido_keep_touching_yubikey;
|
||||
return l10n.l_keep_touching_yk;
|
||||
} else {
|
||||
return AppLocalizations.of(context)!.fido_fingerprint_captured;
|
||||
return l10n.l_fingerprint_captured;
|
||||
}
|
||||
}
|
||||
|
||||
void _submit() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
try {
|
||||
await ref
|
||||
.read(fingerprintProvider(widget.devicePath).notifier)
|
||||
.renameFingerprint(_fingerprint!, _label);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(true);
|
||||
showMessage(
|
||||
context, AppLocalizations.of(context)!.fido_fingerprint_added);
|
||||
showMessage(context, l10n.s_fingerprint_added);
|
||||
} catch (e) {
|
||||
final String errorMessage;
|
||||
// TODO: Make this cleaner than importing desktop specific RpcError.
|
||||
@ -156,7 +157,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
}
|
||||
showMessage(
|
||||
context,
|
||||
'${AppLocalizations.of(context)!.fido_error_setting_name}: $errorMessage',
|
||||
l10n.l_setting_name_failed(errorMessage),
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
}
|
||||
@ -164,16 +165,17 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final progress = _samples == 0 ? 0.0 : _samples / (_samples + _remaining);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(AppLocalizations.of(context)!.fido_add_fingerprint),
|
||||
title: Text(l10n.s_add_fingerprint),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.fido_step_1_2),
|
||||
Text(l10n.l_fp_step_1_capture),
|
||||
Column(
|
||||
children: [
|
||||
Padding(
|
||||
@ -196,7 +198,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(AppLocalizations.of(context)!.fido_step_2_2),
|
||||
Text(l10n.l_fp_step_2_name),
|
||||
TextFormField(
|
||||
focusNode: _nameFocus,
|
||||
maxLength: 15,
|
||||
@ -206,7 +208,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
decoration: InputDecoration(
|
||||
enabled: _fingerprint != null,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.fido_name,
|
||||
labelText: l10n.s_name,
|
||||
prefixIcon: const Icon(Icons.fingerprint_outlined),
|
||||
),
|
||||
onChanged: (value) {
|
||||
@ -232,7 +234,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _fingerprint != null && _label.isNotEmpty ? _submit : null,
|
||||
child: Text(AppLocalizations.of(context)!.fido_save),
|
||||
child: Text(l10n.s_save),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -34,17 +34,18 @@ class DeleteCredentialDialog extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final label = credential.userName;
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(AppLocalizations.of(context)!.fido_delete_credential),
|
||||
title: Text(l10n.s_delete_credential),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.fido_this_will_delete_cred),
|
||||
Text('${AppLocalizations.of(context)!.fido_credential}: $label'),
|
||||
Text(l10n.p_warning_delete_credential),
|
||||
Text(l10n.l_credential(label)),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
@ -62,12 +63,11 @@ class DeleteCredentialDialog extends ConsumerWidget {
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
Navigator.of(context).pop(true);
|
||||
showMessage(context,
|
||||
AppLocalizations.of(context)!.fido_credential_deleted);
|
||||
showMessage(context, l10n.s_credential_deleted);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.fido_delete),
|
||||
child: Text(l10n.s_delete),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -32,10 +32,11 @@ class DeleteFingerprintDialog extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final label = fingerprint.label;
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(AppLocalizations.of(context)!.fido_delete_fingerprint),
|
||||
title: Text(l10n.s_delete_fingerprint),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
@ -44,11 +45,10 @@ class DeleteFingerprintDialog extends ConsumerWidget {
|
||||
.deleteFingerprint(fingerprint);
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
Navigator.of(context).pop(true);
|
||||
showMessage(context,
|
||||
AppLocalizations.of(context)!.fido_fingerprint_deleted);
|
||||
showMessage(context, l10n.s_fingerprint_deleted);
|
||||
});
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.fido_delete),
|
||||
child: Text(l10n.s_delete),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
@ -56,8 +56,8 @@ class DeleteFingerprintDialog extends ConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.fido_this_will_delete_fp),
|
||||
Text('${AppLocalizations.of(context)!.fido_fingerprint}: $label'),
|
||||
Text(l10n.p_warning_delete_fingerprint),
|
||||
Text(l10n.l_fingerprint(label)),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -33,45 +33,47 @@ class FidoScreen extends ConsumerWidget {
|
||||
const FidoScreen(this.deviceData, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) =>
|
||||
ref.watch(fidoStateProvider(deviceData.node.path)).when(
|
||||
loading: () => AppPage(
|
||||
title: Text(AppLocalizations.of(context)!.fido_webauthn),
|
||||
centered: true,
|
||||
delayedContent: true,
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, _) {
|
||||
final supported = deviceData
|
||||
.info.supportedCapabilities[deviceData.node.transport] ??
|
||||
0;
|
||||
if (Capability.fido2.value & supported == 0) {
|
||||
return MessagePage(
|
||||
title: Text(AppLocalizations.of(context)!.fido_webauthn),
|
||||
graphic: manageAccounts,
|
||||
header: AppLocalizations.of(context)!.fido_ready_to_use,
|
||||
message: AppLocalizations.of(context)!.fido_register_as_a_key,
|
||||
);
|
||||
}
|
||||
final enabled = deviceData.info.config
|
||||
.enabledCapabilities[deviceData.node.transport] ??
|
||||
0;
|
||||
if (Capability.fido2.value & enabled == 0) {
|
||||
return MessagePage(
|
||||
title: Text(AppLocalizations.of(context)!.fido_webauthn),
|
||||
header: AppLocalizations.of(context)!.fido_fido_disabled,
|
||||
message: AppLocalizations.of(context)!.fido_webauthn_req_fido,
|
||||
);
|
||||
}
|
||||
|
||||
return AppFailurePage(
|
||||
title: Text(AppLocalizations.of(context)!.fido_webauthn),
|
||||
cause: error,
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ref.watch(fidoStateProvider(deviceData.node.path)).when(
|
||||
loading: () => AppPage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
centered: true,
|
||||
delayedContent: true,
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, _) {
|
||||
final supported = deviceData
|
||||
.info.supportedCapabilities[deviceData.node.transport] ??
|
||||
0;
|
||||
if (Capability.fido2.value & supported == 0) {
|
||||
return MessagePage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
graphic: manageAccounts,
|
||||
header: l10n.l_ready_to_use,
|
||||
message: l10n.l_register_sk_on_websites,
|
||||
);
|
||||
},
|
||||
data: (fidoState) {
|
||||
return fidoState.unlocked
|
||||
? FidoUnlockedPage(deviceData.node, fidoState)
|
||||
: FidoLockedPage(deviceData.node, fidoState);
|
||||
});
|
||||
}
|
||||
final enabled = deviceData
|
||||
.info.config.enabledCapabilities[deviceData.node.transport] ??
|
||||
0;
|
||||
if (Capability.fido2.value & enabled == 0) {
|
||||
return MessagePage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
header: l10n.s_fido_disabled,
|
||||
message: l10n.l_webauthn_req_fido2,
|
||||
);
|
||||
}
|
||||
|
||||
return AppFailurePage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
cause: error,
|
||||
);
|
||||
},
|
||||
data: (fidoState) {
|
||||
return fidoState.unlocked
|
||||
? FidoUnlockedPage(deviceData.node, fidoState)
|
||||
: FidoLockedPage(deviceData.node, fidoState);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -27,21 +27,21 @@ import 'reset_dialog.dart';
|
||||
|
||||
Widget fidoBuildActions(
|
||||
BuildContext context, DeviceNode node, FidoState state, int fingerprints) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context).colorScheme;
|
||||
return SimpleDialog(
|
||||
children: [
|
||||
if (state.bioEnroll != null) ...[
|
||||
ListTitle(AppLocalizations.of(context)!.general_setup,
|
||||
ListTitle(l10n.s_setup,
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge),
|
||||
ListTile(
|
||||
leading: const CircleAvatar(child: Icon(Icons.fingerprint_outlined)),
|
||||
title: Text(AppLocalizations.of(context)!.fido_add_fingerprint),
|
||||
title: Text(l10n.s_add_fingerprint),
|
||||
subtitle: state.unlocked
|
||||
? Text(AppLocalizations.of(context)!
|
||||
.fido_fingerprints_used(fingerprints))
|
||||
? Text(l10n.l_fingerprints_used(fingerprints))
|
||||
: Text(state.hasPin
|
||||
? AppLocalizations.of(context)!.fido_unlock_first
|
||||
: AppLocalizations.of(context)!.fido_set_pin_first),
|
||||
? l10n.l_unlock_pin_first
|
||||
: l10n.l_set_pin_first),
|
||||
enabled: state.unlocked && fingerprints < 5,
|
||||
onTap: state.unlocked && fingerprints < 5
|
||||
? () {
|
||||
@ -54,16 +54,14 @@ Widget fidoBuildActions(
|
||||
: null,
|
||||
),
|
||||
],
|
||||
ListTitle(AppLocalizations.of(context)!.general_manage,
|
||||
ListTitle(l10n.s_manage,
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge),
|
||||
ListTile(
|
||||
leading: const CircleAvatar(child: Icon(Icons.pin_outlined)),
|
||||
title: Text(state.hasPin
|
||||
? AppLocalizations.of(context)!.fido_change_pin
|
||||
: AppLocalizations.of(context)!.fido_set_pin),
|
||||
title: Text(state.hasPin ? l10n.s_change_pin : l10n.s_set_pin),
|
||||
subtitle: Text(state.hasPin
|
||||
? AppLocalizations.of(context)!.fido_pin_protection
|
||||
: AppLocalizations.of(context)!.fido_pin_protection_optional),
|
||||
? l10n.s_fido_pin_protection
|
||||
: l10n.l_fido_pin_protection_optional),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
@ -77,9 +75,8 @@ Widget fidoBuildActions(
|
||||
backgroundColor: theme.error,
|
||||
child: const Icon(Icons.delete_outline),
|
||||
),
|
||||
title: Text(AppLocalizations.of(context)!.fido_reset_fido),
|
||||
subtitle:
|
||||
Text(AppLocalizations.of(context)!.fido_factory_reset_description),
|
||||
title: Text(l10n.s_reset_fido),
|
||||
subtitle: Text(l10n.l_factory_reset_this_app),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
|
@ -34,23 +34,24 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
if (!state.hasPin) {
|
||||
if (state.bioEnroll != null) {
|
||||
return MessagePage(
|
||||
title: Text(AppLocalizations.of(context)!.fido_webauthn),
|
||||
title: Text(l10n.s_webauthn),
|
||||
graphic: noFingerprints,
|
||||
header: AppLocalizations.of(context)!.fido_no_fingerprints,
|
||||
message: AppLocalizations.of(context)!.fido_set_pin_fingerprints,
|
||||
header: l10n.s_no_fingerprints,
|
||||
message: l10n.l_set_pin_fingerprints,
|
||||
keyActionsBuilder: _buildActions,
|
||||
);
|
||||
} else {
|
||||
return MessagePage(
|
||||
title: Text(AppLocalizations.of(context)!.fido_webauthn),
|
||||
title: Text(l10n.s_webauthn),
|
||||
graphic: manageAccounts,
|
||||
header: state.credMgmt
|
||||
? AppLocalizations.of(context)!.fido_no_discoverable_acc
|
||||
: AppLocalizations.of(context)!.fido_ready_to_use,
|
||||
message: AppLocalizations.of(context)!.fido_optionally_set_a_pin,
|
||||
? l10n.l_no_discoverable_accounts
|
||||
: l10n.l_ready_to_use,
|
||||
message: l10n.l_optionally_set_a_pin,
|
||||
keyActionsBuilder: _buildActions,
|
||||
);
|
||||
}
|
||||
@ -58,16 +59,16 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
|
||||
if (!state.credMgmt && state.bioEnroll == null) {
|
||||
return MessagePage(
|
||||
title: Text(AppLocalizations.of(context)!.fido_webauthn),
|
||||
title: Text(l10n.s_webauthn),
|
||||
graphic: manageAccounts,
|
||||
header: AppLocalizations.of(context)!.fido_ready_to_use,
|
||||
message: AppLocalizations.of(context)!.fido_register_as_a_key,
|
||||
header: l10n.l_ready_to_use,
|
||||
message: l10n.l_register_sk_on_websites,
|
||||
keyActionsBuilder: _buildActions,
|
||||
);
|
||||
}
|
||||
|
||||
return AppPage(
|
||||
title: Text(AppLocalizations.of(context)!.fido_webauthn),
|
||||
title: Text(l10n.s_webauthn),
|
||||
keyActionsBuilder: _buildActions,
|
||||
child: Column(
|
||||
children: [
|
||||
@ -116,27 +117,29 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
||||
}
|
||||
|
||||
String? _getErrorText() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
if (_retries == 0) {
|
||||
return AppLocalizations.of(context)!.fido_pin_blocked_factory_reset;
|
||||
return l10n.l_pin_blocked_reset;
|
||||
}
|
||||
if (_blocked) {
|
||||
return AppLocalizations.of(context)!.fido_pin_temp_blocked;
|
||||
return l10n.l_pin_soft_locked;
|
||||
}
|
||||
if (_retries != null) {
|
||||
return AppLocalizations.of(context)!.fido_wrong_pin_attempts(_retries!);
|
||||
return l10n.l_wrong_pin_attempts_remaining(_retries!);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final noFingerprints = widget._state.bioEnroll == false;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 18.0, right: 18, top: 32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.fido_enter_fido2_pin),
|
||||
Text(l10n.l_enter_fido2_pin),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 16.0),
|
||||
child: TextField(
|
||||
@ -145,7 +148,7 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
||||
controller: _pinController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.fido_pin,
|
||||
labelText: l10n.s_pin,
|
||||
helperText: '', // Prevents dialog resizing
|
||||
errorText: _pinIsWrong ? _getErrorText() : null,
|
||||
errorMaxLines: 3,
|
||||
@ -175,7 +178,7 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
||||
noFingerprints ? const Icon(Icons.warning_amber_rounded) : null,
|
||||
title: noFingerprints
|
||||
? Text(
|
||||
AppLocalizations.of(context)!.fido_no_fp_added,
|
||||
l10n.l_no_fps_added,
|
||||
overflow: TextOverflow.fade,
|
||||
)
|
||||
: null,
|
||||
@ -184,7 +187,7 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
||||
minLeadingWidth: 0,
|
||||
trailing: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.lock_open),
|
||||
label: Text(AppLocalizations.of(context)!.fido_unlock),
|
||||
label: Text(l10n.s_unlock),
|
||||
onPressed:
|
||||
_pinController.text.isNotEmpty && !_blocked ? _submit : null,
|
||||
),
|
||||
|
@ -49,6 +49,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasPin = widget.state.hasPin;
|
||||
final isValid = _newPin.isNotEmpty &&
|
||||
_newPin == _confirmPin &&
|
||||
@ -56,13 +57,11 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
final minPinLength = widget.state.minPinLength;
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(hasPin
|
||||
? AppLocalizations.of(context)!.fido_change_pin
|
||||
: AppLocalizations.of(context)!.fido_set_pin),
|
||||
title: Text(hasPin ? l10n.s_change_pin : l10n.s_set_pin),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isValid ? _submit : null,
|
||||
child: Text(AppLocalizations.of(context)!.fido_save),
|
||||
child: Text(l10n.s_save),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
@ -71,14 +70,14 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (hasPin) ...[
|
||||
Text(AppLocalizations.of(context)!.fido_enter_current_pin),
|
||||
Text(l10n.p_enter_current_pin_or_reset),
|
||||
TextFormField(
|
||||
initialValue: _currentPin,
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.fido_current_pin,
|
||||
labelText: l10n.s_current_pin,
|
||||
errorText: _currentIsWrong ? _currentPinError : null,
|
||||
errorMaxLines: 3,
|
||||
prefixIcon: const Icon(Icons.pin_outlined),
|
||||
@ -91,8 +90,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
},
|
||||
),
|
||||
],
|
||||
Text(
|
||||
AppLocalizations.of(context)!.fido_enter_new_pin(minPinLength)),
|
||||
Text(l10n.p_enter_new_fido2_pin(minPinLength)),
|
||||
// TODO: Set max characters based on UTF-8 bytes
|
||||
TextFormField(
|
||||
initialValue: _newPin,
|
||||
@ -100,7 +98,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.fido_new_pin,
|
||||
labelText: l10n.s_new_pin,
|
||||
enabled: !hasPin || _currentPin.isNotEmpty,
|
||||
errorText: _newIsWrong ? _newPinError : null,
|
||||
errorMaxLines: 3,
|
||||
@ -118,7 +116,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.fido_confirm_pin,
|
||||
labelText: l10n.s_confirm_pin,
|
||||
prefixIcon: const Icon(Icons.pin_outlined),
|
||||
enabled:
|
||||
(!hasPin || _currentPin.isNotEmpty) && _newPin.isNotEmpty,
|
||||
@ -146,12 +144,12 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
}
|
||||
|
||||
void _submit() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final minPinLength = widget.state.minPinLength;
|
||||
final oldPin = _currentPin.isNotEmpty ? _currentPin : null;
|
||||
if (_newPin.length < minPinLength) {
|
||||
setState(() {
|
||||
_newPinError =
|
||||
AppLocalizations.of(context)!.fido_new_pin_chars(minPinLength);
|
||||
_newPinError = l10n.l_new_pin_len(minPinLength);
|
||||
_newIsWrong = true;
|
||||
});
|
||||
return;
|
||||
@ -162,15 +160,14 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
.setPin(_newPin, oldPin: oldPin);
|
||||
result.when(success: () {
|
||||
Navigator.of(context).pop(true);
|
||||
showMessage(context, AppLocalizations.of(context)!.fido_pin_set);
|
||||
showMessage(context, l10n.s_pin_set);
|
||||
}, failed: (retries, authBlocked) {
|
||||
setState(() {
|
||||
if (authBlocked) {
|
||||
_currentPinError = AppLocalizations.of(context)!.fido_pin_blocked;
|
||||
_currentPinError = l10n.l_pin_soft_locked;
|
||||
_currentIsWrong = true;
|
||||
} else {
|
||||
_currentPinError = AppLocalizations.of(context)!
|
||||
.fido_wrong_pin_retries_remaining(retries);
|
||||
_currentPinError = l10n.l_wrong_pin_attempts_remaining(retries);
|
||||
_currentIsWrong = true;
|
||||
}
|
||||
});
|
||||
@ -186,7 +183,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
}
|
||||
showMessage(
|
||||
context,
|
||||
'${AppLocalizations.of(context)!.fido_fail_set_pin}: $errorMessage',
|
||||
l10n.l_set_pin_failed(errorMessage),
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
}
|
||||
|
@ -47,14 +47,14 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
}
|
||||
|
||||
_submit() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
try {
|
||||
final renamed = await ref
|
||||
.read(fingerprintProvider(widget.devicePath).notifier)
|
||||
.renameFingerprint(widget.fingerprint, _label);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(renamed);
|
||||
showMessage(
|
||||
context, AppLocalizations.of(context)!.fido_fingerprint_renamed);
|
||||
showMessage(context, l10n.s_fingerprint_renamed);
|
||||
} catch (e) {
|
||||
final String errorMessage;
|
||||
// TODO: Make this cleaner than importing desktop specific RpcError.
|
||||
@ -65,7 +65,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
}
|
||||
showMessage(
|
||||
context,
|
||||
'${AppLocalizations.of(context)!.fido_error_renaming}: $errorMessage',
|
||||
l10n.l_rename_fp_failed(errorMessage),
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
}
|
||||
@ -73,12 +73,13 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ResponsiveDialog(
|
||||
title: Text(AppLocalizations.of(context)!.fido_rename_fingerprint),
|
||||
title: Text(l10n.s_rename_fp),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _label.isNotEmpty ? _submit : null,
|
||||
child: Text(AppLocalizations.of(context)!.fido_save),
|
||||
child: Text(l10n.s_save),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
@ -86,9 +87,8 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!
|
||||
.fido_rename(widget.fingerprint.label)),
|
||||
Text(AppLocalizations.of(context)!.fido_will_change_label_fp),
|
||||
Text(l10n.q_rename_target(widget.fingerprint.label)),
|
||||
Text(l10n.p_will_change_label_fp),
|
||||
TextFormField(
|
||||
initialValue: _label,
|
||||
maxLength: 15,
|
||||
@ -96,7 +96,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
buildCounter: buildByteCounterFor(_label),
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.fido_label,
|
||||
labelText: l10n.s_label,
|
||||
prefixIcon: const Icon(Icons.fingerprint_outlined),
|
||||
),
|
||||
onChanged: (value) {
|
||||
|
@ -45,27 +45,25 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||
InteractionEvent? _interaction;
|
||||
|
||||
String _getMessage() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final nfc = widget.node.transport == Transport.nfc;
|
||||
switch (_interaction) {
|
||||
case InteractionEvent.remove:
|
||||
return nfc
|
||||
? AppLocalizations.of(context)!.fido_remove_from_reader
|
||||
: AppLocalizations.of(context)!.fido_unplug_yubikey;
|
||||
return nfc ? l10n.l_remove_yk_from_reader : l10n.l_unplug_yk;
|
||||
case InteractionEvent.insert:
|
||||
return nfc
|
||||
? AppLocalizations.of(context)!.fido_place_back_on_reader
|
||||
: AppLocalizations.of(context)!.fido_reinsert_yubikey;
|
||||
return nfc ? l10n.l_replace_yk_on_reader : l10n.l_reinsert_yk;
|
||||
case InteractionEvent.touch:
|
||||
return AppLocalizations.of(context)!.fido_touch_yubikey;
|
||||
return l10n.l_touch_button_now;
|
||||
case null:
|
||||
return AppLocalizations.of(context)!.fido_press_reset;
|
||||
return l10n.l_press_reset_to_begin;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ResponsiveDialog(
|
||||
title: Text(AppLocalizations.of(context)!.fido_factory_reset),
|
||||
title: Text(l10n.s_factory_reset),
|
||||
onCancel: () {
|
||||
_subscription?.cancel();
|
||||
},
|
||||
@ -83,8 +81,7 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||
}, onDone: () {
|
||||
_subscription = null;
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context,
|
||||
AppLocalizations.of(context)!.fido_fido_app_reset);
|
||||
showMessage(context, l10n.l_fido_app_reset);
|
||||
}, onError: (e) {
|
||||
_log.error('Error performing FIDO reset', e);
|
||||
Navigator.of(context).pop();
|
||||
@ -97,13 +94,13 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||
}
|
||||
showMessage(
|
||||
context,
|
||||
'${AppLocalizations.of(context)!.fido_error_reset}: $errorMessage',
|
||||
l10n.l_reset_failed(errorMessage),
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
});
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context)!.fido_reset),
|
||||
child: Text(l10n.s_reset),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
@ -112,11 +109,11 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.fido_warning_will_delete_accounts,
|
||||
l10n.p_warning_deletes_accounts,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.fido_warning_disable_these_creds,
|
||||
l10n.p_warning_disable_accounts,
|
||||
),
|
||||
Center(
|
||||
child: Text(_getMessage(),
|
||||
|
@ -39,6 +39,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
List<Widget> children = [];
|
||||
if (state.credMgmt) {
|
||||
final data = ref.watch(credentialProvider(node.path)).asData;
|
||||
@ -47,7 +48,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
}
|
||||
final creds = data.value;
|
||||
if (creds.isNotEmpty) {
|
||||
children.add(ListTitle(AppLocalizations.of(context)!.fido_credentials));
|
||||
children.add(ListTitle(l10n.s_credentials));
|
||||
children.addAll(
|
||||
creds.map(
|
||||
(cred) => ListTile(
|
||||
@ -95,8 +96,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
final fingerprints = data.value;
|
||||
if (fingerprints.isNotEmpty) {
|
||||
nFingerprints = fingerprints.length;
|
||||
children
|
||||
.add(ListTitle(AppLocalizations.of(context)!.fido_fingerprints));
|
||||
children.add(ListTitle(l10n.s_fingerprints));
|
||||
children.addAll(fingerprints.map((fp) => ListTile(
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
||||
@ -137,7 +137,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
|
||||
if (children.isNotEmpty) {
|
||||
return AppPage(
|
||||
title: Text(AppLocalizations.of(context)!.fido_webauthn),
|
||||
title: Text(l10n.s_webauthn),
|
||||
keyActionsBuilder: (context) =>
|
||||
fidoBuildActions(context, node, state, nFingerprints),
|
||||
child: Column(
|
||||
@ -147,26 +147,26 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
|
||||
if (state.bioEnroll != null) {
|
||||
return MessagePage(
|
||||
title: Text(AppLocalizations.of(context)!.fido_webauthn),
|
||||
title: Text(l10n.s_webauthn),
|
||||
graphic: noFingerprints,
|
||||
header: AppLocalizations.of(context)!.fido_no_fingerprints,
|
||||
message: AppLocalizations.of(context)!.fido_add_one_or_more,
|
||||
header: l10n.s_no_fingerprints,
|
||||
message: l10n.l_add_one_or_more_fps,
|
||||
keyActionsBuilder: (context) =>
|
||||
fidoBuildActions(context, node, state, 0),
|
||||
);
|
||||
}
|
||||
|
||||
return MessagePage(
|
||||
title: Text(AppLocalizations.of(context)!.fido_webauthn),
|
||||
title: Text(l10n.s_webauthn),
|
||||
graphic: manageAccounts,
|
||||
header: AppLocalizations.of(context)!.fido_no_discoverable_acc,
|
||||
message: AppLocalizations.of(context)!.fido_register_as_a_key,
|
||||
header: l10n.l_no_discoverable_accounts,
|
||||
message: l10n.l_register_sk_on_websites,
|
||||
keyActionsBuilder: (context) => fidoBuildActions(context, node, state, 0),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingPage(BuildContext context) => AppPage(
|
||||
title: Text(AppLocalizations.of(context)!.fido_webauthn),
|
||||
title: Text(AppLocalizations.of(context)!.s_webauthn),
|
||||
centered: true,
|
||||
delayedContent: true,
|
||||
child: const CircularProgressIndicator(),
|
||||
|
@ -1,301 +1,443 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"oath_no_credentials": "No credentials",
|
||||
"oath_pinned": "Pinned",
|
||||
"oath_accounts": "Accounts",
|
||||
"oath_copied_to_clipboard": "Code copied to clipboard",
|
||||
"oath_copy_to_clipboard": "Copy to clipboard",
|
||||
"oath_calculate": "Calculate",
|
||||
"oath_pin_account": "Pin account",
|
||||
"oath_unpin_account": "Unpin account",
|
||||
"oath_rename_account": "Rename account",
|
||||
"oath_delete_account": "Delete account",
|
||||
"oath_no_qr_code": "No QR code found",
|
||||
"oath_failed_reading_qr": "Failed reading QR code",
|
||||
"oath_success_add_account": "Account added",
|
||||
"oath_fail_add_account": "Failed adding account",
|
||||
"oath_add_account": "Add account",
|
||||
"oath_save": "Save",
|
||||
"oath_no_qr_code": "No QR code found",
|
||||
"oath_duplicate_name": "This name already exists for the Issuer",
|
||||
"oath_issuer_optional": "Issuer (optional)",
|
||||
"oath_account_name": "Account name",
|
||||
"oath_secret_key": "Secret key",
|
||||
"oath_invalid_length": "Invalid length",
|
||||
"oath_scanned_qr": "Scanned QR code",
|
||||
"oath_scan_qr": "Scan QR code",
|
||||
"oath_require_touch": "Require touch",
|
||||
"oath_touch_required": "Touch Required",
|
||||
"oath_touch_now": "Touch the button on your YubiKey now",
|
||||
"oath_sec": "sec",
|
||||
"oath_digits": "digits",
|
||||
"oath_success_delete_account": "Account deleted",
|
||||
"oath_delete": "Delete",
|
||||
"oath_warning_this_will_delete_account_from_key": "Warning! This action will delete the account from your YubiKey.",
|
||||
"oath_warning_disable_this_cred": "You will no longer be able to generate OTPs for this account. Make sure to first disable this credential from the website to avoid being locked out of your account.",
|
||||
"oath_account": "Account",
|
||||
"oath_password_set": "Password set",
|
||||
"oath_manage_password": "Manage password",
|
||||
"oath_password_description": "Optional password protection",
|
||||
"oath_enter_current_password": "Enter your current password. If you don't know your password, you'll need to reset the YubiKey.",
|
||||
"oath_current_password": "Current password",
|
||||
"oath_wrong_password": "Wrong password",
|
||||
"oath_password_removed": "Password removed",
|
||||
"oath_remove_password": "Remove password",
|
||||
"oath_clear_saved_password": "Clear saved password",
|
||||
"oath_password_forgotten": "Password forgotten",
|
||||
"oath_enter_new_password": "Enter your new password. A password may contain letters, numbers and special characters.",
|
||||
"oath_new_password": "New password",
|
||||
"oath_confirm_password": "Confirm password",
|
||||
"oath_authenticator": "Authenticator",
|
||||
"oath_reset_oath": "Reset OATH",
|
||||
"oath_no_accounts": "No accounts",
|
||||
"oath_search_accounts": "Search accounts",
|
||||
"oath_set_password": "Set password",
|
||||
"oath_failed_remember_pw": "Failed to remember password",
|
||||
"oath_enter_oath_pw": "Enter the OATH password for your YubiKey",
|
||||
"oath_password": "Password",
|
||||
"oath_keystore_unavailable": "OS Keystore unavailable",
|
||||
"oath_remember_password": "Remember password",
|
||||
"oath_unlock": "Unlock",
|
||||
"oath_unlock_first": "Unlock with password first",
|
||||
"oath_warning_will_change_account_displayed": "This will change how the account is displayed in the list.",
|
||||
"oath_account_must_have_name": "Your account must have a name",
|
||||
"oath_name_exists": "This name already exists for the Issuer",
|
||||
"oath_account_renamed": "Account renamed",
|
||||
"oath_rename": "Rename {label}?",
|
||||
"@oath_rename" : {
|
||||
|
||||
"@_readme": {
|
||||
"notes": [
|
||||
"All strings start with a Captial letter.",
|
||||
"Group strings by category, but don't needlessly tie them to a section of the app if they can be re-used between several.",
|
||||
"Run check_strings.py on the .arb file to detect problems, tweak @_lint_rules as needed per language."
|
||||
],
|
||||
"prefixes": {
|
||||
"s_": "A single, or few words. Should be short enough to display on a button, or a header.",
|
||||
"l_": "A single line, can be wrapped. Should not be more than one sentence, and not end with a period.",
|
||||
"p_": "One or more full sentences, with proper punctuation.",
|
||||
"q_": "A question, ending in question mark."
|
||||
}
|
||||
},
|
||||
|
||||
"@_lint_rules": {
|
||||
"s_max_words": 4,
|
||||
"s_max_length": 32
|
||||
},
|
||||
|
||||
"app_name": "Yubico Authenticator",
|
||||
|
||||
"s_save": "Save",
|
||||
"s_cancel": "Cancel",
|
||||
"s_close": "Close",
|
||||
"s_delete": "Delete",
|
||||
"s_quit": "Quit",
|
||||
"s_unlock": "Unlock",
|
||||
"s_calculate": "Calculate",
|
||||
"s_label": "Label",
|
||||
"s_name": "Name",
|
||||
"s_usb": "USB",
|
||||
"s_nfc": "NFC",
|
||||
"s_show_window": "Show window",
|
||||
"s_hide_window": "Hide window",
|
||||
"q_rename_target": "Rename {label}?",
|
||||
"@q_rename_target" : {
|
||||
"placeholders": {
|
||||
"label": {}
|
||||
}
|
||||
},
|
||||
"oath_factory_reset": "Factory reset",
|
||||
"oath_factory_reset_description": "Factory reset this application",
|
||||
"oath_oath_application_reset": "OATH application reset",
|
||||
"oath_reset": "Reset",
|
||||
"oath_warning_will_delete_accounts": "Warning! This will irrevocably delete all OATH TOTP/HOTP accounts from your YubiKey.",
|
||||
"oath_warning_disable_these_creds": "Your OATH credentials, as well as any password set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.",
|
||||
"oath_invalid_character_issuer": "Invalid character: ':' is not allowed in issuer",
|
||||
"oath_accounts_used": "{used} of {capacity} accounts used",
|
||||
"@oath_accounts_used" : {
|
||||
|
||||
"s_about": "About",
|
||||
"s_appearance": "Appearance",
|
||||
"s_authenticator": "Authenticator",
|
||||
"s_manage": "Manage",
|
||||
"s_setup": "Setup",
|
||||
"s_settings": "Settings",
|
||||
"s_webauthn": "WebAuthn",
|
||||
"s_help_and_about": "Help and about",
|
||||
"s_help_and_feedback": "Help and feedback",
|
||||
"s_send_feedback": "Send us feedback",
|
||||
"s_i_need_help": "I need help",
|
||||
"s_troubleshooting": "Troubleshooting",
|
||||
"s_terms_of_use": "Terms of use",
|
||||
"s_privacy_policy": "Privacy policy",
|
||||
"s_open_src_licenses": "Open source licenses",
|
||||
"s_configure_yk": "Configure YubiKey",
|
||||
"s_please_wait": "Please wait\u2026",
|
||||
"s_secret_key": "Secret key",
|
||||
"s_invalid_length": "Invalid length",
|
||||
"s_require_touch": "Require touch",
|
||||
"q_have_account_info": "Have account info?",
|
||||
"s_run_diagnostics": "Run diagnostics",
|
||||
"s_log_level": "Log level: {level}",
|
||||
"@s_log_level": {
|
||||
"placeholders": {
|
||||
"used": {},
|
||||
"capacity": {}
|
||||
"level": {}
|
||||
}
|
||||
},
|
||||
"oath_custom_icons": "Custom icons",
|
||||
"oath_custom_icons_description": "Icon packs can make your accounts more easily distinguishable with familiar logos and colors.",
|
||||
"oath_custom_icons_replace": "Replace icon pack",
|
||||
"oath_custom_icons_loading": "Loading icon pack\u2026",
|
||||
"oath_custom_icons_load": "Load icon pack",
|
||||
"oath_custom_icons_remove": "Remove icon pack",
|
||||
"oath_custom_icons_icon_pack_removed": "Icon pack removed",
|
||||
"oath_custom_icons_err_icon_pack_remove": "Error removing icon pack",
|
||||
"oath_custom_icons_choose_icon_pack": "Choose icon pack",
|
||||
"oath_custom_icons_icon_pack_imported": "Icon pack imported",
|
||||
"oath_custom_icons_err_icon_pack_import": "Error importing icon pack: {message}",
|
||||
"@oath_custom_icons_err_icon_pack_import": {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"oath_custom_icons_learn_more": "Learn\u00a0more",
|
||||
"oath_custom_icons_err_import_general": "Import error",
|
||||
"oath_custom_icons_err_file_not_found": "File not found",
|
||||
"oath_custom_icons_err_file_too_big": "File size too big",
|
||||
"oath_custom_icons_err_invalid_icon_pack": "Invalid icon pack",
|
||||
"oath_custom_icons_err_filesystem_error": "File system operation error",
|
||||
"s_character_count": "Character count",
|
||||
"s_learn_more": "Learn\u00a0more",
|
||||
|
||||
"widgets_cancel": "Cancel",
|
||||
"widgets_close": "Close",
|
||||
"@_language": {},
|
||||
"s_language": "Language",
|
||||
"l_enable_community_translations": "Enable community translations",
|
||||
"p_community_translations_desc": "These translations are provided and maintained by the community. They may contain errors or be incomplete.",
|
||||
|
||||
"mgmt_min_one_interface": "At least one interface must be enabled",
|
||||
"mgmt_reconfiguring_yubikey": "Reconfiguring YubiKey\u2026",
|
||||
"mgmt_configuration_updated": "Configuration updated",
|
||||
"mgmt_configuration_updated_remove_reinsert": "Configuration updated, remove and reinsert your YubiKey",
|
||||
"mgmt_toggle_applications": "Toggle applications",
|
||||
"mgmt_save": "Save",
|
||||
"@_theme": {},
|
||||
"s_app_theme": "App theme",
|
||||
"s_choose_app_theme": "Choose app theme",
|
||||
"s_system_default": "System default",
|
||||
"s_light_mode": "Light mode",
|
||||
"s_dark_mode": "Dark mode",
|
||||
|
||||
"general_app_name": "Yubico Authenticator",
|
||||
"general_about": "About",
|
||||
"general_terms_of_use": "Terms of use",
|
||||
"general_privacy_policy": "Privacy policy",
|
||||
"general_open_src_licenses": "Open source licenses",
|
||||
"general_help_and_feedback": "Help and feedback",
|
||||
"general_send_feedback": "Send us feedback",
|
||||
"general_i_need_help": "I need help",
|
||||
"general_application_error": "Application error",
|
||||
"general_troubleshooting": "Troubleshooting",
|
||||
"general_run_diagnostics": "Run diagnostics",
|
||||
"general_diagnostics_copied": "Diagnostic data copied to clipboard",
|
||||
"general_log_level": "Log level",
|
||||
"general_copy_log": "Copy log",
|
||||
"general_log_copied": "Log copied to clipboard",
|
||||
"general_settings": "Settings",
|
||||
"general_appearance": "Appearance",
|
||||
"general_system_default": "System default",
|
||||
"general_light_mode": "Light mode",
|
||||
"general_dark_mode": "Dark mode",
|
||||
"general_allow_screenshots": "Allow screenshots",
|
||||
"general_usb": "USB",
|
||||
"general_nfc": "NFC",
|
||||
"general_setup": "Setup",
|
||||
"general_manage": "Manage",
|
||||
"general_configure_yubikey": "Configure YubiKey",
|
||||
"general_show_window": "Show window",
|
||||
"general_hide_window": "Hide window",
|
||||
"general_quit": "Quit",
|
||||
|
||||
"fido_press_fingerprint_begin": "Press your finger against the YubiKey to begin.",
|
||||
"fido_keep_touching_yubikey": "Keep touching your YubiKey repeatedly\u2026",
|
||||
"fido_fingerprint_captured": "Fingerprint captured successfully!",
|
||||
"fido_fingerprint_added": "Fingerprint added",
|
||||
"fido_error_setting_name": "Error setting name",
|
||||
"fido_add_fingerprint": "Add fingerprint",
|
||||
"fido_step_1_2": "Step 1/2: Capture fingerprint",
|
||||
"fido_step_2_2": "Step 2/2: Name fingerprint",
|
||||
"fido_name": "Name",
|
||||
"fido_save": "Save",
|
||||
"fido_delete_credential": "Delete credential",
|
||||
"fido_this_will_delete_cred": "This will delete the credential from your YubiKey.",
|
||||
"fido_credential": "Credential",
|
||||
"fido_credential_deleted": "Credential deleted",
|
||||
"fido_delete": "Delete",
|
||||
"fido_delete_fingerprint": "Delete fingerprint",
|
||||
"fido_fingerprint_deleted": "Fingerprint deleted",
|
||||
"fido_this_will_delete_fp": "This will delete the fingerprint from your YubiKey.",
|
||||
"fido_fingerprint": "Fingerprint",
|
||||
"fido_webauthn": "WebAuthn",
|
||||
"fido_ready_to_use": "Ready to use",
|
||||
"fido_register_as_a_key": "Register as a Security Key on websites",
|
||||
"fido_fido_disabled": "FIDO2 disabled",
|
||||
"fido_webauthn_req_fido": "WebAuthn requires the FIDO2 application to be enabled on your YubiKey",
|
||||
"fido_wrong_pin_attempts": "Wrong PIN. {retries} attempt(s) remaining.",
|
||||
"@fido_wrong_pin_attempts" : {
|
||||
"placeholders": {
|
||||
"retries": {}
|
||||
}
|
||||
},
|
||||
"fido_no_fingerprints": "No fingerprints",
|
||||
"fido_set_pin_fingerprints": "Set a PIN to register fingerprints",
|
||||
"fido_no_discoverable_acc": "No discoverable accounts",
|
||||
"fido_optionally_set_a_pin": "Optionally set a PIN to protect access to your YubiKey\nRegister as a Security Key on websites",
|
||||
"fido_set_pin": "Set PIN",
|
||||
"fido_reset_fido": "Reset FIDO",
|
||||
"fido_pin_blocked_factory_reset": "PIN is blocked. Factory reset the FIDO application.",
|
||||
"fido_pin_temp_blocked": "PIN temporarily blocked, remove and reinsert your YubiKey.",
|
||||
"fido_enter_fido2_pin": "Enter the FIDO2 PIN for your YubiKey",
|
||||
"fido_pin": "PIN",
|
||||
"fido_pin_protection": "FIDO PIN protection",
|
||||
"fido_pin_protection_optional": "Optional FIDO PIN protection",
|
||||
"fido_no_fp_added": "No fingerprints have been added",
|
||||
"fido_unlock": "Unlock",
|
||||
"fido_unlock_first": "Unlock with PIN first",
|
||||
"fido_set_pin_first": "A PIN is required first",
|
||||
"fido_change_pin": "Change PIN",
|
||||
"fido_enter_current_pin": "Enter your current PIN. If you don't know your PIN, you'll need to reset the YubiKey.",
|
||||
"fido_current_pin": "Current PIN",
|
||||
"fido_enter_new_pin": "Enter your new PIN. A PIN must be at least {length} characters long and may contain letters, numbers and special characters.",
|
||||
"@fido_enter_new_pin" : {
|
||||
"placeholders": {
|
||||
"length": {}
|
||||
}
|
||||
},
|
||||
"fido_new_pin": "New PIN",
|
||||
"fido_confirm_pin": "Confirm PIN",
|
||||
"fido_new_pin_chars": "New PIN must be at least {length} characters",
|
||||
"@fido_new_pin_chars" : {
|
||||
"placeholders": {
|
||||
"length": {}
|
||||
}
|
||||
},
|
||||
"fido_pin_set": "PIN set",
|
||||
"fido_pin_blocked": "PIN has been blocked until the YubiKey is removed and reinserted",
|
||||
"fido_wrong_pin_retries_remaining": "Wrong PIN ({length} tries remaining)",
|
||||
"@fido_wrong_pin_retries_remaining" : {
|
||||
"placeholders": {
|
||||
"length": {}
|
||||
}
|
||||
},
|
||||
"fido_fail_set_pin": "Failed to set PIN",
|
||||
"fido_fingerprint_renamed": "Fingerprint renamed",
|
||||
"fido_error_renaming": "Error renaming",
|
||||
"fido_rename_fingerprint": "Rename fingerprint",
|
||||
"fido_rename": "Rename {label}?",
|
||||
"@fido_rename" : {
|
||||
"placeholders": {
|
||||
"label": {}
|
||||
}
|
||||
},
|
||||
"fido_will_change_label_fp": "This will change the label of the fingerprint.",
|
||||
"fido_label": "Label",
|
||||
"fido_remove_from_reader": "Remove your YubiKey from the NFC reader",
|
||||
"fido_unplug_yubikey": "Unplug your YubiKey",
|
||||
"fido_place_back_on_reader": "Place your YubiKey back on the reader",
|
||||
"fido_reinsert_yubikey": "Re-insert your YubiKey",
|
||||
"fido_touch_yubikey": "Touch your YubiKey now",
|
||||
"fido_press_reset": "Press reset to begin\u2026",
|
||||
"fido_factory_reset": "Factory reset",
|
||||
"fido_factory_reset_description": "Factory reset this application",
|
||||
"fido_fido_app_reset": "FIDO application reset",
|
||||
"fido_error_reset": "Error performing reset",
|
||||
"fido_reset": "Reset",
|
||||
"fido_warning_will_delete_accounts": "Warning! This will irrevocably delete all U2F and FIDO2 accounts from your YubiKey.",
|
||||
"fido_warning_disable_these_creds": "Your credentials, as well as any PIN set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.",
|
||||
"fido_credentials": "Credentials",
|
||||
"fido_fingerprints": "Fingerprints",
|
||||
"fido_add_one_or_more": "Add one or more (up to five) fingerprints",
|
||||
"fido_fingerprints_used": "{used}/5 fingerprints registered",
|
||||
"@fido_fingerprints_used": {
|
||||
"placeholders": {
|
||||
"used": {}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"appFailurePage_btn_unlock": "Unlock",
|
||||
"appFailurePage_txt_info": "WebAuthn management requires elevated privileges.",
|
||||
"appFailurePage_msg_permission": "Elevating permissions\u2026",
|
||||
|
||||
"mainDrawer_txt_applications": "Toggle applications",
|
||||
"mainDrawer_txt_settings": "Settings",
|
||||
"mainDrawer_txt_help": "Help and about",
|
||||
|
||||
"devicePicker_no_yubikey": "No YubiKey present",
|
||||
"devicePicker_insert_or_tap": "Insert or tap a YubiKey",
|
||||
"devicePicker_select_to_scan": "Select to scan",
|
||||
"devicePicker_inaccessible": "Device inaccessible",
|
||||
"devicePicker_unknown_type": "Unknown type",
|
||||
"devicePicker_unknown_device": "Unrecognized device",
|
||||
"devicePicker_hide_device": "Hide device",
|
||||
"devicePicker_show_hidden": "Show hidden devices",
|
||||
"devicePicker_sn": "S/N: {serial}",
|
||||
"@devicePicker_sn" : {
|
||||
"@_yubikey_selection": {},
|
||||
"s_select_yk": "Select YubiKey",
|
||||
"s_select_to_scan": "Select to scan",
|
||||
"s_hide_device": "Hide device",
|
||||
"s_show_hidden_devices": "Show hidden devices",
|
||||
"s_sn_serial": "S/N: {serial}",
|
||||
"@s_sn_serial" : {
|
||||
"placeholders": {
|
||||
"serial": {}
|
||||
}
|
||||
},
|
||||
"devicePicker_fw": "F/W: {version}",
|
||||
"@devicePicker_fw" : {
|
||||
"s_fw_version": "F/W: {version}",
|
||||
"@s_fw_version" : {
|
||||
"placeholders": {
|
||||
"version": {}
|
||||
}
|
||||
},
|
||||
|
||||
"@_yubikey_interactions": {},
|
||||
"l_insert_yk": "Insert your YubiKey",
|
||||
"l_insert_or_tap_yk": "Insert or tap a YubiKey",
|
||||
"l_unplug_yk": "Unplug your YubiKey",
|
||||
"l_reinsert_yk": "Reinsert your YubiKey",
|
||||
"l_place_on_nfc_reader": "Place your YubiKey on the NFC reader",
|
||||
"l_replace_yk_on_reader": "Place your YubiKey back on the reader",
|
||||
"l_remove_yk_from_reader": "Remove your YubiKey from the NFC reader",
|
||||
"p_try_reinsert_yk": "Try to remove and reinsert your YubiKey.",
|
||||
"s_touch_required": "Touch required",
|
||||
"l_touch_button_now": "Touch the button on your YubiKey now",
|
||||
"l_keep_touching_yk": "Keep touching your YubiKey repeatedly\u2026",
|
||||
|
||||
"systray_oath_copied": "Code copied",
|
||||
"systray_oath_copied_to_clipboard": "{label} copied to clipboard.",
|
||||
"@systray_oath_copied_to_clipboard" : {
|
||||
"@_app_configuration": {},
|
||||
"s_toggle_applications": "Toggle applications",
|
||||
"l_min_one_interface": "At least one interface must be enabled",
|
||||
"s_reconfiguring_yk": "Reconfiguring YubiKey\u2026",
|
||||
"s_config_updated": "Configuration updated",
|
||||
"l_config_updated_reinsert": "Configuration updated, remove and reinsert your YubiKey",
|
||||
"s_app_not_supported": "Application not supported",
|
||||
"l_app_not_supported_on_yk": "The used YubiKey does not support '${app}' application",
|
||||
"@l_app_not_supported_on_yk" : {
|
||||
"placeholders": {
|
||||
"app": {}
|
||||
}
|
||||
},
|
||||
"l_app_not_supported_desc": "This application is not supported",
|
||||
"s_app_disabled": "Application disabled",
|
||||
"l_app_disabled_desc": "Enable the '{app}' application on your YubiKey to access",
|
||||
"@l_app_disabled_desc" : {
|
||||
"placeholders": {
|
||||
"app": {}
|
||||
}
|
||||
},
|
||||
"s_fido_disabled": "FIDO2 disabled",
|
||||
"l_webauthn_req_fido2": "WebAuthn requires the FIDO2 application to be enabled on your YubiKey",
|
||||
|
||||
"@_connectivity_issues": {},
|
||||
"l_helper_not_responding": "The Helper process isn't responding",
|
||||
"l_yk_no_access": "This YubiKey cannot be accessed",
|
||||
"s_yk_inaccessible": "Device inaccessible",
|
||||
"l_open_connection_failed": "Failed to open connection",
|
||||
"l_ccid_connection_failed": "Failed to open smart card connection",
|
||||
"p_ccid_service_unavailable": "Make sure your smart card service is functioning.",
|
||||
"p_pcscd_unavailable": "Make sure pcscd is installed and running.",
|
||||
"l_no_yk_present": "No YubiKey present",
|
||||
"s_unknown_type": "Unknown type",
|
||||
"s_unknown_device": "Unrecognized device",
|
||||
"s_unsupported_yk": "Unsupported YubiKey",
|
||||
"s_yk_not_recognized": "Device not recognized",
|
||||
|
||||
"@_general_errors": {},
|
||||
"l_error_occured": "An error has occured",
|
||||
"s_application_error": "Application error",
|
||||
"l_import_error": "Import error",
|
||||
"l_file_not_found": "File not found",
|
||||
"l_file_too_big": "File size too big",
|
||||
"l_filesystem_error": "File system operation error",
|
||||
|
||||
"@_pins": {},
|
||||
"s_pin": "PIN",
|
||||
"s_set_pin": "Set PIN",
|
||||
"s_change_pin": "Change PIN",
|
||||
"s_current_pin": "Current PIN",
|
||||
"s_new_pin": "New PIN",
|
||||
"s_confirm_pin": "Confirm PIN",
|
||||
"l_new_pin_len": "New PIN must be at least {length} characters",
|
||||
"@l_new_pin_len" : {
|
||||
"placeholders": {
|
||||
"length": {}
|
||||
}
|
||||
},
|
||||
"s_pin_set": "PIN set",
|
||||
"l_set_pin_failed": "Failed to set PIN: {message}",
|
||||
"@l_set_pin_failed" : {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"l_wrong_pin_attempts_remaining": "Wrong PIN, {retries} attempt(s) remaining",
|
||||
"@l_wrong_pin_attempts_remaining" : {
|
||||
"placeholders": {
|
||||
"retries": {}
|
||||
}
|
||||
},
|
||||
"s_fido_pin_protection": "FIDO PIN protection",
|
||||
"l_fido_pin_protection_optional": "Optional FIDO PIN protection",
|
||||
"l_enter_fido2_pin": "Enter the FIDO2 PIN for your YubiKey",
|
||||
"l_optionally_set_a_pin": "Optionally set a PIN to protect access to your YubiKey\nRegister as a Security Key on websites",
|
||||
"l_pin_blocked_reset": "PIN is blocked; factory reset the FIDO application",
|
||||
"l_set_pin_first": "A PIN is required first",
|
||||
"l_unlock_pin_first": "Unlock with PIN first",
|
||||
"l_pin_soft_locked": "PIN has been blocked until the YubiKey is removed and reinserted",
|
||||
"p_enter_current_pin_or_reset": "Enter your current PIN. If you don't know your PIN, you'll need to reset the YubiKey.",
|
||||
"p_enter_new_fido2_pin": "Enter your new PIN. A PIN must be at least {length} characters long and may contain letters, numbers and special characters.",
|
||||
"@p_enter_new_fido2_pin" : {
|
||||
"placeholders": {
|
||||
"length": {}
|
||||
}
|
||||
},
|
||||
|
||||
"@_passwords": {},
|
||||
"s_password": "Password",
|
||||
"s_manage_password": "Manage password",
|
||||
"s_set_password": "Set password",
|
||||
"s_password_set": "Password set",
|
||||
"l_optional_password_protection": "Optional password protection",
|
||||
"s_new_password": "New password",
|
||||
"s_current_password": "Current password",
|
||||
"s_confirm_password": "Confirm password",
|
||||
"s_wrong_password": "Wrong password",
|
||||
"s_remove_password": "Remove password",
|
||||
"s_password_removed": "Password removed",
|
||||
"s_remember_password": "Remember password",
|
||||
"s_clear_saved_password": "Clear saved password",
|
||||
"s_password_forgotten": "Password forgotten",
|
||||
"l_keystore_unavailable": "OS Keystore unavailable",
|
||||
"l_remember_pw_failed": "Failed to remember password",
|
||||
"l_unlock_first": "Unlock with password first",
|
||||
"l_enter_oath_pw": "Enter the OATH password for your YubiKey",
|
||||
"p_enter_current_password_or_reset": "Enter your current password. If you don't know your password, you'll need to reset the YubiKey.",
|
||||
"p_enter_new_password": "Enter your new password. A password may contain letters, numbers and special characters.",
|
||||
|
||||
"@_oath_accounts": {},
|
||||
"l_account": "Account: {label}",
|
||||
"@l_account" : {
|
||||
"placeholders": {
|
||||
"label": {}
|
||||
}
|
||||
},
|
||||
"systray_no_pinned": "No pinned accounts"
|
||||
"s_accounts": "Accounts",
|
||||
"s_no_accounts": "No accounts",
|
||||
"s_add_account": "Add account",
|
||||
"s_account_added": "Account added",
|
||||
"l_account_add_failed": "Failed adding account: {message}",
|
||||
"@l_account_add_failed" : {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"l_account_name_required": "Your account must have a name",
|
||||
"l_name_already_exists": "This name already exists for the issuer",
|
||||
"l_invalid_character_issuer": "Invalid character: ':' is not allowed in issuer",
|
||||
"s_pinned": "Pinned",
|
||||
"s_pin_account": "Pin account",
|
||||
"s_unpin_account": "Unpin account",
|
||||
"s_no_pinned_accounts": "No pinned accounts",
|
||||
"s_rename_account": "Rename account",
|
||||
"s_account_renamed": "Account renamed",
|
||||
"p_rename_will_change_account_displayed": "This will change how the account is displayed in the list.",
|
||||
"s_delete_account": "Delete account",
|
||||
"s_account_deleted": "Account deleted",
|
||||
"p_warning_delete_account": "Warning! This action will delete the account from your YubiKey.",
|
||||
"p_warning_disable_credential": "You will no longer be able to generate OTPs for this account. Make sure to first disable this credential from the website to avoid being locked out of your account.",
|
||||
"s_account_name": "Account name",
|
||||
"s_search_accounts": "Search accounts",
|
||||
"l_accounts_used": "{used} of {capacity} accounts used",
|
||||
"@l_accounts_used" : {
|
||||
"placeholders": {
|
||||
"used": {},
|
||||
"capacity": {}
|
||||
}
|
||||
},
|
||||
"s_num_digits": "{num} digits",
|
||||
"@s_num_digits" : {
|
||||
"placeholders": {
|
||||
"num": {}
|
||||
}
|
||||
},
|
||||
"s_num_sec": "{num} sec",
|
||||
"@s_num_sec" : {
|
||||
"placeholders": {
|
||||
"num": {}
|
||||
}
|
||||
},
|
||||
"s_issuer_optional": "Issuer (optional)",
|
||||
"s_counter_based": "Counter based",
|
||||
"s_time_based": "Time based",
|
||||
|
||||
"@_fido_credentials": {},
|
||||
"l_credential": "Credential: {label}",
|
||||
"@l_credential" : {
|
||||
"placeholders": {
|
||||
"label": {}
|
||||
}
|
||||
},
|
||||
"s_credentials": "Credentials",
|
||||
"l_ready_to_use": "Ready to use",
|
||||
"l_register_sk_on_websites": "Register as a Security Key on websites",
|
||||
"l_no_discoverable_accounts": "No discoverable accounts",
|
||||
"s_delete_credential": "Delete credential",
|
||||
"s_credential_deleted": "Credential deleted",
|
||||
"p_warning_delete_credential": "This will delete the credential from your YubiKey.",
|
||||
|
||||
"@_fingerprints": {},
|
||||
"l_fingerprint": "Fingerprint: {label}",
|
||||
"@l_fingerprint" : {
|
||||
"placeholders": {
|
||||
"label": {}
|
||||
}
|
||||
},
|
||||
"s_fingerprints": "Fingerprints",
|
||||
"l_fingerprint_captured": "Fingerprint captured successfully!",
|
||||
"s_fingerprint_added": "Fingerprint added",
|
||||
"l_setting_name_failed": "Error setting name: {message}",
|
||||
"@l_setting_name_failed" : {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"s_add_fingerprint": "Add fingerprint",
|
||||
"l_fp_step_1_capture": "Step 1/2: Capture fingerprint",
|
||||
"l_fp_step_2_name": "Step 2/2: Name fingerprint",
|
||||
"s_delete_fingerprint": "Delete fingerprint",
|
||||
"s_fingerprint_deleted": "Fingerprint deleted",
|
||||
"p_warning_delete_fingerprint": "This will delete the fingerprint from your YubiKey.",
|
||||
"s_no_fingerprints": "No fingerprints",
|
||||
"l_set_pin_fingerprints": "Set a PIN to register fingerprints",
|
||||
"l_no_fps_added": "No fingerprints have been added",
|
||||
"s_rename_fp": "Rename fingerprint",
|
||||
"s_fingerprint_renamed": "Fingerprint renamed",
|
||||
"l_rename_fp_failed": "Error renaming: {message}",
|
||||
"@l_rename_fp_failed" : {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"l_add_one_or_more_fps": "Add one or more (up to five) fingerprints",
|
||||
"l_fingerprints_used": "{used}/5 fingerprints registered",
|
||||
"@l_fingerprints_used": {
|
||||
"placeholders": {
|
||||
"used": {}
|
||||
}
|
||||
},
|
||||
"p_press_fingerprint_begin": "Press your finger against the YubiKey to begin.",
|
||||
"p_will_change_label_fp": "This will change the label of the fingerprint.",
|
||||
|
||||
"@_permissions": {},
|
||||
"s_enable_nfc": "Enable NFC",
|
||||
"s_permission_denied": "Permission denied",
|
||||
"l_elevating_permissions": "Elevating permissions\u2026",
|
||||
"s_review_permissions": "Review permissions",
|
||||
"p_elevated_permissions_required": "Managing this device requires elevated privileges.",
|
||||
"p_webauthn_elevated_permissions_required": "WebAuthn management requires elevated privileges.",
|
||||
"p_need_camera_permission": "Yubico Authenticator needs Camera permissions for scanning QR codes.",
|
||||
|
||||
"@_qr_codes": {},
|
||||
"s_qr_scan": "Scan QR code",
|
||||
"l_qr_scanned": "Scanned QR code",
|
||||
"l_invalid_qr": "Invalid QR code",
|
||||
"l_qr_not_found": "No QR code found",
|
||||
"l_qr_not_read": "Failed reading QR code: {message}",
|
||||
"@l_qr_not_read" : {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"l_point_camera_scan": "Point your camera at a QR code to scan it",
|
||||
"q_want_to_scan": "Would like to scan?",
|
||||
"q_no_qr": "No QR code?",
|
||||
"s_enter_manually": "Enter manually",
|
||||
|
||||
"@_factory_reset": {},
|
||||
"s_reset": "Reset",
|
||||
"s_factory_reset": "Factory reset",
|
||||
"l_factory_reset_this_app": "Factory reset this application",
|
||||
"s_reset_oath": "Reset OATH",
|
||||
"l_oath_application_reset": "OATH application reset",
|
||||
"s_reset_fido": "Reset FIDO",
|
||||
"l_fido_app_reset": "FIDO application reset",
|
||||
"l_press_reset_to_begin": "Press reset to begin\u2026",
|
||||
"l_reset_failed": "Error performing reset: {message}",
|
||||
"@l_reset_failed" : {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"p_warning_factory_reset": "Warning! This will irrevocably delete all OATH TOTP/HOTP accounts from your YubiKey.",
|
||||
"p_warning_disable_credentials": "Your OATH credentials, as well as any password set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.",
|
||||
"p_warning_deletes_accounts": "Warning! This will irrevocably delete all U2F and FIDO2 accounts from your YubiKey.",
|
||||
"p_warning_disable_accounts": "Your credentials, as well as any PIN set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.",
|
||||
|
||||
"@_copy_to_clipboard": {},
|
||||
"l_copy_to_clipboard": "Copy to clipboard",
|
||||
"s_code_copied": "Code copied",
|
||||
"l_code_copied_clipboard": "Code copied to clipboard",
|
||||
"s_copy_log": "Copy log",
|
||||
"l_log_copied": "Log copied to clipboard",
|
||||
"l_diagnostics_copied": "Diagnostic data copied to clipboard",
|
||||
"p_target_copied_clipboard": "{label} copied to clipboard.",
|
||||
"@p_target_copied_clipboard" : {
|
||||
"placeholders": {
|
||||
"label": {}
|
||||
}
|
||||
},
|
||||
|
||||
"@_custom_icons": {},
|
||||
"s_custom_icons": "Custom icons",
|
||||
"l_set_icons_for_accounts": "Set icons for accounts",
|
||||
"p_custom_icons_description": "Icon packs can make your accounts more easily distinguishable with familiar logos and colors.",
|
||||
"s_replace_icon_pack": "Replace icon pack",
|
||||
"l_loading_icon_pack": "Loading icon pack\u2026",
|
||||
"s_load_icon_pack": "Load icon pack",
|
||||
"s_remove_icon_pack": "Remove icon pack",
|
||||
"l_icon_pack_removed": "Icon pack removed",
|
||||
"l_remove_icon_pack_failed": "Error removing icon pack",
|
||||
"s_choose_icon_pack": "Choose icon pack",
|
||||
"l_icon_pack_imported": "Icon pack imported",
|
||||
"l_import_icon_pack_failed": "Error importing icon pack: {message}",
|
||||
"@l_import_icon_pack_failed": {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"l_invalid_icon_pack": "Invalid icon pack",
|
||||
|
||||
"@_android_settings": {},
|
||||
"s_nfc_options": "NFC options",
|
||||
"l_on_yk_nfc_tap": "On YubiKey NFC tap",
|
||||
"l_launch_ya": "Launch Yubico Authenticator",
|
||||
"l_copy_otp_clipboard": "Copy OTP to clipboard",
|
||||
"l_launch_and_copy_otp": "Launch app and copy OTP",
|
||||
"l_kbd_layout_for_static": "Keyboard layout (for static password)",
|
||||
"s_choose_kbd_layout": "Choose keyboard layout",
|
||||
"l_bypass_touch_requirement": "Bypass touch requirement",
|
||||
"l_bypass_touch_requirement_on": "Accounts that require touch are automatically shown over NFC",
|
||||
"l_bypass_touch_requirement_off": "Accounts that require touch need an additional tap over NFC",
|
||||
"s_silence_nfc_sounds": "Silence NFC sounds",
|
||||
"l_silence_nfc_sounds_on": "No sounds will be played on NFC tap",
|
||||
"l_silence_nfc_sounds_off": "Sound will play on NFC tap",
|
||||
"s_usb_options": "USB options",
|
||||
"l_launch_app_on_usb": "Launch when YubiKey is connected",
|
||||
"l_launch_app_on_usb_on": "This prevents other apps from using the YubiKey over USB",
|
||||
"l_launch_app_on_usb_off": "Other apps can use the YubiKey over USB",
|
||||
"s_allow_screenshots": "Allow screenshots",
|
||||
|
||||
"@_eof": {}
|
||||
}
|
@ -87,7 +87,7 @@ class _ModeForm extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(interfaces == 0
|
||||
? AppLocalizations.of(context)!.mgmt_min_one_interface
|
||||
? AppLocalizations.of(context)!.l_min_one_interface
|
||||
: ''),
|
||||
]);
|
||||
}
|
||||
@ -106,6 +106,7 @@ class _CapabilitiesForm extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final usbCapabilities = supported[Transport.usb] ?? 0;
|
||||
final nfcCapabilities = supported[Transport.nfc] ?? 0;
|
||||
|
||||
@ -113,10 +114,10 @@ class _CapabilitiesForm extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (usbCapabilities != 0) ...[
|
||||
const ListTile(
|
||||
leading: Icon(Icons.usb),
|
||||
title: Text('USB'),
|
||||
contentPadding: EdgeInsets.only(bottom: 8),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.usb),
|
||||
title: Text(l10n.s_usb),
|
||||
contentPadding: const EdgeInsets.only(bottom: 8),
|
||||
horizontalTitleGap: 0,
|
||||
),
|
||||
_CapabilityForm(
|
||||
@ -136,7 +137,7 @@ class _CapabilitiesForm extends StatelessWidget {
|
||||
),
|
||||
ListTile(
|
||||
leading: nfcIcon,
|
||||
title: const Text('NFC'),
|
||||
title: Text(l10n.s_nfc),
|
||||
contentPadding: const EdgeInsets.only(bottom: 8),
|
||||
horizontalTitleGap: 0,
|
||||
),
|
||||
@ -191,6 +192,7 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
}
|
||||
|
||||
void _submitCapabilitiesForm() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final bool reboot;
|
||||
if (widget.deviceData.node is UsbYubiKeyNode) {
|
||||
// Reboot if USB device descriptor is changed.
|
||||
@ -210,7 +212,7 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
// This will take longer, show a message
|
||||
close = showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!.mgmt_reconfiguring_yubikey,
|
||||
l10n.s_reconfiguring_yk,
|
||||
duration: const Duration(seconds: 8),
|
||||
);
|
||||
}
|
||||
@ -223,8 +225,7 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (!reboot) Navigator.pop(context);
|
||||
showMessage(
|
||||
context, AppLocalizations.of(context)!.mgmt_configuration_updated);
|
||||
showMessage(context, l10n.s_config_updated);
|
||||
} finally {
|
||||
close?.call();
|
||||
}
|
||||
@ -241,6 +242,7 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
);
|
||||
|
||||
void _submitModeForm() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
await ref
|
||||
.read(managementStateProvider(widget.deviceData.node.path).notifier)
|
||||
.setMode(interfaces: _interfaces);
|
||||
@ -248,10 +250,8 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
showMessage(
|
||||
context,
|
||||
widget.deviceData.node.maybeMap(
|
||||
nfcReader: (_) =>
|
||||
AppLocalizations.of(context)!.mgmt_configuration_updated,
|
||||
orElse: () => AppLocalizations.of(context)!
|
||||
.mgmt_configuration_updated_remove_reinsert));
|
||||
nfcReader: (_) => l10n.s_config_updated,
|
||||
orElse: () => l10n.l_config_updated_reinsert));
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@ -265,6 +265,7 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
var canSave = false;
|
||||
final child = ref
|
||||
.watch(managementStateProvider(widget.deviceData.node.path))
|
||||
@ -317,12 +318,12 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(AppLocalizations.of(context)!.mgmt_toggle_applications),
|
||||
title: Text(l10n.s_toggle_applications),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: canSave ? _submitForm : null,
|
||||
key: management_keys.saveButtonKey,
|
||||
child: Text(AppLocalizations.of(context)!.mgmt_save),
|
||||
child: Text(l10n.s_save),
|
||||
),
|
||||
],
|
||||
child: child,
|
||||
|
@ -34,7 +34,7 @@ class IconPackDialog extends ConsumerWidget {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final iconPack = ref.watch(iconPackProvider);
|
||||
return ResponsiveDialog(
|
||||
title: Text(l10n.oath_custom_icons),
|
||||
title: Text(l10n.s_custom_icons),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Column(
|
||||
@ -65,13 +65,12 @@ class IconPackDialog extends ConsumerWidget {
|
||||
|
||||
Widget? _action(AsyncValue<IconPack?> iconPack, AppLocalizations l10n) =>
|
||||
iconPack.when(
|
||||
data: (IconPack? data) => _ImportActionChip(data != null
|
||||
? l10n.oath_custom_icons_replace
|
||||
: l10n.oath_custom_icons_load),
|
||||
data: (IconPack? data) => _ImportActionChip(
|
||||
data != null ? l10n.s_replace_icon_pack : l10n.s_load_icon_pack),
|
||||
error: (Object error, StackTrace stackTrace) =>
|
||||
_ImportActionChip(l10n.oath_custom_icons_load),
|
||||
_ImportActionChip(l10n.s_load_icon_pack),
|
||||
loading: () => _ImportActionChip(
|
||||
l10n.oath_custom_icons_loading,
|
||||
l10n.l_loading_icon_pack,
|
||||
avatar: const CircularProgressIndicator(),
|
||||
));
|
||||
}
|
||||
@ -83,7 +82,7 @@ class _DialogDescription extends ConsumerWidget {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
text: l10n.oath_custom_icons_description,
|
||||
text: l10n.p_custom_icons_description,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
children: [const TextSpan(text: ' '), _createLearnMoreLink(context)],
|
||||
),
|
||||
@ -97,7 +96,7 @@ class _DialogDescription extends ConsumerWidget {
|
||||
TextSpan _createLearnMoreLink(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return TextSpan(
|
||||
text: AppLocalizations.of(context)!.oath_custom_icons_learn_more,
|
||||
text: AppLocalizations.of(context)!.s_learn_more,
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(color: theme.colorScheme.primary),
|
||||
recognizer: TapGestureRecognizer()
|
||||
@ -135,18 +134,16 @@ class _IconPackDescription extends ConsumerWidget {
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: l10n.oath_custom_icons_remove,
|
||||
tooltip: l10n.s_remove_icon_pack,
|
||||
onPressed: () async {
|
||||
final removePackStatus =
|
||||
await ref.read(iconPackProvider.notifier).removePack();
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
if (removePackStatus) {
|
||||
showMessage(context,
|
||||
l10n.oath_custom_icons_icon_pack_removed);
|
||||
showMessage(context, l10n.l_icon_pack_removed);
|
||||
} else {
|
||||
showMessage(context,
|
||||
l10n.oath_custom_icons_err_icon_pack_remove);
|
||||
showMessage(context, l10n.l_remove_icon_pack_failed);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -182,20 +179,20 @@ class _ImportActionChip extends ConsumerWidget {
|
||||
type: FileType.custom,
|
||||
allowMultiple: false,
|
||||
lockParentWindow: true,
|
||||
dialogTitle: l10n.oath_custom_icons_choose_icon_pack);
|
||||
dialogTitle: l10n.s_choose_icon_pack);
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
final importStatus = await ref
|
||||
.read(iconPackProvider.notifier)
|
||||
.importPack(l10n, result.paths.first!);
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
if (importStatus) {
|
||||
showMessage(context, l10n.oath_custom_icons_icon_pack_imported);
|
||||
showMessage(context, l10n.l_icon_pack_imported);
|
||||
} else {
|
||||
showMessage(
|
||||
context,
|
||||
l10n.oath_custom_icons_err_icon_pack_import(
|
||||
l10n.l_import_icon_pack_failed(
|
||||
ref.read(iconPackProvider.notifier).lastError ??
|
||||
l10n.oath_custom_icons_err_import_general));
|
||||
l10n.l_import_error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -95,14 +95,14 @@ class IconPackManager extends StateNotifier<AsyncValue<IconPack?>> {
|
||||
|
||||
if (!await packFile.exists()) {
|
||||
_log.error('Input file does not exist');
|
||||
_lastError = l10n.oath_custom_icons_err_file_not_found;
|
||||
_lastError = l10n.l_file_not_found;
|
||||
state = AsyncValue.error('Input file does not exist', StackTrace.current);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await packFile.length() > 5 * 1024 * 1024) {
|
||||
_log.error('File size too big.');
|
||||
_lastError = l10n.oath_custom_icons_err_file_too_big;
|
||||
_lastError = l10n.l_file_too_big;
|
||||
state = AsyncValue.error('File size too big', StackTrace.current);
|
||||
return false;
|
||||
}
|
||||
@ -133,7 +133,7 @@ class IconPackManager extends StateNotifier<AsyncValue<IconPack?>> {
|
||||
File(join(unpackDirectory.path, getLocalIconFileName('pack.json')));
|
||||
if (!await packJsonFile.exists()) {
|
||||
_log.error('File is not an icon pack: missing pack.json');
|
||||
_lastError = l10n.oath_custom_icons_err_invalid_icon_pack;
|
||||
_lastError = l10n.l_invalid_icon_pack;
|
||||
state = AsyncValue.error('File is not an icon pack', StackTrace.current);
|
||||
await _deleteDirectory(tempDirectory);
|
||||
return false;
|
||||
@ -145,7 +145,7 @@ class IconPackManager extends StateNotifier<AsyncValue<IconPack?>> {
|
||||
const JsonDecoder().convert(packContent);
|
||||
} catch (e) {
|
||||
_log.error('Failed to parse pack.json: $e');
|
||||
_lastError = l10n.oath_custom_icons_err_invalid_icon_pack;
|
||||
_lastError = l10n.l_invalid_icon_pack;
|
||||
state = AsyncValue.error('File is not an icon pack', StackTrace.current);
|
||||
await _deleteDirectory(tempDirectory);
|
||||
return false;
|
||||
@ -155,7 +155,7 @@ class IconPackManager extends StateNotifier<AsyncValue<IconPack?>> {
|
||||
final packDirectory = await _packDirectory;
|
||||
if (!await _deleteDirectory(packDirectory)) {
|
||||
_log.error('Failure when deleting original pack directory');
|
||||
_lastError = l10n.oath_custom_icons_err_filesystem_error;
|
||||
_lastError = l10n.l_filesystem_error;
|
||||
state = AsyncValue.error(
|
||||
'Failure deleting original pack directory', StackTrace.current);
|
||||
await _deleteDirectory(tempDirectory);
|
||||
|
@ -18,7 +18,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
const _prefix = 'oath.keys';
|
||||
|
||||
const setOrManagePasswordAction = Key('$_prefix.set_or_manage_oath_password');
|
||||
const setOrManagePasswordAction = Key('$_prefix.set_or_manage_password');
|
||||
const addAccountAction = Key('$_prefix.add_account');
|
||||
const resetAction = Key('$_prefix.reset');
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../core/models.dart';
|
||||
|
||||
@ -41,12 +42,20 @@ enum HashAlgorithm {
|
||||
|
||||
enum OathType {
|
||||
@JsonValue(0x10)
|
||||
hotp('Counter based'),
|
||||
hotp,
|
||||
@JsonValue(0x20)
|
||||
totp('Time based');
|
||||
totp;
|
||||
|
||||
final String displayName;
|
||||
const OathType(this.displayName);
|
||||
const OathType();
|
||||
|
||||
String getDisplayName(AppLocalizations l10n) {
|
||||
switch (this) {
|
||||
case OathType.hotp:
|
||||
return l10n.s_counter_based;
|
||||
case OathType.totp:
|
||||
return l10n.s_time_based;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum KeystoreState { unknown, allowed, failed }
|
||||
|
@ -18,6 +18,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../../app/message.dart';
|
||||
import '../../app/shortcuts.dart';
|
||||
@ -37,13 +38,15 @@ class AccountDialog extends ConsumerWidget {
|
||||
const AccountDialog(this.credential, {super.key});
|
||||
|
||||
List<Widget> _buildActions(BuildContext context, AccountHelper helper) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final actions = helper.buildActions();
|
||||
|
||||
final theme =
|
||||
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
|
||||
|
||||
final copy = actions.firstWhere(((e) => e.text.startsWith('Copy')));
|
||||
final delete = actions.firstWhere(((e) => e.text.startsWith('Delete')));
|
||||
final copy =
|
||||
actions.firstWhere(((e) => e.text == l10n.l_copy_to_clipboard));
|
||||
final delete = actions.firstWhere(((e) => e.text == l10n.s_delete_account));
|
||||
final colors = {
|
||||
copy: Pair(theme.primary, theme.onPrimary),
|
||||
delete: Pair(theme.error, theme.onError),
|
||||
@ -51,7 +54,7 @@ class AccountDialog extends ConsumerWidget {
|
||||
|
||||
// If we can't copy, but can calculate, highlight that button instead
|
||||
if (copy.intent == null) {
|
||||
final calculates = actions.where(((e) => e.text.startsWith('Calculate')));
|
||||
final calculates = actions.where(((e) => e.text == l10n.s_calculate));
|
||||
if (calculates.isNotEmpty) {
|
||||
colors[calculates.first] = Pair(theme.primary, theme.onPrimary);
|
||||
}
|
||||
|
@ -62,25 +62,23 @@ class AccountHelper {
|
||||
final ready = expired || credential.oathType == OathType.hotp;
|
||||
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';
|
||||
return [
|
||||
MenuAction(
|
||||
text: appLocalizations.oath_copy_to_clipboard,
|
||||
text: l10n.l_copy_to_clipboard,
|
||||
icon: const Icon(Icons.copy),
|
||||
intent: code == null || expired ? null : const CopyIntent(),
|
||||
trailing: shortcut,
|
||||
),
|
||||
if (manual)
|
||||
MenuAction(
|
||||
text: appLocalizations.oath_calculate,
|
||||
text: l10n.s_calculate,
|
||||
icon: const Icon(Icons.refresh),
|
||||
intent: ready ? const CalculateIntent() : null,
|
||||
),
|
||||
MenuAction(
|
||||
text: pinned
|
||||
? appLocalizations.oath_unpin_account
|
||||
: appLocalizations.oath_pin_account,
|
||||
text: pinned ? l10n.s_unpin_account : l10n.s_pin_account,
|
||||
icon: pinned
|
||||
? pushPinStrokeIcon
|
||||
: const Icon(Icons.push_pin_outlined),
|
||||
@ -89,11 +87,11 @@ class AccountHelper {
|
||||
if (data.info.version.isAtLeast(5, 3))
|
||||
MenuAction(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
text: appLocalizations.oath_rename_account,
|
||||
text: l10n.s_rename_account,
|
||||
intent: const EditIntent(),
|
||||
),
|
||||
MenuAction(
|
||||
text: appLocalizations.oath_delete_account,
|
||||
text: l10n.s_delete_account,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
intent: const DeleteIntent(),
|
||||
),
|
||||
|
@ -29,11 +29,12 @@ class AccountList extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final credentials = ref.watch(filteredCredentialsProvider(accounts));
|
||||
final favorites = ref.watch(favoritesProvider);
|
||||
if (credentials.isEmpty) {
|
||||
return Center(
|
||||
child: Text(AppLocalizations.of(context)!.oath_no_credentials),
|
||||
child: Text(l10n.s_no_accounts),
|
||||
);
|
||||
}
|
||||
|
||||
@ -46,15 +47,13 @@ class AccountList extends ConsumerWidget {
|
||||
policy: WidgetOrderTraversalPolicy(),
|
||||
child: Column(
|
||||
children: [
|
||||
if (pinnedCreds.isNotEmpty)
|
||||
ListTitle(AppLocalizations.of(context)!.oath_pinned),
|
||||
if (pinnedCreds.isNotEmpty) ListTitle(l10n.s_pinned),
|
||||
...pinnedCreds.map(
|
||||
(entry) => AccountView(
|
||||
entry.credential,
|
||||
),
|
||||
),
|
||||
if (creds.isNotEmpty)
|
||||
ListTitle(AppLocalizations.of(context)!.oath_accounts),
|
||||
if (creds.isNotEmpty) ListTitle(l10n.s_accounts),
|
||||
...creds.map(
|
||||
(entry) => AccountView(
|
||||
entry.credential,
|
||||
|
@ -185,7 +185,6 @@ class _AccountViewState extends ConsumerState<AccountView> {
|
||||
setState(() {
|
||||
_lastTap = 0;
|
||||
});
|
||||
//triggerCopy();
|
||||
Actions.maybeInvoke(context, const CopyIntent());
|
||||
} else {
|
||||
_focusNode.requestFocus();
|
||||
|
@ -53,7 +53,7 @@ Widget registerOathActions(
|
||||
if (!clipboard.platformGivesFeedback()) {
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
showMessage(context,
|
||||
AppLocalizations.of(context)!.oath_copied_to_clipboard);
|
||||
AppLocalizations.of(context)!.l_code_copied_clipboard);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
}
|
||||
|
||||
_scanQrCode(QrScanner qrScanner) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
try {
|
||||
setState(() {
|
||||
// If we have a previous scan result stored, clear it
|
||||
@ -125,7 +126,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
final otpauth = await qrScanner.scanQr();
|
||||
if (otpauth == null) {
|
||||
if (!mounted) return;
|
||||
showMessage(context, AppLocalizations.of(context)!.oath_no_qr_code);
|
||||
showMessage(context, l10n.l_qr_not_found);
|
||||
setState(() {
|
||||
_qrState = _QrScanState.failed;
|
||||
});
|
||||
@ -145,7 +146,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
if (e is! CancellationException) {
|
||||
showMessage(
|
||||
context,
|
||||
'${AppLocalizations.of(context)!.oath_failed_reading_qr}: $errorMessage',
|
||||
l10n.l_qr_not_read(errorMessage),
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
}
|
||||
@ -173,6 +174,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
|
||||
Future<void> _doAddCredential(
|
||||
{DevicePath? devicePath, required Uri credUri}) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
try {
|
||||
if (devicePath == null) {
|
||||
assert(Platform.isAndroid, 'devicePath is only optional for Android');
|
||||
@ -186,8 +188,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
}
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
showMessage(
|
||||
context, AppLocalizations.of(context)!.oath_success_add_account);
|
||||
showMessage(context, l10n.s_account_added);
|
||||
} on CancellationException catch (_) {
|
||||
// ignored
|
||||
} catch (e) {
|
||||
@ -203,7 +204,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
}
|
||||
showMessage(
|
||||
context,
|
||||
'${AppLocalizations.of(context)!.oath_fail_add_account}: $errorMessage',
|
||||
l10n.l_account_add_failed(errorMessage),
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
}
|
||||
@ -211,6 +212,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final deviceNode = ref.watch(currentDeviceProvider);
|
||||
if (widget.devicePath != null && widget.devicePath != deviceNode?.path) {
|
||||
// If the dialog was started for a specific device and it was
|
||||
@ -233,7 +235,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
}
|
||||
|
||||
final otpauthUri = _otpauthUri;
|
||||
_promptController?.updateContent(title: 'Insert YubiKey');
|
||||
_promptController?.updateContent(title: l10n.l_insert_yk);
|
||||
if (otpauthUri != null && deviceNode != null) {
|
||||
final deviceData = ref.watch(currentDeviceDataProvider);
|
||||
deviceData.when(data: (data) {
|
||||
@ -242,7 +244,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
0) !=
|
||||
0) {
|
||||
if (oathState == null) {
|
||||
_promptController?.updateContent(title: 'Please wait...');
|
||||
_promptController?.updateContent(title: l10n.s_please_wait);
|
||||
} else if (oathState.locked) {
|
||||
_promptController?.close();
|
||||
} else {
|
||||
@ -254,12 +256,12 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
));
|
||||
}
|
||||
} else {
|
||||
_promptController?.updateContent(title: 'Unsupported YubiKey');
|
||||
_promptController?.updateContent(title: l10n.s_unsupported_yk);
|
||||
}
|
||||
}, error: (error, _) {
|
||||
_promptController?.updateContent(title: 'Unsupported YubiKey');
|
||||
_promptController?.updateContent(title: l10n.s_unsupported_yk);
|
||||
}, loading: () {
|
||||
_promptController?.updateContent(title: 'Please wait...');
|
||||
_promptController?.updateContent(title: l10n.s_please_wait);
|
||||
});
|
||||
}
|
||||
|
||||
@ -340,8 +342,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
_otpauthUri = cred.toUri();
|
||||
_promptController = promptUserInteraction(
|
||||
context,
|
||||
title: 'Insert YubiKey',
|
||||
description: 'Add account',
|
||||
title: l10n.l_insert_yk,
|
||||
description: l10n.s_add_account,
|
||||
icon: const Icon(Icons.usb),
|
||||
onCancel: () {
|
||||
_otpauthUri = null;
|
||||
@ -356,12 +358,11 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
}
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(AppLocalizations.of(context)!.oath_add_account),
|
||||
title: Text(l10n.s_add_account),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isValid ? submit : null,
|
||||
child: Text(AppLocalizations.of(context)!.oath_save,
|
||||
key: keys.saveButton),
|
||||
child: Text(l10n.s_save, key: keys.saveButton),
|
||||
),
|
||||
],
|
||||
child: FileDropTarget(
|
||||
@ -371,8 +372,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
final otpauth = await qrScanner.scanQr(b64Image);
|
||||
if (otpauth == null) {
|
||||
if (!mounted) return;
|
||||
showMessage(
|
||||
context, AppLocalizations.of(context)!.oath_no_qr_code);
|
||||
showMessage(context, l10n.l_qr_not_found);
|
||||
} else {
|
||||
final data = CredentialData.fromUri(Uri.parse(otpauth));
|
||||
_loadCredentialData(data);
|
||||
@ -402,8 +402,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
buildCounter: buildByteCounterFor(issuerText),
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText:
|
||||
AppLocalizations.of(context)!.oath_issuer_optional,
|
||||
labelText: l10n.s_issuer_optional,
|
||||
helperText:
|
||||
'', // Prevents dialog resizing when disabled
|
||||
prefixIcon: const Icon(Icons.business_outlined),
|
||||
@ -411,8 +410,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
? '' // needs empty string to render as error
|
||||
: issuerNoColon
|
||||
? null
|
||||
: AppLocalizations.of(context)!
|
||||
.oath_invalid_character_issuer,
|
||||
: l10n.l_invalid_character_issuer,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
@ -433,16 +431,14 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
labelText:
|
||||
AppLocalizations.of(context)!.oath_account_name,
|
||||
labelText: l10n.s_account_name,
|
||||
helperText:
|
||||
'', // Prevents dialog resizing when disabled
|
||||
errorText: (byteLength(nameText) > nameMaxLength)
|
||||
? '' // needs empty string to render as error
|
||||
: isUnique
|
||||
? null
|
||||
: AppLocalizations.of(context)!
|
||||
.oath_duplicate_name,
|
||||
: l10n.l_name_already_exists,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
@ -478,11 +474,9 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.key_outlined),
|
||||
labelText:
|
||||
AppLocalizations.of(context)!.oath_secret_key,
|
||||
labelText: l10n.s_secret_key,
|
||||
errorText: _validateSecretLength && !secretLengthValid
|
||||
? AppLocalizations.of(context)!
|
||||
.oath_invalid_length
|
||||
? l10n.s_invalid_length
|
||||
: null),
|
||||
readOnly: _qrState == _QrScanState.success,
|
||||
textInputAction: TextInputAction.done,
|
||||
@ -507,10 +501,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
: const CircularProgressIndicator(
|
||||
strokeWidth: 2.0),
|
||||
label: _qrState == _QrScanState.success
|
||||
? Text(AppLocalizations.of(context)!
|
||||
.oath_scanned_qr)
|
||||
: Text(
|
||||
AppLocalizations.of(context)!.oath_scan_qr),
|
||||
? Text(l10n.l_qr_scanned)
|
||||
: Text(l10n.s_qr_scan),
|
||||
onPressed: () {
|
||||
_scanQrCode(qrScanner);
|
||||
}),
|
||||
@ -523,8 +515,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
children: [
|
||||
if (oathState?.version.isAtLeast(4, 2) ?? true)
|
||||
FilterChip(
|
||||
label: Text(AppLocalizations.of(context)!
|
||||
.oath_require_touch),
|
||||
label: Text(l10n.s_require_touch),
|
||||
selected: _touch,
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
@ -536,7 +527,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
items: OathType.values,
|
||||
value: _oathType,
|
||||
selected: _oathType != defaultOathType,
|
||||
itemBuilder: (value) => Text(value.displayName),
|
||||
itemBuilder: (value) =>
|
||||
Text(value.getDisplayName(l10n)),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (value) {
|
||||
setState(() {
|
||||
@ -565,8 +557,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
defaultPeriod,
|
||||
selected: int.tryParse(_periodController.text) !=
|
||||
defaultPeriod,
|
||||
itemBuilder: ((value) => Text(
|
||||
'$value ${AppLocalizations.of(context)!.oath_sec}')),
|
||||
itemBuilder: ((value) =>
|
||||
Text(l10n.s_num_sec(value))),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (period) {
|
||||
setState(() {
|
||||
@ -579,8 +571,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
items: _digitsValues,
|
||||
value: _digits,
|
||||
selected: _digits != defaultDigits,
|
||||
itemBuilder: (value) => Text(
|
||||
'$value ${AppLocalizations.of(context)!.oath_digits}'),
|
||||
itemBuilder: (value) =>
|
||||
Text(l10n.s_num_digits(value)),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (digits) {
|
||||
setState(() {
|
||||
|
@ -35,8 +35,9 @@ class DeleteAccountDialog extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ResponsiveDialog(
|
||||
title: Text(AppLocalizations.of(context)!.oath_delete_account),
|
||||
title: Text(l10n.s_delete_account),
|
||||
actions: [
|
||||
TextButton(
|
||||
key: keys.deleteButton,
|
||||
@ -48,17 +49,14 @@ class DeleteAccountDialog extends ConsumerWidget {
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
Navigator.of(context).pop(true);
|
||||
showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!
|
||||
.oath_success_delete_account);
|
||||
showMessage(context, l10n.s_account_deleted);
|
||||
},
|
||||
);
|
||||
} on CancellationException catch (_) {
|
||||
// ignored
|
||||
}
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.oath_delete),
|
||||
child: Text(l10n.s_delete),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
@ -66,14 +64,12 @@ class DeleteAccountDialog extends ConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!
|
||||
.oath_warning_this_will_delete_account_from_key),
|
||||
Text(l10n.p_warning_delete_account),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.oath_warning_disable_this_cred,
|
||||
l10n.p_warning_disable_credential,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
Text(
|
||||
'${AppLocalizations.of(context)!.oath_account} ${getTextName(credential)}'),
|
||||
Text(l10n.l_account(getTextName(credential))),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -40,23 +40,20 @@ Widget oathBuildActions(
|
||||
WidgetRef ref, {
|
||||
int? used,
|
||||
}) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final capacity = oathState.version.isAtLeast(4) ? 32 : null;
|
||||
final theme = Theme.of(context).colorScheme;
|
||||
return SimpleDialog(
|
||||
children: [
|
||||
ListTitle(AppLocalizations.of(context)!.general_setup,
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge),
|
||||
ListTitle(l10n.s_setup, textStyle: Theme.of(context).textTheme.bodyLarge),
|
||||
ListTile(
|
||||
title: Text(AppLocalizations.of(context)!.oath_add_account),
|
||||
title: Text(l10n.s_add_account),
|
||||
key: keys.addAccountAction,
|
||||
leading:
|
||||
const CircleAvatar(child: Icon(Icons.person_add_alt_1_outlined)),
|
||||
subtitle: Text(used == null
|
||||
? AppLocalizations.of(context)!.oath_unlock_first
|
||||
: (capacity != null
|
||||
? AppLocalizations.of(context)!
|
||||
.oath_accounts_used(used, capacity)
|
||||
: '')),
|
||||
? l10n.l_unlock_first
|
||||
: (capacity != null ? l10n.l_accounts_used(used, capacity) : '')),
|
||||
enabled: used != null && (capacity == null || capacity > used),
|
||||
onTap: used != null && (capacity == null || capacity > used)
|
||||
? () async {
|
||||
@ -90,31 +87,29 @@ Widget oathBuildActions(
|
||||
}
|
||||
: null,
|
||||
),
|
||||
ListTitle(AppLocalizations.of(context)!.general_manage,
|
||||
ListTitle(l10n.s_manage,
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge),
|
||||
ListTile(
|
||||
key: keys.customIconsAction,
|
||||
title: const Text('Custom icons'),
|
||||
subtitle: const Text('Set icons for accounts'),
|
||||
title: Text(l10n.s_custom_icons),
|
||||
subtitle: Text(l10n.l_set_icons_for_accounts),
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Icons.image_outlined),
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
await ref.read(withContextProvider)((context) => showBlurDialog(
|
||||
context: context,
|
||||
routeSettings:
|
||||
const RouteSettings(name: 'oath_icon_pack_dialog'),
|
||||
builder: (context) => const IconPackDialog(),
|
||||
));
|
||||
context: context,
|
||||
routeSettings:
|
||||
const RouteSettings(name: 'oath_icon_pack_dialog'),
|
||||
builder: (context) => const IconPackDialog(),
|
||||
));
|
||||
}),
|
||||
ListTile(
|
||||
key: keys.setOrManagePasswordAction,
|
||||
title: Text(oathState.hasKey
|
||||
? AppLocalizations.of(context)!.oath_manage_password
|
||||
: AppLocalizations.of(context)!.oath_set_password),
|
||||
subtitle:
|
||||
Text(AppLocalizations.of(context)!.oath_password_description),
|
||||
title: Text(
|
||||
oathState.hasKey ? l10n.s_manage_password : l10n.s_set_password),
|
||||
subtitle: Text(l10n.l_optional_password_protection),
|
||||
leading: const CircleAvatar(child: Icon(Icons.password_outlined)),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
@ -125,9 +120,8 @@ Widget oathBuildActions(
|
||||
}),
|
||||
ListTile(
|
||||
key: keys.resetAction,
|
||||
title: Text(AppLocalizations.of(context)!.oath_reset_oath),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)!.oath_factory_reset_description),
|
||||
title: Text(l10n.s_reset_oath),
|
||||
subtitle: Text(l10n.l_factory_reset_this_app),
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: theme.onError,
|
||||
backgroundColor: theme.error,
|
||||
|
@ -48,7 +48,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
if (result) {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context, AppLocalizations.of(context)!.oath_password_set);
|
||||
showMessage(context, AppLocalizations.of(context)!.s_password_set);
|
||||
} else {
|
||||
setState(() {
|
||||
_currentIsWrong = true;
|
||||
@ -58,17 +58,18 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final isValid = _newPassword.isNotEmpty &&
|
||||
_newPassword == _confirmPassword &&
|
||||
(!widget.state.hasKey || _currentPassword.isNotEmpty);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(AppLocalizations.of(context)!.oath_manage_password),
|
||||
title: Text(l10n.s_manage_password),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isValid ? _submit : null,
|
||||
key: keys.savePasswordButton,
|
||||
child: Text(AppLocalizations.of(context)!.oath_save),
|
||||
child: Text(l10n.s_save),
|
||||
)
|
||||
],
|
||||
child: Padding(
|
||||
@ -77,19 +78,16 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.state.hasKey) ...[
|
||||
Text(AppLocalizations.of(context)!.oath_enter_current_password),
|
||||
Text(l10n.p_enter_current_password_or_reset),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
key: keys.currentPasswordField,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText:
|
||||
AppLocalizations.of(context)!.oath_current_password,
|
||||
labelText: l10n.s_current_password,
|
||||
prefixIcon: const Icon(Icons.password_outlined),
|
||||
errorText: _currentIsWrong
|
||||
? AppLocalizations.of(context)!.oath_wrong_password
|
||||
: null,
|
||||
errorText: _currentIsWrong ? l10n.s_wrong_password : null,
|
||||
errorMaxLines: 3),
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
@ -113,10 +111,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
if (result) {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!
|
||||
.oath_password_removed);
|
||||
showMessage(context, l10n.s_password_removed);
|
||||
} else {
|
||||
setState(() {
|
||||
_currentIsWrong = true;
|
||||
@ -124,36 +119,31 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.oath_remove_password),
|
||||
child: Text(l10n.s_remove_password),
|
||||
),
|
||||
if (widget.state.remembered)
|
||||
OutlinedButton(
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.oath_clear_saved_password),
|
||||
child: Text(l10n.s_clear_saved_password),
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.forgetPassword();
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!
|
||||
.oath_password_forgotten);
|
||||
showMessage(context, l10n.s_password_forgotten);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
Text(AppLocalizations.of(context)!.oath_enter_new_password),
|
||||
Text(l10n.p_enter_new_password),
|
||||
TextField(
|
||||
key: keys.newPasswordField,
|
||||
autofocus: !widget.state.hasKey,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.oath_new_password,
|
||||
labelText: l10n.s_new_password,
|
||||
prefixIcon: const Icon(Icons.password_outlined),
|
||||
enabled: !widget.state.hasKey || _currentPassword.isNotEmpty,
|
||||
),
|
||||
@ -174,7 +164,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.oath_confirm_password,
|
||||
labelText: l10n.s_confirm_password,
|
||||
prefixIcon: const Icon(Icons.password_outlined),
|
||||
enabled:
|
||||
(!widget.state.hasKey || _currentPassword.isNotEmpty) &&
|
||||
|
@ -39,14 +39,15 @@ class OathScreen extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ref.watch(oathStateProvider(devicePath)).when(
|
||||
loading: () => MessagePage(
|
||||
title: Text(AppLocalizations.of(context)!.oath_authenticator),
|
||||
title: Text(l10n.s_authenticator),
|
||||
graphic: const CircularProgressIndicator(),
|
||||
delayedContent: true,
|
||||
),
|
||||
error: (error, _) => AppFailurePage(
|
||||
title: Text(AppLocalizations.of(context)!.oath_authenticator),
|
||||
title: Text(l10n.s_authenticator),
|
||||
cause: error,
|
||||
),
|
||||
data: (oathState) => oathState.locked
|
||||
@ -65,7 +66,7 @@ class _LockedView extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AppPage(
|
||||
title: Text(AppLocalizations.of(context)!.oath_authenticator),
|
||||
title: Text(AppLocalizations.of(context)!.s_authenticator),
|
||||
keyActionsBuilder: (context) =>
|
||||
oathBuildActions(context, devicePath, oathState, ref),
|
||||
child: Padding(
|
||||
@ -109,15 +110,16 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
// ONLY rebuild if the number of credentials changes.
|
||||
final numCreds = ref.watch(credentialListProvider(widget.devicePath)
|
||||
.select((value) => value?.length));
|
||||
if (numCreds == 0) {
|
||||
return MessagePage(
|
||||
title: Text(AppLocalizations.of(context)!.oath_authenticator),
|
||||
title: Text(l10n.s_authenticator),
|
||||
key: keys.noAccountsView,
|
||||
graphic: noAccounts,
|
||||
header: AppLocalizations.of(context)!.oath_no_accounts,
|
||||
header: l10n.s_no_accounts,
|
||||
keyActionsBuilder: (context) => oathBuildActions(
|
||||
context, widget.devicePath, widget.oathState, ref,
|
||||
used: 0),
|
||||
@ -152,7 +154,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
style: textTheme.titleMedium
|
||||
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
|
||||
decoration: InputDecoration(
|
||||
hintText: AppLocalizations.of(context)!.oath_search_accounts,
|
||||
hintText: l10n.s_search_accounts,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(32)),
|
||||
),
|
||||
|
@ -58,6 +58,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
}
|
||||
|
||||
void _submit() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
try {
|
||||
// Rename credentials
|
||||
final renamed = await ref
|
||||
@ -72,7 +73,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(renamed);
|
||||
showMessage(context, AppLocalizations.of(context)!.oath_account_renamed);
|
||||
showMessage(context, l10n.s_account_renamed);
|
||||
} on CancellationException catch (_) {
|
||||
// ignored
|
||||
} catch (e) {
|
||||
@ -86,7 +87,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
}
|
||||
showMessage(
|
||||
context,
|
||||
'${AppLocalizations.of(context)!.oath_fail_add_account}: $errorMessage',
|
||||
l10n.l_account_add_failed(errorMessage),
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
}
|
||||
@ -94,6 +95,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final credential = widget.credential;
|
||||
|
||||
final remaining = getRemainingKeySpace(
|
||||
@ -125,12 +127,12 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
final isValid = isUnique && isValidFormat;
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(AppLocalizations.of(context)!.oath_rename_account),
|
||||
title: Text(l10n.s_rename_account),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: didChange && isValid ? _submit : null,
|
||||
key: keys.saveButton,
|
||||
child: Text(AppLocalizations.of(context)!.oath_save),
|
||||
child: Text(l10n.s_save),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
@ -138,10 +140,8 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!
|
||||
.oath_rename(getTextName(credential))),
|
||||
Text(AppLocalizations.of(context)!
|
||||
.oath_warning_will_change_account_displayed),
|
||||
Text(l10n.q_rename_target(getTextName(credential))),
|
||||
Text(l10n.p_rename_will_change_account_displayed),
|
||||
TextFormField(
|
||||
initialValue: _issuer,
|
||||
enabled: issuerRemaining > 0,
|
||||
@ -151,7 +151,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
key: keys.issuerField,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.oath_issuer_optional,
|
||||
labelText: l10n.s_issuer_optional,
|
||||
helperText: '', // Prevents dialog resizing when disabled
|
||||
prefixIcon: const Icon(Icons.business_outlined),
|
||||
),
|
||||
@ -170,12 +170,12 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
key: keys.nameField,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.oath_account_name,
|
||||
labelText: l10n.s_account_name,
|
||||
helperText: '', // Prevents dialog resizing when disabled
|
||||
errorText: !isValidFormat
|
||||
? AppLocalizations.of(context)!.oath_account_must_have_name
|
||||
? l10n.l_account_name_required
|
||||
: !isUnique
|
||||
? AppLocalizations.of(context)!.oath_name_exists
|
||||
? l10n.l_name_already_exists
|
||||
: null,
|
||||
prefixIcon: const Icon(Icons.people_alt_outlined),
|
||||
),
|
||||
|
@ -30,19 +30,19 @@ class ResetDialog extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ResponsiveDialog(
|
||||
title: Text(AppLocalizations.of(context)!.oath_factory_reset),
|
||||
title: Text(l10n.s_factory_reset),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref.read(oathStateProvider(devicePath).notifier).reset();
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context,
|
||||
AppLocalizations.of(context)!.oath_oath_application_reset);
|
||||
showMessage(context, l10n.l_oath_application_reset);
|
||||
});
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.oath_reset),
|
||||
child: Text(l10n.s_reset),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
@ -50,11 +50,10 @@ class ResetDialog extends ConsumerWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.oath_warning_will_delete_accounts,
|
||||
l10n.p_warning_factory_reset,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.oath_warning_disable_these_creds),
|
||||
Text(l10n.p_warning_disable_credentials),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -53,13 +53,13 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
_passwordController.clear();
|
||||
});
|
||||
} else if (_remember && !result.second) {
|
||||
showMessage(
|
||||
context, AppLocalizations.of(context)!.oath_failed_remember_pw);
|
||||
showMessage(context, AppLocalizations.of(context)!.l_remember_pw_failed);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final keystoreFailed = widget.keystore == KeystoreState.failed;
|
||||
return Column(
|
||||
children: [
|
||||
@ -69,7 +69,7 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.oath_enter_oath_pw,
|
||||
l10n.l_enter_oath_pw,
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
TextField(
|
||||
@ -79,10 +79,8 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
obscureText: _isObscure,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.oath_password,
|
||||
errorText: _passwordIsWrong
|
||||
? AppLocalizations.of(context)!.oath_wrong_password
|
||||
: null,
|
||||
labelText: l10n.s_password,
|
||||
errorText: _passwordIsWrong ? l10n.s_wrong_password : null,
|
||||
helperText: '', // Prevents resizing when errorText shown
|
||||
prefixIcon: const Icon(Icons.password_outlined),
|
||||
suffixIcon: IconButton(
|
||||
@ -108,14 +106,12 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
keystoreFailed
|
||||
? ListTile(
|
||||
leading: const Icon(Icons.warning_amber_rounded),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.oath_keystore_unavailable),
|
||||
title: Text(l10n.l_keystore_unavailable),
|
||||
dense: true,
|
||||
minLeadingWidth: 0,
|
||||
)
|
||||
: CheckboxListTile(
|
||||
title:
|
||||
Text(AppLocalizations.of(context)!.oath_remember_password),
|
||||
title: Text(l10n.s_remember_password),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: _remember,
|
||||
@ -131,7 +127,7 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton.icon(
|
||||
key: keys.unlockButton,
|
||||
label: Text(AppLocalizations.of(context)!.oath_unlock),
|
||||
label: Text(l10n.s_unlock),
|
||||
icon: const Icon(Icons.lock_open),
|
||||
onPressed: _passwordController.text.isNotEmpty ? _submit : null,
|
||||
),
|
||||
|
@ -14,6 +14,8 @@
|
||||
* 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';
|
||||
@ -31,11 +33,13 @@ class SettingsPage extends ConsumerWidget {
|
||||
|
||||
@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(AppLocalizations.of(context)!.general_settings),
|
||||
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.
|
||||
@ -48,9 +52,9 @@ class SettingsPage extends ConsumerWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTitle(AppLocalizations.of(context)!.general_appearance),
|
||||
ListTitle(l10n.s_appearance),
|
||||
RadioListTile<ThemeMode>(
|
||||
title: Text(AppLocalizations.of(context)!.general_system_default),
|
||||
title: Text(l10n.s_system_default),
|
||||
value: ThemeMode.system,
|
||||
groupValue: themeMode,
|
||||
onChanged: (mode) {
|
||||
@ -59,7 +63,7 @@ class SettingsPage extends ConsumerWidget {
|
||||
},
|
||||
),
|
||||
RadioListTile<ThemeMode>(
|
||||
title: Text(AppLocalizations.of(context)!.general_light_mode),
|
||||
title: Text(l10n.s_light_mode),
|
||||
value: ThemeMode.light,
|
||||
groupValue: themeMode,
|
||||
onChanged: (mode) {
|
||||
@ -68,7 +72,7 @@ class SettingsPage extends ConsumerWidget {
|
||||
},
|
||||
),
|
||||
RadioListTile<ThemeMode>(
|
||||
title: Text(AppLocalizations.of(context)!.general_dark_mode),
|
||||
title: Text(l10n.s_dark_mode),
|
||||
value: ThemeMode.dark,
|
||||
groupValue: themeMode,
|
||||
onChanged: (mode) {
|
||||
@ -76,6 +80,22 @@ class SettingsPage extends ConsumerWidget {
|
||||
_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);
|
||||
}),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -40,6 +40,7 @@ class _ResponsiveDialogState extends State<ResponsiveDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
LayoutBuilder(builder: ((context, constraints) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
if (constraints.maxWidth < 540) {
|
||||
// Fullscreen
|
||||
return Scaffold(
|
||||
@ -61,8 +62,8 @@ class _ResponsiveDialogState extends State<ResponsiveDialog> {
|
||||
} else {
|
||||
// Dialog
|
||||
final cancelText = widget.onCancel == null && widget.actions.isEmpty
|
||||
? AppLocalizations.of(context)!.widgets_close
|
||||
: AppLocalizations.of(context)!.widgets_cancel;
|
||||
? l10n.s_close
|
||||
: l10n.s_cancel;
|
||||
return AlertDialog(
|
||||
title: widget.title,
|
||||
titlePadding: const EdgeInsets.only(top: 24, left: 18, right: 18),
|
||||
|
@ -18,6 +18,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.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.
|
||||
int byteLength(String value) => utf8.encode(value).length;
|
||||
@ -37,7 +38,7 @@ InputCounterWidgetBuilder buildByteCounterFor(String currentValue) =>
|
||||
return Text(
|
||||
maxLength != null ? '${byteLength(currentValue)}/$maxLength' : '',
|
||||
style: style,
|
||||
semanticsLabel: 'Character count',
|
||||
semanticsLabel: AppLocalizations.of(context)!.s_character_count,
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -278,7 +278,7 @@ void main() {
|
||||
|
||||
// we expect System theme default
|
||||
expect((tester.themeModeListTile().subtitle as Text).data,
|
||||
equals('Light theme'));
|
||||
equals('Light mode'));
|
||||
});
|
||||
|
||||
testWidgets('Theme preferences update', (WidgetTester tester) async {
|
||||
|
Loading…
Reference in New Issue
Block a user