173 lines
4.7 KiB
Dart
173 lines
4.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:accessible_terminal/state/terminal_provider.dart';
|
|
import 'package:dartssh2/dartssh2.dart';
|
|
import 'file_editor_view.dart';
|
|
|
|
class FileBrowserView extends ConsumerStatefulWidget {
|
|
final String initialPath;
|
|
const FileBrowserView({super.key, this.initialPath = '.'});
|
|
|
|
@override
|
|
ConsumerState<FileBrowserView> createState() => _FileBrowserViewState();
|
|
}
|
|
|
|
class _FileBrowserViewState extends ConsumerState<FileBrowserView> {
|
|
late String _currentPath;
|
|
List<SftpName>? _files;
|
|
bool _isLoading = true;
|
|
String? _error;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_currentPath = widget.initialPath;
|
|
_loadDirectory();
|
|
}
|
|
|
|
Future<void> _loadDirectory() async {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_error = null;
|
|
});
|
|
|
|
try {
|
|
final sshService = ref.read(sshServiceProvider);
|
|
final files = await sshService.listDirectory(_currentPath);
|
|
|
|
// Sort: Directories first, then alphabetically
|
|
files.sort((a, b) {
|
|
if (a.attr.isDirectory && !b.attr.isDirectory) return -1;
|
|
if (!a.attr.isDirectory && b.attr.isDirectory) return 1;
|
|
return a.filename.toLowerCase().compareTo(b.filename.toLowerCase());
|
|
});
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_files = files.where((f) => f.filename != '.' && f.filename != '..').toList();
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_error = e.toString();
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void _navigate(String filename) {
|
|
setState(() {
|
|
if (_currentPath == '/') {
|
|
_currentPath = '/$filename';
|
|
} else {
|
|
_currentPath = '$_currentPath/$filename'.replaceAll('//', '/');
|
|
}
|
|
});
|
|
_loadDirectory();
|
|
}
|
|
|
|
void _goUp() {
|
|
if (_currentPath == '/' || _currentPath == '.') return;
|
|
final parts = _currentPath.split('/');
|
|
parts.removeLast();
|
|
setState(() {
|
|
_currentPath = parts.isEmpty ? '/' : parts.join('/');
|
|
if (_currentPath.isEmpty) _currentPath = '/';
|
|
});
|
|
_loadDirectory();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(_currentPath),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: _loadDirectory,
|
|
tooltip: 'Refresh',
|
|
),
|
|
],
|
|
),
|
|
body: _buildBody(),
|
|
);
|
|
}
|
|
|
|
Widget _buildBody() {
|
|
if (_isLoading) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
if (_error != null) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.error, color: Colors.red, size: 48),
|
|
const SizedBox(height: 16),
|
|
Text('Error: $_error', textAlign: TextAlign.center),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton(onPressed: _loadDirectory, child: const Text('Retry')),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
if (_currentPath != '/' && _currentPath != '.')
|
|
ListTile(
|
|
leading: const Icon(Icons.arrow_upward),
|
|
title: const Text('.. (Go Up)'),
|
|
onTap: _goUp,
|
|
),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
itemCount: _files?.length ?? 0,
|
|
itemBuilder: (context, index) {
|
|
final file = _files![index];
|
|
final isDir = file.attr.isDirectory;
|
|
|
|
return ListTile(
|
|
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
|
|
title: Text(file.filename),
|
|
subtitle: isDir ? null : Text(_formatSize(file.attr.size ?? 0)),
|
|
onTap: () {
|
|
if (isDir) {
|
|
_navigate(file.filename);
|
|
} else {
|
|
_openFile(file.filename);
|
|
}
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
String _formatSize(int bytes) {
|
|
if (bytes < 1024) return '$bytes B';
|
|
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
|
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
|
}
|
|
|
|
void _openFile(String filename) {
|
|
final fullPath = _currentPath == '/' ? '/$filename' : '$_currentPath/$filename';
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => FileEditorView(path: fullPath),
|
|
),
|
|
).then((_) => _loadDirectory()); // Refresh on return in case of changes
|
|
}
|
|
}
|