💄 Optimize file list UI
This commit is contained in:
@@ -31,6 +31,10 @@ class FileListScreen extends HookConsumerWidget {
|
||||
final quotaAsync = ref.watch(billingQuotaProvider);
|
||||
|
||||
final viewMode = useState(FileListViewMode.list);
|
||||
final isSelectionMode = useState<bool>(false);
|
||||
final recycled = useState<bool>(false);
|
||||
|
||||
final unindexedNotifier = ref.read(unindexedFileListProvider.notifier);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
@@ -38,6 +42,36 @@ class FileListScreen extends HookConsumerWidget {
|
||||
title: Text('files').tr(),
|
||||
leading: const PageBackButton(backTo: '/account'),
|
||||
actions: [
|
||||
// Selection mode toggle
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isSelectionMode.value
|
||||
? Symbols.close
|
||||
: Symbols.select_check_box,
|
||||
),
|
||||
onPressed: () => isSelectionMode.value = !isSelectionMode.value,
|
||||
tooltip: isSelectionMode.value
|
||||
? 'Exit Selection Mode'
|
||||
: 'Enter Selection Mode',
|
||||
),
|
||||
|
||||
// Recycle toggle (only in unindexed mode)
|
||||
if (mode.value == FileListMode.unindexed)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
recycled.value
|
||||
? Symbols.delete_forever
|
||||
: Symbols.restore_from_trash,
|
||||
),
|
||||
onPressed: () {
|
||||
recycled.value = !recycled.value;
|
||||
unindexedNotifier.setRecycled(recycled.value);
|
||||
},
|
||||
tooltip: recycled.value
|
||||
? 'Show Active Files'
|
||||
: 'Show Recycle Bin',
|
||||
),
|
||||
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.bar_chart),
|
||||
onPressed: () =>
|
||||
@@ -46,6 +80,13 @@ class FileListScreen extends HookConsumerWidget {
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
floatingActionButton: mode.value == FileListMode.normal
|
||||
? FloatingActionButton(
|
||||
onPressed: () => _showActionBottomSheet(context, ref, currentPath, selectedPool),
|
||||
child: const Icon(Symbols.add),
|
||||
tooltip: 'Add files or create directory',
|
||||
)
|
||||
: null,
|
||||
body: usageAsync.when(
|
||||
data: (usage) => quotaAsync.when(
|
||||
data: (quota) => FileListView(
|
||||
@@ -61,6 +102,7 @@ class FileListScreen extends HookConsumerWidget {
|
||||
onShowCreateDirectory: _showCreateDirectoryDialog,
|
||||
mode: mode,
|
||||
viewMode: viewMode,
|
||||
isSelectionMode: isSelectionMode,
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(child: Text('Error loading quota')),
|
||||
@@ -208,4 +250,43 @@ class FileListScreen extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showActionBottomSheet(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
ValueNotifier<String> currentPath,
|
||||
ValueNotifier<SnFilePool?> selectedPool,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.create_new_folder),
|
||||
title: const Text('Create Directory'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_showCreateDirectoryDialog(context, currentPath);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.upload_file),
|
||||
title: const Text('Upload File'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickAndUploadFile(
|
||||
ref,
|
||||
currentPath.value,
|
||||
selectedPool.value?.id,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@@ -19,7 +18,7 @@ import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/utils/file_icon_utils.dart';
|
||||
import 'package:island/utils/format.dart';
|
||||
import 'package:island/utils/text.dart';
|
||||
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/paging/pagination_list.dart';
|
||||
@@ -40,6 +39,7 @@ class FileListView extends HookConsumerWidget {
|
||||
final Function(BuildContext, ValueNotifier<String>) onShowCreateDirectory;
|
||||
final ValueNotifier<FileListMode> mode;
|
||||
final ValueNotifier<FileListViewMode> viewMode;
|
||||
final ValueNotifier<bool> isSelectionMode;
|
||||
|
||||
const FileListView({
|
||||
required this.usage,
|
||||
@@ -50,6 +50,7 @@ class FileListView extends HookConsumerWidget {
|
||||
required this.onShowCreateDirectory,
|
||||
required this.mode,
|
||||
required this.viewMode,
|
||||
required this.isSelectionMode,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -159,16 +160,99 @@ class FileListView extends HookConsumerWidget {
|
||||
),
|
||||
};
|
||||
|
||||
late Widget pathContent;
|
||||
late Widget pathWidget;
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
pathContent = const Text(
|
||||
pathWidget = InkWell(
|
||||
onTap: () async {
|
||||
final result = await showMenu<String>(
|
||||
context: context,
|
||||
position: const RelativeRect.fromLTRB(50, 100, 50, 100),
|
||||
items: [
|
||||
PopupMenuItem<String>(
|
||||
value: 'root',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.folder),
|
||||
const Gap(12),
|
||||
Text('Root Directory'),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'unindexed',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.inventory_2),
|
||||
const Gap(12),
|
||||
Text('Unindexed Files'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (result == 'root') {
|
||||
mode.value = FileListMode.normal;
|
||||
currentPath.value = '/';
|
||||
}
|
||||
// 'unindexed' does nothing as we're already in unindexed mode
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Symbols.inventory_2, size: 20),
|
||||
const Gap(8),
|
||||
const Text(
|
||||
'Unindexed Files',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (currentPath.value == '/') {
|
||||
pathContent = const Text(
|
||||
pathWidget = InkWell(
|
||||
onTap: () async {
|
||||
final result = await showMenu<String>(
|
||||
context: context,
|
||||
position: const RelativeRect.fromLTRB(50, 100, 50, 100),
|
||||
items: [
|
||||
PopupMenuItem<String>(
|
||||
value: 'unindexed',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.inventory_2),
|
||||
const Gap(12),
|
||||
Text('Unindexed Files'),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'root',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.folder),
|
||||
const Gap(12),
|
||||
Text('Root Directory'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (result == 'unindexed') {
|
||||
mode.value = FileListMode.unindexed;
|
||||
}
|
||||
// 'root' does nothing as we're already at root
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Symbols.folder, size: 20),
|
||||
const Gap(8),
|
||||
const Text(
|
||||
'Root Directory',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final pathParts = currentPath.value
|
||||
@@ -181,7 +265,17 @@ class FileListView extends HookConsumerWidget {
|
||||
breadcrumbs.add(
|
||||
InkWell(
|
||||
onTap: () => currentPath.value = '/',
|
||||
child: const Text('Root'),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Symbols.folder, size: 20),
|
||||
const Gap(4),
|
||||
const Text(
|
||||
'Root',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -197,7 +291,7 @@ class FileListView extends HookConsumerWidget {
|
||||
breadcrumbs.add(
|
||||
Text(
|
||||
pathParts[i],
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -205,13 +299,16 @@ class FileListView extends HookConsumerWidget {
|
||||
breadcrumbs.add(
|
||||
InkWell(
|
||||
onTap: () => currentPath.value = path,
|
||||
child: Text(pathParts[i]),
|
||||
child: Text(
|
||||
pathParts[i],
|
||||
style: const TextStyle(color: Colors.blue),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pathContent = Wrap(
|
||||
pathWidget = Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: breadcrumbs,
|
||||
);
|
||||
@@ -263,10 +360,48 @@ class FileListView extends HookConsumerWidget {
|
||||
? Theme.of(context).primaryColor.withOpacity(0.1)
|
||||
: null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Gap(12),
|
||||
_buildGlobalFilters(
|
||||
|
||||
// Breadcrumbs and view switch at the top
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 16, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AbsorbPointer(
|
||||
absorbing: isRefreshing,
|
||||
child: pathWidget,
|
||||
),
|
||||
),
|
||||
const Gap(12),
|
||||
SegmentedButton<FileListViewMode>(
|
||||
segments: const [
|
||||
ButtonSegment<FileListViewMode>(
|
||||
value: FileListViewMode.list,
|
||||
icon: Icon(Symbols.list),
|
||||
tooltip: 'List View',
|
||||
),
|
||||
ButtonSegment<FileListViewMode>(
|
||||
value: FileListViewMode.waterfall,
|
||||
icon: Icon(Symbols.view_module),
|
||||
tooltip: 'Waterfall View',
|
||||
),
|
||||
],
|
||||
selected: {viewMode.value},
|
||||
onSelectionChanged: (Set<FileListViewMode> newSelection) {
|
||||
viewMode.value = newSelection.first;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Gap(12),
|
||||
|
||||
// Chip-based filters
|
||||
_buildChipFilters(
|
||||
ref,
|
||||
poolsAsync,
|
||||
selectedPool,
|
||||
@@ -280,128 +415,8 @@ class FileListView extends HookConsumerWidget {
|
||||
orderDesc,
|
||||
queryDebounceTimer,
|
||||
),
|
||||
const Gap(6),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
mode.value == FileListMode.unindexed
|
||||
? Symbols.inventory_2
|
||||
: currentPath.value != '/'
|
||||
? Symbols.arrow_back
|
||||
: Symbols.folder,
|
||||
),
|
||||
onPressed: isRefreshing
|
||||
? null
|
||||
: () {
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
mode.value = FileListMode.normal;
|
||||
currentPath.value = '/';
|
||||
} else {
|
||||
final pathParts = currentPath.value
|
||||
.split('/')
|
||||
.where((part) => part.isNotEmpty)
|
||||
.toList();
|
||||
if (pathParts.isNotEmpty) {
|
||||
pathParts.removeLast();
|
||||
currentPath.value = pathParts.isEmpty
|
||||
? '/'
|
||||
: '/${pathParts.join('/')}';
|
||||
}
|
||||
}
|
||||
},
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: AbsorbPointer(
|
||||
absorbing: isRefreshing,
|
||||
child: pathContent,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
viewMode.value == FileListViewMode.list
|
||||
? Symbols.view_module
|
||||
: Symbols.list,
|
||||
),
|
||||
onPressed: () => viewMode.value =
|
||||
viewMode.value == FileListViewMode.list
|
||||
? FileListViewMode.waterfall
|
||||
: FileListViewMode.list,
|
||||
tooltip: viewMode.value == FileListViewMode.list
|
||||
? 'Switch to Waterfall View'
|
||||
: 'Switch to List View',
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isSelectionMode.value
|
||||
? Symbols.close
|
||||
: Symbols.select_check_box,
|
||||
),
|
||||
onPressed: () =>
|
||||
isSelectionMode.value = !isSelectionMode.value,
|
||||
tooltip: isSelectionMode.value
|
||||
? 'Exit Selection Mode'
|
||||
: 'Enter Selection Mode',
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
if (mode.value == FileListMode.normal)
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.create_new_folder),
|
||||
onPressed: () =>
|
||||
onShowCreateDirectory(ref.context, currentPath),
|
||||
tooltip: 'Create Directory',
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
if (mode.value == FileListMode.unindexed)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
recycled.value
|
||||
? Symbols.delete_forever
|
||||
: Symbols.restore_from_trash,
|
||||
),
|
||||
onPressed: () {
|
||||
recycled.value = !recycled.value;
|
||||
unindexedNotifier.setRecycled(recycled.value);
|
||||
},
|
||||
tooltip: recycled.value
|
||||
? 'Show Active Files'
|
||||
: 'Show Recycle Bin',
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.upload_file),
|
||||
onPressed: onPickAndUpload,
|
||||
tooltip: 'Upload File',
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
).padding(horizontal: 8),
|
||||
|
||||
if (mode.value == FileListMode.unindexed && recycled.value)
|
||||
_buildClearRecycledButton(ref).padding(horizontal: 8),
|
||||
if (isRefreshing)
|
||||
@@ -409,8 +424,6 @@ class FileListView extends HookConsumerWidget {
|
||||
minHeight: 4,
|
||||
).padding(horizontal: 16, top: 6, bottom: 4),
|
||||
const Gap(8),
|
||||
if (mode.value == FileListMode.normal && currentPath.value == '/')
|
||||
_buildUnindexedFilesEntry(ref).padding(bottom: 12),
|
||||
Expanded(
|
||||
child:
|
||||
CustomScrollView(
|
||||
@@ -649,35 +662,6 @@ class FileListView extends HookConsumerWidget {
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildUnindexedFilesEntry(WidgetRef ref) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(ref.context).colorScheme.outline),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.inventory_2).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
const Text('Unindexed Files').bold(),
|
||||
const Spacer(),
|
||||
const Icon(Symbols.chevron_right).padding(horizontal: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
mode.value = FileListMode.unindexed;
|
||||
currentPath.value = '/';
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyDirectoryHint(
|
||||
WidgetRef ref,
|
||||
ValueNotifier<String> currentPath,
|
||||
@@ -1349,7 +1333,7 @@ class FileListView extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGlobalFilters(
|
||||
Widget _buildChipFilters(
|
||||
WidgetRef ref,
|
||||
AsyncValue<List<SnFilePool>> poolsAsync,
|
||||
ValueNotifier<SnFilePool?> selectedPool,
|
||||
@@ -1363,27 +1347,84 @@ class FileListView extends HookConsumerWidget {
|
||||
ValueNotifier<bool> orderDesc,
|
||||
ObjectRef<Timer?> queryDebounceTimer,
|
||||
) {
|
||||
final poolDropdownItems = poolsAsync.when(
|
||||
data: (pools) => [
|
||||
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),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<SnFilePool>(
|
||||
value: selectedPool.value,
|
||||
items: [
|
||||
const DropdownMenuItem<SnFilePool>(
|
||||
value: null,
|
||||
child: Text('All Pools', style: TextStyle(fontSize: 14)),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Symbols.database, size: 16),
|
||||
Gap(6),
|
||||
Text('All files', style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
...pools.map(
|
||||
(p) => DropdownMenuItem<SnFilePool>(
|
||||
value: p,
|
||||
child: Text(p.name, style: const TextStyle(fontSize: 14)),
|
||||
),
|
||||
...poolsAsync.maybeWhen(
|
||||
data: (pools) => pools.map(
|
||||
(pool) => DropdownMenuItem<SnFilePool>(
|
||||
value: pool,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Symbols.database, size: 16),
|
||||
Gap(6),
|
||||
Text(
|
||||
pool.name,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
orElse: () => <DropdownMenuItem<SnFilePool>>[],
|
||||
),
|
||||
],
|
||||
loading: () => const <DropdownMenuItem<SnFilePool>>[],
|
||||
error: (err, stack) => const <DropdownMenuItem<SnFilePool>>[],
|
||||
);
|
||||
|
||||
final poolDropdown = DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SnFilePool>(
|
||||
value: selectedPool.value,
|
||||
items: poolDropdownItems,
|
||||
onChanged: isRefreshing
|
||||
? null
|
||||
: (value) {
|
||||
@@ -1394,116 +1435,94 @@ class FileListView extends HookConsumerWidget {
|
||||
cloudNotifier.setPool(value?.id);
|
||||
}
|
||||
},
|
||||
customButton: Container(
|
||||
height: 28,
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(ref.context).colorScheme.outline,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 6,
|
||||
children: [
|
||||
const Icon(Symbols.pool, size: 16),
|
||||
Flexible(
|
||||
child: Text(
|
||||
selectedPool.value?.name ?? 'All files',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).fontSize(12),
|
||||
),
|
||||
],
|
||||
).height(24),
|
||||
),
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.zero,
|
||||
height: 28,
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
dropdownStyleData: const DropdownStyleData(maxHeight: 200),
|
||||
),
|
||||
);
|
||||
|
||||
final queryField = SizedBox(
|
||||
width: 200,
|
||||
height: 28,
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'fileName'.tr(),
|
||||
icon: const Icon(Symbols.arrow_drop_down, size: 16),
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 6,
|
||||
),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
style: const TextStyle(fontSize: 13, height: 1),
|
||||
onChanged: (value) {
|
||||
queryDebounceTimer.value?.cancel();
|
||||
queryDebounceTimer.value = Timer(
|
||||
const Duration(milliseconds: 300),
|
||||
() {
|
||||
query.value = value.isEmpty ? null : value;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final orderDropdown = DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<String>(
|
||||
value: order.value,
|
||||
items: ['date', 'size', 'name']
|
||||
.map(
|
||||
(e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(
|
||||
e == 'date' ? e : 'file${e.capitalizeEachWord()}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
).tr(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) => order.value = value,
|
||||
customButton: Container(
|
||||
height: 28,
|
||||
width: 80,
|
||||
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,
|
||||
color: Theme.of(
|
||||
ref.context,
|
||||
).colorScheme.outline.withOpacity(0.5),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
(order.value ?? 'date') == 'date'
|
||||
? (order.value ?? 'date')
|
||||
: 'file${order.value?.capitalizeEachWord()}',
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'size',
|
||||
child: Row(
|
||||
spacing: 6,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Symbols.data_usage, size: 16),
|
||||
Text(
|
||||
'fileSize'.tr(),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
).tr(),
|
||||
),
|
||||
if (order.value == 'size')
|
||||
Icon(
|
||||
orderDesc.value
|
||||
? Symbols.arrow_downward
|
||||
: Symbols.arrow_upward,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
height: 28,
|
||||
width: 80,
|
||||
padding: EdgeInsets.zero,
|
||||
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),
|
||||
),
|
||||
dropdownStyleData: const DropdownStyleData(maxHeight: 200),
|
||||
if (order.value == 'name')
|
||||
Icon(
|
||||
orderDesc.value
|
||||
? Symbols.arrow_downward
|
||||
: Symbols.arrow_upward,
|
||||
size: 16,
|
||||
),
|
||||
);
|
||||
|
||||
final orderDescToggle = IconButton(
|
||||
icon: Icon(
|
||||
orderDesc.value ? Symbols.arrow_upward : Symbols.arrow_downward,
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value == order.value) {
|
||||
// Toggle direction if same option selected
|
||||
final newValue = !orderDesc.value;
|
||||
orderDesc.value = newValue;
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
@@ -1511,39 +1530,49 @@ class FileListView extends HookConsumerWidget {
|
||||
} else {
|
||||
cloudNotifier.setOrderDesc(newValue);
|
||||
}
|
||||
} else {
|
||||
// Change sort option
|
||||
order.value = value;
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
unindexedNotifier.setOrder(value);
|
||||
} else {
|
||||
cloudNotifier.setOrder(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: orderDesc.value ? 'descendingOrder'.tr() : 'ascendingOrder'.tr(),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
);
|
||||
icon: const SizedBox.shrink(),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
final refreshButton = IconButton(
|
||||
icon: const Icon(Symbols.refresh),
|
||||
onPressed: () {
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: 'Refresh',
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
);
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
poolDropdown,
|
||||
queryField,
|
||||
orderDropdown,
|
||||
orderDescToggle,
|
||||
refreshButton,
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 8),
|
||||
),
|
||||
).padding(horizontal: 12);
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user