Skip to content

Commit c691b35

Browse files
author
Jérémy Christillin
committed
feat: add sortable columns and auto-refresh after transfer
- Add sortable columns (Name, Date, Size) to both local and remote file browsers - Click column headers to sort, click again to toggle ascending/descending - Add auto-refresh: destination file browser refreshes after upload/download completes - Add TransferProvider.onTransferComplete callback for transfer notifications
1 parent 2418ca0 commit c691b35

4 files changed

Lines changed: 217 additions & 22 deletions

File tree

flutter_app/lib/providers/transfer_provider.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ class TransferProvider extends ChangeNotifier {
6262
final int _maxConcurrent = 3;
6363
int _idCounter = 0;
6464

65+
/// Callback called when a transfer completes successfully
66+
/// Parameters: (TransferType type, String serverName, String destinationPath)
67+
void Function(TransferType type, String serverName, String destinationPath)? onTransferComplete;
68+
6569
// Getters
6670
List<TransferItem> get transfers => List.unmodifiable(_transfers);
6771
List<TransferItem> get pendingTransfers =>
@@ -165,6 +169,12 @@ class TransferProvider extends ChangeNotifier {
165169
pending.status = TransferStatus.completed;
166170
pending.progress = 1.0;
167171
pending.completedAt = DateTime.now();
172+
173+
// Notify completion callback
174+
final destinationPath = pending.type == TransferType.upload
175+
? pending.remotePath
176+
: pending.localPath;
177+
onTransferComplete?.call(pending.type, pending.serverName, destinationPath);
168178
} catch (e) {
169179
pending.status = TransferStatus.failed;
170180
pending.error = e.toString();

flutter_app/lib/screens/home_screen.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@ class _HomeScreenState extends State<HomeScreen> {
169169
// Initialize transfer provider when connected
170170
if (connectionProvider.isConnected && _transferProvider == null) {
171171
_transferProvider = TransferProvider(client: connectionProvider.client);
172+
// Set up callback to refresh destination after transfer completes
173+
_transferProvider!.onTransferComplete = (type, serverName, destinationPath) {
174+
_refreshAfterTransfer(type, serverName, destinationPath);
175+
};
172176
} else if (!connectionProvider.isConnected && _transferProvider != null) {
173177
_transferProvider = null;
174178
}
@@ -850,4 +854,37 @@ class _HomeScreenState extends State<HomeScreen> {
850854
),
851855
);
852856
}
857+
858+
/// Refresh the appropriate browser after a transfer completes
859+
void _refreshAfterTransfer(TransferType type, String serverName, String destinationPath) {
860+
if (type == TransferType.upload) {
861+
// Upload completed - refresh remote browser
862+
final remoteBrowserState = _remoteBrowserKey.currentState;
863+
if (remoteBrowserState != null) {
864+
// Extract directory from destination path
865+
final destDir = destinationPath.contains('/')
866+
? destinationPath.substring(0, destinationPath.lastIndexOf('/'))
867+
: destinationPath;
868+
// Only refresh if we're viewing the same directory
869+
if (remoteBrowserState.currentPath == destDir ||
870+
destinationPath.startsWith(remoteBrowserState.currentPath)) {
871+
remoteBrowserState.refresh();
872+
}
873+
}
874+
} else {
875+
// Download completed - refresh local browser
876+
final localBrowserState = _localBrowserKey.currentState;
877+
if (localBrowserState != null) {
878+
// Extract directory from destination path
879+
final destDir = destinationPath.contains('/')
880+
? destinationPath.substring(0, destinationPath.lastIndexOf('/'))
881+
: destinationPath;
882+
// Only refresh if we're viewing the same directory
883+
if (localBrowserState.currentPath == destDir ||
884+
destinationPath.startsWith(localBrowserState.currentPath)) {
885+
localBrowserState.refresh();
886+
}
887+
}
888+
}
889+
}
853890
}

flutter_app/lib/widgets/local_file_browser.dart

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ enum LocalFileAction {
2020
refresh,
2121
}
2222

