diff --git a/android/fastlane/metadata/android/en-US/changelogs/126.txt b/android/fastlane/metadata/android/en-US/changelogs/126.txt new file mode 100644 index 0000000000..6a41b2fd27 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/126.txt @@ -0,0 +1 @@ +- New Feature: Search (for quickly locating files and navigating through folders and drives) \ No newline at end of file diff --git a/lib/app_shell.dart b/lib/app_shell.dart index 2a51c6fb2c..3550d09c06 100644 --- a/lib/app_shell.dart +++ b/lib/app_shell.dart @@ -222,20 +222,6 @@ class MobileAppBar extends StatelessWidget implements PreferredSizeWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.only( - left: 24.0, - ), - child: ArDriveImage( - image: AssetImage( - isLightMode - ? Resources.images.brand.blackLogo1 - : Resources.images.brand.whiteLogo1, - ), - width: 128, - height: 28, - ), - ), Padding( padding: const EdgeInsets.only(left: 7.0), child: leading ?? @@ -252,6 +238,21 @@ class MobileAppBar extends StatelessWidget implements PreferredSizeWidget { ) : Container()), ), + if (!showDrawerButton) + Padding( + padding: const EdgeInsets.only( + left: 24.0, + ), + child: ArDriveImage( + image: AssetImage( + isLightMode + ? Resources.images.brand.blackLogo1 + : Resources.images.brand.whiteLogo1, + ), + width: 128, + height: 28, + ), + ), const Spacer(), const SyncButton(), const SizedBox( diff --git a/lib/components/app_top_bar.dart b/lib/components/app_top_bar.dart index b0cf597db2..35feb1cb55 100644 --- a/lib/components/app_top_bar.dart +++ b/lib/components/app_top_bar.dart @@ -10,7 +10,6 @@ import 'package:ardrive/sync/domain/cubit/sync_cubit.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/plausible_event_tracker/plausible_custom_event_properties.dart'; import 'package:ardrive/utils/plausible_event_tracker/plausible_event_tracker.dart'; -import 'package:ardrive/utils/show_general_dialog.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -21,7 +20,6 @@ class AppTopBar extends StatelessWidget { @override Widget build(BuildContext context) { final enableSearch = context.read().config.enableSearch; - final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; final controller = TextEditingController(); return SizedBox( @@ -40,14 +38,10 @@ class AppTopBar extends StatelessWidget { child: SearchTextField( controller: controller, onFieldSubmitted: (query) { - showArDriveDialog( - context, - content: FileSearchModal( - initialQuery: query, - driveDetailCubit: context.read(), - controller: controller, - ), - barrierColor: colorTokens.containerL1.withOpacity(0.8), + showSearchModalDesktop( + context: context, + driveDetailCubit: context.read(), + controller: controller, ); }, ), diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 0dfb220ecf..9af301be25 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -32,6 +32,8 @@ import 'package:ardrive/pages/drive_detail/components/dropdown_item.dart'; import 'package:ardrive/pages/drive_detail/components/file_icon.dart'; import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; import 'package:ardrive/pages/drive_detail/components/unpreviewable_content.dart'; +import 'package:ardrive/search/search_modal.dart'; +import 'package:ardrive/search/search_text_field.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/sharing/sharing_file_listener.dart'; import 'package:ardrive/sync/domain/cubit/sync_cubit.dart'; @@ -83,6 +85,7 @@ class DriveDetailPage extends StatefulWidget { class _DriveDetailPageState extends State { bool checkboxEnabled = false; final _scrollController = ScrollController(); + final controller = TextEditingController(); @override void initState() { @@ -742,8 +745,47 @@ class _DriveDetailPageState extends State { filteredItems = items.where((item) => item.isHidden == false).toList(); } + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; return Column( children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: SearchTextField( + controller: controller, + onFieldSubmitted: (query) { + if (query.isEmpty) { + return; + } + + showModalBottomSheet( + isScrollControlled: true, + context: context, + backgroundColor: Colors.transparent, + builder: (_) => Container( + height: MediaQuery.of(context).size.height * 0.85, + decoration: BoxDecoration( + color: colorTokens.containerL2, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6.0), + topRight: Radius.circular(6.0), + ), + ), + child: Padding( + padding: MediaQuery.of(context).viewInsets, + child: BlocProvider.value( + value: context.read(), + child: FileSearchModal( + initialQuery: query, + driveDetailCubit: context.read(), + controller: controller, + ), + ), + ), + ), + ); + }, + ), + ), Padding( padding: const EdgeInsets.only(top: 8), child: Row( @@ -924,42 +966,52 @@ class MobileFolderNavigation extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( - child: InkWell( - onTap: () { - String? targetId; + child: SizedBox( + height: 45, + child: InkWell( + onTap: () { + if (path.length == 1) { + // If we are at the root folder, open the drive + context.read().openFolder(); + return; + } + String? targetId; - if (path.isNotEmpty) { - targetId = path.first.targetId; - } + if (path.isNotEmpty) { + targetId = path.first.targetId; + } - context.read().openFolder(folderId: targetId); - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (path.isNotEmpty) - Padding( - padding: const EdgeInsets.only( - left: 16, - right: 8, - top: 4, + context + .read() + .openFolder(folderId: targetId); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (path.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 16, + right: 8, + top: 4, + ), + child: ArDriveIcons.arrowLeft(), ), - child: ArDriveIcons.arrowLeft(), - ), - Expanded( - child: Padding( - padding: path.isEmpty - ? const EdgeInsets.only(left: 16, top: 6, bottom: 6) - : EdgeInsets.zero, - child: Text( - _pathToName( - path.isEmpty ? driveName : path.last.text, + Expanded( + child: Padding( + padding: path.isEmpty + ? const EdgeInsets.only(left: 16, top: 6, bottom: 6) + : EdgeInsets.zero, + child: Text( + _pathToName( + path.isEmpty ? driveName : path.last.text, + ), + style: ArDriveTypography.body.buttonNormalBold(), ), - style: ArDriveTypography.body.buttonNormalBold(), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/search/search_modal.dart b/lib/search/search_modal.dart index 0c07a256c7..f83a9d8b00 100644 --- a/lib/search/search_modal.dart +++ b/lib/search/search_modal.dart @@ -11,13 +11,68 @@ import 'package:ardrive/search/domain/bloc/search_bloc.dart'; import 'package:ardrive/search/domain/repository/search_repository.dart'; import 'package:ardrive/search/search_result.dart'; import 'package:ardrive/search/search_text_field.dart'; +import 'package:ardrive/utils/show_general_dialog.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:responsive_builder/responsive_builder.dart'; import '../models/models.dart'; +Future showSearchModalBottomSheet({ + required BuildContext context, + required DriveDetailCubit driveDetailCubit, + required TextEditingController controller, + String? query, +}) { + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + return showModalBottomSheet( + isScrollControlled: true, + context: context, + backgroundColor: Colors.transparent, + builder: (_) => Container( + height: MediaQuery.of(context).size.height * 0.85, + decoration: BoxDecoration( + color: colorTokens.containerL2, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6.0), + topRight: Radius.circular(6.0), + ), + ), + child: Padding( + padding: MediaQuery.of(context).viewInsets, + child: BlocProvider.value( + value: context.read(), + child: FileSearchModal( + initialQuery: query, + driveDetailCubit: context.read(), + controller: controller, + ), + ), + ), + ), + ); +} + +Future showSearchModalDesktop({ + required BuildContext context, + required DriveDetailCubit driveDetailCubit, + required TextEditingController controller, + String? query, +}) { + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + return showArDriveDialog( + context, + content: FileSearchModal( + initialQuery: query, + driveDetailCubit: context.read(), + controller: controller, + ), + barrierColor: colorTokens.containerL1.withOpacity(0.8), + ); +} + class FileSearchModal extends StatelessWidget { const FileSearchModal({ super.key, @@ -85,32 +140,71 @@ class _FileSearchModalState extends State<_FileSearchModal> { } Future searchFiles(String query) async { - context.read().add(SearchQueryChanged(query)); + if (mounted) { + context.read().add(SearchQueryChanged(query)); + } } @override Widget build(BuildContext context) { final typography = ArDriveTypographyNew.of(context); final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; - return ArDriveLoginModal( - width: MediaQuery.of(context).size.width * 0.6, - content: SizedBox( - height: MediaQuery.of(context).size.height * 0.7, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildSearchHeader(typography, colorTokens), - const SizedBox(height: 16), - SearchTextField( - controller: widget.controller, - onFieldSubmitted: (_) => searchFiles(widget.controller.text), + return ScreenTypeLayout.builder(mobile: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: colorTokens.containerRed, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(9), + topRight: Radius.circular(9), + ), ), - const SizedBox(height: 16), - _buildSearchResults(context, typography, colorTokens), - ], + height: 6, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _buildSearchHeader(typography, colorTokens), + const SizedBox(height: 16), + SearchTextField( + controller: widget.controller, + onFieldSubmitted: (_) => + searchFiles(widget.controller.text), + ), + const SizedBox(height: 16), + _buildSearchResults(context, typography, colorTokens), + ], + ), + ), + ), + ], + ); + }, desktop: (context) { + return ArDriveLoginModal( + width: MediaQuery.of(context).size.width * 0.6, + content: SizedBox( + height: MediaQuery.of(context).size.height * 0.7, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildSearchHeader(typography, colorTokens), + const SizedBox(height: 16), + SearchTextField( + controller: widget.controller, + onFieldSubmitted: (_) => searchFiles(widget.controller.text), + ), + const SizedBox(height: 16), + _buildSearchResults(context, typography, colorTokens), + ], + ), ), - ), - ); + ); + }); } Widget _buildSearchHeader( @@ -192,8 +286,7 @@ class _FileSearchModalState extends State<_FileSearchModal> { child: ListTile( onTap: () => _handleNavigation(context, searchResult), leading: leadingIcon, - title: Row( - mainAxisSize: MainAxisSize.min, + title: Wrap( children: [ Text( name, diff --git a/lib/search/search_text_field.dart b/lib/search/search_text_field.dart index b567c9ff23..f7155a9bb7 100644 --- a/lib/search/search_text_field.dart +++ b/lib/search/search_text_field.dart @@ -27,6 +27,9 @@ class SearchTextField extends StatelessWidget { color: colorTokens.textMid, ), ), + onTapOutside: (_) { + FocusScope.of(context).unfocus(); + }, suffixIcon: Transform( // move 4 pixels bottom transform: Matrix4.translationValues(0.0, 4.0, 0.0), diff --git a/packages/ardrive_ui/lib/src/components/text_field_new.dart b/packages/ardrive_ui/lib/src/components/text_field_new.dart index 03e98a8a2f..31ca3dc3f4 100644 --- a/packages/ardrive_ui/lib/src/components/text_field_new.dart +++ b/packages/ardrive_ui/lib/src/components/text_field_new.dart @@ -218,6 +218,7 @@ class ArDriveTextFieldNew extends StatefulWidget { this.maxLines = 1, this.errorMessage, this.prefixIcon, + this.onTapOutside, }); final bool isEnabled; @@ -253,6 +254,7 @@ class ArDriveTextFieldNew extends StatefulWidget { final int? maxLines; final String? errorMessage; final Widget? prefixIcon; + final Function(PointerDownEvent)? onTapOutside; @override State createState() => ArDriveTextFieldStateNew(); @@ -307,6 +309,7 @@ class ArDriveTextFieldStateNew extends State { ), ), TextFormField( + onTapOutside: widget.onTapOutside, controller: controller, autocorrect: widget.autocorrect, autofocus: widget.autofocus, diff --git a/pubspec.yaml b/pubspec.yaml index 9dc4906a2a..c3aae8d1c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Secure, permanent storage publish_to: 'none' -version: 2.43.0 +version: 2.44.0 environment: sdk: '>=3.2.0 <4.0.0'