diff --git a/lib/pages/chat/events/state_message.dart b/lib/pages/chat/events/state_message.dart index d55f286b1..2975c8a06 100644 --- a/lib/pages/chat/events/state_message.dart +++ b/lib/pages/chat/events/state_message.dart @@ -29,8 +29,10 @@ class StateMessage extends StatelessWidget { // event.calcLocalizedBodyFallback( // MatrixLocals(L10n.of(context)), // ), - event.type == EventTypes.RoomMember && - event.roomMemberChangeType == RoomMemberChangeType.leave + (event.type == EventTypes.RoomMember) && + (event.roomMemberChangeType == + RoomMemberChangeType.leave) && + (event.stateKey == event.room.client.userID) ? L10n.of(context).youLeftTheChat : event.calcLocalizedBodyFallback( MatrixLocals(L10n.of(context)), diff --git a/lib/pangea/analytics_downloads/analytics_dowload_dialog.dart b/lib/pangea/analytics_downloads/analytics_dowload_dialog.dart index fe0af6e37..ea0739580 100644 --- a/lib/pangea/analytics_downloads/analytics_dowload_dialog.dart +++ b/lib/pangea/analytics_downloads/analytics_dowload_dialog.dart @@ -50,7 +50,13 @@ class AnalyticsDownloadDialogState extends State { } void _setDownloadType(DownloadType type) { - if (mounted) setState(() => _downloadType = type); + if (mounted) { + setState(() { + _downloadType = type; + _downloaded = false; + _error = null; + }); + } } Future _downloadAnalytics() async { @@ -427,7 +433,8 @@ class AnalyticsDownloadDialogState extends State { padding: const EdgeInsets.all(8.0), child: SegmentedButton( selected: {_downloadType}, - onSelectionChanged: (c) => _setDownloadType(c.first), + onSelectionChanged: + _downloading ? null : (c) => _setDownloadType(c.first), segments: [ ButtonSegment( value: DownloadType.csv, diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index c46151ce3..6fb2da694 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -21,6 +21,7 @@ import 'package:fluffychat/pangea/practice_activities/message_activity_request.d import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_generation_repo.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; +import 'package:fluffychat/pangea/text_to_speech/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_controller.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -200,6 +201,15 @@ class AnalyticsPracticeState extends State } } + void _playAudio() { + if (activityTarget.value == null) return; + if (widget.type != ConstructTypeEnum.vocab) return; + TtsController.tryToSpeak( + activityTarget.value!.tokens.first.vocabConstructID.lemma, + langCode: MatrixState.pangeaController.userController.userL2!.langCode, + ); + } + Future _saveSession() async { if (_sessionLoader.isLoaded) { await AnalyticsPracticeSessionRepo.update( @@ -276,7 +286,10 @@ class AnalyticsPracticeState extends State } else { activityState.value = const AsyncState.loading(); final nextActivityCompleter = _queue.removeFirst(); + activityTarget.value = nextActivityCompleter.key; + _playAudio(); + final activity = await nextActivityCompleter.value.future; activityState.value = AsyncState.loaded(activity); } @@ -295,12 +308,14 @@ class AnalyticsPracticeState extends State try { activityState.value = const AsyncState.loading(); - final req = requests.first; + + activityTarget.value = req.target; + _playAudio(); + final res = await _fetchActivity(req); if (!mounted) return; - activityTarget.value = req.target; activityState.value = AsyncState.loaded(res); } catch (e) { if (!mounted) return; @@ -401,6 +416,8 @@ class AnalyticsPracticeState extends State await _saveSession(); if (!activity.multipleChoiceContent.isCorrect(choiceContent)) return; + _playAudio(); + // Display the fact that the choice was correct before loading the next activity await Future.delayed(const Duration(milliseconds: 1000)); diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index 8dfc22999..a41f0ec3e 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_practice/analytics_practice_page.dart'; import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart'; import 'package:fluffychat/pangea/analytics_practice/choice_cards/audio_choice_card.dart'; @@ -15,9 +16,11 @@ import 'package:fluffychat/pangea/common/utils/async_state.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import 'package:fluffychat/widgets/matrix.dart'; class AnalyticsPracticeView extends StatelessWidget { final AnalyticsPracticeState controller; @@ -68,26 +71,28 @@ class AnalyticsPracticeView extends StatelessWidget { ], ), ), - body: MaxWidthBody( - withScrolling: false, + body: Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 24.0, ), - showBorder: false, - child: ValueListenableBuilder( - valueListenable: controller.sessionState, - builder: (context, state, __) { - return switch (state) { - AsyncError(:final error) => - ErrorIndicator(message: error.toString()), - AsyncLoaded(:final value) => - value.isComplete - ? CompletedActivitySessionView(state.value, controller) - : _AnalyticsActivityView(controller), - _ => loading, - }; - }, + child: MaxWidthBody( + withScrolling: false, + showBorder: false, + child: ValueListenableBuilder( + valueListenable: controller.sessionState, + builder: (context, state, __) { + return switch (state) { + AsyncError(:final error) => + ErrorIndicator(message: error.toString()), + AsyncLoaded(:final value) => + value.isComplete + ? CompletedActivitySessionView(state.value, controller) + : _AnalyticsActivityView(controller), + _ => loading, + }; + }, + ), ), ), ); @@ -122,13 +127,29 @@ class _AnalyticsActivityView extends StatelessWidget { child: ValueListenableBuilder( valueListenable: controller.activityTarget, builder: (context, target, __) => target != null - ? Text( - target.promptText(context), - textAlign: TextAlign.center, - style: - Theme.of(context).textTheme.titleLarge?.copyWith( + ? Column( + spacing: 12.0, + children: [ + Text( + target.promptText(context), + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( fontWeight: FontWeight.bold, ), + ), + if (controller.widget.type == + ConstructTypeEnum.vocab) + PhoneticTranscriptionWidget( + text: + target.tokens.first.vocabConstructID.lemma, + textLanguage: MatrixState + .pangeaController.userController.userL2!, + style: const TextStyle(fontSize: 14.0), + ), + ], ) : const SizedBox(), ), diff --git a/lib/pangea/analytics_practice/choice_cards/game_choice_card.dart b/lib/pangea/analytics_practice/choice_cards/game_choice_card.dart index c4bed1767..ad2e5c461 100644 --- a/lib/pangea/analytics_practice/choice_cards/game_choice_card.dart +++ b/lib/pangea/analytics_practice/choice_cards/game_choice_card.dart @@ -100,7 +100,6 @@ class _GameChoiceCardState extends State animation: _scaleAnim, builder: (context, _) { final scale = _scaleAnim.value; - final showAlt = scale < 0.1 && widget.altChild != null; final showContent = scale > 0.05; return Transform.scale( @@ -113,7 +112,7 @@ class _GameChoiceCardState extends State : (hovered ? hoverColor : Colors.transparent), child: Opacity( opacity: showContent ? 1 : 0, - child: showAlt ? widget.altChild! : widget.child, + child: _revealed ? widget.altChild! : widget.child, ), ), ); diff --git a/lib/pangea/bot/widgets/bot_chat_settings_dialog.dart b/lib/pangea/bot/widgets/bot_chat_settings_dialog.dart index 87c603de2..47aa375e6 100644 --- a/lib/pangea/bot/widgets/bot_chat_settings_dialog.dart +++ b/lib/pangea/bot/widgets/bot_chat_settings_dialog.dart @@ -154,6 +154,7 @@ class BotChatSettingsDialogState extends State { initialLevel: _selectedLevel, onChanged: _setLevel, enabled: !widget.room.isActivitySession, + width: 300, ), DropdownButtonFormField2( customButton: _selectedVoice != null diff --git a/lib/pangea/chat_settings/pages/space_details_content.dart b/lib/pangea/chat_settings/pages/space_details_content.dart index 6a4db73b4..b57233edd 100644 --- a/lib/pangea/chat_settings/pages/space_details_content.dart +++ b/lib/pangea/chat_settings/pages/space_details_content.dart @@ -296,7 +296,7 @@ class SpaceDetailsContent extends StatelessWidget { ], Flexible( child: Column( - spacing: 12.0, + spacing: isColumnMode ? 12.0 : 6.0, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -311,7 +311,7 @@ class SpaceDetailsContent extends StatelessWidget { : FontWeight.bold, ), ), - if (isColumnMode && room.coursePlan != null) + if (room.coursePlan != null) CourseInfoChips( room.coursePlan!.uuid, fontSize: 12.0, diff --git a/lib/pangea/chat_settings/widgets/language_level_dropdown.dart b/lib/pangea/chat_settings/widgets/language_level_dropdown.dart index 6dafafaad..11e9cd0d8 100644 --- a/lib/pangea/chat_settings/widgets/language_level_dropdown.dart +++ b/lib/pangea/chat_settings/widgets/language_level_dropdown.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/common/widgets/dropdown_text_button.dart'; import 'package:fluffychat/pangea/learning_settings/language_level_type_enum.dart'; @@ -14,6 +13,7 @@ class LanguageLevelDropdown extends StatelessWidget { final FormFieldValidator? validator; final bool enabled; final Color? backgroundColor; + final double? width; const LanguageLevelDropdown({ super.key, @@ -22,6 +22,7 @@ class LanguageLevelDropdown extends StatelessWidget { this.validator, this.enabled = true, this.backgroundColor, + this.width, }); @override @@ -33,12 +34,12 @@ class LanguageLevelDropdown extends StatelessWidget { LanguageLevelTypeEnum.values.contains(initialLevel) ? CustomDropdownTextButton(text: initialLevel!.title(context)) : null, - menuItemStyleData: MenuItemStyleData( - padding: const EdgeInsets.symmetric( + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( vertical: 8.0, horizontal: 16.0, ), - height: FluffyThemes.isColumnMode(context) ? 100.0 : 150.0, + height: 100.0, ), decoration: InputDecoration( labelText: l10n.cefrLevelLabel, @@ -51,6 +52,7 @@ class LanguageLevelDropdown extends StatelessWidget { Theme.of(context).colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(14.0), ), + width: width, ), items: LanguageLevelTypeEnum.values.map((LanguageLevelTypeEnum levelOption) { diff --git a/lib/pangea/choreographer/choreo_constants.dart b/lib/pangea/choreographer/choreo_constants.dart index 85b9e6186..a303266ce 100644 --- a/lib/pangea/choreographer/choreo_constants.dart +++ b/lib/pangea/choreographer/choreo_constants.dart @@ -11,4 +11,5 @@ class ChoreoConstants { static const int msBeforeIGCStart = 10000; static const int maxLength = 1000; static const String inputTransformTargetKey = 'input_text_field'; + static const int defaultErrorBackoffSeconds = 5; } diff --git a/lib/pangea/choreographer/choreographer.dart b/lib/pangea/choreographer/choreographer.dart index 52348dea6..a04c1e738 100644 --- a/lib/pangea/choreographer/choreographer.dart +++ b/lib/pangea/choreographer/choreographer.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:async/async.dart'; + import 'package:fluffychat/pangea/choreographer/assistance_state_enum.dart'; import 'package:fluffychat/pangea/choreographer/choreo_constants.dart'; import 'package:fluffychat/pangea/choreographer/choreo_mode_enum.dart'; @@ -45,6 +47,11 @@ class Choreographer extends ChangeNotifier { String? _lastChecked; ChoreoModeEnum _choreoMode = ChoreoModeEnum.igc; + DateTime? _lastIgcError; + DateTime? _lastTokensError; + int _igcErrorBackoff = ChoreoConstants.defaultErrorBackoffSeconds; + int _tokenErrorBackoff = ChoreoConstants.defaultErrorBackoffSeconds; + StreamSubscription? _languageSub; StreamSubscription? _settingsUpdateSub; StreamSubscription? _acceptedContinuanceSub; @@ -68,6 +75,12 @@ class Choreographer extends ChangeNotifier { openMatches: [], ); + bool _backoffRequest(DateTime? error, int backoffSeconds) { + if (error == null) return false; + final secondsSinceError = DateTime.now().difference(error).inSeconds; + return secondsSinceError <= backoffSeconds; + } + void _initialize() { textController = PangeaTextController(choreographer: this); textController.addListener(_onChange); @@ -82,7 +95,14 @@ class Choreographer extends ChangeNotifier { itController.editing.addListener(_onSubmitSourceTextEdits); igcController = IgcController( - (e) => errorService.setErrorAndLock(ChoreoError(raw: e)), + (e) { + errorService.setErrorAndLock(ChoreoError(raw: e)); + _lastIgcError = DateTime.now(); + _igcErrorBackoff *= 2; + }, + () { + _igcErrorBackoff = ChoreoConstants.defaultErrorBackoffSeconds; + }, ); _languageSub ??= MatrixState @@ -233,7 +253,8 @@ class Choreographer extends ChangeNotifier { !ToolSetting.interactiveTranslator.enabled) || (!ToolSetting.autoIGC.enabled && !manual && - _choreoMode != ChoreoModeEnum.it)) { + _choreoMode != ChoreoModeEnum.it) || + _backoffRequest(_lastIgcError, _igcErrorBackoff)) { return; } @@ -275,7 +296,9 @@ class Choreographer extends ChangeNotifier { MatrixState.pangeaController.userController.userL2?.langCode; final l1LangCode = MatrixState.pangeaController.userController.userL1?.langCode; - if (l1LangCode != null && l2LangCode != null) { + if (l1LangCode != null && + l2LangCode != null && + !_backoffRequest(_lastTokensError, _tokenErrorBackoff)) { final res = await TokensRepo.get( MatrixState.pangeaController.userController.accessToken, TokensRequestModel( @@ -283,7 +306,21 @@ class Choreographer extends ChangeNotifier { senderL1: l1LangCode, senderL2: l2LangCode, ), + ).timeout( + const Duration(seconds: 10), + onTimeout: () { + return Result.error("Token request timed out"); + }, ); + + if (res.isError) { + _lastTokensError = DateTime.now(); + _tokenErrorBackoff *= 2; + } else { + // reset backoff on success + _tokenErrorBackoff = ChoreoConstants.defaultErrorBackoffSeconds; + } + tokensResp = res.isValue ? res.result : null; } diff --git a/lib/pangea/choreographer/igc/igc_controller.dart b/lib/pangea/choreographer/igc/igc_controller.dart index afe661ff9..9b78cb439 100644 --- a/lib/pangea/choreographer/igc/igc_controller.dart +++ b/lib/pangea/choreographer/igc/igc_controller.dart @@ -18,7 +18,8 @@ import 'package:fluffychat/widgets/matrix.dart'; class IgcController { final Function(Object) onError; - IgcController(this.onError); + final VoidCallback onFetch; + IgcController(this.onError, this.onFetch); bool _isFetching = false; String? _currentText; @@ -321,6 +322,8 @@ class IgcController { onError(res.asError!); clear(); return; + } else { + onFetch(); } if (!_isFetching) return; diff --git a/lib/pangea/common/controllers/pangea_controller.dart b/lib/pangea/common/controllers/pangea_controller.dart index 3d80b2204..b1339a3e4 100644 --- a/lib/pangea/common/controllers/pangea_controller.dart +++ b/lib/pangea/common/controllers/pangea_controller.dart @@ -122,7 +122,9 @@ class PangeaController { } Future _clearCache({List exclude = const []}) async { - final List> futures = []; + final List> futures = [ + matrixState.store.setString(SettingKeys.fontSizeFactor, ''), + ]; for (final key in _storageKeys) { if (exclude.contains(key)) continue; futures.add(GetStorage(key).erase()); @@ -140,6 +142,7 @@ class PangeaController { ); } + AppConfig.fontSizeFactor = 1.0; await Future.wait(futures); } diff --git a/lib/pangea/course_creation/selected_course_page.dart b/lib/pangea/course_creation/selected_course_page.dart index 1b8b3848e..299c784f4 100644 --- a/lib/pangea/course_creation/selected_course_page.dart +++ b/lib/pangea/course_creation/selected_course_page.dart @@ -169,12 +169,12 @@ class SelectedCourseController extends State : await client.joinRoom(widget.roomChunk!.roomId); Room? room = client.getRoomById(roomId); - if (!knock && room == null) { - await client.waitForRoomInSync(roomId); + if (!knock && room?.membership != Membership.join) { + await client.waitForRoomInSync(roomId, join: true); room = client.getRoomById(roomId); } - if (knock && room == null) { + if (knock) { Navigator.of(context).pop(); await showOkAlertDialog( context: context, diff --git a/lib/pangea/space_analytics/analytics_request_indicator.dart b/lib/pangea/space_analytics/analytics_request_indicator.dart index 180a0bc6f..9ba4e910b 100644 --- a/lib/pangea/space_analytics/analytics_request_indicator.dart +++ b/lib/pangea/space_analytics/analytics_request_indicator.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/space_analytics/space_analytics_requested_dialog.dart'; +import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; class AnalyticsRequestIndicator extends StatefulWidget { @@ -26,51 +27,68 @@ class AnalyticsRequestIndicator extends StatefulWidget { class AnalyticsRequestIndicatorState extends State { AnalyticsRequestIndicatorState(); - - final Map> _knockingAdmins = {}; + StreamSubscription? _analyticsRoomSub; @override void initState() { super.initState(); - _fetchKnockingAdmins(); + _init(); } @override void didUpdateWidget(covariant AnalyticsRequestIndicator oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.room.id != widget.room.id) { - _fetchKnockingAdmins(); + _init(); } } - Future _fetchKnockingAdmins() async { - setState(() => _knockingAdmins.clear()); + @override + void dispose() { + _analyticsRoomSub?.cancel(); + super.dispose(); + } - final admins = (await widget.room.requestParticipants( - [Membership.join, Membership.invite, Membership.knock], - false, - true, - )) - .where((u) => u.powerLevel >= 100); + Future _init() async { + final analyticsRooms = widget.room.client.allMyAnalyticsRooms; + final futures = analyticsRooms.map( + (r) => r.requestParticipants( + [Membership.join, Membership.invite, Membership.knock], + false, + true, + ), + ); + await Future.wait(futures); + final analyicsRoomIds = analyticsRooms.map((r) => r.id).toSet(); + _analyticsRoomSub?.cancel(); + _analyticsRoomSub = widget.room.client.onRoomState.stream + .where( + (event) => + analyicsRoomIds.contains(event.roomId) && + event.state.type == EventTypes.RoomMember, + ) + .rateLimit(const Duration(seconds: 1)) + .listen((_) => setState(() {})); + + if (mounted) setState(() {}); + } + + Map> get _knockingAdmins { + final Map> knockingAdmins = {}; for (final analyticsRoom in widget.room.client.allMyAnalyticsRooms) { - final knocking = await analyticsRoom.requestParticipants( - [Membership.knock], - ); - final knockingSpace = - knocking.where((u) => u.content['reason'] == widget.room.id).toList(); - if (knockingSpace.isEmpty) continue; + final knocking = analyticsRoom + .getParticipants([Membership.knock]) + .where((u) => u.content['reason'] == widget.room.id) + .toList(); - for (final admin in admins) { - if (knockingSpace.any((u) => u.id == admin.id)) { - _knockingAdmins.putIfAbsent(admin, () => []).add(analyticsRoom); - } + if (knocking.isEmpty) continue; + for (final admin in knocking) { + knockingAdmins.putIfAbsent(admin, () => []).add(analyticsRoom); } } - if (mounted) { - setState(() {}); - } + return knockingAdmins; } Future _onTap(BuildContext context) async { @@ -109,8 +127,6 @@ class AnalyticsRequestIndicatorState extends State { } }, ); - - if (mounted) _fetchKnockingAdmins(); } @override diff --git a/lib/pangea/spaces/space_navigation_column.dart b/lib/pangea/spaces/space_navigation_column.dart index cc6891cdf..3c04607d7 100644 --- a/lib/pangea/spaces/space_navigation_column.dart +++ b/lib/pangea/spaces/space_navigation_column.dart @@ -31,40 +31,38 @@ class SpaceNavigationColumn extends StatefulWidget { } class SpaceNavigationColumnState extends State { + bool _hovered = false; bool _expanded = false; - Timer? _debounceTimer; + Timer? _timer; + + void _onHoverUpdate(bool hovered) { + if (hovered == _hovered) return; + _hovered = hovered; + _cancelTimer(); + + if (hovered) { + _timer = Timer(const Duration(milliseconds: 200), () { + if (_hovered && mounted) { + setState(() => _expanded = true); + } + _cancelTimer(); + }); + } else { + setState(() => _expanded = false); + } + } + + void _cancelTimer() { + _timer?.cancel(); + _timer = null; + } @override void dispose() { - _debounceTimer?.cancel(); - _debounceTimer = null; + _cancelTimer(); super.dispose(); } - void _expand() { - if (_debounceTimer?.isActive == true) return; - if (!_expanded) { - setState(() => _expanded = true); - } - } - - void _collapse() { - if (_expanded) { - setState(() { - _expanded = false; - _debounce(); - }); - } - } - - void _debounce() { - _debounceTimer?.cancel(); - _debounceTimer = Timer(const Duration(milliseconds: 300), () { - _debounceTimer?.cancel(); - _debounceTimer = null; - }); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -115,7 +113,7 @@ class SpaceNavigationColumnState extends State { HoverBuilder( builder: (context, hovered) { WidgetsBinding.instance.addPostFrameCallback((_) { - hovered ? _expand() : _collapse(); + _onHoverUpdate(hovered); }); return Row( @@ -128,7 +126,10 @@ class SpaceNavigationColumnState extends State { ? navRailWidth + navRailExtraWidth : navRailWidth, expanded: _expanded, - collapse: _collapse, + collapse: () { + _cancelTimer(); + setState(() => _expanded = false); + }, ), Container( width: 1,