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 '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 ConsumerWidget {
|
|
|
|
final YubiKeyData deviceData;
|
|
|
|
const HomeScreen(this.deviceData, {super.key});
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
|
|
|
|
final serial = deviceData.info.serial;
|
|
|
|
final keyCustomization = ref.watch(keyCustomizationManagerProvider)[serial];
|
|
|
|
final enabledCapabilities =
|
|
|
|
deviceData.info.config.enabledCapabilities[deviceData.node.transport] ??
|
|
|
|
0;
|
2024-03-08 13:28:33 +03:00
|
|
|
final primaryColor = ref.watch(defaultColorProvider);
|
|
|
|
|
2024-03-06 23:06:48 +03:00
|
|
|
return AppPage(
|
|
|
|
title: l10n.s_home,
|
|
|
|
keyActionsBuilder: (context) =>
|
|
|
|
homeBuildActions(context, deviceData, ref),
|
|
|
|
builder: (context, expanded) {
|
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
2024-03-08 17:17:18 +03:00
|
|
|
_DeviceContent(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(
|
2024-03-08 17:17:18 +03:00
|
|
|
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(),
|
|
|
|
),
|
|
|
|
if (serial != null) ...[
|
|
|
|
const SizedBox(height: 32.0),
|
|
|
|
_DeviceColor(
|
|
|
|
deviceData: deviceData,
|
|
|
|
initialCustomization: keyCustomization ??
|
|
|
|
KeyCustomization(serial: serial))
|
|
|
|
]
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Flexible(
|
|
|
|
flex: 6,
|
|
|
|
child: ConstrainedBox(
|
|
|
|
constraints: const BoxConstraints(maxWidth: 200),
|
2024-03-08 13:28:33 +03:00
|
|
|
child: _HeroAvatar(
|
|
|
|
color: keyCustomization?.color ?? primaryColor,
|
|
|
|
child: ProductImage(
|
|
|
|
name: deviceData.name,
|
|
|
|
formFactor: deviceData.info.formFactor,
|
|
|
|
isNfc: deviceData.info.supportedCapabilities
|
|
|
|
.containsKey(Transport.nfc),
|
|
|
|
),
|
|
|
|
),
|
2024-03-06 23:06:48 +03:00
|
|
|
),
|
|
|
|
)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-08 17:17:18 +03:00
|
|
|
class _DeviceContent extends ConsumerWidget {
|
2024-03-06 23:06:48 +03:00
|
|
|
final YubiKeyData deviceData;
|
|
|
|
final KeyCustomization? initialCustomization;
|
2024-03-08 17:17:18 +03:00
|
|
|
const _DeviceContent(this.deviceData, this.initialCustomization);
|
2024-03-06 23:06:48 +03:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
2024-03-08 17:17:18 +03:00
|
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
|
2024-03-06 23:06:48 +03:00
|
|
|
final name = deviceData.name;
|
|
|
|
final serial = deviceData.info.serial;
|
2024-03-08 17:17:18 +03:00
|
|
|
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 Row(
|
2024-03-08 13:28:33 +03:00
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
2024-03-06 23:06:48 +03:00
|
|
|
children: [
|
|
|
|
Flexible(
|
2024-03-08 17:17:18 +03:00
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Text(
|
|
|
|
displayName,
|
|
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
|
|
),
|
|
|
|
const SizedBox(
|
|
|
|
height: 12,
|
|
|
|
),
|
|
|
|
if (serial != null)
|
|
|
|
Text(
|
|
|
|
l10n.l_serial_number(serial),
|
|
|
|
style: Theme.of(context).textTheme.titleSmall?.apply(
|
|
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
|
|
|
),
|
|
|
|
Text(
|
|
|
|
l10n.l_firmware_version(version),
|
|
|
|
style: Theme.of(context).textTheme.titleSmall?.apply(
|
|
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
2024-03-06 23:06:48 +03:00
|
|
|
if (serial != null)
|
|
|
|
Padding(
|
|
|
|
padding: const EdgeInsets.only(left: 8.0),
|
|
|
|
child: IconButton(
|
2024-03-08 17:50:19 +03:00
|
|
|
icon: const Icon(Symbols.edit),
|
2024-03-06 23:06:48 +03:00
|
|
|
onPressed: () async {
|
|
|
|
await ref.read(withContextProvider)((context) async {
|
|
|
|
await _showManageLabelDialog(
|
|
|
|
initialCustomization ?? KeyCustomization(serial: serial),
|
|
|
|
context,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
),
|
|
|
|
)
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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)!;
|
2024-03-06 23:06:48 +03:00
|
|
|
final theme = Theme.of(context);
|
|
|
|
final primaryColor = ref.watch(defaultColorProvider);
|
|
|
|
final defaultColor =
|
|
|
|
(isAndroid && ref.read(androidSdkVersionProvider) >= 31)
|
|
|
|
? theme.colorScheme.onSurface
|
|
|
|
: primaryColor;
|
|
|
|
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 != defaultColor,
|
|
|
|
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 == e,
|
|
|
|
onPressed: () {
|
|
|
|
_updateColor(e, ref);
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
},
|
|
|
|
)),
|
|
|
|
|
|
|
|
// remove color button
|
|
|
|
RawMaterialButton(
|
|
|
|
onPressed: () {
|
|
|
|
_updateColor(null, ref);
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
},
|
|
|
|
constraints: const BoxConstraints(minWidth: 26.0, minHeight: 26.0),
|
|
|
|
fillColor: (isAndroid && ref.read(androidSdkVersionProvider) >= 31)
|
|
|
|
? theme.colorScheme.onSurface
|
|
|
|
: primaryColor,
|
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.cancel,
|
2024-03-06 23:06:48 +03:00
|
|
|
size: 16,
|
|
|
|
color: customColor == null
|
|
|
|
? theme.colorScheme.onSurface
|
|
|
|
: theme.colorScheme.surface.withOpacity(0.2),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
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),
|
|
|
|
child: Theme(
|
|
|
|
// Give the avatar a transparent background
|
|
|
|
data: theme.copyWith(
|
|
|
|
colorScheme:
|
|
|
|
theme.colorScheme.copyWith(surfaceVariant: Colors.transparent)),
|
|
|
|
child: child,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|