diff --git a/lib/api/token_container_api_endpoint.dart b/lib/api/token_container_api_endpoint.dart index f1cf5e7a9..5a38fd887 100644 --- a/lib/api/token_container_api_endpoint.dart +++ b/lib/api/token_container_api_endpoint.dart @@ -24,6 +24,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:cryptography/cryptography.dart'; import 'package:http/http.dart'; +import 'package:privacyidea_authenticator/model/extensions/token_folder_extension.dart'; import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart'; import 'package:privacyidea_authenticator/utils/ecc_utils.dart'; import 'package:privacyidea_authenticator/utils/privacyidea_io_client.dart'; diff --git a/lib/model/extensions/token_folder_extension.dart b/lib/model/extensions/token_folder_extension.dart new file mode 100644 index 000000000..ae8999b28 --- /dev/null +++ b/lib/model/extensions/token_folder_extension.dart @@ -0,0 +1,69 @@ +/* + * 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 '../../utils/logger.dart'; +import '../enums/token_origin_source_type.dart'; +import '../token_folder.dart'; +import '../token_template.dart'; +import '../tokens/token.dart'; + +extension TokenListExtension on List { + List get piTokens { + final piTokens = where((token) => token.isPrivacyIdeaToken == true).toList(); + Logger.debug('${piTokens.length}/$length tokens with "isPrivacyIdeaToken == true"'); + return piTokens; + } + + List get nonPiTokens { + final nonPiTokens = where((token) => token.isPrivacyIdeaToken == false).toList(); + Logger.debug('${nonPiTokens.length}/$length tokens with "isPrivacyIdeaToken == false"'); + return nonPiTokens; + } + + List get maybePiTokens { + final maybePiTokens = where((token) => token.isPrivacyIdeaToken == null).toList(); + Logger.debug('${maybePiTokens.length}/$length tokens with "isPrivacyIdeaToken == null"'); + return maybePiTokens; + } + + List inFolder([TokenFolder? folder]) { + if (folder == null) return where((token) => token.folderId != null).toList(); + return where((token) => token.folderId == folder.folderId).toList(); + } + + List inNoFolder() => where((token) => token.folderId == null).toList(); + + List ofContainer(String containerSerial) { + final filtered = where((token) => token.origin?.source == TokenOriginSourceType.container && token.containerSerial == containerSerial).toList(); + Logger.debug('${filtered.length}/$length tokens with containerSerial: $containerSerial'); + return filtered; + } + + List whereNotType(List types) => where((token) => !types.contains(token.runtimeType)).toList(); + + List toTemplates() { + if (isEmpty) return []; + final templates = []; + for (var token in this) { + final template = token.toTemplate(); + if (template != null) templates.add(template); + } + return templates; + } +} diff --git a/lib/model/riverpod_states/token_state.dart b/lib/model/riverpod_states/token_state.dart index ee370e037..c88e598f5 100644 --- a/lib/model/riverpod_states/token_state.dart +++ b/lib/model/riverpod_states/token_state.dart @@ -19,11 +19,10 @@ */ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:privacyidea_authenticator/model/extensions/token_folder_extension.dart'; import '../../utils/logger.dart'; import '../enums/push_token_rollout_state.dart'; -import '../enums/token_origin_source_type.dart'; -import '../token_template.dart'; import '../token_folder.dart'; import '../tokens/otp_token.dart'; import '../tokens/push_token.dart'; @@ -172,66 +171,15 @@ class TokenState { return (TokenState(tokens: newTokens, lastlyUpdatedTokens: updatedTokens), failedToReplace); } - List tokensInFolder(TokenFolder folder, {List only = const [], List exclude = const []}) => - tokens.inFolder(folder, only: only, exclude: exclude); + List tokensInFolder([TokenFolder? folder]) => tokens.inFolder(folder); - List tokensWithoutFolder({List only = const [], List exclude = const []}) => tokens.withoutFolder(only: only, exclude: exclude); + List tokensInNoFolder() => tokens.inNoFolder(); List containerTokens(String containerSerial) { final piTokens = tokens.piTokens; - Logger.debug('PiTokens: ${piTokens}'); + Logger.debug('PiTokens: $piTokens'); final containerTokens = piTokens.ofContainer(containerSerial); Logger.debug('${containerTokens.length}/${piTokens.length} tokens with containerSerial: $containerSerial'); return containerTokens; } } - -extension TokenListExtension on List { - List get piTokens { - final piTokens = where((token) => token.isPrivacyIdeaToken == true).toList(); - Logger.debug('${piTokens.length}/$length tokens with "isPrivacyIdeaToken == true"'); - return piTokens; - } - - List get nonPiTokens { - final nonPiTokens = where((token) => token.isPrivacyIdeaToken == false).toList(); - Logger.debug('${nonPiTokens.length}/$length tokens with "isPrivacyIdeaToken == false"'); - return nonPiTokens; - } - - List get maybePiTokens { - final maybePiTokens = where((token) => token.isPrivacyIdeaToken == null).toList(); - Logger.debug('${maybePiTokens.length}/$length tokens with "isPrivacyIdeaToken == null"'); - return maybePiTokens; - } - - List inFolder(TokenFolder folder, {List only = const [], List exclude = const []}) => where((token) { - if (token.folderId != folder.folderId) return false; - if (exclude.contains(token.runtimeType)) return false; - if (only.isNotEmpty && !only.contains(token.runtimeType)) return false; - return true; - }).toList(); - - List withoutFolder({List only = const [], List exclude = const []}) => where((token) { - if (token.folderId != null) return false; - if (exclude.contains(token.runtimeType)) return false; - if (only.isNotEmpty && !only.contains(token.runtimeType)) return false; - return true; - }).toList(); - - List ofContainer(String containerSerial) { - final filtered = where((token) => token.origin?.source == TokenOriginSourceType.container && token.containerSerial == containerSerial).toList(); - Logger.debug('${filtered.length}/$length tokens with containerSerial: $containerSerial'); - return filtered; - } - - List toTemplates() { - if (isEmpty) return []; - final templates = []; - for (var token in this) { - final template = token.toTemplate(); - if (template != null) templates.add(template); - } - return templates; - } -} diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart index 31bf3167a..418366ed5 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart @@ -32,6 +32,7 @@ import '../../../logger.dart'; part 'settings_notifier.g.dart'; final settingsProvider = settingsNotifierProviderOf(repo: PreferenceSettingsRepository()); +final hidePushTokensProvider = settingsProvider.select((asyncValue) => asyncValue.value?.hidePushTokens ?? false); @Riverpod(keepAlive: true) class SettingsNotifier extends _$SettingsNotifier { diff --git a/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart b/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart index 5925c4ec1..5b5065b62 100644 --- a/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart +++ b/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart @@ -19,12 +19,14 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../../model/token_container.dart'; import '../../../../utils/customization/theme_extentions/action_theme.dart'; import '../../../../utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; +import '../../../../widgets/dialog_widgets/default_dialog.dart'; import '../../../main_view/main_view_widgets/token_widgets/slideable_action.dart'; import '../../../view_interface.dart'; @@ -55,3 +57,30 @@ class DeleteContainerAction extends ConsumerSlideableAction { ); void _showDeleteDialog(BuildContext context, WidgetRef ref) {} } + +class DeleteContainerDialog extends ConsumerWidget { + final TokenContainer container; + + const DeleteContainerDialog(this.container); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return DefaultDialog( + title: Text('AppLocalizations.of(context)!.deleteContainerDialogTitle'), + content: Text('AppLocalizations.of(context)!.deleteContainerDialogContent'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context)!.cancel), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + ref.read(tokenContainerProvider.notifier).deleteContainer(container); + }, + child: Text(AppLocalizations.of(context)!.delete), + ), + ], + ); + } +} diff --git a/lib/views/container_view/container_widgets/container_actions/edit_container_action.dart b/lib/views/container_view/container_widgets/container_actions/edit_container_action.dart index f151a22f1..61a2170e8 100644 --- a/lib/views/container_view/container_widgets/container_actions/edit_container_action.dart +++ b/lib/views/container_view/container_widgets/container_actions/edit_container_action.dart @@ -20,6 +20,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:privacyidea_authenticator/widgets/dialog_widgets/default_dialog.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../../model/token_container.dart'; @@ -35,6 +36,10 @@ class EditContainerAction extends ConsumerSlideableAction { super.key, }); + void _showEditContainerDialog(BuildContext context) { + showDialog(useRootNavigator: false, context: context, builder: (_) => EditContainerDialog(context)); + } + @override CustomSlidableAction build(BuildContext context, WidgetRef ref) => CustomSlidableAction( onPressed: (BuildContext context) {}, @@ -53,3 +58,32 @@ class EditContainerAction extends ConsumerSlideableAction { ), ); } + +class EditContainerDialog extends StatelessWidget { + final BuildContext context; + + const EditContainerDialog(this.context); + + @override + Widget build(BuildContext context) { + return DefaultDialog( + title: Text('AppLocalizations.of(context)!.editContainer'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Edit Container'), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context)!.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context)!.save), + ), + ], + ); + } +} diff --git a/lib/views/main_view/main_view_widgets/drag_target_divider.dart b/lib/views/main_view/main_view_widgets/drag_target_divider.dart index 8ae51ce70..528cdd989 100644 --- a/lib/views/main_view/main_view_widgets/drag_target_divider.dart +++ b/lib/views/main_view/main_view_widgets/drag_target_divider.dart @@ -104,16 +104,10 @@ class _DragTargetDividerState extends ConsumerState Container( + height: dividerHeight, + decoration: BoxDecoration( + color: Theme.of(context).dividerColor, + borderRadius: BorderRadius.circular(dividerHeight / 4), + ), + padding: padding, + margin: margin, + ); +} diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart index a358864a1..01fb1c4dd 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart @@ -21,8 +21,8 @@ import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacyidea_authenticator/model/extensions/token_folder_extension.dart'; -import '../../../../model/riverpod_states/settings_state.dart'; import '../../../../model/riverpod_states/token_filter.dart'; import '../../../../model/token_folder.dart'; import '../../../../model/tokens/push_token.dart'; @@ -39,7 +39,12 @@ class TokenFolderExpandable extends ConsumerStatefulWidget { final TokenFilter? filter; final bool? expandOverride; - const TokenFolderExpandable({super.key, required this.folder, this.filter, this.expandOverride}); + const TokenFolderExpandable({ + super.key, + required this.folder, + this.filter, + this.expandOverride, + }); @override ConsumerState createState() => _TokenFolderExpandableState(); @@ -82,13 +87,14 @@ class _TokenFolderExpandableState extends ConsumerState w @override Widget build(BuildContext context) { - final hidePushTokens = ref.watch(settingsProvider).whenOrNull(data: (data) => data.hidePushTokens) ?? SettingsState.hidePushTokensDefault; - final tokens = ref.watch(tokenProvider).tokensInFolder(widget.folder, exclude: hidePushTokens ? [PushToken] : []); - - tokens.sort((a, b) => a.compareTo(b)); + final hidePushTokens = ref.watch(hidePushTokensProvider); + final folderTokens = ref.watch(tokenProvider.select((state) => state.tokensInFolder(widget.folder).whereNotType(hidePushTokens ? [PushToken] : []))); + final tokensFiltered = widget.filter?.filterTokens(folderTokens) ?? folderTokens; + if (tokensFiltered.isEmpty) return const SizedBox(); + tokensFiltered.sort((a, b) => a.compareTo(b)); final draggingSortable = ref.watch(draggingSortableProvider); if (widget.expandOverride == null) { - if (tokens.isEmpty && expandableController.expanded) { + if (tokensFiltered.isEmpty && expandableController.expanded) { expandableController.value = false; } else if (widget.folder.isExpanded != expandableController.expanded) { expandableController.value = widget.folder.isExpanded; @@ -98,52 +104,61 @@ class _TokenFolderExpandableState extends ConsumerState w } final isExpanded = expandableController.value; const double borderRadius = 6; - return Container( - padding: const EdgeInsets.fromLTRB(0, borderRadius, 0, borderRadius), - margin: const EdgeInsets.only(bottom: 8, left: 14), - decoration: BoxDecoration( - color: isExpanded ? Theme.of(context).scaffoldBackgroundColor : Colors.transparent, - borderRadius: isExpanded - ? const BorderRadius.only( - topLeft: Radius.circular(borderRadius), - bottomLeft: Radius.circular(borderRadius), - ) - : const BorderRadius.all(Radius.circular(borderRadius)), - boxShadow: [ - if (isExpanded) - BoxShadow( - color: Theme.of(context).shadowColor, - offset: const Offset(0, 2), - blurRadius: 4, + return Stack( + children: [ + Positioned.fill( + child: Container( + padding: const EdgeInsets.symmetric(vertical: borderRadius), + margin: const EdgeInsets.only(bottom: 8, left: 14), + decoration: BoxDecoration( + color: isExpanded ? Theme.of(context).scaffoldBackgroundColor : Colors.transparent, + borderRadius: isExpanded + ? const BorderRadius.only( + topLeft: Radius.circular(borderRadius), + bottomLeft: Radius.circular(borderRadius), + ) + : const BorderRadius.all(Radius.circular(borderRadius)), + boxShadow: [ + if (isExpanded) + BoxShadow( + color: Theme.of(context).shadowColor, + offset: const Offset(0, 2), + blurRadius: 4, + ), + ], ), - ], - ), - child: ExpandablePanel( - theme: const ExpandableThemeData( - useInkWell: false, - hasIcon: false, - fadeCurve: InstantCurve(), - tapBodyToCollapse: false, - tapBodyToExpand: false, + ), ), - controller: expandableController, - header: TokenFolderExpandableHeader( - tokens: tokens, - expandableController: expandableController, - animationController: animationController, - expandOverride: widget.expandOverride, - folder: widget.folder, + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: ExpandablePanel( + theme: const ExpandableThemeData( + useInkWell: false, + hasIcon: false, + fadeCurve: InstantCurve(), + tapBodyToCollapse: false, + tapBodyToExpand: false, + ), + controller: expandableController, + header: TokenFolderExpandableHeader( + tokens: tokensFiltered, + expandableController: expandableController, + animationController: animationController, + expandOverride: widget.expandOverride, + folder: widget.folder, + ), + collapsed: const SizedBox(), + expanded: tokensFiltered.isEmpty || (tokensFiltered.length == 1 && tokensFiltered.first == draggingSortable) + ? const SizedBox() + : TokenFolderExpandableBody( + tokens: tokensFiltered, + draggingSortable: draggingSortable, + folder: widget.folder, + isFilterd: widget.filter != null, + ), + ), ), - collapsed: const SizedBox(), - expanded: tokens.isEmpty || (tokens.length == 1 && tokens.first == draggingSortable) - ? const SizedBox() - : TokenFolderExpandableBody( - tokens: tokens, - draggingSortable: draggingSortable, - folder: widget.folder, - filter: widget.filter, - ), - ), + ], ); } } diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_body.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_body.dart index 5aac54a9a..5603ecc24 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_body.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_body.dart @@ -21,7 +21,6 @@ import 'package:flutter/material.dart'; import '../../../../../model/mixins/sortable_mixin.dart'; -import '../../../../../model/riverpod_states/token_filter.dart'; import '../../../../../model/token_folder.dart'; import '../../../../../model/tokens/token.dart'; import '../../drag_target_divider.dart'; @@ -31,35 +30,41 @@ class TokenFolderExpandableBody extends StatelessWidget { final List tokens; final SortableMixin? draggingSortable; final TokenFolder folder; - final TokenFilter? filter; + final bool isFilterd; const TokenFolderExpandableBody({ super.key, required this.tokens, required this.draggingSortable, required this.folder, - required this.filter, + required this.isFilterd, }); @override - Widget build(BuildContext context) => Container( - color: Theme.of(context).scaffoldBackgroundColor, + Widget build(BuildContext context) => Padding( + padding: EdgeInsets.only(left: 14), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ for (var i = 0; i < tokens.length; i++) ...[ if (draggingSortable != tokens[i] && (i != 0 || draggingSortable is Token)) - filter == null - ? DragTargetDivider( + isFilterd + ? const DefaultDivider() + : DragTargetDivider( dependingFolder: folder, previousSortable: (i - 1) < 0 ? null : tokens[i - 1], nextSortable: tokens[i], - ) - : const Divider(), + ), TokenWidgetBuilder.fromToken(tokens[i]), ], if (tokens.isNotEmpty && draggingSortable is Token) - filter == null ? DragTargetDivider(dependingFolder: folder, previousSortable: tokens.last, nextSortable: null) : const Divider(), + isFilterd + ? const DefaultDivider() + : DragTargetDivider( + dependingFolder: folder, + previousSortable: tokens.last, + nextSortable: null, + ), ], ), ); diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header.dart index 08219753e..b8f16535c 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header.dart @@ -36,6 +36,8 @@ import '../token_folder_actions.dart/lock_token_folder_action.dart'; import '../token_folder_actions.dart/rename_token_folder_action.dart'; import 'token_folder_expandable_header_icon.dart'; +final tokenFolderPaddingProvider = Provider((ref) => const EdgeInsets.all(0)); + class TokenFolderExpandableHeader extends ConsumerStatefulWidget { final TokenFolder folder; final List tokens; @@ -69,85 +71,88 @@ class _TokenFolderExpandableHeaderState extends ConsumerState( - onWillAcceptWithDetails: (details) { - if (details.data.folderId != widget.folder.folderId) { - if (widget.folder.isLocked || widget.tokens.isEmpty) return true; - if (isExpanded) return true; - _expandTimer?.cancel(); - _expandTimer = Timer(const Duration(milliseconds: 500), () { - if (!mounted) return; - widget.expandableController.value = true; - }); - return true; - } - return false; - }, - onLeave: (data) => _expandTimer?.cancel(), - onAcceptWithDetails: (details) => dragSortableOnAccept( - previousSortable: widget.folder, - dragedSortable: details.data, - nextSortable: null, - dependingFolder: widget.folder, - ref: ref, - ), - builder: (context, willAccept, willReject) => Center( - child: SizedBox( - height: 50, - child: DefaultInkWell( - highlight: willAccept.isNotEmpty, - onTap: () async { - if (widget.expandOverride != null) return; - if (isExpanded) { - widget.expandableController.value = false; - return; - } - if (widget.tokens.isEmpty || (widget.tokens.length == 1 && widget.tokens.first == draggingSortable)) return; - if (widget.folder.isLocked && await lockAuth(localizedReason: AppLocalizations.of(context)!.expandLockedFolder) == false) { - return; - } + return Padding( + padding: EdgeInsets.fromLTRB(isExpanded ? 14 : 0, 4, 0, 0), + child: PiSliable( + key: ValueKey('tokenFolder-${widget.folder.folderId}'), + groupTag: TokenWidget.groupTag, + identifier: widget.folder.folderId.toString(), + actions: [ + DeleteTokenFolderAction(folder: widget.folder), + RenameTokenFolderAction(folder: widget.folder), + LockTokenFolderAction(folder: widget.folder), + ], + child: Padding( + padding: EdgeInsets.only(left: isExpanded ? 8 : 14 + 8, right: 8), + child: DragTarget( + onWillAcceptWithDetails: (details) { + if (details.data.folderId != widget.folder.folderId) { + if (widget.folder.isLocked || widget.tokens.isEmpty) return true; + if (isExpanded) return true; + _expandTimer?.cancel(); + _expandTimer = Timer(const Duration(milliseconds: 500), () { if (!mounted) return; widget.expandableController.value = true; - }, - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - const SizedBox(width: 8), - RotationTransition( - turns: Tween(begin: 0.25, end: 0.0).animate(widget.animationController), - child: SizedBox.square( - dimension: 25, - child: (widget.tokens.isEmpty || (widget.tokens.length == 1 && widget.tokens.first == draggingSortable)) - ? null - : const Icon(Icons.arrow_forward_ios_sharp), - )), - const SizedBox(width: 8), - Expanded( - flex: 2, - child: Text( - widget.folder.label, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, + }); + return true; + } + return false; + }, + onLeave: (data) => _expandTimer?.cancel(), + onAcceptWithDetails: (details) => dragSortableOnAccept( + previousSortable: widget.folder, + dragedSortable: details.data, + nextSortable: null, + dependingFolder: widget.folder, + ref: ref, + ), + builder: (context, willAccept, willReject) => Center( + child: SizedBox( + height: 50, + child: DefaultInkWell( + highlight: willAccept.isNotEmpty, + onTap: () async { + if (widget.expandOverride != null) return; + if (isExpanded) { + widget.expandableController.value = false; + return; + } + if (widget.tokens.isEmpty || (widget.tokens.length == 1 && widget.tokens.first == draggingSortable)) return; + if (widget.folder.isLocked && await lockAuth(localizedReason: AppLocalizations.of(context)!.expandLockedFolder) == false) { + return; + } + if (!mounted) return; + widget.expandableController.value = true; + }, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(width: 8), + RotationTransition( + turns: Tween(begin: 0.25, end: 0.0).animate(widget.animationController), + child: SizedBox.square( + dimension: 25, + child: (widget.tokens.isEmpty || (widget.tokens.length == 1 && widget.tokens.first == draggingSortable)) + ? null + : const Icon(Icons.arrow_forward_ios_sharp), + )), + const SizedBox(width: 8), + Expanded( + flex: 2, + child: Text( + widget.folder.label, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), + ), + TokenFolderExpandableHeaderIcon( + showEmptyFolderIcon: (widget.tokens.isEmpty || (widget.tokens.length == 1 && widget.tokens.first == draggingSortable)), + isLocked: widget.folder.isLocked, + isExpanded: isExpanded, ), - ), - TokenFolderExpandableHeaderIcon( - showEmptyFolderIcon: (widget.tokens.isEmpty || (widget.tokens.length == 1 && widget.tokens.first == draggingSortable)), - isLocked: widget.folder.isLocked, - isExpanded: isExpanded, - ), - ], + ], + ), ), ), ), diff --git a/lib/views/main_view/main_view_widgets/main_view_tokens_list_filtered.dart b/lib/views/main_view/main_view_widgets/main_view_tokens_list_filtered.dart index a1e7bfba3..c25b30d5c 100644 --- a/lib/views/main_view/main_view_widgets/main_view_tokens_list_filtered.dart +++ b/lib/views/main_view/main_view_widgets/main_view_tokens_list_filtered.dart @@ -19,6 +19,7 @@ */ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacyidea_authenticator/model/extensions/token_folder_extension.dart'; import '../../../model/mixins/sortable_mixin.dart'; import '../../../model/riverpod_states/token_filter.dart'; @@ -50,7 +51,9 @@ class MainViewTokensListFiltered extends ConsumerWidget { final filter = ref.watch(tokenFilterProvider); if (filter == null) return []; final tokenFolders = ref.watch(tokenFolderProvider).folders; - final tokensInNoFolder = filter.filterTokens(ref.watch(tokenProvider).tokensWithoutFolder()); + final allTokens = ref.watch(tokenProvider).tokens; + final tokensInFolder = allTokens.inFolder(); + final tokensInNoFolder = allTokens.inNoFolder(); List sortables = [...tokenFolders, ...tokensInNoFolder]; sortables.sort((a, b) => a.compareTo(b)); @@ -63,21 +66,27 @@ class MainViewTokensListFiltered extends ConsumerWidget { widgets.add(const Divider()); } } else if (sortable is TokenFolder) { - widgets.addAll(_buildFilteredFolder(ref: ref, folder: sortable, filter: filter)); + widgets.addAll(_buildFilteredFolders(ref: ref, folder: sortable, filter: filter, allFolderTokens: tokensInFolder)); } } return widgets; } - List _buildFilteredFolder({required WidgetRef ref, required TokenFolder folder, required TokenFilter filter}) { - if (filter.filterTokens(ref.watch(tokenProvider).tokensInFolder(folder)).isEmpty) return []; - final expanded = filter.searchQuery.isNotEmpty && !folder.isLocked ? true : null; // Auto expand if search query is not empty and folder is not locked. + List _buildFilteredFolders({ + required WidgetRef ref, + required TokenFolder folder, + required TokenFilter filter, + required List allFolderTokens, + }) { + final folderTokens = allFolderTokens.inFolder(folder); + final filtered = filter.filterTokens(folderTokens); + if (filtered.isEmpty) return []; + // Auto expand if search query is not empty and folder is not locked. + final expanded = filter.searchQuery.isNotEmpty && !folder.isLocked ? true : null; Logger.warning('Expanded: $expanded'); - final List widgets = []; - widgets.add( + return [ TokenFolderExpandable(folder: folder, filter: filter, expandOverride: expanded, key: ValueKey('filteredFolder:${folder.folderId}')), - ); - widgets.add(const Divider()); - return widgets; + const Divider(), + ]; } } diff --git a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_tokens_dialog.dart b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_tokens_dialog.dart index 2d37a7ee3..fcc1cc3e8 100644 --- a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_tokens_dialog.dart +++ b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_tokens_dialog.dart @@ -19,9 +19,9 @@ */ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacyidea_authenticator/model/extensions/token_folder_extension.dart'; import '../../../../../l10n/app_localizations.dart'; -import '../../../../../model/riverpod_states/token_state.dart'; import '../../../../../model/tokens/token.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/app_constraints_notifier.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; diff --git a/lib/widgets/dialog_widgets/add_container_progress_dialog.dart b/lib/widgets/dialog_widgets/add_container_progress_dialog.dart index 0f1d99dc1..b070c69cd 100644 --- a/lib/widgets/dialog_widgets/add_container_progress_dialog.dart +++ b/lib/widgets/dialog_widgets/add_container_progress_dialog.dart @@ -20,7 +20,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:privacyidea_authenticator/model/extensions/enums/rollout_state_extension.dart'; -import 'package:privacyidea_authenticator/model/riverpod_states/token_state.dart'; +import 'package:privacyidea_authenticator/model/extensions/token_folder_extension.dart'; import '../../l10n/app_localizations.dart'; import '../../model/token_container.dart';