mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 16:32:01 +03:00
Merge branch 'main' into bump/andorid-deps-20230821
This commit is contained in:
commit
acb97f941a
@ -32,10 +32,12 @@ Future<T?> showBlurDialog<T>({
|
||||
required BuildContext context,
|
||||
required Widget Function(BuildContext) builder,
|
||||
RouteSettings? routeSettings,
|
||||
Color barrierColor = const Color(0x80000000),
|
||||
}) async =>
|
||||
await showGeneralDialog<T>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierColor: barrierColor,
|
||||
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
pageBuilder: (ctx, anim1, anim2) => builder(ctx),
|
||||
transitionDuration: const Duration(milliseconds: 150),
|
||||
|
@ -224,7 +224,11 @@ class AppPage extends StatelessWidget {
|
||||
child: IconButton(
|
||||
key: actionsIconButtonKey,
|
||||
onPressed: () {
|
||||
showBlurDialog(context: context, builder: keyActionsBuilder!);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: keyActionsBuilder!,
|
||||
);
|
||||
},
|
||||
icon: keyActionsBadge
|
||||
? const Badge(
|
||||
|
@ -26,7 +26,8 @@ class FsDialog extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Dialog.fullscreen(
|
||||
backgroundColor: Theme.of(context).colorScheme.background.withAlpha(100),
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.background.withOpacity(0.7),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
@ -59,6 +59,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
OpenIntent: CallbackAction<OpenIntent>(
|
||||
onInvoke: (_) => showBlurDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (context) => CredentialDialog(cred),
|
||||
)),
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(
|
||||
@ -91,6 +92,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
OpenIntent: CallbackAction<OpenIntent>(
|
||||
onInvoke: (_) => showBlurDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (context) => FingerprintDialog(fp),
|
||||
)),
|
||||
EditIntent: CallbackAction<EditIntent>(
|
||||
|
@ -48,6 +48,12 @@
|
||||
"item": {}
|
||||
}
|
||||
},
|
||||
"s_definition": "{item}:",
|
||||
"@s_definition" : {
|
||||
"placeholders": {
|
||||
"item": {}
|
||||
}
|
||||
},
|
||||
|
||||
"s_about": "About",
|
||||
"s_appearance": "Appearance",
|
||||
@ -212,6 +218,12 @@
|
||||
"retries": {}
|
||||
}
|
||||
},
|
||||
"l_wrong_puk_attempts_remaining": "Wrong PUK, {retries} attempt(s) remaining",
|
||||
"@l_wrong_puk_attempts_remaining" : {
|
||||
"placeholders": {
|
||||
"retries": {}
|
||||
}
|
||||
},
|
||||
"s_fido_pin_protection": "FIDO PIN protection",
|
||||
"l_fido_pin_protection_optional": "Optional FIDO PIN protection",
|
||||
"l_enter_fido2_pin": "Enter the FIDO2 PIN for your YubiKey",
|
||||
@ -231,6 +243,7 @@
|
||||
"s_pin_required": "PIN required",
|
||||
"p_pin_required_desc": "The action you are about to perform requires the PIV PIN to be entered.",
|
||||
"l_piv_pin_blocked": "Blocked, use PUK to reset",
|
||||
"l_piv_pin_puk_blocked": "Blocked, factory reset needed",
|
||||
"p_enter_new_piv_pin_puk": "Enter a new {name} to set. Must be 6-8 characters.",
|
||||
"@p_enter_new_piv_pin_puk" : {
|
||||
"placeholders": {
|
||||
@ -418,32 +431,11 @@
|
||||
"l_import_desc": "Import a key and/or certificate",
|
||||
"l_delete_certificate": "Delete certificate",
|
||||
"l_delete_certificate_desc": "Remove the certificate from your YubiKey",
|
||||
"l_subject_issuer": "Subject: {subject}, Issuer: {issuer}",
|
||||
"@l_subject_issuer" : {
|
||||
"placeholders": {
|
||||
"subject": {},
|
||||
"issuer": {}
|
||||
}
|
||||
},
|
||||
"l_serial": "Serial: {serial}",
|
||||
"@l_serial" : {
|
||||
"placeholders": {
|
||||
"serial": {}
|
||||
}
|
||||
},
|
||||
"l_certificate_fingerprint": "Fingerprint: {fingerprint}",
|
||||
"@l_certificate_fingerprint" : {
|
||||
"placeholders": {
|
||||
"fingerprint": {}
|
||||
}
|
||||
},
|
||||
"l_valid": "Valid: {not_before} - {not_after}",
|
||||
"@l_valid" : {
|
||||
"placeholders": {
|
||||
"not_before": {},
|
||||
"not_after": {}
|
||||
}
|
||||
},
|
||||
"s_issuer": "Issuer",
|
||||
"s_serial": "Serial",
|
||||
"s_certificate_fingerprint": "Fingerprint",
|
||||
"s_valid_from": "Valid from",
|
||||
"s_valid_to": "Valid to",
|
||||
"l_no_certificate": "No certificate loaded",
|
||||
"l_key_no_certificate": "Key without certificate loaded",
|
||||
"s_generate_key": "Generate key",
|
||||
|
@ -121,40 +121,51 @@ class AccountDialog extends ConsumerWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 48, bottom: 16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
IconTheme(
|
||||
data: IconTheme.of(context).copyWith(size: 24),
|
||||
child: helper.buildCodeIcon(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconTheme(
|
||||
data: IconTheme.of(context).copyWith(size: 24),
|
||||
child: helper.buildCodeIcon(),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
DefaultTextStyle.merge(
|
||||
style: const TextStyle(fontSize: 28),
|
||||
child: helper.buildCodeLabel(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
DefaultTextStyle.merge(
|
||||
style: const TextStyle(fontSize: 28),
|
||||
child: helper.buildCodeLabel(),
|
||||
Text(
|
||||
helper.title,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle,
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
// This is what ListTile uses for subtitle
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
helper.title,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle,
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
// This is what ListTile uses for subtitle
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).textTheme.bodySmall!.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ActionListSection.fromMenuActions(
|
||||
context,
|
||||
AppLocalizations.of(context)!.s_actions,
|
||||
|
@ -89,6 +89,7 @@ class _AccountViewState extends ConsumerState<AccountView> {
|
||||
OpenIntent: CallbackAction<OpenIntent>(onInvoke: (_) async {
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (context) => AccountDialog(credential),
|
||||
);
|
||||
return null;
|
||||
|
@ -24,7 +24,7 @@ part 'models.g.dart';
|
||||
|
||||
const defaultManagementKey = '010203040506070801020304050607080102030405060708';
|
||||
const defaultManagementKeyType = ManagementKeyType.tdes;
|
||||
const defaultKeyType = KeyType.rsa2048;
|
||||
const defaultKeyType = KeyType.eccp256;
|
||||
const defaultGenerateType = GenerateType.certificate;
|
||||
|
||||
enum GenerateType {
|
||||
|
@ -39,6 +39,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
||||
|
||||
final pinBlocked = pivState.pinAttempts == 0;
|
||||
final pukAttempts = pivState.metadata?.pukMetadata.attemptsRemaining;
|
||||
final alertIcon = Icon(Icons.warning_amber, color: colors.tertiary);
|
||||
|
||||
return FsDialog(
|
||||
child: Column(
|
||||
@ -50,35 +51,46 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
||||
key: keys.managePinAction,
|
||||
title: l10n.s_pin,
|
||||
subtitle: pinBlocked
|
||||
? l10n.l_piv_pin_blocked
|
||||
? (pukAttempts != 0
|
||||
? l10n.l_piv_pin_blocked
|
||||
: l10n.l_piv_pin_puk_blocked)
|
||||
: l10n.l_attempts_remaining(pivState.pinAttempts),
|
||||
icon: const Icon(Icons.pin_outlined),
|
||||
onTap: (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagePinPukDialog(
|
||||
devicePath,
|
||||
target:
|
||||
pinBlocked ? ManageTarget.unblock : ManageTarget.pin,
|
||||
),
|
||||
);
|
||||
}),
|
||||
trailing: pinBlocked ? alertIcon : null,
|
||||
onTap: !(pinBlocked && pukAttempts == 0)
|
||||
? (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagePinPukDialog(
|
||||
devicePath,
|
||||
target: pinBlocked
|
||||
? ManageTarget.unblock
|
||||
: ManageTarget.pin,
|
||||
),
|
||||
);
|
||||
}
|
||||
: null),
|
||||
ActionListItem(
|
||||
key: keys.managePukAction,
|
||||
title: l10n.s_puk,
|
||||
subtitle: pukAttempts != null
|
||||
? l10n.l_attempts_remaining(pukAttempts)
|
||||
? (pukAttempts == 0
|
||||
? l10n.l_piv_pin_puk_blocked
|
||||
: l10n.l_attempts_remaining(pukAttempts))
|
||||
: null,
|
||||
icon: const Icon(Icons.pin_outlined),
|
||||
onTap: (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagePinPukDialog(devicePath,
|
||||
target: ManageTarget.puk),
|
||||
);
|
||||
}),
|
||||
trailing: pukAttempts == 0 ? alertIcon : null,
|
||||
onTap: pukAttempts != 0
|
||||
? (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagePinPukDialog(devicePath,
|
||||
target: ManageTarget.puk),
|
||||
);
|
||||
}
|
||||
: null),
|
||||
ActionListItem(
|
||||
key: keys.manageManagementKeyAction,
|
||||
title: l10n.s_management_key,
|
||||
@ -88,9 +100,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
||||
? l10n.l_pin_protected_key
|
||||
: l10n.l_change_management_key),
|
||||
icon: const Icon(Icons.key_outlined),
|
||||
trailing: usingDefaultMgmtKey
|
||||
? Icon(Icons.warning_amber, color: colors.tertiary)
|
||||
: null,
|
||||
trailing: usingDefaultMgmtKey ? alertIcon : null,
|
||||
onTap: (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
|
@ -114,7 +114,11 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
: l10n.s_current_puk,
|
||||
prefixIcon: const Icon(Icons.password_outlined),
|
||||
errorText: _currentIsWrong
|
||||
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
||||
? (widget.target == ManageTarget.puk
|
||||
? l10n.l_wrong_pin_attempts_remaining(
|
||||
_attemptsRemaining)
|
||||
: l10n.l_wrong_puk_attempts_remaining(
|
||||
_attemptsRemaining))
|
||||
: null,
|
||||
errorMaxLines: 3),
|
||||
textInputAction: TextInputAction.next,
|
||||
|
@ -70,6 +70,7 @@ class PivScreen extends ConsumerWidget {
|
||||
CallbackAction<OpenIntent>(onInvoke: (_) async {
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (context) => SlotDialog(e.slot),
|
||||
);
|
||||
return null;
|
||||
@ -104,7 +105,7 @@ class _CertificateListItem extends StatelessWidget {
|
||||
),
|
||||
title: slot.getDisplayName(l10n),
|
||||
subtitle: certInfo != null
|
||||
? l10n.l_subject_issuer(certInfo.subject, certInfo.issuer)
|
||||
? certInfo.subject
|
||||
: pivSlot.hasKey == true
|
||||
? l10n.l_key_no_certificate
|
||||
: l10n.l_no_certificate,
|
||||
|
@ -1,10 +1,29 @@
|
||||
/*
|
||||
* 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:intl/intl.dart';
|
||||
|
||||
import '../../app/message.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/fs_dialog.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../widgets/tooltip_if_truncated.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'actions.dart';
|
||||
@ -29,6 +48,8 @@ class SlotDialog extends ConsumerWidget {
|
||||
final subtitleStyle = textTheme.bodyMedium!.copyWith(
|
||||
color: textTheme.bodySmall!.color,
|
||||
);
|
||||
final clipboard = ref.watch(clipboardProvider);
|
||||
final withContext = ref.read(withContextProvider);
|
||||
|
||||
final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull;
|
||||
final slotData = ref.watch(pivSlotsProvider(node.path).select((value) =>
|
||||
@ -40,6 +61,32 @@ class SlotDialog extends ConsumerWidget {
|
||||
return const FsDialog(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
TableRow detailRow(String title, String value) {
|
||||
return TableRow(
|
||||
children: [
|
||||
Text(
|
||||
l10n.s_definition(title),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
GestureDetector(
|
||||
onDoubleTap: () async {
|
||||
await clipboard.setText(value);
|
||||
if (!clipboard.platformGivesFeedback()) {
|
||||
await withContext((context) async {
|
||||
showMessage(context, l10n.p_target_copied_clipboard(title));
|
||||
});
|
||||
}
|
||||
},
|
||||
child: TooltipIfTruncated(
|
||||
text: value,
|
||||
style: subtitleStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final certInfo = slotData.certInfo;
|
||||
return registerPivActions(
|
||||
node.path,
|
||||
@ -52,7 +99,7 @@ class SlotDialog extends ConsumerWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 48, bottom: 32),
|
||||
padding: const EdgeInsets.only(top: 48, bottom: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
@ -62,31 +109,29 @@ class SlotDialog extends ConsumerWidget {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (certInfo != null) ...[
|
||||
Text(
|
||||
l10n.l_subject_issuer(
|
||||
certInfo.subject, certInfo.issuer),
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: subtitleStyle,
|
||||
),
|
||||
Text(
|
||||
l10n.l_serial(certInfo.serial),
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: subtitleStyle,
|
||||
),
|
||||
Text(
|
||||
l10n.l_certificate_fingerprint(certInfo.fingerprint),
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: subtitleStyle,
|
||||
),
|
||||
Text(
|
||||
l10n.l_valid(
|
||||
certInfo.notValidBefore, certInfo.notValidAfter),
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: subtitleStyle,
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Table(
|
||||
defaultColumnWidth: const IntrinsicColumnWidth(),
|
||||
columnWidths: const {2: FlexColumnWidth()},
|
||||
children: [
|
||||
detailRow(l10n.s_subject, certInfo.subject),
|
||||
detailRow(l10n.s_issuer, certInfo.issuer),
|
||||
detailRow(l10n.s_serial, certInfo.serial),
|
||||
detailRow(l10n.s_certificate_fingerprint,
|
||||
certInfo.fingerprint),
|
||||
detailRow(
|
||||
l10n.s_valid_from,
|
||||
DateFormat.yMMMEd().format(
|
||||
DateTime.parse(certInfo.notValidBefore)),
|
||||
),
|
||||
detailRow(
|
||||
l10n.s_valid_to,
|
||||
DateFormat.yMMMEd().format(
|
||||
DateTime.parse(certInfo.notValidAfter)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
Padding(
|
||||
@ -98,8 +143,8 @@ class SlotDialog extends ConsumerWidget {
|
||||
style: subtitleStyle,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -33,14 +33,21 @@ class AppTheme {
|
||||
).copyWith(
|
||||
primary: primaryBlue,
|
||||
//secondary: accentGreen,
|
||||
secondary: Colors.grey.shade400,
|
||||
onSecondary: Colors.grey.shade800,
|
||||
tertiary: amber.withOpacity(0.7),
|
||||
error: darkRed,
|
||||
onError: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
textTheme: TextTheme(
|
||||
bodySmall: TextStyle(color: Colors.grey.shade900),
|
||||
bodySmall: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
dialogTheme: const DialogTheme(
|
||||
surfaceTintColor: Colors.white70,
|
||||
),
|
||||
tooltipTheme: const TooltipThemeData(
|
||||
waitDuration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
|
||||
static ThemeData get darkTheme => ThemeData(
|
||||
@ -52,8 +59,7 @@ class AppTheme {
|
||||
).copyWith(
|
||||
primary: primaryGreen,
|
||||
//onPrimary: Colors.grey.shade900,
|
||||
//secondary: accentGreen,
|
||||
//secondary: const Color(0xff5d7d90),
|
||||
secondary: Colors.grey.shade400,
|
||||
//onSecondary: Colors.grey.shade900,
|
||||
//primaryContainer: Colors.grey.shade800,
|
||||
//onPrimaryContainer: Colors.grey.shade100,
|
||||
@ -67,6 +73,9 @@ class AppTheme {
|
||||
dialogTheme: DialogTheme(
|
||||
surfaceTintColor: Colors.grey.shade700,
|
||||
),
|
||||
tooltipTheme: const TooltipThemeData(
|
||||
waitDuration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
|
||||
/* TODO: Remove this. It is left here as a reference as we adjust styles to work with Flutter 3.7.
|
||||
|
51
lib/widgets/tooltip_if_truncated.dart
Normal file
51
lib/widgets/tooltip_if_truncated.dart
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
class TooltipIfTruncated extends StatelessWidget {
|
||||
final String text;
|
||||
final TextStyle style;
|
||||
const TooltipIfTruncated(
|
||||
{super.key, required this.text, required this.style});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final textWidget = Text(
|
||||
text,
|
||||
textAlign: TextAlign.left,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
style: style,
|
||||
);
|
||||
final TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(text: text, style: style),
|
||||
textDirection: TextDirection.ltr,
|
||||
maxLines: 1,
|
||||
)..layout(minWidth: 0, maxWidth: constraints.maxWidth);
|
||||
return textPainter.didExceedMaxLines
|
||||
? Tooltip(
|
||||
margin: const EdgeInsets.all(16),
|
||||
message: text,
|
||||
child: textWidget,
|
||||
)
|
||||
: textWidget;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user