23+
/// Sort field for file list
24+
enum SortField { name, date, size }
25+
26+
/// Sort direction
27+
enum SortDirection { ascending, descending }
28+
2329
/// Data model for drag operations
2430
class DraggedLocalFiles {
2531
final List<LocalFile> files;
@@ -107,10 +113,56 @@ class _LocalFileBrowserState extends State<LocalFileBrowser> {
107113
String? _error;
108114
bool _showHidden = false;
109115
bool _isDragOver = false;
116+
SortField _sortField = SortField.name;
117+
SortDirection _sortDirection = SortDirection.ascending;
110118

111119
/// Get current path for external access
112120
String get currentPath => _currentPath;
113121

122+
/// Refresh the current directory
123+
void refresh() => _loadFiles();
124+
125+
/// Sort files according to current sort settings
126+
void _sortFiles(List<LocalFile> files) {
127+
files.sort((a, b) {
128+
// Directories always first
129+
if (a.isDirectory && !b.isDirectory) return -1;
130+
if (!a.isDirectory && b.isDirectory) return 1;
131+
132+
int comparison;
133+
switch (_sortField) {
134+
case SortField.name:
135+
comparison = a.name.toLowerCase().compareTo(b.name.toLowerCase());
136+
break;
137+
case SortField.date:
138+
comparison = a.modified.compareTo(b.modified);
139+
break;
140+
case SortField.size:
141+
comparison = a.size.compareTo(b.size);
142+
break;
143+
}
144+
145+
return _sortDirection == SortDirection.ascending ? comparison : -comparison;
146+
});
147+
}
148+
149+
/// Toggle sort for a field
150+
void _toggleSort(SortField field) {
151+
setState(() {
152+
if (_sortField == field) {
153+
// Toggle direction
154+
_sortDirection = _sortDirection == SortDirection.ascending
155+
? SortDirection.descending
156+
: SortDirection.ascending;
157+
} else {
158+
// New field, default to ascending
159+
_sortField = field;
160+
_sortDirection = SortDirection.ascending;
161+
}
162+
_sortFiles(_files);
163+
});
164+
}
165+
114166
@override
115167
void initState() {
116168
super.initState();
@@ -149,12 +201,8 @@ class _LocalFileBrowserState extends State<LocalFileBrowser> {
149201
}
150202
}
151203

152-
// Sort: directories first, then by name
153-
files.sort((a, b) {
154-
if (a.isDirectory && !b.isDirectory) return -1;
155-
if (!a.isDirectory && b.isDirectory) return 1;
156-
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
157-
});
204+
// Sort files
205+
_sortFiles(files);
158206

159207
setState(() {
160208
_files = files;
@@ -363,22 +411,48 @@ class _LocalFileBrowserState extends State<LocalFileBrowser> {
363411
),
364412
child: Row(
365413
children: [
366-
SizedBox(
367-
width: 24,
368-
child: Text('', style: TextStyle(fontSize: 11, color: colorScheme.onSurfaceVariant)),
369-
),
414+
const SizedBox(width: 24),
370415
Expanded(
371416
flex: 3,
372-
child: Text('Name', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colorScheme.onSurfaceVariant)),
417+
child: _buildSortableHeader('Name', SortField.name, colorScheme),
373418
),
374419
SizedBox(
375420
width: 100,
376-
child: Text('Date', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colorScheme.onSurfaceVariant)),
421+
child: _buildSortableHeader('Date', SortField.date, colorScheme),
377422
),
378423
SizedBox(
379424
width: 70,
380-
child: Text('Size', textAlign: TextAlign.right, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colorScheme.onSurfaceVariant)),
425+
child: _buildSortableHeader('Size', SortField.size, colorScheme, align: TextAlign.right),
426+
),
427+
],
428+
),
429+
);
430+
}
431+
432+
Widget _buildSortableHeader(String label, SortField field, ColorScheme colorScheme, {TextAlign align = TextAlign.left}) {
433+
final isActive = _sortField == field;
434+
final icon = _sortDirection == SortDirection.ascending
435+
? HugeIcons.strokeRoundedArrowUp01
436+
: HugeIcons.strokeRoundedArrowDown01;
437+
438+
return InkWell(
439+
onTap: () => _toggleSort(field),
440+
child: Row(
441+
mainAxisSize: MainAxisSize.min,
442+
mainAxisAlignment: align == TextAlign.right ? MainAxisAlignment.end : MainAxisAlignment.start,
443+
children: [
444+
Text(
445+
label,
446+
style: TextStyle(
447+
fontSize: 11,
448+
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
449+
color: isActive ? colorScheme.primary : colorScheme.onSurfaceVariant,
450+
),
381451
),
452+
if (isActive) ...[
453+
const SizedBox(width: 2),
454+
HugeIcon(icon: icon, size: 10, color: colorScheme.primary),
455+
],
382456
],
383457
),
384458
);

flutter_app/lib/widgets/remote_file_browser.dart

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import 'package:hugeicons/hugeicons.dart';
66

77
import '../mcp/mcp_client.dart';
88
import 'server_selector.dart';
9-
import 'local_file_browser.dart' show DraggedLocalFiles, DraggedRemoteFiles;
9+
import 'local_file_browser.dart' show DraggedLocalFiles, DraggedRemoteFiles, SortField, SortDirection;
1010

1111
/// Context menu action types
1212
enum FileAction {
@@ -57,13 +57,59 @@ class _RemoteFileBrowserState extends State<RemoteFileBrowser> {
5757
String? _error;
5858
bool _showHidden = false;
5959
bool _isDragOver = false;
60+
SortField _sortField = SortField.name;
61+
SortDirection _sortDirection = SortDirection.ascending;
6062

6163
/// Get current path for external access
6264
String get currentPath => _currentPath;
6365

6466
/// Get selected server for external access
6567
SshServer? get selectedServer => _selectedServer;
6668

69+
/// Refresh the current directory
70+
void refresh() => _loadFiles();
71+
72+
/// Sort files according to current sort settings
73+
void _sortFiles(List<RemoteFile> files) {
74+
files.sort((a, b) {
75+
// Directories always first
76+
if (a.isDirectory && !b.isDirectory) return -1;
77+
if (!a.isDirectory && b.isDirectory) return 1;
78+
79+
int comparison;
80+
switch (_sortField) {
81+
case SortField.name:
82+
comparison = a.name.toLowerCase().compareTo(b.name.toLowerCase());
83+
break;
84+
case SortField.date:
85+
comparison = a.modified.compareTo(b.modified);
86+
break;
87+
case SortField.size:
88+
comparison = a.size.compareTo(b.size);
89+
break;
90+
}
91+
92+
return _sortDirection == SortDirection.ascending ? comparison : -comparison;
93+
});
94+
}
95+
96+
/// Toggle sort for a field
97+
void _toggleSort(SortField field) {
98+
setState(() {
99+
if (_sortField == field) {
100+
// Toggle direction
101+
_sortDirection = _sortDirection == SortDirection.ascending
102+
? SortDirection.descending
103+
: SortDirection.ascending;
104+
} else {
105+
// New field, default to ascending
106+
_sortField = field;
107+
_sortDirection = SortDirection.ascending;
108+
}
109+
_sortFiles(_files);
110+
});
111+
}
112+
67113
// Convert SshServer list to ServerInfo list for the selector
68114
List<ServerInfo> get _serverInfos {
69115
return widget.servers.map((s) => ServerInfo(
@@ -89,8 +135,10 @@ class _RemoteFileBrowserState extends State<RemoteFileBrowser> {
89135
showHidden: _showHidden,
90136
);
91137

138+
final files = result.files;
139+
_sortFiles(files);
92140
setState(() {
93-
_files = result.files;
141+
_files = files;
94142
_currentPath = result.path;
95143
_isLoading = false;
96144
_selectedFiles.clear();
@@ -417,27 +465,53 @@ class _RemoteFileBrowserState extends State<RemoteFileBrowser> {
417465
),
418466
child: Row(
419467
children: [
420-
SizedBox(
421-
width: 24,
422-
child: Text('', style: TextStyle(fontSize: 11, color: colorScheme.onSurfaceVariant)),
423-
),
468+
const SizedBox(width: 24),
424469
Expanded(
425470
flex: 3,
426-
child: Text('Name', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colorScheme.onSurfaceVariant)),
471+
child: _buildSortableHeader('Name', SortField.name, colorScheme),
427472
),
428473
SizedBox(
429474
width: 100,
430-
child: Text('Date', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colorScheme.onSurfaceVariant)),
475+
child: _buildSortableHeader('Date', SortField.date, colorScheme),
431476
),
432477
SizedBox(
433478
width: 70,
434-
child: Text('Size', textAlign: TextAlign.right, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colorScheme.onSurfaceVariant)),
479+
child: _buildSortableHeader('Size', SortField.size, colorScheme, align: TextAlign.right),
435480
),
436481
],
437482
),
438483
);
439484
}
440485

486+
Widget _buildSortableHeader(String label, SortField field, ColorScheme colorScheme, {TextAlign align = TextAlign.left}) {
487+
final isActive = _sortField == field;
488+
final icon = _sortDirection == SortDirection.ascending
489+
? HugeIcons.strokeRoundedArrowUp01
490+
: HugeIcons.strokeRoundedArrowDown01;
491+
492+
return InkWell(
493+
onTap: () => _toggleSort(field),
494+
child: Row(
495+
mainAxisSize: MainAxisSize.min,
496+
mainAxisAlignment: align == TextAlign.right ? MainAxisAlignment.end : MainAxisAlignment.start,
497+
children: [
498+
Text(
499+
label,
500+
style: TextStyle(
501+
fontSize: 11,
502+
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
503+
color: isActive ? colorScheme.primary : colorScheme.onSurfaceVariant,
504+
),
505+
),
506+
if (isActive) ...[
507+
const SizedBox(width: 2),
508+
HugeIcon(icon: icon, size: 10, color: colorScheme.primary),
509+
],
510+
],
511+
),
512+
);
513+
}
514+
441515
Widget _buildFileList(ColorScheme colorScheme) {
442516
if (_files.isEmpty) {
443517
return GestureDetector(

0 commit comments

Comments
 (0)