diff --git a/lib/about_page.dart b/lib/about_page.dart index b9fcae021..5de7ea3cf 100755 --- a/lib/about_page.dart +++ b/lib/about_page.dart @@ -14,29 +14,15 @@ * limitations under the License. */ -import 'dart:convert'; -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logging/logging.dart'; -import 'package:material_symbols_icons/symbols.dart'; -import 'android/state.dart'; import 'app/app_url_launcher.dart'; -import 'app/logging.dart'; -import 'app/message.dart'; -import 'app/state.dart'; import 'app/views/keys.dart'; -import 'core/state.dart'; -import 'desktop/state.dart'; import 'generated/l10n/app_localizations.dart'; import 'version.dart'; -import 'widgets/choice_filter_chip.dart'; import 'widgets/responsive_dialog.dart'; -final _log = Logger('about'); - class AboutPage extends ConsumerWidget { const AboutPage({super.key}); @@ -145,127 +131,9 @@ class AboutPage extends ConsumerWidget { ), ], ), - const Padding( - padding: EdgeInsets.only(top: 24.0, bottom: 8.0), - child: Divider(), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Text( - l10n.s_troubleshooting, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - const LoggingPanel(), - - // Diagnostics (desktop only) - if (isDesktop) ...[ - const SizedBox(height: 12.0), - ActionChip( - key: diagnosticsChip, - avatar: const Icon(Symbols.bug_report), - label: Text(l10n.s_run_diagnostics), - onPressed: () async { - _log.info('Running diagnostics...'); - final response = await ref - .read(rpcProvider) - .requireValue - .command('diagnose', []); - final data = response['diagnostics'] as List; - data.insert(0, { - 'app_version': version, - 'dart': Platform.version, - 'os': Platform.operatingSystem, - 'os_version': Platform.operatingSystemVersion, - }); - data.insert( - data.length - 1, - ref.read(featureFlagProvider), - ); - final text = const JsonEncoder.withIndent( - ' ', - ).convert(data); - await ref.read(clipboardProvider).setText(text); - await ref.read(withContextProvider)((context) async { - showMessage(context, l10n.l_diagnostics_copied); - }); - }, - ), - ], - - // Enable screenshots (Android only) - if (isAndroid) ...[ - const SizedBox(height: 12.0), - FilterChip( - key: screenshotChip, - label: Text(l10n.s_allow_screenshots), - selected: ref.watch(androidAllowScreenshotsProvider), - onSelected: (value) async { - ref - .read(androidAllowScreenshotsProvider.notifier) - .setAllowScreenshots(value); - }, - ), - ], ], ), ), ); } } - -class LoggingPanel extends ConsumerWidget { - const LoggingPanel({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final l10n = AppLocalizations.of(context); - final logLevel = ref.watch(logLevelProvider); - return Wrap( - alignment: WrapAlignment.center, - spacing: 4.0, - runSpacing: 8.0, - children: [ - ChoiceFilterChip( - avatar: Icon( - Symbols.insights, - color: Theme.of(context).colorScheme.primary, - ), - value: logLevel, - items: Levels.LEVELS, - selected: logLevel != Level.INFO, - 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) { - ref.read(logLevelProvider.notifier).setLogLevel(level); - _log.debug('Log level set to $level'); - }, - ), - ActionChip( - key: logChip, - avatar: const Icon(Symbols.content_copy), - label: Text(l10n.s_copy_log), - onPressed: () async { - _log.info('Copying log to clipboard ($version)...'); - final logs = await ref.read(logLevelProvider.notifier).getLogs(); - var clipboard = ref.read(clipboardProvider); - await clipboard.setText(logs.join('\n')); - if (!clipboard.platformGivesFeedback()) { - await ref.read(withContextProvider)((context) async { - showMessage(context, l10n.l_log_copied); - }); - } - }, - ), - ], - ); - } -} diff --git a/lib/android/init.dart b/lib/android/init.dart index 5df2946ca..9273732a2 100644 --- a/lib/android/init.dart +++ b/lib/android/init.dart @@ -83,6 +83,9 @@ Future initialize() async { clipboardProvider.overrideWith( (ref) => ref.watch(androidClipboardProvider), ), + logPanelVisibilityProvider.overrideWith( + (ref) => LogPanelVisibilityNotifier(kDebugMode), + ), androidSdkVersionProvider.overrideWithValue(await getAndroidSdkVersion()), androidNfcSupportProvider.overrideWithValue(await getHasNfc()), supportedSectionsProvider.overrideWithValue([ diff --git a/lib/android/keys.dart b/lib/android/keys.dart index 2dcee72a4..706f404ab 100755 --- a/lib/android/keys.dart +++ b/lib/android/keys.dart @@ -27,6 +27,7 @@ const readFromImage = Key('$_prefix.read_image_file'); const nfcBypassTouchSetting = Key('$_prefix.nfc_bypass_touch'); const nfcSilenceSoundsSettings = Key('$_prefix.nfc_silence_sounds'); const usbOpenApp = Key('$_prefix.usb_open_app'); +const allowScreenshotsSetting = Key('$_prefix.allow_screenshots'); const nfcTapSetting = Key('$_prefix.nfc_tap'); Key nfcTapOption(NfcTapAction action) => diff --git a/lib/android/views/settings_views.dart b/lib/android/views/settings_views.dart index 0f8ec0708..b7a1dc144 100755 --- a/lib/android/views/settings_views.dart +++ b/lib/android/views/settings_views.dart @@ -204,3 +204,24 @@ class UsbOpenAppView extends ConsumerWidget { ); } } + +class AllowScreenshotsView extends ConsumerWidget { + const AllowScreenshotsView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context); + final allowScreenshots = ref.watch(androidAllowScreenshotsProvider); + return SwitchListTile( + title: Text(l10n.s_allow_screenshots), + subtitle: Text(l10n.l_allow_screenshots_desc), + value: allowScreenshots, + key: keys.allowScreenshotsSetting, + onChanged: (value) { + ref + .read(androidAllowScreenshotsProvider.notifier) + .setAllowScreenshots(value); + }, + ); + } +} diff --git a/lib/app/app.dart b/lib/app/app.dart index 1add8e7e1..46aa93ece 100755 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -21,7 +21,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../generated/l10n/app_localizations.dart'; import '../theme.dart'; -import 'logging.dart'; import 'shortcuts.dart'; import 'state.dart'; @@ -31,28 +30,26 @@ class YubicoAuthenticatorApp extends StatelessWidget { @override Widget build(BuildContext context) => GlobalShortcuts( - child: LogWarningOverlay( - child: Consumer( - builder: (context, ref, _) { - final primaryColor = ref.watch(primaryColorProvider); - return MaterialApp( - title: ref.watch(l10nProvider).app_name, - theme: AppTheme.getLightTheme(primaryColor), - darkTheme: AppTheme.getDarkTheme(primaryColor), - themeMode: ref.watch(themeModeProvider), - home: page, - debugShowCheckedModeBanner: false, - locale: ref.watch(currentLocaleProvider), - supportedLocales: AppLocalizations.supportedLocales, - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - ); - }, - ), + child: Consumer( + builder: (context, ref, _) { + final primaryColor = ref.watch(primaryColorProvider); + return MaterialApp( + title: ref.watch(l10nProvider).app_name, + theme: AppTheme.getLightTheme(primaryColor), + darkTheme: AppTheme.getDarkTheme(primaryColor), + themeMode: ref.watch(themeModeProvider), + home: page, + debugShowCheckedModeBanner: false, + locale: ref.watch(currentLocaleProvider), + supportedLocales: AppLocalizations.supportedLocales, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + ); + }, ), ); } diff --git a/lib/app/logging.dart b/lib/app/logging.dart index f1192171e..627b817ef 100755 --- a/lib/app/logging.dart +++ b/lib/app/logging.dart @@ -16,12 +16,25 @@ // ignore_for_file: constant_identifier_names +import 'dart:convert'; +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; +import 'package:material_symbols_icons/symbols.dart'; import '../android/state.dart'; import '../core/state.dart'; +import '../desktop/state.dart'; +import '../generated/l10n/app_localizations.dart'; +import '../version.dart'; +import '../widgets/choice_filter_chip.dart'; +import 'message.dart'; +import 'state.dart'; +import 'views/keys.dart'; + +final _log = Logger('logging'); String _pad(int value, int zeroes) => value.toString().padLeft(zeroes, '0'); @@ -88,66 +101,275 @@ class LogLevelNotifier extends StateNotifier { } } -class LogWarningOverlay extends StatelessWidget { - final Widget child; +final logPanelVisibilityProvider = + StateNotifierProvider((ref) { + return LogPanelVisibilityNotifier(false); + }); + +class LogPanelVisibilityNotifier extends StateNotifier { + LogPanelVisibilityNotifier(super.initialVisiblity); + + void setVisibility(bool visibility) { + state = visibility; + } +} - const LogWarningOverlay({super.key, required this.child}); +class _LoggingPanel extends ConsumerStatefulWidget { + final bool safeArea; + const _LoggingPanel({this.safeArea = false}); @override - Widget build(BuildContext context) { - return Stack( - alignment: Alignment.center, - children: [ - child, - Consumer( - builder: (context, ref, _) { - final sensitiveLogs = ref.watch( - logLevelProvider.select( - (level) => level.value <= Level.CONFIG.value, + ConsumerState<_LoggingPanel> createState() => _LoggingPanelState(); +} + +class _LoggingPanelState extends ConsumerState<_LoggingPanel> { + bool _runningDiagnostics = false; + + List _buildChipsList(Level logLevel) { + final l10n = AppLocalizations.of(context); + return [ + ChoiceFilterChip( + avatar: Icon(Symbols.insights), + value: logLevel, + items: Levels.LEVELS, + selected: logLevel != Level.INFO, + labelBuilder: + (value) => Text( + l10n.s_log_level( + value.name[0] + value.name.substring(1).toLowerCase(), ), - ); - final allowScreenshots = - isAndroid ? ref.watch(androidAllowScreenshotsProvider) : false; - - if (!(sensitiveLogs || allowScreenshots)) { - return const SizedBox(); - } - - final String message; - if (sensitiveLogs && allowScreenshots) { - message = - 'Potentially sensitive data is being logged, and other apps can potentially record the screen'; - } else if (sensitiveLogs) { - message = 'Potentially sensitive data is being logged'; - } else if (allowScreenshots) { - message = 'Other apps can potentially record the screen'; - } else { - return const SizedBox(); - } - - var mediaQueryData = MediaQueryData.fromView(View.of(context)); - var bottomPadding = mediaQueryData.systemGestureInsets.bottom; - return Padding( - padding: EdgeInsets.fromLTRB(5, 0, 5, bottomPadding), - child: Align( - alignment: Alignment.bottomCenter, - child: IgnorePointer( - child: Text( - 'WARNING: $message!', - textDirection: TextDirection.ltr, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.red, - fontWeight: FontWeight.bold, - height: 1.5, - fontSize: 16, - ), + ), + itemBuilder: + (value) => Text( + '${value.name[0]}${value.name.substring(1).toLowerCase()}', + ), + onChanged: (level) { + ref.read(logLevelProvider.notifier).setLogLevel(level); + _log.debug('Log level set to $level'); + }, + ), + ActionChip( + key: logChip, + avatar: const Icon(Symbols.content_copy), + label: Text(l10n.s_copy_log), + onPressed: () async { + _log.info('Copying log to clipboard ($version)...'); + final logs = await ref.read(logLevelProvider.notifier).getLogs(); + var clipboard = ref.read(clipboardProvider); + await clipboard.setText(logs.join('\n')); + if (!clipboard.platformGivesFeedback()) { + await ref.read(withContextProvider)((context) async { + showMessage(context, l10n.l_log_copied); + }); + } + }, + ), + if (isDesktop) ...[ + ActionChip( + key: diagnosticsChip, + avatar: + _runningDiagnostics + ? SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator(strokeWidth: 2.0), + ) + : const Icon(Symbols.bug_report), + label: Text(l10n.s_run_diagnostics), + onPressed: () async { + setState(() { + _runningDiagnostics = true; + }); + _log.info('Running diagnostics...'); + final response = await ref + .read(rpcProvider) + .requireValue + .command('diagnose', []); + final data = response['diagnostics'] as List; + data.insert(0, { + 'app_version': version, + 'dart': Platform.version, + 'os': Platform.operatingSystem, + 'os_version': Platform.operatingSystemVersion, + }); + data.insert(data.length - 1, ref.read(featureFlagProvider)); + final text = const JsonEncoder.withIndent(' ').convert(data); + await ref.read(clipboardProvider).setText(text); + setState(() { + _runningDiagnostics = false; + }); + await ref.read(withContextProvider)((context) async { + showMessage(context, l10n.l_diagnostics_copied); + }); + }, + ), + ], + ]; + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final logLevel = ref.watch(logLevelProvider); + final sensitiveLogs = ref.watch( + logLevelProvider.select((level) => level.value <= Level.CONFIG.value), + ); + + return _Panel( + sensitive: sensitiveLogs, + safeArea: widget.safeArea, + child: Wrap( + alignment: WrapAlignment.spaceBetween, + runSpacing: 4.0, + spacing: 4.0, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (sensitiveLogs) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 8.0, + bottom: 8.0, ), + child: Icon(Symbols.warning_amber, size: 24), ), - ), - ); - }, + if (sensitiveLogs) ...[ + const SizedBox(width: 8.0), + Flexible(child: Text(l10n.l_sensitive_data_logged)), + ], + ], + ), + if (!sensitiveLogs) + IconButton( + onPressed: () { + ref + .read(logPanelVisibilityProvider.notifier) + .setVisibility(false); + }, + icon: Icon(Symbols.close), + ), + Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: _buildChipsList(logLevel), + ), + ], + ), + ); + } +} + +class _WarningPanel extends StatelessWidget { + final bool safeArea; + const _WarningPanel({this.safeArea = false}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + + return _Panel( + sensitive: true, + safeArea: safeArea, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 12.0, top: 8.0, bottom: 8.0), + child: Icon(Symbols.warning_amber, size: 24), + ), + const SizedBox(width: 8.0), + Flexible(child: Text(l10n.l_warning_allow_screenshots)), + ], + ), + ); + } +} + +class _Panel extends StatelessWidget { + final Widget child; + final bool sensitive; + final bool safeArea; + const _Panel({ + required this.child, + required this.sensitive, + this.safeArea = true, + }); + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + + final sensitiveColor = Color(0xFFFF1A1A); + final sensitiveChipColor = + themeData.brightness == Brightness.dark + ? Color(0xFF832E2E) + : Color.fromARGB(255, 223, 134, 134); + final sensitiveChipBorderColor = + themeData.brightness == Brightness.dark + ? Color(0xFFA24848) + : Color.fromARGB(255, 191, 98, 98); + final seedColor = + sensitive ? sensitiveColor : themeData.colorScheme.primary; + final colorScheme = ColorScheme.fromSeed( + seedColor: seedColor, + brightness: themeData.brightness, + ); + + final localThemeData = themeData.copyWith( + colorScheme: colorScheme, + chipTheme: themeData.chipTheme.copyWith( + backgroundColor: + sensitive ? sensitiveChipColor : colorScheme.secondaryContainer, + selectedColor: sensitive ? sensitiveChipColor : null, + shape: + sensitive + ? RoundedRectangleBorder( + side: BorderSide(color: sensitiveChipBorderColor), + borderRadius: BorderRadius.circular(8.0), + ) + : null, + ), + ); + + final panelBackgroundColor = + sensitive + ? sensitiveColor.withValues(alpha: 0.3) + : colorScheme.secondaryContainer.withValues(alpha: 0.3); + + final content = Padding(padding: const EdgeInsets.all(4.0), child: child); + + return ColoredBox( + color: colorScheme.surface, + child: Theme( + data: localThemeData, + child: ColoredBox( + color: panelBackgroundColor, + child: safeArea ? SafeArea(child: content) : content, ), + ), + ); + } +} + +class PanelList extends ConsumerWidget { + const PanelList({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final logPanelVisible = ref.watch(logPanelVisibilityProvider); + final allowScreenshots = + isAndroid ? ref.watch(androidAllowScreenshotsProvider) : false; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (allowScreenshots) + Flexible(child: _WarningPanel(safeArea: !logPanelVisible)), + if (allowScreenshots && logPanelVisible) const SizedBox(height: 4.0), + if (logPanelVisible) Flexible(child: _LoggingPanel(safeArea: true)), ], ); } diff --git a/lib/app/views/app_page.dart b/lib/app/views/app_page.dart index e0d66a635..12e109b33 100755 --- a/lib/app/views/app_page.dart +++ b/lib/app/views/app_page.dart @@ -29,6 +29,7 @@ import '../../generated/l10n/app_localizations.dart'; import '../../management/models.dart'; import '../../widgets/delayed_visibility.dart'; import '../../widgets/file_drop_target.dart'; +import '../logging.dart'; import '../message.dart'; import '../shortcuts.dart'; import '../state.dart'; @@ -823,6 +824,7 @@ class _AppPageState extends ConsumerState { ], ), ), + bottomNavigationBar: PanelList(key: loggingPanelKey), drawer: hasDrawer ? _buildDrawer(context) : null, body: body, ); diff --git a/lib/app/views/keys.dart b/lib/app/views/keys.dart index 608491993..07c100bde 100644 --- a/lib/app/views/keys.dart +++ b/lib/app/views/keys.dart @@ -21,6 +21,8 @@ final scaffoldGlobalKey = GlobalKey(); final headerSliverGlobalKey = GlobalKey(); // This is global so we can access it from the global Ctrl+F shortcut. final searchField = GlobalKey(); +// Logging panel key (needed to calculate its height) +final loggingPanelKey = GlobalKey(); const _prefix = 'app.keys'; const deviceInfoListTile = Key('$_prefix.device_info_list_tile'); @@ -64,6 +66,7 @@ const factoryResetReset = Key('$_prefix.yubikey_factory_reset_reset'); // settings page const settingDrawerIcon = Key('$_prefix.settings_drawer_icon'); const helpDrawerIcon = Key('$_prefix.setting_drawer_icon'); +const loggingPanelButton = Key('$_prefix.settings_logging_panel_button'); const themeModeSetting = Key('$_prefix.settings.theme_mode'); const languageSetting = Key('$_prefix.settings.language'); const toggleDevicesSetting = Key('$_prefix.settings.toggle_devices'); diff --git a/lib/app/views/settings_page.dart b/lib/app/views/settings_page.dart index fc75d9c45..31e75d4df 100755 --- a/lib/app/views/settings_page.dart +++ b/lib/app/views/settings_page.dart @@ -346,6 +346,10 @@ class SettingsPage extends ConsumerWidget { ListTitle(l10n.s_usb_options), const UsbOpenAppView(), ], + if (isAndroid) ...[ + ListTitle(l10n.s_privacy_options), + const AllowScreenshotsView(), + ], ListTitle(l10n.s_appearance), const _ThemeModeView(), const _IconsView(), diff --git a/lib/desktop/init.dart b/lib/desktop/init.dart index 523ee88d5..320e889ba 100755 --- a/lib/desktop/init.dart +++ b/lib/desktop/init.dart @@ -125,7 +125,7 @@ Future initialize(List argv) async { parser.addFlag(_hidden); parser.addFlag(_shown); final args = parser.parse(argv); - _initLogging(args); + final levelFromArg = _initLogging(args); await windowManager.ensureInitialized(); SharedPreferences prefs; @@ -235,6 +235,9 @@ Future initialize(List argv) async { currentSectionProvider.overrideWith( (ref) => desktopCurrentSectionNotifier(ref), ), + logPanelVisibilityProvider.overrideWith( + (ref) => LogPanelVisibilityNotifier(levelFromArg), + ), // OATH oathStateProvider.overrideWithProvider(desktopOathState.call), credentialListProvider.overrideWithProvider( @@ -311,7 +314,7 @@ Future _initHelper(String exe) async { return rpc; } -void _initLogging(ArgResults args) { +bool _initLogging(ArgResults args) { final path = args[_logFile]; final levelName = args[_logLevel]; @@ -341,12 +344,15 @@ void _initLogging(ArgResults args) { } }); + bool levelFromArg = false; + if (levelName != null) { try { Level level = Levels.LEVELS.firstWhere( (level) => level.name == levelName.toUpperCase(), ); Logger.root.level = level; + levelFromArg = true; _log.info('Log level initialized from command line argument'); } catch (error) { _log.error('Failed to set log level', error); @@ -354,6 +360,7 @@ void _initLogging(ArgResults args) { } _log.info('Logging initialized, outputting to stderr'); + return levelFromArg; } void _initLicenses() async { diff --git a/lib/home/views/home_message_page.dart b/lib/home/views/home_message_page.dart index d92af967d..bad597a6a 100644 --- a/lib/home/views/home_message_page.dart +++ b/lib/home/views/home_message_page.dart @@ -66,7 +66,7 @@ class HomeMessagePage extends ConsumerWidget { header: header, message: message, footnote: footnote, - keyActionsBuilder: (context) => homeBuildActions(context, null, ref), + keyActionsBuilder: (context) => HomeActions(), actionButtonBuilder: actionButtonBuilder, actionsBuilder: actionsBuilder, fileDropOverlay: fileDropOverlay, diff --git a/lib/home/views/home_screen.dart b/lib/home/views/home_screen.dart index a0a57056b..d075e9737 100644 --- a/lib/home/views/home_screen.dart +++ b/lib/home/views/home_screen.dart @@ -73,7 +73,7 @@ class _HomeScreenState extends ConsumerState { title: hide ? null : l10n.s_home, delayedContent: hide, keyActionsBuilder: - (context) => homeBuildActions(context, widget.deviceData, ref), + (context) => HomeActions(deviceData: widget.deviceData), builder: (context, expanded) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 18.0), diff --git a/lib/home/views/key_actions.dart b/lib/home/views/key_actions.dart index 95d7592f6..ac311a154 100644 --- a/lib/home/views/key_actions.dart +++ b/lib/home/views/key_actions.dart @@ -16,10 +16,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../app/features.dart' as features; +import '../../app/logging.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/shortcuts.dart'; @@ -31,111 +33,136 @@ import '../../core/state.dart'; import '../../generated/l10n/app_localizations.dart'; import '../../management/views/management_screen.dart'; -Widget homeBuildActions( - BuildContext context, - YubiKeyData? deviceData, - WidgetRef ref, -) { - final l10n = AppLocalizations.of(context); - final hasFeature = ref.watch(featureProvider); - final interfacesLocked = deviceData?.info.resetBlocked != 0; - final managementAvailability = - hasFeature(features.management) && - switch (deviceData?.info.version) { - Version version => - (version.major > 4 || // YK5 and up - (version.major == 4 && version.minor >= 1) || // YK4.1 and up - version.major == 3), // NEO, - null => false, - }; +class HomeActions extends ConsumerWidget { + final YubiKeyData? deviceData; + const HomeActions({super.key, this.deviceData}); - return Column( - children: [ - if (deviceData != null) + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context); + final hasFeature = ref.watch(featureProvider); + final interfacesLocked = deviceData?.info.resetBlocked != 0; + final managementAvailability = + hasFeature(features.management) && + switch (deviceData?.info.version) { + Version version => + (version.major > 4 || // YK5 and up + (version.major == 4 && version.minor >= 1) || // YK4.1 and up + version.major == 3), // NEO, + null => false, + }; + final logPanelVisible = ref.watch(logPanelVisibilityProvider); + final sensitiveLogs = ref.watch( + logLevelProvider.select((level) => level.value <= Level.CONFIG.value), + ); + + return Column( + children: [ + if (deviceData != null) + ActionListSection( + l10n.s_device, + children: [ + if (managementAvailability) + ActionListItem( + feature: features.management, + icon: const Icon(Symbols.construction), + actionStyle: ActionStyle.primary, + title: + deviceData!.info.version.major > 4 + ? l10n.s_toggle_applications + : l10n.s_toggle_interfaces, + key: yubikeyApplicationToggleMenuButton, + subtitle: + interfacesLocked + ? l10n.l_factory_reset_required + : (deviceData!.info.version.major > 4 + ? l10n.l_toggle_applications_desc + : l10n.l_toggle_interfaces_desc), + onTap: + interfacesLocked + ? null + : (context) { + Navigator.of( + context, + ).popUntil((route) => route.isFirst); + showBlurDialog( + context: context, + builder: + (context) => ManagementScreen(deviceData!), + ); + }, + ), + if (getResetCapabilities(hasFeature).any( + (c) => + c.value & + (deviceData!.info.supportedCapabilities[deviceData! + .node + .transport] ?? + 0) != + 0, + )) + ActionListItem( + icon: const Icon(Symbols.delete_forever), + title: l10n.s_factory_reset, + key: yubikeyFactoryResetMenuButton, + subtitle: l10n.l_factory_reset_desc, + actionStyle: ActionStyle.primary, + onTap: (context) { + Navigator.of(context).popUntil((route) => route.isFirst); + showBlurDialog( + context: context, + builder: (context) => ResetDialog(deviceData!), + ); + }, + ), + ], + ), ActionListSection( - l10n.s_device, + l10n.s_application, children: [ - if (managementAvailability) - ActionListItem( - feature: features.management, - icon: const Icon(Symbols.construction), - actionStyle: ActionStyle.primary, - title: - deviceData.info.version.major > 4 - ? l10n.s_toggle_applications - : l10n.s_toggle_interfaces, - key: yubikeyApplicationToggleMenuButton, - subtitle: - interfacesLocked - ? l10n.l_factory_reset_required - : (deviceData.info.version.major > 4 - ? l10n.l_toggle_applications_desc - : l10n.l_toggle_interfaces_desc), - onTap: - interfacesLocked - ? null - : (context) { - Navigator.of( - context, - ).popUntil((route) => route.isFirst); - showBlurDialog( - context: context, - builder: (context) => ManagementScreen(deviceData), - ); - }, - ), - if (getResetCapabilities(hasFeature).any( - (c) => - c.value & - (deviceData.info.supportedCapabilities[deviceData - .node - .transport] ?? - 0) != - 0, - )) - ActionListItem( - icon: const Icon(Symbols.delete_forever), - title: l10n.s_factory_reset, - key: yubikeyFactoryResetMenuButton, - subtitle: l10n.l_factory_reset_desc, - actionStyle: ActionStyle.primary, - onTap: (context) { - Navigator.of(context).popUntil((route) => route.isFirst); - showBlurDialog( - context: context, - builder: (context) => ResetDialog(deviceData), - ); - }, - ), + ActionListItem( + icon: const Icon(Symbols.settings), + key: settingDrawerIcon, + title: l10n.s_settings, + subtitle: l10n.l_settings_desc, + actionStyle: ActionStyle.primary, + onTap: (context) { + Navigator.of(context).popUntil((route) => route.isFirst); + Actions.maybeInvoke(context, const SettingsIntent()); + }, + ), + ActionListItem( + icon: Icon(Symbols.analytics, fill: logPanelVisible ? 1 : 0), + key: loggingPanelButton, + title: + logPanelVisible + ? l10n.s_hide_log_panel + : l10n.s_show_log_panel, + subtitle: l10n.s_troubleshooting, + actionStyle: ActionStyle.primary, + onTap: + !sensitiveLogs + ? (context) { + ref + .read(logPanelVisibilityProvider.notifier) + .setVisibility(!logPanelVisible); + } + : null, + ), + ActionListItem( + icon: const Icon(Symbols.help), + key: helpDrawerIcon, + title: l10n.s_help_and_about, + subtitle: l10n.l_help_and_about_desc, + actionStyle: ActionStyle.primary, + onTap: (context) { + Navigator.of(context).popUntil((route) => route.isFirst); + Actions.maybeInvoke(context, const AboutIntent()); + }, + ), ], ), - ActionListSection( - l10n.s_application, - children: [ - ActionListItem( - icon: const Icon(Symbols.settings), - key: settingDrawerIcon, - title: l10n.s_settings, - subtitle: l10n.l_settings_desc, - actionStyle: ActionStyle.primary, - onTap: (context) { - Navigator.of(context).popUntil((route) => route.isFirst); - Actions.maybeInvoke(context, const SettingsIntent()); - }, - ), - ActionListItem( - icon: const Icon(Symbols.help), - key: helpDrawerIcon, - title: l10n.s_help_and_about, - subtitle: l10n.l_help_and_about_desc, - actionStyle: ActionStyle.primary, - onTap: (context) { - Navigator.of(context).popUntil((route) => route.isFirst); - Actions.maybeInvoke(context, const AboutIntent()); - }, - ), - ], - ), - ], - ); + ], + ); + } } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8676c5134..12415d301 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -85,12 +85,16 @@ "s_security_key": "Sicherheitsschlüssel", "s_slots": "Slots", "s_help_and_about": "Hilfe und Info", + "s_hide_log_panel": null, + "s_show_log_panel": null, + "s_troubleshooting": "Fehlerbehebung", + "l_sensitive_data_logged": null, + "l_warning_allow_screenshots": null, "l_help_and_about_desc": "Problembehebung und Unterstützung", "s_help_and_feedback": "Hilfe und Feedback", "s_home": "Start", "s_user_guide": "Nutzeranleitung", "s_i_need_help": "Ich brauche Hilfe", - "s_troubleshooting": "Fehlerbehebung", "s_terms_of_use": "Nutzungsbedingungen", "s_privacy_policy": "Datenschutzerklärung", "s_open_src_licenses": "Open Source Lizenzen", @@ -1104,7 +1108,9 @@ "l_launch_app_on_usb": "Starten, wenn YubiKey angesteckt wird", "l_launch_app_on_usb_on": "Das verhindert, dass andere Anwendungen den YubiKey über USB nutzen", "l_launch_app_on_usb_off": "Andere Anwendungen können den YubiKey über USB nutzen", + "s_privacy_options": null, "s_allow_screenshots": "Bildschirmfotos erlauben", + "l_allow_screenshots_desc": null, "@_nfc": {}, "s_nfc_ready_to_scan": "Bereit zum Scannen", @@ -1125,6 +1131,5 @@ "s_color": "Farbe", "p_set_will_add_custom_name": "Dies gibt deinem YubiKey einen eigenen Namen.", "p_rename_will_change_custom_name": "Dies ändert das Label deines YubiKeys.", - "@_eof": {} } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c4f8f35a2..fd4ef66fb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -85,12 +85,16 @@ "s_security_key": "Security Key", "s_slots": "Slots", "s_help_and_about": "Help and about", - "l_help_and_about_desc": "Troubleshoot and support", + "s_hide_log_panel": "Hide log panel", + "s_show_log_panel": "Show log panel", + "s_troubleshooting": "Troubleshooting", + "l_sensitive_data_logged": "Potentially sensitive data is being logged", + "l_warning_allow_screenshots": "Other apps can potentially record the screen", + "l_help_and_about_desc": "Information and support", "s_help_and_feedback": "Help and feedback", "s_home": "Home", "s_user_guide": "User guide", "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", @@ -1104,7 +1108,9 @@ "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_privacy_options": "Privacy options", "s_allow_screenshots": "Allow screenshots", + "l_allow_screenshots_desc": "Allow other apps to record the screen", "@_nfc": {}, "s_nfc_ready_to_scan": "Ready to scan", @@ -1125,6 +1131,5 @@ "s_color": "Color", "p_set_will_add_custom_name": "This will give your YubiKey a custom name.", "p_rename_will_change_custom_name": "This will change the label of your YubiKey.", - "@_eof": {} } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 50f25b511..cb186b0ff 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -85,12 +85,16 @@ "s_security_key": "Clé de sécurité", "s_slots": "Slots", "s_help_and_about": "Aide et à propos", + "s_hide_log_panel": null, + "s_show_log_panel": null, + "s_troubleshooting": "Dépannage", + "l_sensitive_data_logged": null, + "l_warning_allow_screenshots": null, "l_help_and_about_desc": "Dépannage et assistance", "s_help_and_feedback": "Aide/commentaires", "s_home": "Accueil", "s_user_guide": "Guide d'utilisation", "s_i_need_help": "J'ai besoin d'aide", - "s_troubleshooting": "Dépannage", "s_terms_of_use": "Conditions d'utilisation", "s_privacy_policy": "Confidentialité", "s_open_src_licenses": "Licences Open Source", @@ -1104,7 +1108,9 @@ "l_launch_app_on_usb": "Lancer lorsque la YubiKey est connectée", "l_launch_app_on_usb_on": "Cela empêche d'autres applications d'utiliser la YubiKey en USB", "l_launch_app_on_usb_off": "D'autres applications peuvent utiliser la YubiKey en USB", + "s_privacy_options": null, "s_allow_screenshots": "Autoriser captures d'écran", + "l_allow_screenshots_desc": null, "@_nfc": {}, "s_nfc_ready_to_scan": "Prêt à numériser", @@ -1125,6 +1131,5 @@ "s_color": "Couleur", "p_set_will_add_custom_name": "Cela donnera un nom personnalisé à votre YubiKey.", "p_rename_will_change_custom_name": "Cela changera l'étiquette de votre YubiKey.", - "@_eof": {} } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 43be67d73..98efee096 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -85,12 +85,16 @@ "s_security_key": "セキュリティキー", "s_slots": "スロット", "s_help_and_about": "ヘルプと概要", + "s_hide_log_panel": null, + "s_show_log_panel": null, + "s_troubleshooting": "トラブルシューティング", + "l_sensitive_data_logged": null, + "l_warning_allow_screenshots": null, "l_help_and_about_desc": "トラブルシューティングとサポート", "s_help_and_feedback": "ヘルプとフィードバック", "s_home": "ホーム", "s_user_guide": "ユーザーガイド", "s_i_need_help": "ヘルプが必要", - "s_troubleshooting": "トラブルシューティング", "s_terms_of_use": "利用規約", "s_privacy_policy": "プライバシーポリシー", "s_open_src_licenses": "オープンソースライセンス", @@ -1104,7 +1108,9 @@ "l_launch_app_on_usb": "YubiKeyの接続時に起動", "l_launch_app_on_usb_on": "これにより、他のアプリがUSB経由でYubiKeyを使用できなくなります", "l_launch_app_on_usb_off": "他のアプリがUSB経由でYubiKeyを使用できます", + "s_privacy_options": null, "s_allow_screenshots": "スクリーンショットを許可", + "l_allow_screenshots_desc": null, "@_nfc": {}, "s_nfc_ready_to_scan": "スキャンの準備完了", @@ -1125,6 +1131,5 @@ "s_color": "色", "p_set_will_add_custom_name": "これにより、YubiKey にカスタム名を付けることができます。", "p_rename_will_change_custom_name": "YubiKeyのラベルが変更されます。", - "@_eof": {} } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 50241e708..fa335984d 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -85,12 +85,16 @@ "s_security_key": "Klucz bezpieczeństwa", "s_slots": "Sloty", "s_help_and_about": "Pomoc i informacje", + "s_hide_log_panel": null, + "s_show_log_panel": null, + "s_troubleshooting": "Rozwiązywanie problemów", + "l_sensitive_data_logged": null, + "l_warning_allow_screenshots": null, "l_help_and_about_desc": "Rozwiązywanie problemów i wsparcie", "s_help_and_feedback": "Pomoc i opinie", "s_home": "Strona główna", "s_user_guide": "Przewodnik użytkownika", "s_i_need_help": "Pomoc", - "s_troubleshooting": "Rozwiązywanie problemów", "s_terms_of_use": "Warunki użytkowania", "s_privacy_policy": "Polityka prywatności", "s_open_src_licenses": "Licencje open source", @@ -1104,7 +1108,9 @@ "l_launch_app_on_usb": "Uruchom po podłączeniu klucza YubiKey", "l_launch_app_on_usb_on": "Inne aplikacje nie mogą korzystać z klucza YubiKey przez USB", "l_launch_app_on_usb_off": "Inne aplikacje mogą korzystać z klucza YubiKey przez USB", + "s_privacy_options": null, "s_allow_screenshots": "Zezwalaj na zrzuty ekranu", + "l_allow_screenshots_desc": null, "@_nfc": {}, "s_nfc_ready_to_scan": "Gotowy do skanowania", @@ -1125,6 +1131,5 @@ "s_color": "Kolor", "p_set_will_add_custom_name": "Spowoduje to nazwanie klucza YubiKey.", "p_rename_will_change_custom_name": "Spowoduje to zmianę etykiety klucza YubiKey.", - "@_eof": {} } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index bd7abb318..ad5a90a10 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -85,12 +85,16 @@ "s_security_key": "Bezpečnostný kľúč", "s_slots": "Sloty", "s_help_and_about": "Pomoc a informácie", + "s_hide_log_panel": null, + "s_show_log_panel": null, + "s_troubleshooting": "Riešenie problémov", + "l_sensitive_data_logged": null, + "l_warning_allow_screenshots": null, "l_help_and_about_desc": "Odstraňovanie problémov a podpora", "s_help_and_feedback": "Pomoc a spätná väzba", "s_home": "Domov", "s_user_guide": "Používateľská príručka", "s_i_need_help": "Potrebujem pomoc", - "s_troubleshooting": "Riešenie problémov", "s_terms_of_use": "Podmienky používania", "s_privacy_policy": "Zásady ochrany osobných údajov", "s_open_src_licenses": "Licencie otvoreného zdrojového kódu", @@ -1104,7 +1108,9 @@ "l_launch_app_on_usb": "Spustiť po pripojení YubiKey", "l_launch_app_on_usb_on": "Toto zabráni používaniu YubiKey cez USB inými aplikáciami", "l_launch_app_on_usb_off": "Ostatné aplikácie môžu používať YubiKey cez USB", + "s_privacy_options": null, "s_allow_screenshots": "Povoliť snímky obrazovky", + "l_allow_screenshots_desc": null, "@_nfc": {}, "s_nfc_ready_to_scan": "Pripravené na skenovanie", @@ -1125,6 +1131,5 @@ "s_color": "Farba", "p_set_will_add_custom_name": "Týmto sa vášmu kľúču YubiKey pridelí vlastný názov.", "p_rename_will_change_custom_name": "Týmto sa zmení menovka vášho kľúča YubiKey.", - "@_eof": {} } diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 2af821843..2ef85aee0 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -85,12 +85,16 @@ "s_security_key": "Khóa bảo mật", "s_slots": "Khe cắm", "s_help_and_about": "Trợ giúp và giới thiệu", + "s_hide_log_panel": null, + "s_show_log_panel": null, + "s_troubleshooting": "Khắc phục sự cố", + "l_sensitive_data_logged": null, + "l_warning_allow_screenshots": null, "l_help_and_about_desc": "Khắc phục sự cố và hỗ trợ", "s_help_and_feedback": "Trợ giúp và phản hồi", "s_home": "Trang chủ", "s_user_guide": "Hướng dẫn sử dụng", "s_i_need_help": "Tôi cần trợ giúp", - "s_troubleshooting": "Khắc phục sự cố", "s_terms_of_use": "Điều khoản sử dụng", "s_privacy_policy": "Chính sách bảo mật", "s_open_src_licenses": "Giấy phép mã nguồn mở", @@ -1104,7 +1108,9 @@ "l_launch_app_on_usb": "Khởi chạy khi kết nối YubiKey", "l_launch_app_on_usb_on": "Điều này ngăn cản các ứng dụng khác sử dụng YubiKey qua USB", "l_launch_app_on_usb_off": "Các ứng dụng khác có thể sử dụng YubiKey qua USB", + "s_privacy_options": null, "s_allow_screenshots": "Cho phép chụp ảnh màn hình", + "l_allow_screenshots_desc": null, "@_nfc": {}, "s_nfc_ready_to_scan": "Sẵn sàng để quét", @@ -1125,6 +1131,5 @@ "s_color": "Màu sắc", "p_set_will_add_custom_name": "Điều này sẽ đặt tên tùy chỉnh cho YubiKey của bạn.", "p_rename_will_change_custom_name": "Điều này sẽ thay đổi nhãn của YubiKey của bạn.", - "@_eof": {} } diff --git a/lib/widgets/toast.dart b/lib/widgets/toast.dart index 4296b926d..074cf9a1a 100755 --- a/lib/widgets/toast.dart +++ b/lib/widgets/toast.dart @@ -19,6 +19,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import '../app/views/keys.dart'; + class Toast extends StatefulWidget { final String message; final Duration duration; @@ -124,29 +126,36 @@ void Function() showToast( } } + final RenderBox? panelBox = + loggingPanelKey.currentContext?.findRenderObject() as RenderBox?; + final panelHeight = panelBox?.size.height ?? 0.0; + + final content = Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 50, + width: 400, + margin: const EdgeInsets.all(8), + child: Toast( + message, + duration, + backgroundColor: backgroundColor, + textStyle: textStyle, + onComplete: close, + ), + ), + ); + entry = OverlayEntry( builder: (context) { return Positioned( - bottom: MediaQuery.of(context).viewInsets.bottom, + bottom: MediaQuery.of(context).viewInsets.bottom + panelHeight, left: 0, right: 0, - child: SafeArea( - child: Align( - alignment: Alignment.bottomCenter, - child: Container( - height: 50, - width: 400, - margin: const EdgeInsets.all(8), - child: Toast( - message, - duration, - backgroundColor: backgroundColor, - textStyle: textStyle, - onComplete: close, - ), - ), - ), - ), + child: + panelHeight > 0 + ? content // Panel already adds SafeArea + : SafeArea(child: content), ); }, );