diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index f7c8fc9a..15c51d38 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -13,7 +13,6 @@ import 'package:fluffychat/pages/chat_list/stories_header.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/profile_bottom_sheet.dart'; import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; -import '../../utils/stream_extension.dart'; import '../../widgets/connection_status_header.dart'; import '../../widgets/matrix.dart'; @@ -42,248 +41,237 @@ class ChatListViewBody extends StatelessWidget { child: child, ); }, - child: StreamBuilder( - key: ValueKey(client.userID.toString() + - controller.activeFilter.toString() + - controller.activeSpaceId.toString()), - stream: client.onSync.stream - .where((s) => s.hasRoomUpdate) - .rateLimit(const Duration(seconds: 1)), - builder: (context, _) { - if (controller.activeFilter == ActiveFilter.spaces && - !controller.isSearchMode) { - return SpaceView( - controller, - scrollController: controller.scrollController, - key: Key(controller.activeSpaceId ?? 'Spaces'), - ); - } - if (controller.waitForFirstSync && client.prevBatch != null) { - final rooms = controller.filteredRooms; - final displayStoriesHeader = { - ActiveFilter.allChats, - ActiveFilter.messages, - }.contains(controller.activeFilter); - return ListView.builder( - controller: controller.scrollController, - // add +1 space below in order to properly scroll below the spaces bar - itemCount: rooms.length + 1, - itemBuilder: (BuildContext context, int i) { - if (i == 0) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (roomSearchResult != null) ...[ - SearchTitle( - title: L10n.of(context)!.publicRooms, - icon: const Icon(Icons.explore_outlined), - ), - AnimatedContainer( - height: roomSearchResult.chunk.isEmpty ? 0 : 106, - duration: const Duration(milliseconds: 250), - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: roomSearchResult.chunk.length, - itemBuilder: (context, i) => _SearchItem( - title: roomSearchResult.chunk[i].name ?? - roomSearchResult - .chunk[i].canonicalAlias?.localpart ?? - L10n.of(context)!.group, - avatar: roomSearchResult.chunk[i].avatarUrl, - onPressed: () => showModalBottomSheet( - context: context, - builder: (c) => PublicRoomBottomSheet( - roomAlias: roomSearchResult - .chunk[i].canonicalAlias ?? + child: Builder(builder: (context) { + if (controller.activeFilter == ActiveFilter.spaces && + !controller.isSearchMode) { + return SpaceView( + controller, + scrollController: controller.scrollController, + key: Key(controller.activeSpaceId ?? 'Spaces'), + ); + } + if (controller.waitForFirstSync && client.prevBatch != null) { + final rooms = controller.filteredRooms; + final displayStoriesHeader = { + ActiveFilter.allChats, + ActiveFilter.messages, + }.contains(controller.activeFilter); + return ListView.builder( + controller: controller.scrollController, + // add +1 space below in order to properly scroll below the spaces bar + itemCount: rooms.length + 1, + itemBuilder: (BuildContext context, int i) { + if (i == 0) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (roomSearchResult != null) ...[ + SearchTitle( + title: L10n.of(context)!.publicRooms, + icon: const Icon(Icons.explore_outlined), + ), + AnimatedContainer( + height: roomSearchResult.chunk.isEmpty ? 0 : 106, + duration: const Duration(milliseconds: 250), + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: roomSearchResult.chunk.length, + itemBuilder: (context, i) => _SearchItem( + title: roomSearchResult.chunk[i].name ?? + roomSearchResult + .chunk[i].canonicalAlias?.localpart ?? + L10n.of(context)!.group, + avatar: roomSearchResult.chunk[i].avatarUrl, + onPressed: () => showModalBottomSheet( + context: context, + builder: (c) => PublicRoomBottomSheet( + roomAlias: + roomSearchResult.chunk[i].canonicalAlias ?? roomSearchResult.chunk[i].roomId, - outerContext: context, - chunk: roomSearchResult.chunk[i], - ), - ), + outerContext: context, + chunk: roomSearchResult.chunk[i], ), ), ), - ], - if (userSearchResult != null) ...[ - SearchTitle( - title: L10n.of(context)!.users, - icon: const Icon(Icons.group_outlined), - ), - AnimatedContainer( - height: userSearchResult.results.isEmpty ? 0 : 106, - duration: const Duration(milliseconds: 250), - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: userSearchResult.results.length, - itemBuilder: (context, i) => _SearchItem( - title: - userSearchResult.results[i].displayName ?? - userSearchResult - .results[i].userId.localpart ?? - L10n.of(context)!.unknownDevice, - avatar: userSearchResult.results[i].avatarUrl, - onPressed: () => showModalBottomSheet( - context: context, - builder: (c) => ProfileBottomSheet( - userId: userSearchResult.results[i].userId, - outerContext: context, - ), - ), - ), - ), - ), - ], - if (controller.isSearchMode) - SearchTitle( - title: L10n.of(context)!.stories, - icon: const Icon(Icons.camera_alt_outlined), - ), - if (displayStoriesHeader) - StoriesHeader( - key: const Key('stories_header'), - filter: controller.searchController.text, - ), - const ConnectionStatusHeader(), - AnimatedContainer( - height: controller.isTorBrowser ? 64 : 0, - duration: const Duration(milliseconds: 300), - clipBehavior: Clip.hardEdge, - curve: Curves.bounceInOut, - decoration: const BoxDecoration(), - child: Material( - color: Theme.of(context).colorScheme.surface, - child: ListTile( - leading: const Icon(Icons.vpn_key), - title: Text(L10n.of(context)!.dehydrateTor), - subtitle: - Text(L10n.of(context)!.dehydrateTorLong), - trailing: - const Icon(Icons.chevron_right_outlined), - onTap: controller.dehydrate, - ), - ), - ), - if (controller.isSearchMode) - SearchTitle( - title: L10n.of(context)!.chats, - icon: const Icon(Icons.chat_outlined), - ), - if (rooms.isEmpty && !controller.isSearchMode) - Padding( - padding: const EdgeInsets.all(32.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 32), - Image.asset( - 'assets/start_chat.png', - ), - Divider( - height: 1, - color: Theme.of(context) - .colorScheme - .onBackground, - ), - const SizedBox(height: 32), - FloatingActionButton.extended( - backgroundColor: - Theme.of(context).colorScheme.primary, - foregroundColor: - Theme.of(context).colorScheme.onPrimary, - icon: const Icon(Icons.edit_outlined), - onPressed: () => - VRouter.of(context).to('/newprivatechat'), - label: Text(L10n.of(context)!.startFirstChat), - ), - ], - ), - ), - ], - ); - } - i--; - if (!rooms[i].displayname.toLowerCase().contains( - controller.searchController.text.toLowerCase())) { - return Container(); - } - return ChatListItem( - rooms[i], - key: Key('chat_list_item_${rooms[i].id}'), - selected: controller.selectedRoomIds.contains(rooms[i].id), - onTap: controller.selectMode == SelectMode.select - ? () => controller.toggleSelection(rooms[i].id) - : null, - onLongPress: () => controller.toggleSelection(rooms[i].id), - activeChat: controller.activeChat == rooms[i].id, - ); - }, - ); - } - const dummyChatCount = 5; - final titleColor = - Theme.of(context).textTheme.bodyText1!.color!.withAlpha(100); - final subtitleColor = - Theme.of(context).textTheme.bodyText1!.color!.withAlpha(50); - return ListView.builder( - key: const Key('dummychats'), - itemCount: dummyChatCount, - itemBuilder: (context, i) => Opacity( - opacity: (dummyChatCount - i) / dummyChatCount, - child: ListTile( - leading: CircleAvatar( - backgroundColor: titleColor, - child: CircularProgressIndicator( - strokeWidth: 1, - color: Theme.of(context).textTheme.bodyText1!.color, - ), - ), - title: Row( - children: [ - Expanded( - child: Container( - height: 14, - decoration: BoxDecoration( - color: titleColor, - borderRadius: BorderRadius.circular(3), - ), - ), - ), - const SizedBox(width: 36), - Container( - height: 14, - width: 14, - decoration: BoxDecoration( - color: subtitleColor, - borderRadius: BorderRadius.circular(14), - ), - ), - const SizedBox(width: 12), - Container( - height: 14, - width: 14, - decoration: BoxDecoration( - color: subtitleColor, - borderRadius: BorderRadius.circular(14), ), ), ], - ), - subtitle: Container( - decoration: BoxDecoration( - color: subtitleColor, - borderRadius: BorderRadius.circular(3), + if (userSearchResult != null) ...[ + SearchTitle( + title: L10n.of(context)!.users, + icon: const Icon(Icons.group_outlined), + ), + AnimatedContainer( + height: userSearchResult.results.isEmpty ? 0 : 106, + duration: const Duration(milliseconds: 250), + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: userSearchResult.results.length, + itemBuilder: (context, i) => _SearchItem( + title: userSearchResult.results[i].displayName ?? + userSearchResult.results[i].userId.localpart ?? + L10n.of(context)!.unknownDevice, + avatar: userSearchResult.results[i].avatarUrl, + onPressed: () => showModalBottomSheet( + context: context, + builder: (c) => ProfileBottomSheet( + userId: userSearchResult.results[i].userId, + outerContext: context, + ), + ), + ), + ), + ), + ], + if (controller.isSearchMode) + SearchTitle( + title: L10n.of(context)!.stories, + icon: const Icon(Icons.camera_alt_outlined), + ), + if (displayStoriesHeader) + StoriesHeader( + key: const Key('stories_header'), + filter: controller.searchController.text, + ), + const ConnectionStatusHeader(), + AnimatedContainer( + height: controller.isTorBrowser ? 64 : 0, + duration: const Duration(milliseconds: 300), + clipBehavior: Clip.hardEdge, + curve: Curves.bounceInOut, + decoration: const BoxDecoration(), + child: Material( + color: Theme.of(context).colorScheme.surface, + child: ListTile( + leading: const Icon(Icons.vpn_key), + title: Text(L10n.of(context)!.dehydrateTor), + subtitle: Text(L10n.of(context)!.dehydrateTorLong), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: controller.dehydrate, + ), + ), ), - height: 12, - margin: const EdgeInsets.only(right: 22), - ), + if (controller.isSearchMode) + SearchTitle( + title: L10n.of(context)!.chats, + icon: const Icon(Icons.chat_outlined), + ), + if (rooms.isEmpty && !controller.isSearchMode) + Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 32), + Image.asset( + 'assets/start_chat.png', + ), + Divider( + height: 1, + color: Theme.of(context).colorScheme.onBackground, + ), + const SizedBox(height: 32), + FloatingActionButton.extended( + backgroundColor: + Theme.of(context).colorScheme.primary, + foregroundColor: + Theme.of(context).colorScheme.onPrimary, + icon: const Icon(Icons.edit_outlined), + onPressed: () => + VRouter.of(context).to('/newprivatechat'), + label: Text(L10n.of(context)!.startFirstChat), + ), + ], + ), + ), + ], + ); + } + i--; + if (!rooms[i] + .displayname + .toLowerCase() + .contains(controller.searchController.text.toLowerCase())) { + return Container(); + } + return ChatListItem( + rooms[i], + key: Key('chat_list_item_${rooms[i].id}'), + selected: controller.selectedRoomIds.contains(rooms[i].id), + onTap: controller.selectMode == SelectMode.select + ? () => controller.toggleSelection(rooms[i].id) + : null, + onLongPress: () => controller.toggleSelection(rooms[i].id), + activeChat: controller.activeChat == rooms[i].id, + ); + }, + ); + } + const dummyChatCount = 5; + final titleColor = + Theme.of(context).textTheme.bodyText1!.color!.withAlpha(100); + final subtitleColor = + Theme.of(context).textTheme.bodyText1!.color!.withAlpha(50); + return ListView.builder( + key: const Key('dummychats'), + itemCount: dummyChatCount, + itemBuilder: (context, i) => Opacity( + opacity: (dummyChatCount - i) / dummyChatCount, + child: ListTile( + leading: CircleAvatar( + backgroundColor: titleColor, + child: CircularProgressIndicator( + strokeWidth: 1, + color: Theme.of(context).textTheme.bodyText1!.color, ), ), - ); - }), + title: Row( + children: [ + Expanded( + child: Container( + height: 14, + decoration: BoxDecoration( + color: titleColor, + borderRadius: BorderRadius.circular(3), + ), + ), + ), + const SizedBox(width: 36), + Container( + height: 14, + width: 14, + decoration: BoxDecoration( + color: subtitleColor, + borderRadius: BorderRadius.circular(14), + ), + ), + const SizedBox(width: 12), + Container( + height: 14, + width: 14, + decoration: BoxDecoration( + color: subtitleColor, + borderRadius: BorderRadius.circular(14), + ), + ), + ], + ), + subtitle: Container( + decoration: BoxDecoration( + color: subtitleColor, + borderRadius: BorderRadius.circular(3), + ), + height: 12, + margin: const EdgeInsets.only(right: 22), + ), + ), + ), + ); + }), ); } } diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 806c5e93..70c370cf 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -4,11 +4,13 @@ import 'package:flutter/services.dart'; import 'package:badges/badges.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; +import 'package:matrix/matrix.dart'; import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/unread_rooms_badge.dart'; import '../../widgets/matrix.dart'; @@ -103,155 +105,177 @@ class ChatListView extends StatelessWidget { return; } }, - child: Row( - children: [ - if (FluffyThemes.isColumnMode(context) && - FluffyThemes.getDisplayNavigationRail(context)) ...[ - Builder(builder: (context) { - final allSpaces = client.rooms.where((room) => room.isSpace); - final rootSpaces = allSpaces - .where( - (space) => !allSpaces.any( - (parentSpace) => parentSpace.spaceChildren - .any((child) => child.roomId == space.id), - ), - ) - .toList(); - final destinations = getNavigationDestinations(context); + child: StreamBuilder( + key: ValueKey(client.userID.toString() + + controller.activeFilter.toString() + + controller.activeSpaceId.toString()), + stream: client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, snapshot) { + return Row( + children: [ + if (FluffyThemes.isColumnMode(context) && + FluffyThemes.getDisplayNavigationRail(context)) ...[ + Builder(builder: (context) { + final allSpaces = + client.rooms.where((room) => room.isSpace); + final rootSpaces = allSpaces + .where( + (space) => !allSpaces.any( + (parentSpace) => parentSpace.spaceChildren + .any((child) => child.roomId == space.id), + ), + ) + .toList(); + final destinations = getNavigationDestinations(context); - return SizedBox( - width: 64, - child: ListView.builder( - scrollDirection: Axis.vertical, - itemCount: rootSpaces.length + destinations.length, - itemBuilder: (context, i) { - if (i < destinations.length) { - final isSelected = i == controller.selectedIndex; - return Container( - height: 64, - width: 64, - decoration: BoxDecoration( - border: Border( - bottom: i == (destinations.length - 1) - ? BorderSide( - width: 1, - color: Theme.of(context).dividerColor, - ) - : BorderSide.none, - left: BorderSide( - color: isSelected - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - width: 4, - ), - right: const BorderSide( - color: Colors.transparent, - width: 4, - ), - ), - ), - alignment: Alignment.center, - child: IconButton( - color: isSelected - ? Theme.of(context).colorScheme.secondary - : null, - icon: CircleAvatar( - backgroundColor: isSelected - ? Theme.of(context).colorScheme.secondary - : Theme.of(context) - .colorScheme - .background, - foregroundColor: isSelected - ? Theme.of(context) - .colorScheme - .onSecondary - : Theme.of(context) - .colorScheme - .onBackground, - child: i == controller.selectedIndex - ? destinations[i].selectedIcon ?? - destinations[i].icon - : destinations[i].icon), - tooltip: destinations[i].label, - onPressed: () => - controller.onDestinationSelected(i), - ), - ); - } - i -= destinations.length; - final isSelected = - controller.activeFilter == ActiveFilter.spaces && - rootSpaces[i].id == controller.activeSpaceId; - return Container( - height: 64, + return SizedBox( width: 64, - decoration: BoxDecoration( - border: Border( - left: BorderSide( - color: isSelected - ? Theme.of(context).colorScheme.secondary - : Colors.transparent, - width: 4, - ), - right: const BorderSide( - color: Colors.transparent, - width: 4, - ), - ), - ), - alignment: Alignment.center, - child: IconButton( - tooltip: rootSpaces[i].displayname, - icon: Avatar( - mxContent: rootSpaces[i].avatar, - name: rootSpaces[i].displayname, - size: 32, - fontSize: 12, - ), - onPressed: () => - controller.setActiveSpace(rootSpaces[i].id), + child: ListView.builder( + scrollDirection: Axis.vertical, + itemCount: rootSpaces.length + destinations.length, + itemBuilder: (context, i) { + if (i < destinations.length) { + final isSelected = + i == controller.selectedIndex; + return Container( + height: 64, + width: 64, + decoration: BoxDecoration( + border: Border( + bottom: i == (destinations.length - 1) + ? BorderSide( + width: 1, + color: Theme.of(context) + .dividerColor, + ) + : BorderSide.none, + left: BorderSide( + color: isSelected + ? Theme.of(context) + .colorScheme + .primary + : Colors.transparent, + width: 4, + ), + right: const BorderSide( + color: Colors.transparent, + width: 4, + ), + ), + ), + alignment: Alignment.center, + child: IconButton( + color: isSelected + ? Theme.of(context) + .colorScheme + .secondary + : null, + icon: CircleAvatar( + backgroundColor: isSelected + ? Theme.of(context) + .colorScheme + .secondary + : Theme.of(context) + .colorScheme + .background, + foregroundColor: isSelected + ? Theme.of(context) + .colorScheme + .onSecondary + : Theme.of(context) + .colorScheme + .onBackground, + child: i == controller.selectedIndex + ? destinations[i].selectedIcon ?? + destinations[i].icon + : destinations[i].icon), + tooltip: destinations[i].label, + onPressed: () => + controller.onDestinationSelected(i), + ), + ); + } + i -= destinations.length; + final isSelected = controller.activeFilter == + ActiveFilter.spaces && + rootSpaces[i].id == controller.activeSpaceId; + return Container( + height: 64, + width: 64, + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: isSelected + ? Theme.of(context) + .colorScheme + .secondary + : Colors.transparent, + width: 4, + ), + right: const BorderSide( + color: Colors.transparent, + width: 4, + ), + ), + ), + alignment: Alignment.center, + child: IconButton( + tooltip: rootSpaces[i].displayname, + icon: Avatar( + mxContent: rootSpaces[i].avatar, + name: rootSpaces[i].displayname, + size: 32, + fontSize: 12, + ), + onPressed: () => controller + .setActiveSpace(rootSpaces[i].id), + ), + ); + }, ), ); - }, + }), + Container( + color: Theme.of(context).dividerColor, + width: 1, + ), + ], + Expanded( + child: Scaffold( + appBar: ChatListHeader(controller: controller), + body: ChatListViewBody(controller), + bottomNavigationBar: controller.displayNavigationBar + ? NavigationBar( + height: 64, + selectedIndex: controller.selectedIndex, + onDestinationSelected: + controller.onDestinationSelected, + destinations: + getNavigationDestinations(context), + ) + : null, + floatingActionButton: controller + .filteredRooms.isNotEmpty && + selectMode == SelectMode.normal + ? KeyBoardShortcuts( + keysToPress: { + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.keyN + }, + onKeysPressed: () => + VRouter.of(context).to('/newprivatechat'), + helpLabel: L10n.of(context)!.newChat, + child: StartChatFloatingActionButton( + controller: controller), + ) + : null, + ), ), - ); - }), - Container( - color: Theme.of(context).dividerColor, - width: 1, - ), - ], - Expanded( - child: Scaffold( - appBar: ChatListHeader(controller: controller), - body: ChatListViewBody(controller), - bottomNavigationBar: controller.displayNavigationBar - ? NavigationBar( - height: 64, - selectedIndex: controller.selectedIndex, - onDestinationSelected: - controller.onDestinationSelected, - destinations: getNavigationDestinations(context), - ) - : null, - floatingActionButton: controller.filteredRooms.isNotEmpty && - selectMode == SelectMode.normal - ? KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.keyN - }, - onKeysPressed: () => - VRouter.of(context).to('/newprivatechat'), - helpLabel: L10n.of(context)!.newChat, - child: StartChatFloatingActionButton( - controller: controller), - ) - : null, - ), - ), - ], - ), + ], + ); + }), ); }, ); diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index 30362ab3..b91dbe76 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -17,144 +17,150 @@ class HomeserverPickerView extends StatelessWidget { Widget build(BuildContext context) { final benchmarkResults = controller.benchmarkResults; return LoginScaffold( - body: Column( - children: [ - HomeserverAppBar(controller: controller), - // display a prominent banner to import session for TOR browser - // users. This feature is just some UX sugar as TOR users are - // usually forced to logout as TOR browser is non-persistent - AnimatedContainer( - height: controller.isTorBrowser ? 64 : 0, - duration: const Duration(milliseconds: 300), - clipBehavior: Clip.hardEdge, - curve: Curves.bounceInOut, - decoration: const BoxDecoration(), - child: Material( + body: SafeArea( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: HomeserverAppBar(controller: controller), + ), + // display a prominent banner to import session for TOR browser + // users. This feature is just some UX sugar as TOR users are + // usually forced to logout as TOR browser is non-persistent + AnimatedContainer( + height: controller.isTorBrowser ? 64 : 0, + duration: const Duration(milliseconds: 300), clipBehavior: Clip.hardEdge, - borderRadius: - const BorderRadius.vertical(bottom: Radius.circular(8)), - color: Theme.of(context).colorScheme.surface, - child: ListTile( - leading: const Icon(Icons.vpn_key), - title: Text(L10n.of(context)!.hydrateTor), - subtitle: Text(L10n.of(context)!.hydrateTorLong), - trailing: const Icon(Icons.chevron_right_outlined), - onTap: controller.restoreBackup, + curve: Curves.bounceInOut, + decoration: const BoxDecoration(), + child: Material( + clipBehavior: Clip.hardEdge, + borderRadius: + const BorderRadius.vertical(bottom: Radius.circular(8)), + color: Theme.of(context).colorScheme.surface, + child: ListTile( + leading: const Icon(Icons.vpn_key), + title: Text(L10n.of(context)!.hydrateTor), + subtitle: Text(L10n.of(context)!.hydrateTorLong), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: controller.restoreBackup, + ), ), ), - ), - Expanded( - child: ListView( - children: [ - if (controller.displayServerList) - Padding( - padding: const EdgeInsets.all(12.0), - child: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - color: Theme.of(context).colorScheme.onInverseSurface, - clipBehavior: Clip.hardEdge, - child: benchmarkResults == null - ? const Center( - child: Padding( - padding: EdgeInsets.all(12.0), - child: CircularProgressIndicator.adaptive(), - )) - : Column( - children: controller.filteredHomeservers - .map( - (server) => ListTile( - trailing: IconButton( - icon: const Icon( - Icons.info_outlined, - color: Colors.black, - ), - onPressed: () => - controller.showServerInfo(server), - ), - onTap: () => controller.setServer( - server.homeserver.baseUrl.host), - title: Text( - server.homeserver.baseUrl.host, - style: const TextStyle( - color: Colors.black), - ), - subtitle: Text( - server.homeserver.description ?? '', - style: TextStyle( - color: Colors.grey.shade700), - ), - ), - ) - .toList(), - ), - ), - ) - else ...[ - Container( - alignment: Alignment.center, - height: 200, - child: Image.asset( - 'assets/info-logo.png', - filterQuality: FilterQuality.medium, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Center( - child: Text( - AppConfig.applicationWelcomeMessage ?? - L10n.of(context)!.welcomeText, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 20), - ), - ), - ), - ], - ], - ), - ), - SafeArea( - child: Container( - padding: const EdgeInsets.all(12), - width: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, + Expanded( + child: ListView( children: [ - TextButton( - style: TextButton.styleFrom( - foregroundColor: - Theme.of(context).colorScheme.onSurfaceVariant, - ), - onPressed: controller.restoreBackup, - child: Text(L10n.of(context)!.hydrate), - ), - TextButton( - onPressed: () => launch(AppConfig.privacyUrl), - child: Text(L10n.of(context)!.privacy), - ), - Hero( - tag: 'loginButton', - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: - Theme.of(context).colorScheme.onPrimary, + if (controller.displayServerList) + Padding( + padding: const EdgeInsets.all(12.0), + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + color: Theme.of(context).colorScheme.onInverseSurface, + clipBehavior: Clip.hardEdge, + child: benchmarkResults == null + ? const Center( + child: Padding( + padding: EdgeInsets.all(12.0), + child: CircularProgressIndicator.adaptive(), + )) + : Column( + children: controller.filteredHomeservers + .map( + (server) => ListTile( + trailing: IconButton( + icon: const Icon( + Icons.info_outlined, + color: Colors.black, + ), + onPressed: () => + controller.showServerInfo(server), + ), + onTap: () => controller.setServer( + server.homeserver.baseUrl.host), + title: Text( + server.homeserver.baseUrl.host, + style: const TextStyle( + color: Colors.black), + ), + subtitle: Text( + server.homeserver.description ?? '', + style: TextStyle( + color: Colors.grey.shade700), + ), + ), + ) + .toList(), + ), + ), + ) + else ...[ + Container( + alignment: Alignment.center, + height: 200, + child: Image.asset( + 'assets/info-logo.png', + filterQuality: FilterQuality.medium, ), - onPressed: controller.isLoading - ? null - : controller.checkHomeserverAction, - child: controller.isLoading - ? const LinearProgressIndicator() - : Text(L10n.of(context)!.connect), ), - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Center( + child: Text( + AppConfig.applicationWelcomeMessage ?? + L10n.of(context)!.welcomeText, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 20), + ), + ), + ), + ], ], ), ), - ), - ], + SafeArea( + child: Container( + padding: const EdgeInsets.all(12), + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextButton( + style: TextButton.styleFrom( + foregroundColor: + Theme.of(context).colorScheme.onSurfaceVariant, + ), + onPressed: controller.restoreBackup, + child: Text(L10n.of(context)!.hydrate), + ), + TextButton( + onPressed: () => launch(AppConfig.privacyUrl), + child: Text(L10n.of(context)!.privacy), + ), + Hero( + tag: 'loginButton', + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.primary, + foregroundColor: + Theme.of(context).colorScheme.onPrimary, + ), + onPressed: controller.isLoading + ? null + : controller.checkHomeserverAction, + child: controller.isLoading + ? const LinearProgressIndicator() + : Text(L10n.of(context)!.connect), + ), + ), + ], + ), + ), + ), + ], + ), ), ); }