/* * Copyright (C) 2023 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../models.dart'; import '../shortcuts.dart'; import '../state.dart'; import 'device_picker.dart'; import 'keys.dart'; class NavigationItem extends StatelessWidget { final Widget leading; final String title; final bool collapsed; final bool selected; final void Function()? onTap; const NavigationItem({ super.key, required this.leading, required this.title, this.collapsed = false, this.selected = false, this.onTap, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; if (collapsed) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: selected ? Theme( data: theme.copyWith( colorScheme: colorScheme.copyWith( primary: colorScheme.secondaryContainer, onPrimary: colorScheme.onSecondaryContainer)), child: IconButton.filled( icon: leading, tooltip: title, padding: const EdgeInsets.symmetric(horizontal: 16), onPressed: onTap, ), ) : IconButton( icon: leading, tooltip: title, padding: const EdgeInsets.symmetric(horizontal: 16), onPressed: onTap, ), ); } else { return ListTile( enabled: onTap != null, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(48)), leading: leading, title: Text(title), minVerticalPadding: 16, onTap: onTap, tileColor: selected ? colorScheme.secondaryContainer : null, textColor: selected ? colorScheme.onSecondaryContainer : null, iconColor: selected ? colorScheme.onSecondaryContainer : null, contentPadding: const EdgeInsets.only(left: 16.0), ); } } } extension on Application { IconData get _icon => switch (this) { Application.accounts => Symbols.supervisor_account, Application.webauthn => Symbols.security_key, Application.passkeys => Symbols.passkey, Application.fingerprints => Symbols.fingerprint, Application.slots => Symbols.touch_app, Application.certificates => Symbols.approval, Application.management => Symbols.construction, }; Key get _key => switch (this) { Application.accounts => oathAppDrawer, Application.webauthn => u2fAppDrawer, Application.passkeys => fidoPasskeysAppDrawer, Application.fingerprints => fidoFingerprintsAppDrawer, Application.slots => otpAppDrawer, Application.certificates => pivAppDrawer, Application.management => managementAppDrawer, }; } class NavigationContent extends ConsumerWidget { final bool shouldPop; final bool extended; const NavigationContent( {super.key, this.shouldPop = true, this.extended = false}); @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; final supportedApps = ref.watch(supportedAppsProvider); final data = ref.watch(currentDeviceDataProvider).valueOrNull; final availableApps = data != null ? supportedApps .where( (app) => app.getAvailability(data) != Availability.unsupported) .toList() : []; availableApps.remove(Application.management); final currentApp = ref.watch(currentAppProvider); return Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ AnimatedSize( duration: const Duration(milliseconds: 150), child: DevicePickerContent(extended: extended), ), const SizedBox(height: 32), AnimatedSize( duration: const Duration(milliseconds: 150), child: Column( children: [ if (data != null) ...[ // Normal YubiKey Applications ...availableApps.map((app) => NavigationItem( key: app._key, title: app.getDisplayName(l10n), leading: Icon(app._icon, fill: app == currentApp ? 1.0 : 0.0), collapsed: !extended, selected: app == currentApp, onTap: app.getAvailability(data) == Availability.enabled ? () { ref .read(currentAppProvider.notifier) .setCurrentApp(app); if (shouldPop) { Navigator.of(context).pop(); } } : null, )), const SizedBox(height: 32), ], ], ), ), // Non-YubiKey pages NavigationItem( leading: const Icon(Symbols.settings), key: settingDrawerIcon, title: l10n.s_settings, collapsed: !extended, onTap: () { if (shouldPop) { Navigator.of(context).pop(); } Actions.maybeInvoke(context, const SettingsIntent()); }, ), NavigationItem( leading: const Icon(Symbols.help), key: helpDrawerIcon, title: l10n.s_help_and_about, collapsed: !extended, onTap: () { if (shouldPop) { Navigator.of(context).pop(); } Actions.maybeInvoke(context, const AboutIntent()); }, ), ], ), ); } }