From 6487a1ff65ea13324055ec044d0ee7d182b2ec87 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 17 Jan 2026 21:58:27 +0800 Subject: [PATCH] :lipstick: Lift file list search to appbar --- lib/screens/files/file_list.dart | 34 ++- lib/widgets/file_list_view.dart | 374 ++++++++++++++----------------- 2 files changed, 193 insertions(+), 215 deletions(-) diff --git a/lib/screens/files/file_list.dart b/lib/screens/files/file_list.dart index 889befa2..52b921bd 100644 --- a/lib/screens/files/file_list.dart +++ b/lib/screens/files/file_list.dart @@ -1,5 +1,4 @@ import 'package:cross_file/cross_file.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -33,21 +32,33 @@ class FileListScreen extends HookConsumerWidget { final viewMode = useState(FileListViewMode.list); final isSelectionMode = useState(false); final recycled = useState(false); + final query = useState(null); final unindexedNotifier = ref.read(unindexedFileListProvider.notifier); return AppScaffold( isNoBackground: false, appBar: AppBar( - title: Text('files').tr(), + title: SearchBar( + constraints: const BoxConstraints(maxWidth: 400, minHeight: 32), + hintText: 'Search files...', + hintStyle: WidgetStatePropertyAll(TextStyle(fontSize: 14)), + onChanged: (value) { + // Update the query state that will be passed to FileListView + query.value = value.isEmpty ? null : value; + }, + leading: Icon( + Symbols.search, + size: 20, + color: Theme.of(context).colorScheme.onSurface, + ), + ), leading: const PageBackButton(backTo: '/account'), actions: [ // Selection mode toggle IconButton( icon: Icon( - isSelectionMode.value - ? Symbols.close - : Symbols.select_check_box, + isSelectionMode.value ? Symbols.close : Symbols.select_check_box, ), onPressed: () => isSelectionMode.value = !isSelectionMode.value, tooltip: isSelectionMode.value @@ -82,9 +93,14 @@ class FileListScreen extends HookConsumerWidget { ), floatingActionButton: mode.value == FileListMode.normal ? FloatingActionButton( - onPressed: () => _showActionBottomSheet(context, ref, currentPath, selectedPool), - child: const Icon(Symbols.add), + onPressed: () => _showActionBottomSheet( + context, + ref, + currentPath, + selectedPool, + ), tooltip: 'Add files or create directory', + child: const Icon(Symbols.add), ) : null, body: usageAsync.when( @@ -242,11 +258,11 @@ class FileListScreen extends HookConsumerWidget { context: context, isScrollControlled: true, builder: (context) => SheetScaffold( - titleText: 'Usage Overview', child: UsageOverviewWidget( usage: usage, quota: quota, ).padding(horizontal: 8, vertical: 16), + titleText: 'Usage Overview', ), ); } @@ -289,4 +305,4 @@ class FileListScreen extends HookConsumerWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/file_list_view.dart b/lib/widgets/file_list_view.dart index 34ad7d1f..c539951d 100644 --- a/lib/widgets/file_list_view.dart +++ b/lib/widgets/file_list_view.dart @@ -1347,232 +1347,194 @@ class FileListView extends HookConsumerWidget { ValueNotifier orderDesc, ObjectRef queryDebounceTimer, ) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Search bar below chips - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - child: SearchBar( - constraints: const BoxConstraints(minHeight: 48), - elevation: WidgetStatePropertyAll(2), - hintText: 'Search files...', - onChanged: (value) { - queryDebounceTimer.value?.cancel(); - queryDebounceTimer.value = Timer( - const Duration(milliseconds: 300), - () { - query.value = value.isEmpty ? null : value; - }, - ); - }, - leading: const Icon(Symbols.search).padding(horizontal: 24), - ), - ), - - const Gap(12), - - // Chips row - SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 16), - scrollDirection: Axis.horizontal, - child: Row( - children: [ - // Pool filter dropdown - Container( - height: 32, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of( - ref.context, - ).colorScheme.outline.withOpacity(0.5), + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + child: Row( + children: [ + // Pool filter dropdown + Container( + height: 32, + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(ref.context).colorScheme.outline.withOpacity(0.5), + ), + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: selectedPool.value, + items: [ + const DropdownMenuItem( + value: null, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Symbols.database, size: 16), + Gap(6), + Text('All files', style: TextStyle(fontSize: 12)), + ], + ), ), - borderRadius: BorderRadius.circular(8), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: selectedPool.value, - items: [ - const DropdownMenuItem( - value: null, + ...poolsAsync.maybeWhen( + data: (pools) => pools.map( + (pool) => DropdownMenuItem( + value: pool, child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Symbols.database, size: 16), Gap(6), - Text('All files', style: TextStyle(fontSize: 12)), + Text( + pool.name, + style: const TextStyle(fontSize: 12), + ), ], ), ), - ...poolsAsync.maybeWhen( - data: (pools) => pools.map( - (pool) => DropdownMenuItem( - value: pool, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Symbols.database, size: 16), - Gap(6), - Text( - pool.name, - style: const TextStyle(fontSize: 12), - ), - ], - ), + ), + orElse: () => >[], + ), + ], + onChanged: isRefreshing + ? null + : (value) { + selectedPool.value = value; + if (mode.value == FileListMode.unindexed) { + unindexedNotifier.setPool(value?.id); + } else { + cloudNotifier.setPool(value?.id); + } + }, + icon: const Icon(Symbols.arrow_drop_down, size: 16), + isDense: true, + ), + ), + ), + + const Gap(8), + + // Order filter dropdown + Container( + height: 32, + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(ref.context).colorScheme.outline.withOpacity(0.5), + ), + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: order.value ?? 'date', + items: [ + DropdownMenuItem( + value: 'date', + child: Row( + spacing: 6, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Symbols.schedule, size: 16), + Text('Date', style: const TextStyle(fontSize: 12)), + if (order.value == 'date') + Icon( + orderDesc.value ? Symbols.arrow_downward : Symbols.arrow_upward, + size: 14, ), - ), - orElse: () => >[], - ), - ], - onChanged: isRefreshing - ? null - : (value) { - selectedPool.value = value; - if (mode.value == FileListMode.unindexed) { - unindexedNotifier.setPool(value?.id); - } else { - cloudNotifier.setPool(value?.id); - } - }, - icon: const Icon(Symbols.arrow_drop_down, size: 16), - isDense: true, + ], + ), ), - ), - ), - - const Gap(8), - - // Order filter dropdown - Container( - height: 32, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of( - ref.context, - ).colorScheme.outline.withOpacity(0.5), + DropdownMenuItem( + value: 'size', + child: Row( + spacing: 6, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Symbols.data_usage, size: 16), + Text( + 'fileSize'.tr(), + style: const TextStyle(fontSize: 12), + ), + if (order.value == 'size') + Icon( + orderDesc.value ? Symbols.arrow_downward : Symbols.arrow_upward, + size: 16, + ), + ], + ), ), - borderRadius: BorderRadius.circular(8), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: order.value ?? 'date', - items: [ - DropdownMenuItem( - value: 'date', - child: Row( - spacing: 6, - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Symbols.schedule, size: 16), - Text('Date', style: const TextStyle(fontSize: 12)), - if (order.value == 'date') - Icon( - orderDesc.value - ? Symbols.arrow_downward - : Symbols.arrow_upward, - size: 14, - ), - ], + DropdownMenuItem( + value: 'name', + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 6, + children: [ + Icon(Symbols.sort_by_alpha, size: 16), + Text( + 'fileName'.tr(), + style: const TextStyle(fontSize: 12), ), - ), - DropdownMenuItem( - value: 'size', - child: Row( - spacing: 6, - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Symbols.data_usage, size: 16), - Text( - 'fileSize'.tr(), - style: const TextStyle(fontSize: 12), - ), - if (order.value == 'size') - Icon( - orderDesc.value - ? Symbols.arrow_downward - : Symbols.arrow_upward, - size: 16, - ), - ], - ), - ), - DropdownMenuItem( - value: 'name', - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 6, - children: [ - Icon(Symbols.sort_by_alpha, size: 16), - Text( - 'fileName'.tr(), - style: const TextStyle(fontSize: 12), - ), - if (order.value == 'name') - Icon( - orderDesc.value - ? Symbols.arrow_downward - : Symbols.arrow_upward, - size: 16, - ), - ], - ), - ), - ], - onChanged: (value) { - if (value == order.value) { - // Toggle direction if same option selected - final newValue = !orderDesc.value; - orderDesc.value = newValue; - if (mode.value == FileListMode.unindexed) { - unindexedNotifier.setOrderDesc(newValue); - } else { - cloudNotifier.setOrderDesc(newValue); - } - } else { - // Change sort option - order.value = value; - if (mode.value == FileListMode.unindexed) { - unindexedNotifier.setOrder(value); - } else { - cloudNotifier.setOrder(value); - } - } - }, - icon: const SizedBox.shrink(), - isDense: true, + if (order.value == 'name') + Icon( + orderDesc.value ? Symbols.arrow_downward : Symbols.arrow_upward, + size: 16, + ), + ], + ), ), - ), - ), - - const Gap(8), - - // Refresh chip - FilterChip( - label: const Row( - mainAxisSize: MainAxisSize.min, - spacing: 6, - children: [ - Icon(Symbols.refresh, size: 16), - Text('Refresh', style: TextStyle(fontSize: 12)), - ], - ), - selected: false, - onSelected: (selected) { - if (selected) { + ], + onChanged: (value) { + if (value == order.value) { + // Toggle direction if same option selected + final newValue = !orderDesc.value; + orderDesc.value = newValue; if (mode.value == FileListMode.unindexed) { - ref.invalidate(unindexedFileListProvider); + unindexedNotifier.setOrderDesc(newValue); } else { - cloudNotifier.setPath(currentPath.value); + cloudNotifier.setOrderDesc(newValue); + } + } else { + // Change sort option + order.value = value; + if (mode.value == FileListMode.unindexed) { + unindexedNotifier.setOrder(value); + } else { + cloudNotifier.setOrder(value); } } }, + icon: const SizedBox.shrink(), + isDense: true, ), - ], + ), ), - ), - ], + + const Gap(8), + + // Refresh chip + FilterChip( + label: const Row( + mainAxisSize: MainAxisSize.min, + spacing: 6, + children: [ + Icon(Symbols.refresh, size: 16), + Text('Refresh', style: TextStyle(fontSize: 12)), + ], + ), + selected: false, + onSelected: (selected) { + if (selected) { + if (mode.value == FileListMode.unindexed) { + ref.invalidate(unindexedFileListProvider); + } else { + cloudNotifier.setPath(currentPath.value); + } + } + }, + ), + ], + ), ); } -} +} \ No newline at end of file