diff --git a/lib/api/privacy_idea_container_api.dart b/lib/api/privacy_idea_container_api.dart index 65a1d69d3..322d85329 100644 --- a/lib/api/privacy_idea_container_api.dart +++ b/lib/api/privacy_idea_container_api.dart @@ -38,6 +38,7 @@ import '../model/riverpod_states/token_state.dart'; import '../model/token_container.dart'; import '../model/token_template.dart'; import '../model/tokens/token.dart'; +import '../utils/app_info_utils.dart'; import '../utils/globals.dart'; import '../utils/identifiers.dart'; import '../utils/logger.dart'; @@ -116,14 +117,16 @@ class PrivacyIdeaContainerApi { '|${container.timestamp.toIso8601String().replaceFirst('Z', '+00:00')}' '|${container.finalizationUrl}' '|${container.serial}' + '|${AppInfoUtils.deviceId}' '${passphrase != null ? '|$passphrase' : ''}'; final signature = eccUtils.signWithPrivateKey(ecPrivateClientKey, message); final body = { - 'container_serial': container.serial, - 'public_client_key': container.publicClientKey, - 'signature': signature, + CONTAINER_CONTAINER_SERIAL: container.serial, + CONTAINER_PUBLIC_CLIENT_KEY: container.publicClientKey, + CONTAINER_DEVICE_ID: AppInfoUtils.deviceId, + CONTAINER_SIGNATURE: signature, }; return await _ioClient.doPost(url: container.finalizationUrl, body: body, sslVerify: false); //TODO: sslVerify } @@ -168,8 +171,8 @@ class PrivacyIdeaContainerApi { final containerDict = { CONTAINER_DICT_SERIAL: container.serial, - CONTAINER_DICT_TYPE: 'smartphone', - 'tokens': otpAuthMaps, + CONTAINER_DICT_TYPE: CONTAINER_DICT_TYPE_SMARTPHONE, + CONTAINER_DICT_TOKENS: otpAuthMaps, }; final signMessage = '${challenge.nonce}|${challenge.timeStamp}|${container.serial}|${challenge.finalizeSyncUrl}|$publicKeyBase64|${jsonEncode(containerDict)}'; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e86c4d1b7..e9cf05f51 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1298,7 +1298,7 @@ "exportingTokens": "Exporting tokens...", "failedToFinalizeContainer": "Failed to finalize the container {serial}", "failedToLoad": "Failed to load: \"{name}\"", - "failedToSyncContainer": "Failes to sync container {serial}", + "failedToSyncContainer": "Failed to sync container {serial}", "feedback": "Feedback", "feedbackDescription": "If you have any questions, suggestions or problems, please let us know.", "feedbackHint": "A ready-made e-mail will open, which you can send to us. If desired, information about your device and the version of the application will be added. You can check and edit the email before sending it.", diff --git a/lib/model/exception_errors/response_error.dart b/lib/model/exception_errors/response_error.dart index 3e4a2b1f0..9fa74e121 100644 --- a/lib/model/exception_errors/response_error.dart +++ b/lib/model/exception_errors/response_error.dart @@ -34,4 +34,9 @@ class ResponseError { assert(response.statusCode != 200, 'Status code of an response error should not be 200'); return ResponseError._(response.statusCode, response.body); } + + @override + String toString() { + return '$statusCode: $message'; + } } diff --git a/lib/repo/secure_token_repository.dart b/lib/repo/secure_token_repository.dart index 8a37247e7..f4858f367 100644 --- a/lib/repo/secure_token_repository.dart +++ b/lib/repo/secure_token_repository.dart @@ -225,7 +225,7 @@ class SecureTokenRepository implements TokenRepository { child: SizedBox( height: 50, width: 50, - child: CircularProgressIndicator(), + child: CircularProgressIndicator.adaptive(), ), ), ); diff --git a/lib/utils/app_info_utils.dart b/lib/utils/app_info_utils.dart index eade7e225..022a91174 100644 --- a/lib/utils/app_info_utils.dart +++ b/lib/utils/app_info_utils.dart @@ -39,6 +39,7 @@ class AppInfoUtils { _appBuildNumber = packageInfo.buildNumber; _androidInfo = !kIsWeb && Platform.isAndroid ? await _deviceInfo.androidInfo : null; _iosInfo = !kIsWeb && Platform.isIOS ? await _deviceInfo.iosInfo : null; + _deviceId = !kIsWeb ? (Platform.isAndroid ? _androidInfo!.id : _iosInfo!.identifierForVendor ?? 'N/A') : 'Web: Not available.'; isInitialized = true; } @@ -58,6 +59,9 @@ class AppInfoUtils { static String get currentBuildNumber => isInitialized ? _appBuildNumber : throw Exception('AppInfoUtils not initialized'); static late final String _appBuildNumber; + static String get deviceId => isInitialized ? _deviceId : throw Exception('AppInfoUtils not initialized'); + static late final String _deviceId; + static String get dartVersion => Platform.version; static String get platform => Platform.operatingSystem; diff --git a/lib/utils/firebase_utils.dart b/lib/utils/firebase_utils.dart index b6664d7ad..faf1edb05 100644 --- a/lib/utils/firebase_utils.dart +++ b/lib/utils/firebase_utils.dart @@ -66,9 +66,10 @@ class FirebaseUtils { ); String errorMessage = e.message ?? 'no error message'; final SnackBar snackBar = SnackBar( + behavior: SnackBarBehavior.floating, content: Text( - "Firebase notification permission error! ($errorMessage: ${e.code}", - )); + "Firebase notification permission error! ($errorMessage: ${e.code}", + )); globalSnackbarKey.currentState?.showSnackBar(snackBar); } @@ -87,28 +88,31 @@ class FirebaseUtils { } else { String errorMessage = error.message ?? 'no error message'; final SnackBar snackBar = SnackBar( + behavior: SnackBarBehavior.floating, content: Text( - 'Push cant be initialized, restart the app and try again. ${error.code}: $errorMessage', - overflow: TextOverflow.fade, - softWrap: false, - )); + 'Push cant be initialized, restart the app and try again. ${error.code}: $errorMessage', + overflow: TextOverflow.fade, + softWrap: false, + )); globalSnackbarKey.currentState?.showSnackBar(snackBar); } } on FirebaseException catch (error) { final SnackBar snackBar = SnackBar( + behavior: SnackBarBehavior.floating, content: Text( - "Push cant be initialized, restart the app and try again$error", - overflow: TextOverflow.fade, - softWrap: false, - )); + "Push cant be initialized, restart the app and try again$error", + overflow: TextOverflow.fade, + softWrap: false, + )); globalSnackbarKey.currentState?.showSnackBar(snackBar); } catch (error) { final SnackBar snackBar = SnackBar( + behavior: SnackBarBehavior.floating, content: Text( - "Unknown error: $error", - overflow: TextOverflow.fade, - softWrap: false, - )); + "Unknown error: $error", + overflow: TextOverflow.fade, + softWrap: false, + )); globalSnackbarKey.currentState?.showSnackBar(snackBar); } @@ -120,11 +124,12 @@ class FirebaseUtils { updateFirebaseToken(newToken); } catch (error) { final SnackBar snackBar = SnackBar( + behavior: SnackBarBehavior.floating, content: Text( - "Unknown error: $error", - overflow: TextOverflow.fade, - softWrap: false, - )); + "Unknown error: $error", + overflow: TextOverflow.fade, + softWrap: false, + )); globalSnackbarKey.currentState?.showSnackBar(snackBar); } } diff --git a/lib/utils/identifiers.dart b/lib/utils/identifiers.dart index 390d80ea1..b5d42a439 100644 --- a/lib/utils/identifiers.dart +++ b/lib/utils/identifiers.dart @@ -120,6 +120,11 @@ const String CONTAINER_EC_KEY_ALGORITHM = 'key_algorithm'; const String CONTAINER_HASH_ALGORITHM = 'hash_algorithm'; const String CONTAINER_PASSPHRASE_QUESTION = 'passphrase'; +const String CONTAINER_CONTAINER_SERIAL = 'container_serial'; +const String CONTAINER_PUBLIC_CLIENT_KEY = 'public_client_key'; +const String CONTAINER_DEVICE_ID = 'device_id'; +const String CONTAINER_SIGNATURE = 'signature'; + // Container sync: const String CONTAINER_SYNC_NONCE = 'nonce'; const String CONTAINER_SYNC_TIMESTAMP = 'time_stamp'; @@ -132,7 +137,9 @@ const String CONTAINER_SYNC_DICT_CLIENT = 'container_dict_client'; const String CONTAINER_DICT_SERIAL = 'serial'; const String CONTAINER_DICT_TYPE = 'type'; +const String CONTAINER_DICT_TYPE_SMARTPHONE = 'smartphone'; const String CONTAINER_DICT_TOKENS = 'tokens'; + const String CONTAINER_DICT_TOKENS_ADD = 'add'; const String CONTAINER_DICT_TOKENS_UPDATE = 'update'; diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index 8279dfcf4..5a2f9a1f6 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -260,6 +260,7 @@ Device Parameters $deviceInfo"""; await file.writeAsString('', mode: FileMode.write); globalSnackbarKey.currentState?.showSnackBar( SnackBar( + behavior: SnackBarBehavior.floating, content: Text( _context != null ? AppLocalizations.of(_context!)!.errorLogCleared : 'Error Log Cleared', overflow: TextOverflow.fade, @@ -369,6 +370,7 @@ Device Parameters $deviceInfo"""; WidgetsBinding.instance.addPostFrameCallback((_) { globalSnackbarKey.currentState?.showSnackBar( SnackBar( + behavior: SnackBarBehavior.floating, content: Text( _context != null ? AppLocalizations.of(_context!)!.unexpectedError : 'An unexpected error occurred.', ), diff --git a/lib/utils/view_utils.dart b/lib/utils/view_utils.dart index e4d705536..ceb0e4d11 100644 --- a/lib/utils/view_utils.dart +++ b/lib/utils/view_utils.dart @@ -33,7 +33,11 @@ void showMessage({ return; } globalSnackbarKey.currentState!.showSnackBar( - SnackBar(content: Text(message), duration: duration), + SnackBar( + behavior: SnackBarBehavior.floating, + content: Text(message), + duration: duration, + ), ); } diff --git a/lib/views/container_view/container_widgets/container_widget.dart b/lib/views/container_view/container_widgets/container_widget.dart index 070aca621..08463fbd1 100644 --- a/lib/views/container_view/container_widgets/container_widget.dart +++ b/lib/views/container_view/container_widgets/container_widget.dart @@ -19,19 +19,13 @@ */ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; -import 'package:privacyidea_authenticator/model/extensions/enums/rollout_state_extension.dart'; -import 'package:privacyidea_authenticator/widgets/button_widgets/cooldown_button.dart'; -import '../../../model/enums/sync_state.dart'; import '../../../model/token_container.dart'; -import '../../../utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; -import '../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../../widgets/pi_slidable.dart'; -import '../../main_view/main_view_widgets/token_widgets/token_widget_tile.dart'; import '../container_view.dart'; import 'container_actions/delete_container_action.dart'; import 'container_actions/details_container_action.dart'; +import 'container_widget_tile.dart'; class ContainerWidget extends ConsumerWidget { final TokenContainer container; @@ -56,43 +50,7 @@ class ContainerWidget extends ConsumerWidget { DetailsContainerAction(container: container, key: Key('${container.serial}-EditContainerAction')), ], stack: stack, - child: TokenWidgetTile( - title: Text( - container.serial, - style: Theme.of(context).textTheme.titleMedium, - ), - subtitles: [ - AppLocalizations.of(context)!.issuerLabel(container.issuer), - '${container.finalizationState.rolloutMsgLocalized(AppLocalizations.of(context)!)}', - ], - trailing: _getTrailing(context, ref), - ), + child: ContainerWidgetTile(container: container), ), ); - - Widget _getTrailing(BuildContext context, WidgetRef ref) { - if (container is TokenContainerFinalized) { - return CooldownButton( - styleType: CooldownButtonStyleType.iconButton, - childWhenCooldown: CircularProgressIndicator(), - isPressable: (container as TokenContainerFinalized).syncState != SyncState.syncing, - onPressed: () async { - final tokenState = ref.read(tokenProvider); - await ref.read(tokenContainerProvider.notifier).syncTokens(tokenState, containersToSync: [container as TokenContainerFinalized], isManually: true); - }, - child: (container as TokenContainerFinalized).syncState == SyncState.failed ? const Icon(Icons.sync_problem) : const Icon(Icons.sync), - ); - } - if (container.finalizationState.isFailed) { - return CooldownButton( - styleType: CooldownButtonStyleType.iconButton, - childWhenCooldown: CircularProgressIndicator(), - onPressed: () async { - await ref.read(tokenContainerProvider.notifier).finalize(container, isManually: true); - }, - child: const Icon(Icons.link_rounded), - ); - } - return CircularProgressIndicator(); - } } diff --git a/lib/views/container_view/container_widgets/container_widget_tile.dart b/lib/views/container_view/container_widgets/container_widget_tile.dart new file mode 100644 index 000000000..f3f2036ea --- /dev/null +++ b/lib/views/container_view/container_widgets/container_widget_tile.dart @@ -0,0 +1,111 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2024 NetKnights GmbH + * + * 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_riverpod/flutter_riverpod.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/rollout_state_extension.dart'; + +import '../../../l10n/app_localizations.dart'; +import '../../../model/enums/sync_state.dart'; +import '../../../model/token_container.dart'; +import '../../../utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; +import '../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; +import '../../../widgets/button_widgets/cooldown_button.dart'; + +class ContainerWidgetTile extends ConsumerWidget { + final TokenContainer container; + + const ContainerWidgetTile({required this.container, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 2), + titleAlignment: ListTileTitleAlignment.center, + title: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.topLeft, + child: Align( + alignment: Alignment.centerLeft, + child: Tooltip( + message: AppLocalizations.of(context)!.containerSerial, + triggerMode: TooltipTriggerMode.longPress, + child: Text( + container.serial, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ), + ), + subtitle: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + for (var line in [ + AppLocalizations.of(context)!.issuerLabel(container.issuer), + '${container.finalizationState.rolloutMsgLocalized(AppLocalizations.of(context)!)}', + ]) + Text( + line, + style: Theme.of(context).listTileTheme.subtitleTextStyle, + textAlign: TextAlign.left, + overflow: TextOverflow.fade, + softWrap: false, + ), + ], + ), + ), + ), + ], + ), + trailing: _getTrailing(context, ref), + ); + + Widget _getTrailing(BuildContext context, WidgetRef ref) { + if (container is TokenContainerFinalized) { + return CooldownButton( + styleType: CooldownButtonStyleType.iconButton, + childWhenCooldown: CircularProgressIndicator.adaptive(), + isPressable: (container as TokenContainerFinalized).syncState != SyncState.syncing, + onPressed: () async { + final tokenState = ref.read(tokenProvider); + await ref.read(tokenContainerProvider.notifier).syncTokens(tokenState, containersToSync: [container as TokenContainerFinalized], isManually: true); + }, + child: (container as TokenContainerFinalized).syncState == SyncState.failed ? const Icon(Icons.sync_problem) : const Icon(Icons.sync), + ); + } + if (container.finalizationState.isFailed) { + return CooldownButton( + styleType: CooldownButtonStyleType.iconButton, + childWhenCooldown: CircularProgressIndicator.adaptive(), + onPressed: () async { + await ref.read(tokenContainerProvider.notifier).finalize(container, isManually: true); + }, + child: const Icon(Icons.link_rounded), + ); + } + return CircularProgressIndicator.adaptive(); + } +} diff --git a/lib/views/import_tokens_view/pages/import_start_page.dart b/lib/views/import_tokens_view/pages/import_start_page.dart index 84d52afad..2ef7a1e38 100644 --- a/lib/views/import_tokens_view/pages/import_start_page.dart +++ b/lib/views/import_tokens_view/pages/import_start_page.dart @@ -139,7 +139,7 @@ class _ImportStartPageState extends ConsumerState { }, ), ) - : const CircularProgressIndicator(), + : const CircularProgressIndicator.adaptive(), ], ), ), diff --git a/lib/views/import_tokens_view/widgets/dialogs/qr_not_found_dialog.dart b/lib/views/import_tokens_view/widgets/dialogs/qr_not_found_dialog.dart index 0b3ba27bb..f366eb6f4 100644 --- a/lib/views/import_tokens_view/widgets/dialogs/qr_not_found_dialog.dart +++ b/lib/views/import_tokens_view/widgets/dialogs/qr_not_found_dialog.dart @@ -73,7 +73,12 @@ class QrNotFoundDialog extends StatelessWidget { } catch (e) { if (!context.mounted) return; Navigator.of(context).pop(); - globalSnackbarKey.currentState?.showSnackBar(const SnackBar(content: Text("File not currently available! Please try again."))); + globalSnackbarKey.currentState?.showSnackBar( + const SnackBar( + behavior: SnackBarBehavior.floating, + content: Text("File not currently available! Please try again."), + ), + ); return; } if (!context.mounted) return; diff --git a/lib/views/main_view/main_view_widgets/token_widgets/container_token_sync_icon.dart b/lib/views/main_view/main_view_widgets/token_widgets/container_token_sync_icon.dart new file mode 100644 index 000000000..c2ca3dded --- /dev/null +++ b/lib/views/main_view/main_view_widgets/token_widgets/container_token_sync_icon.dart @@ -0,0 +1,47 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2024 NetKnights GmbH + * + * 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_riverpod/flutter_riverpod.dart'; + +import '../../../../model/enums/sync_state.dart'; +import '../../../../model/tokens/token.dart'; +import '../../../../utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; + +class ContainerTokenSyncIcon extends ConsumerWidget { + final Token token; + + const ContainerTokenSyncIcon(this.token, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final syncState = ref.watch(tokenContainerProvider).valueOrNull?.getSyncState(token); + if (syncState == null) return const SizedBox.shrink(); + final color = Theme.of(context).listTileTheme.subtitleTextStyle?.color ?? Colors.grey; + return Icon( + color: color, + switch (syncState) { + SyncState.notStarted => Icons.sync, + SyncState.syncing => Icons.cloud_sync_outlined, + SyncState.failed => Icons.cloud_off_outlined, + SyncState.completed => Icons.cloud_done_outlined, + }, + ); + } +} diff --git a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart index 84925afd9..a4c9e6444 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart @@ -31,7 +31,6 @@ import '../../../../../model/tokens/day_password_token.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../../../../utils/utils.dart'; -import '../../../../../widgets/custom_texts.dart'; import '../../../../../widgets/custom_trailing.dart'; import '../../../../../widgets/hideable_widget_.dart'; import '../token_widget_tile.dart'; @@ -78,6 +77,7 @@ class _DayPasswordTokenWidgetTileState extends ConsumerState await ref.read(tokenProvider.notifier).showToken(widget.token) - : _copyOtpValue, - child: HideableText( - text: insertCharAt(widget.token.otpValue, ' ', (widget.token.digits / 2).ceil()), - textScaleFactor: 1.9, - enabled: widget.token.isLocked, - isHidden: widget.token.isHidden), - ), - ), - ), - subtitles: widget.isPreview + token: widget.token, + titleTooltip: widget.token.isHidden ? AppLocalizations.of(context)!.authenticateToShowOtp : AppLocalizations.of(context)!.copyOTPToClipboard, + titleOnTap: widget.isPreview + ? null + : widget.token.isLocked && widget.token.isHidden + ? () async => await ref.read(tokenProvider.notifier).showToken(widget.token) + : _copyOtpValue, + title: insertCharAt(widget.token.otpValue, ' ', (widget.token.digits / 2).ceil()), + additionalSubtitles: widget.isPreview ? [ - (widget.token.label.isNotEmpty && widget.token.issuer.isNotEmpty) - ? '${widget.token.issuer}: ${widget.token.label}' - : widget.token.issuer + widget.token.label, 'Algorithm: ${widget.token.algorithm.name}', 'Period: ${widget.token.period.toString().split('.').first}', ] - : [ - if (widget.token.label.isNotEmpty) widget.token.label, - if (widget.token.issuer.isNotEmpty) widget.token.issuer, - ], + : [], trailing: SizedBox( height: double.maxFinite, child: CustomTrailing( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart index 5a9bbd30a..9f87e4a29 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart @@ -26,7 +26,6 @@ import '../../../../../model/tokens/hotp_token.dart'; import '../../../../../utils/globals.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../../../../utils/utils.dart'; -import '../../../../../widgets/custom_texts.dart'; import '../../../../../widgets/custom_trailing.dart'; import '../../../../../widgets/hideable_widget_.dart'; import '../token_widget_tile.dart'; @@ -72,6 +71,7 @@ class _HOTPTokenWidgetTileState extends ConsumerState { Clipboard.setData(ClipboardData(text: widget.token.otpValue)); ScaffoldMessenger.of(context).showSnackBar( SnackBar( + behavior: SnackBarBehavior.floating, content: Text(AppLocalizations.of(context)!.otpValueCopiedMessage(widget.token.otpValue)), ), ); @@ -83,41 +83,20 @@ class _HOTPTokenWidgetTileState extends ConsumerState { @override Widget build(BuildContext context) => TokenWidgetTile( key: Key('${widget.token.hashCode}TokenWidgetTile'), - tokenImage: widget.token.tokenImage, - tokenIsLocked: widget.token.isLocked, - isPreview: widget.isPreview, - title: Align( - alignment: Alignment.centerLeft, - child: Tooltip( - message: widget.token.isHidden ? AppLocalizations.of(context)!.authenticateToShowOtp : AppLocalizations.of(context)!.copyOTPToClipboard, - triggerMode: TooltipTriggerMode.longPress, - child: InkWell( - onTap: widget.isPreview - ? null - : widget.token.isLocked && widget.token.isHidden - ? () async => await ref.read(tokenProvider.notifier).showToken(widget.token) - : _copyOtpValue, - child: HideableText( - textScaleFactor: 1.9, - isHidden: widget.token.isHidden, - text: insertCharAt(widget.token.otpValue, ' ', (widget.token.digits / 2).ceil()), - enabled: widget.token.isLocked, - ), - ), - ), - ), - subtitles: widget.isPreview + token: widget.token, + titleTooltip: widget.token.isHidden ? AppLocalizations.of(context)!.authenticateToShowOtp : AppLocalizations.of(context)!.copyOTPToClipboard, + titleOnTap: widget.isPreview + ? null + : widget.token.isLocked && widget.token.isHidden + ? () async => await ref.read(tokenProvider.notifier).showToken(widget.token) + : _copyOtpValue, + title: insertCharAt(widget.token.otpValue, ' ', (widget.token.digits / 2).ceil()), + additionalSubtitles: widget.isPreview ? [ - (widget.token.label.isNotEmpty && widget.token.issuer.isNotEmpty) - ? '${widget.token.issuer}: ${widget.token.label}' - : '${widget.token.issuer}${widget.token.label}', 'Algorithm: ${widget.token.algorithm.name}', 'Counter: ${widget.token.counter}', ] - : [ - if (widget.token.label.isNotEmpty) widget.token.label, - if (widget.token.issuer.isNotEmpty) widget.token.issuer, - ], + : [], trailing: CustomTrailing( child: widget.isPreview ? FittedBox( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart index 23f5aaff6..99d7ae9d1 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart @@ -37,18 +37,9 @@ class PushTokenWidgetTile extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return TokenWidgetTile( key: Key('${token.hashCode}TokenWidgetTile'), - tokenIsLocked: token.isLocked, - tokenImage: token.tokenImage, - isPreview: isPreview, - title: Text( - token.label, - textScaler: const TextScaler.linear(1.9), - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - subtitles: [ - if (token.issuer.isNotEmpty) token.issuer, - ], + token: token, + title: token.label, + titleTooltip: AppLocalizations.of(context)!.containerSerial, trailing: FocusedItemAsOverlay( tooltipWhenFocused: AppLocalizations.of(context)!.introPollForChallenges, alignment: Alignment.centerLeft, diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart index 64dde0d11..8a1227b25 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart @@ -31,7 +31,7 @@ class RolloutWidget extends StatelessWidget { Widget build(BuildContext context) => Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const CircularProgressIndicator(), + const CircularProgressIndicator.adaptive(), Text( token.rolloutState.rolloutMsg(AppLocalizations.of(context)!), style: Theme.of(context).textTheme.bodyLarge, diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_image.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_image.dart index cf0d7905f..57fe15ba9 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_image.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_image.dart @@ -122,7 +122,7 @@ class _TokenImageState extends State { child: tokenImage ?? const SizedBox( width: 32, - child: CircularProgressIndicator(), + child: CircularProgressIndicator.adaptive(), ), )) : const SizedBox(); diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart index 1564c5cd0..801210d3d 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart @@ -21,9 +21,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:privacyidea_authenticator/model/enums/sync_state.dart'; import 'package:privacyidea_authenticator/utils/default_inkwell.dart'; -import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; import '../../../../model/mixins/sortable_mixin.dart'; import '../../../../model/tokens/token.dart'; @@ -92,37 +90,13 @@ class TokenWidgetBase extends ConsumerWidget { lockAction ?? DefaultLockAction(token: token, key: Key('${token.id}lockAction')), ); } - final syncState = ref.watch(tokenContainerProvider).valueOrNull?.getSyncState(token); if (draggingSortable == token) return const SizedBox(); final child = Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: DefaultInkWell( onTap: () {}, - child: Stack( - children: [ - if (syncState != null) - Positioned.fill( - top: 8, - bottom: 8, - left: 0, - right: 0, - child: FittedBox( - fit: BoxFit.contain, - child: Opacity( - opacity: 0.05, - child: switch (syncState) { - SyncState.notStarted => const Icon(Icons.sync), - SyncState.syncing => const Icon(Icons.cloud_sync_outlined), - SyncState.failed => const Icon(Icons.cloud_off_outlined), - SyncState.completed => const Icon(Icons.cloud_done_outlined), - }, - ), - ), - ), - tile, - ], - ), + child: tile, ), ); diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart index 62ec542a7..283e18a56 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart @@ -19,72 +19,110 @@ */ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacyidea_authenticator/views/main_view/main_view_widgets/token_widgets/container_token_sync_icon.dart'; +import '../../../../model/tokens/token.dart'; +import '../../../../widgets/custom_texts.dart'; import 'token_image.dart'; final disableCopyOtpProvider = StateProvider((ref) => false); class TokenWidgetTile extends ConsumerWidget { - final Widget? title; - final List subtitles; + final Token token; + final String title; + final List additionalSubtitles; final Widget? leading; final Widget? trailing; final Function()? onTap; - final bool tokenIsLocked; - final String? tokenImage; - - final bool isPreview; + final String titleTooltip; + final Function()? titleOnTap; const TokenWidgetTile({ + required this.token, + required this.title, + required this.titleTooltip, + this.additionalSubtitles = const [], this.leading, - this.title, - this.subtitles = const [], this.trailing, this.onTap, - this.tokenIsLocked = false, - this.tokenImage, - this.isPreview = false, + this.titleOnTap, super.key, }); @override - Widget build(BuildContext context, WidgetRef ref) => ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 2), - titleAlignment: ListTileTitleAlignment.center, - horizontalTitleGap: isPreview ? 0 : 8, - leading: (leading != null) ? leading! : null, - onTap: onTap, - title: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.topLeft, - child: title, + Widget build(BuildContext context, WidgetRef ref) { + String subtitle1 = ''; + if (token.label != title) subtitle1 = token.label; + String subtitle2 = token.issuer; + final subtitles = [...additionalSubtitles]; + if (subtitle1.isEmpty && additionalSubtitles.length < 2) subtitles.add(''); + if (subtitle2.isEmpty && additionalSubtitles.length < 2) subtitles.add(''); + + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 2), + titleAlignment: ListTileTitleAlignment.center, + leading: (leading != null) ? leading! : null, + onTap: onTap, + title: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.topLeft, + child: Align( + alignment: Alignment.centerLeft, + child: Tooltip( + message: titleTooltip, + triggerMode: TooltipTriggerMode.longPress, + child: InkWell( + onTap: titleOnTap, + child: HideableText(textScaleFactor: 1.9, isHidden: token.isHidden, text: title), + ), + ), ), - subtitle: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TokenImage(tokenImage: tokenImage), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - for (var line in subtitles) - Text( - line, - style: Theme.of(context).listTileTheme.subtitleTextStyle, - textAlign: TextAlign.left, - overflow: TextOverflow.fade, - softWrap: false, - ), - for (var i = 0; i < 2 - subtitles.length; i++) Text(''), - ], - ), + ), + subtitle: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TokenImage(tokenImage: token.tokenImage), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + if (subtitle1.isNotEmpty) + Text( + subtitle1, + textAlign: TextAlign.left, + overflow: TextOverflow.fade, + softWrap: false, + ), + if (subtitle2.isNotEmpty) + Row( + children: [ + Text( + subtitle2, + textAlign: TextAlign.left, + overflow: TextOverflow.fade, + softWrap: false, + ), + SizedBox(width: 6), + ContainerTokenSyncIcon(token), + ], + ), + for (var line in additionalSubtitles) + Text( + line, + textAlign: TextAlign.left, + overflow: TextOverflow.fade, + softWrap: false, + ), + ], ), ), - ], - ), - trailing: trailing ?? const SizedBox(), - ); + ), + ], + ), + trailing: trailing ?? const SizedBox(), + ); + } } diff --git a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart index 1c3544750..5bf16cfdc 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart @@ -26,7 +26,6 @@ import '../../../../../model/tokens/totp_token.dart'; import '../../../../../utils/globals.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../../../../utils/utils.dart'; -import '../../../../../widgets/custom_texts.dart'; import '../../../../../widgets/custom_trailing.dart'; import '../../../../../widgets/hideable_widget_.dart'; import '../token_widget_tile.dart'; @@ -49,7 +48,7 @@ class _TOTPTokenWidgetTileState extends ConsumerState { globalRef?.read(disableCopyOtpProvider.notifier).state = true; Clipboard.setData(ClipboardData(text: widget.token.otpValue)); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context)!.otpValueCopiedMessage(widget.token.otpValue))), + SnackBar(behavior: SnackBarBehavior.floating, content: Text(AppLocalizations.of(context)!.otpValueCopiedMessage(widget.token.otpValue))), ); Future.delayed(const Duration(seconds: 5), () => globalRef?.read(disableCopyOtpProvider.notifier).state = false); } @@ -59,43 +58,21 @@ class _TOTPTokenWidgetTileState extends ConsumerState { @override Widget build(BuildContext context) { return TokenWidgetTile( - isPreview: widget.isPreview, key: Key('${widget.token.hashCode}TokenWidgetTile'), - tokenImage: widget.token.tokenImage, - tokenIsLocked: widget.token.isLocked, - title: Align( - alignment: Alignment.centerLeft, - child: Tooltip( - message: widget.token.isHidden ? AppLocalizations.of(context)!.authenticateToShowOtp : AppLocalizations.of(context)!.copyOTPToClipboard, - triggerMode: TooltipTriggerMode.longPress, - child: InkWell( - onTap: widget.isPreview - ? null - : widget.token.isLocked && widget.token.isHidden - ? () async => await ref.read(tokenProvider.notifier).showToken(widget.token) - : () => _copyOtpValue(context), - child: HideableText( - key: Key(widget.token.hashCode.toString()), - text: insertCharAt(widget.token.otpValue, ' ', (widget.token.digits / 2).ceil()), - textScaleFactor: 1.9, - enabled: widget.token.isLocked, - isHidden: widget.token.isHidden, - ), - ), - ), - ), - subtitles: widget.isPreview + titleTooltip: widget.token.isHidden ? AppLocalizations.of(context)!.authenticateToShowOtp : AppLocalizations.of(context)!.copyOTPToClipboard, + titleOnTap: widget.isPreview + ? null + : widget.token.isLocked && widget.token.isHidden + ? () async => await ref.read(tokenProvider.notifier).showToken(widget.token) + : () => _copyOtpValue(context), + token: widget.token, + title: insertCharAt(widget.token.otpValue, ' ', (widget.token.digits / 2).ceil()), + additionalSubtitles: widget.isPreview ? [ - (widget.token.label.isNotEmpty && widget.token.issuer.isNotEmpty) - ? '${widget.token.issuer}: ${widget.token.label}' - : widget.token.issuer + widget.token.label, 'Algorithm: ${widget.token.algorithm.name}', 'Period: ${widget.token.period} seconds', ] - : [ - if (widget.token.label.isNotEmpty) widget.token.label, - if (widget.token.issuer.isNotEmpty) widget.token.issuer, - ], + : [], trailing: CustomTrailing( child: HideableWidget( token: widget.token, diff --git a/lib/views/settings_view/settings_view_widgets/errorlog_buttons/show_errorlog_button.dart b/lib/views/settings_view/settings_view_widgets/errorlog_buttons/show_errorlog_button.dart index 8e685a74c..2bcc38e41 100644 --- a/lib/views/settings_view/settings_view_widgets/errorlog_buttons/show_errorlog_button.dart +++ b/lib/views/settings_view/settings_view_widgets/errorlog_buttons/show_errorlog_button.dart @@ -77,7 +77,7 @@ void _pressShowErrorLog(BuildContext context) { errorLog.data.toString(), style: const TextStyle(fontFamily: 'monospace', fontSize: 8), ) - : const CircularProgressIndicator(), + : const CircularProgressIndicator.adaptive(), ); }), ), diff --git a/lib/widgets/custom_texts.dart b/lib/widgets/custom_texts.dart index 909c58191..f75db315f 100644 --- a/lib/widgets/custom_texts.dart +++ b/lib/widgets/custom_texts.dart @@ -26,10 +26,7 @@ import 'package:flutter/material.dart'; /// If [hideOnDefault] is true, the [text] is obfuscated, if set to false the /// [text] is visible to the user. /// [textScaleFactor] mirrors the field of the [Text] widget. -/// [showDuration] specifies how long the [text] should be shown to the user -/// before it is hidden again. /// [textStyle] mirrors the field of the [Text] widget. -/// If [enabled] is set to true, the widget can be toggled to show its content. /// [replaceCharacter] defines the character that is shown to the user instead /// of the real characters in [text]. /// If [replaceWhitespaces] is true, whitespaces in [text] are replaced by @@ -40,7 +37,6 @@ class HideableText extends StatelessWidget { final bool hideOnDefault; final double textScaleFactor; final TextStyle? textStyle; - final bool enabled; final String replaceCharacter; final bool replaceWhitespaces; final bool isHidden; @@ -52,7 +48,6 @@ class HideableText extends StatelessWidget { this.hideOnDefault = true, this.textScaleFactor = 1.0, this.textStyle, - this.enabled = true, this.replaceCharacter = '\u2022', this.replaceWhitespaces = false, }); @@ -60,7 +55,7 @@ class HideableText extends StatelessWidget { @override Widget build(BuildContext context) { return Text( - isHidden && enabled ? text.replaceAll(RegExp(replaceWhitespaces ? r'.' : r'[^\s]'), replaceCharacter) : text, + isHidden ? text.replaceAll(RegExp(replaceWhitespaces ? r'.' : r'[^\s]'), replaceCharacter) : text, textScaler: const TextScaler.linear(1.9), style: textStyle != null ? textStyle!.copyWith(fontFamily: 'monospace', fontWeight: FontWeight.bold)