💄 Lift file list search to appbar

This commit is contained in:
2026-01-17 21:58:27 +08:00
parent 2e90d243de
commit 6487a1ff65
2 changed files with 193 additions and 215 deletions

View File

@@ -1,5 +1,4 @@
import 'package:cross_file/cross_file.dart'; import 'package:cross_file/cross_file.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@@ -33,21 +32,33 @@ class FileListScreen extends HookConsumerWidget {
final viewMode = useState(FileListViewMode.list); final viewMode = useState(FileListViewMode.list);
final isSelectionMode = useState<bool>(false); final isSelectionMode = useState<bool>(false);
final recycled = useState<bool>(false); final recycled = useState<bool>(false);
final query = useState<String?>(null);
final unindexedNotifier = ref.read(unindexedFileListProvider.notifier); final unindexedNotifier = ref.read(unindexedFileListProvider.notifier);
return AppScaffold( return AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: AppBar( 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'), leading: const PageBackButton(backTo: '/account'),
actions: [ actions: [
// Selection mode toggle // Selection mode toggle
IconButton( IconButton(
icon: Icon( icon: Icon(
isSelectionMode.value isSelectionMode.value ? Symbols.close : Symbols.select_check_box,
? Symbols.close
: Symbols.select_check_box,
), ),
onPressed: () => isSelectionMode.value = !isSelectionMode.value, onPressed: () => isSelectionMode.value = !isSelectionMode.value,
tooltip: isSelectionMode.value tooltip: isSelectionMode.value
@@ -82,9 +93,14 @@ class FileListScreen extends HookConsumerWidget {
), ),
floatingActionButton: mode.value == FileListMode.normal floatingActionButton: mode.value == FileListMode.normal
? FloatingActionButton( ? FloatingActionButton(
onPressed: () => _showActionBottomSheet(context, ref, currentPath, selectedPool), onPressed: () => _showActionBottomSheet(
child: const Icon(Symbols.add), context,
ref,
currentPath,
selectedPool,
),
tooltip: 'Add files or create directory', tooltip: 'Add files or create directory',
child: const Icon(Symbols.add),
) )
: null, : null,
body: usageAsync.when( body: usageAsync.when(
@@ -242,11 +258,11 @@ class FileListScreen extends HookConsumerWidget {
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: (context) => SheetScaffold( builder: (context) => SheetScaffold(
titleText: 'Usage Overview',
child: UsageOverviewWidget( child: UsageOverviewWidget(
usage: usage, usage: usage,
quota: quota, quota: quota,
).padding(horizontal: 8, vertical: 16), ).padding(horizontal: 8, vertical: 16),
titleText: 'Usage Overview',
), ),
); );
} }

View File

@@ -1347,232 +1347,194 @@ class FileListView extends HookConsumerWidget {
ValueNotifier<bool> orderDesc, ValueNotifier<bool> orderDesc,
ObjectRef<Timer?> queryDebounceTimer, ObjectRef<Timer?> queryDebounceTimer,
) { ) {
return Column( return SingleChildScrollView(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.symmetric(horizontal: 16),
children: [ scrollDirection: Axis.horizontal,
// Search bar below chips child: Row(
Padding( children: [
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), // Pool filter dropdown
child: SearchBar( Container(
constraints: const BoxConstraints(minHeight: 48), height: 32,
elevation: WidgetStatePropertyAll(2), padding: const EdgeInsets.symmetric(horizontal: 8),
hintText: 'Search files...', decoration: BoxDecoration(
onChanged: (value) { border: Border.all(
queryDebounceTimer.value?.cancel(); color: Theme.of(ref.context).colorScheme.outline.withOpacity(0.5),
queryDebounceTimer.value = Timer( ),
const Duration(milliseconds: 300), borderRadius: BorderRadius.circular(8),
() { ),
query.value = value.isEmpty ? null : value; child: DropdownButtonHideUnderline(
}, child: DropdownButton<SnFilePool>(
); value: selectedPool.value,
}, items: [
leading: const Icon(Symbols.search).padding(horizontal: 24), const DropdownMenuItem<SnFilePool>(
), value: null,
), child: Row(
mainAxisSize: MainAxisSize.min,
const Gap(12), children: [
Icon(Symbols.database, size: 16),
// Chips row Gap(6),
SingleChildScrollView( Text('All files', style: TextStyle(fontSize: 12)),
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), ...poolsAsync.maybeWhen(
), data: (pools) => pools.map(
child: DropdownButtonHideUnderline( (pool) => DropdownMenuItem<SnFilePool>(
child: DropdownButton<SnFilePool>( value: pool,
value: selectedPool.value,
items: [
const DropdownMenuItem<SnFilePool>(
value: null,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Symbols.database, size: 16), Icon(Symbols.database, size: 16),
Gap(6), Gap(6),
Text('All files', style: TextStyle(fontSize: 12)), Text(
pool.name,
style: const TextStyle(fontSize: 12),
),
], ],
), ),
), ),
...poolsAsync.maybeWhen( ),
data: (pools) => pools.map( orElse: () => <DropdownMenuItem<SnFilePool>>[],
(pool) => DropdownMenuItem<SnFilePool>( ),
value: pool, ],
child: Row( onChanged: isRefreshing
mainAxisSize: MainAxisSize.min, ? null
children: [ : (value) {
Icon(Symbols.database, size: 16), selectedPool.value = value;
Gap(6), if (mode.value == FileListMode.unindexed) {
Text( unindexedNotifier.setPool(value?.id);
pool.name, } else {
style: const TextStyle(fontSize: 12), 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<String>(
value: order.value ?? 'date',
items: [
DropdownMenuItem<String>(
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: () => <DropdownMenuItem<SnFilePool>>[], ),
),
],
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,
), ),
), DropdownMenuItem<String>(
), value: 'size',
child: Row(
const Gap(8), spacing: 6,
mainAxisSize: MainAxisSize.min,
// Order filter dropdown children: [
Container( Icon(Symbols.data_usage, size: 16),
height: 32, Text(
padding: const EdgeInsets.symmetric(horizontal: 8), 'fileSize'.tr(),
decoration: BoxDecoration( style: const TextStyle(fontSize: 12),
border: Border.all( ),
color: Theme.of( if (order.value == 'size')
ref.context, Icon(
).colorScheme.outline.withOpacity(0.5), orderDesc.value ? Symbols.arrow_downward : Symbols.arrow_upward,
size: 16,
),
],
),
), ),
borderRadius: BorderRadius.circular(8), DropdownMenuItem<String>(
), value: 'name',
child: DropdownButtonHideUnderline( child: Row(
child: DropdownButton<String>( mainAxisSize: MainAxisSize.min,
value: order.value ?? 'date', spacing: 6,
items: [ children: [
DropdownMenuItem<String>( Icon(Symbols.sort_by_alpha, size: 16),
value: 'date', Text(
child: Row( 'fileName'.tr(),
spacing: 6, style: const TextStyle(fontSize: 12),
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,
),
],
), ),
), if (order.value == 'name')
DropdownMenuItem<String>( Icon(
value: 'size', orderDesc.value ? Symbols.arrow_downward : Symbols.arrow_upward,
child: Row( size: 16,
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<String>(
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,
), ),
), ],
), onChanged: (value) {
if (value == order.value) {
const Gap(8), // Toggle direction if same option selected
final newValue = !orderDesc.value;
// Refresh chip orderDesc.value = newValue;
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) { if (mode.value == FileListMode.unindexed) {
ref.invalidate(unindexedFileListProvider); unindexedNotifier.setOrderDesc(newValue);
} else { } 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);
}
}
},
),
],
),
); );
} }
} }