yubioath-flutter/lib/home/views/home_screen.dart

425 lines
13 KiB
Dart
Raw Normal View History

2024-03-06 23:06:48 +03:00
/*
* Copyright (C) 2024 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 'dart:async';
2024-03-06 23:06:48 +03:00
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
2024-03-08 17:50:19 +03:00
import 'package:material_symbols_icons/symbols.dart';
2024-03-06 23:06:48 +03:00
import '../../android/state.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../app/views/app_page.dart';
import '../../core/models.dart';
import '../../core/state.dart';
import '../../management/models.dart';
import '../../widgets/choice_filter_chip.dart';
import '../../widgets/product_image.dart';
import 'key_actions.dart';
import 'manage_label_dialog.dart';
class HomeScreen extends ConsumerStatefulWidget {
2024-03-06 23:06:48 +03:00
final YubiKeyData deviceData;
const HomeScreen(this.deviceData, {super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen> {
bool hide = true;
@override
Widget build(BuildContext context) {
2024-03-06 23:06:48 +03:00
final l10n = AppLocalizations.of(context)!;
final serial = widget.deviceData.info.serial;
2024-03-06 23:06:48 +03:00
final keyCustomization = ref.watch(keyCustomizationManagerProvider)[serial];
final enabledCapabilities = widget.deviceData.info.config
.enabledCapabilities[widget.deviceData.node.transport] ??
0;
final primaryColor = ref.watch(primaryColorProvider);
2024-03-08 13:28:33 +03:00
// We need this to avoid unwanted app switch animation
if (hide) {
Timer.run(() {
setState(() {
hide = false;
});
});
}
2024-03-06 23:06:48 +03:00
return AppPage(
title: hide ? null : l10n.s_home,
delayedContent: hide,
2024-03-06 23:06:48 +03:00
keyActionsBuilder: (context) =>
homeBuildActions(context, widget.deviceData, ref),
2024-03-06 23:06:48 +03:00
builder: (context, expanded) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_DeviceContent(widget.deviceData, keyCustomization),
2024-03-06 23:06:48 +03:00
const SizedBox(height: 16.0),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 4,
runSpacing: 8,
2024-03-06 23:06:48 +03:00
children: Capability.values
.where((c) => enabledCapabilities & c.value != 0)
.map((c) => CapabilityBadge(c))
.toList(),
),
2024-07-14 20:51:14 +03:00
if (widget.deviceData.info.fipsCapable != 0)
2024-08-13 12:40:53 +03:00
_FipsLegend(),
2024-03-06 23:06:48 +03:00
if (serial != null) ...[
const SizedBox(height: 32.0),
_DeviceColor(
deviceData: widget.deviceData,
2024-03-06 23:06:48 +03:00
initialCustomization: keyCustomization ??
KeyCustomization(serial: serial))
]
],
),
),
if (widget.deviceData.info.version != const Version(0, 0, 0))
Flexible(
flex: 6,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: _HeroAvatar(
color: keyCustomization?.color ?? primaryColor,
child: ProductImage(
name: widget.deviceData.name,
formFactor: widget.deviceData.info.formFactor,
isNfc: widget.deviceData.info.supportedCapabilities
.containsKey(Transport.nfc),
),
2024-03-08 13:28:33 +03:00
),
),
)
2024-03-06 23:06:48 +03:00
],
)
],
),
);
},
);
}
}
2024-08-13 12:40:53 +03:00
class _FipsLegend extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.only(top: 16),
child: Opacity(
opacity: 0.6,
child: RichText(
text: TextSpan(
children: [
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: EdgeInsets.only(right: 4),
child: Icon(
Symbols.shield,
size: 12,
fill: 0.0,
),
),
),
TextSpan(
text: l10n.l_fips_capable,
style: Theme.of(context).textTheme.bodySmall,
),
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: EdgeInsets.only(left: 16, right: 4),
child: Icon(
Symbols.shield,
size: 12,
fill: 1.0,
),
),
),
TextSpan(
text: l10n.l_fips_approved,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
);
}
}
class _DeviceContent extends ConsumerWidget {
2024-03-06 23:06:48 +03:00
final YubiKeyData deviceData;
final KeyCustomization? initialCustomization;
const _DeviceContent(this.deviceData, this.initialCustomization);
2024-03-06 23:06:48 +03:00
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
2024-03-06 23:06:48 +03:00
final name = deviceData.name;
final serial = deviceData.info.serial;
final version = deviceData.info.version;
2024-03-06 23:06:48 +03:00
final label = initialCustomization?.name;
String displayName = label != null ? '$label ($name)' : name;
return Column(
2024-03-08 13:28:33 +03:00
crossAxisAlignment: CrossAxisAlignment.start,
2024-03-06 23:06:48 +03:00
children: [
Row(
children: [
Flexible(
child: Text(
displayName,
style: Theme.of(context).textTheme.titleLarge,
),
),
if (serial != null)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: IconButton(
icon: const Icon(Symbols.edit),
onPressed: () async {
await ref.read(withContextProvider)((context) async {
await _showManageLabelDialog(
initialCustomization ??
KeyCustomization(serial: serial),
context,
);
});
},
),
)
],
),
const SizedBox(
height: 12,
),
2024-03-06 23:06:48 +03:00
if (serial != null)
Text(
l10n.l_serial_number(serial),
style: Theme.of(context)
.textTheme
.titleSmall
?.apply(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
if (version != const Version(0, 0, 0))
Text(
l10n.l_firmware_version(version),
style: Theme.of(context)
.textTheme
.titleSmall
?.apply(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
2024-08-13 12:40:53 +03:00
if (deviceData.info.pinComplexity)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
l10n.l_pin_complexity,
style: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
2024-03-06 23:06:48 +03:00
],
);
}
Future<void> _showManageLabelDialog(
KeyCustomization keyCustomization, BuildContext context) async {
await showBlurDialog(
context: context,
builder: (context) => ManageLabelDialog(
initialCustomization: keyCustomization,
),
);
}
}
class _DeviceColor extends ConsumerWidget {
final YubiKeyData deviceData;
final KeyCustomization initialCustomization;
const _DeviceColor(
{required this.deviceData, required this.initialCustomization});
@override
Widget build(BuildContext context, WidgetRef ref) {
2024-03-08 13:28:33 +03:00
final l10n = AppLocalizations.of(context)!;
final defaultColor = ref.watch(defaultColorProvider);
2024-03-06 23:06:48 +03:00
final customColor = initialCustomization.color;
return ChoiceFilterChip<Color?>(
2024-03-08 13:28:33 +03:00
disableHover: true,
2024-03-06 23:06:48 +03:00
value: customColor,
items: const [null],
selected: customColor != null && customColor.value != defaultColor.value,
2024-03-06 23:06:48 +03:00
itemBuilder: (e) => Wrap(
alignment: WrapAlignment.center,
runSpacing: 8,
spacing: 16,
children: [
...[
Colors.teal,
Colors.cyan,
Colors.blueAccent,
Colors.deepPurple,
Colors.red,
Colors.orange,
Colors.yellow,
// add nice color to devices with dynamic color
if (isAndroid && ref.read(androidSdkVersionProvider) >= 31)
Colors.lightGreen
].map((e) => _ColorButton(
color: e,
isSelected: customColor?.value == e.value,
2024-03-06 23:06:48 +03:00
onPressed: () {
_updateColor(e, ref);
Navigator.of(context).pop();
},
)),
// "use default color" button
2024-03-06 23:06:48 +03:00
RawMaterialButton(
onPressed: () {
_updateColor(null, ref);
Navigator.of(context).pop();
},
constraints: const BoxConstraints(minWidth: 26.0, minHeight: 26.0),
fillColor: defaultColor,
2024-03-08 13:28:33 +03:00
hoverColor: Colors.black12,
2024-03-06 23:06:48 +03:00
shape: const CircleBorder(),
child: Icon(customColor == null ? Symbols.circle : Symbols.clear,
fill: 1,
size: 16,
weight: 700,
opticalSize: 20,
color: defaultColor.computeLuminance() > 0.7
? Colors.grey // for bright colors
: Colors.white),
2024-03-06 23:06:48 +03:00
),
],
),
labelBuilder: (e) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
constraints: const BoxConstraints(minWidth: 22.0, minHeight: 22.0),
decoration: BoxDecoration(
color: customColor ?? defaultColor, shape: BoxShape.circle),
),
const SizedBox(
width: 12,
),
2024-03-08 13:28:33 +03:00
Flexible(child: Text(l10n.s_color))
2024-03-06 23:06:48 +03:00
],
),
onChanged: (e) {},
);
}
void _updateColor(Color? color, WidgetRef ref) async {
final manager = ref.read(keyCustomizationManagerProvider.notifier);
await manager.set(
serial: initialCustomization.serial,
name: initialCustomization.name,
color: color,
);
}
}
class _ColorButton extends StatefulWidget {
final Color? color;
final bool isSelected;
final Function()? onPressed;
const _ColorButton({
required this.color,
required this.isSelected,
required this.onPressed,
});
@override
State<_ColorButton> createState() => _ColorButtonState();
}
class _ColorButtonState extends State<_ColorButton> {
@override
Widget build(BuildContext context) {
return RawMaterialButton(
onPressed: widget.onPressed,
constraints: const BoxConstraints(minWidth: 26.0, minHeight: 26.0),
fillColor: widget.color,
2024-03-08 13:28:33 +03:00
hoverColor: Colors.black12,
2024-03-06 23:06:48 +03:00
shape: const CircleBorder(),
child: Icon(
2024-03-08 17:50:19 +03:00
Symbols.circle,
fill: 1,
2024-03-06 23:06:48 +03:00
size: 16,
color: widget.isSelected ? Colors.white : Colors.transparent,
),
);
}
}
2024-03-08 13:28:33 +03:00
class _HeroAvatar extends StatelessWidget {
final Widget child;
final Color color;
const _HeroAvatar({required this.color, required this.child});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
color.withOpacity(0.6),
color.withOpacity(0.25),
(DialogTheme.of(context).backgroundColor ??
theme.dialogBackgroundColor)
.withOpacity(0),
],
),
),
padding: const EdgeInsets.all(12),
2024-07-03 15:27:54 +03:00
child: child,
2024-03-08 13:28:33 +03:00
);
}
}