yubioath-flutter/lib/app/views/navigation.dart
2024-05-24 13:55:31 +02:00

178 lines
6.0 KiB
Dart

/*
* 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 '../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 Section {
IconData get _icon => switch (this) {
Section.home => Symbols.home,
Section.accounts => Symbols.supervisor_account,
Section.securityKey => Symbols.security_key,
Section.passkeys => Symbols.passkey,
Section.fingerprints => Symbols.fingerprint,
Section.slots => Symbols.touch_app,
Section.certificates => Symbols.id_card,
};
Key get _key => switch (this) {
Section.home => homeDrawer,
Section.accounts => oathAppDrawer,
Section.securityKey => u2fAppDrawer,
Section.passkeys => fidoPasskeysAppDrawer,
Section.fingerprints => fidoFingerprintsAppDrawer,
Section.slots => otpAppDrawer,
Section.certificates => pivAppDrawer,
};
}
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 supportedSections = ref.watch(supportedSectionsProvider);
final data = ref.watch(currentDeviceDataProvider).valueOrNull;
final availableSections = data != null
? supportedSections
.where((section) =>
section.getAvailability(data) != Availability.unsupported)
.toList()
: [Section.home];
final currentSection = ref.watch(currentSectionProvider);
return Padding(
padding:
const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 8.0, top: 12),
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: [
// Normal YubiKey Applications
...availableSections.map((app) => NavigationItem(
key: app._key,
title: app.getDisplayName(l10n),
leading: Icon(
app._icon,
fill: app == currentSection ? 1.0 : 0.0,
semanticLabel:
!extended ? app.getDisplayName(l10n) : null,
),
collapsed: !extended,
selected: app == currentSection,
onTap: data == null && currentSection == Section.home ||
data != null &&
app.getAvailability(data) ==
Availability.enabled
? () {
ref
.read(currentSectionProvider.notifier)
.setCurrentSection(app);
if (shouldPop) {
Navigator.of(context).pop();
}
}
: null,
)),
],
),
),
],
),
);
}
}