feat: Implement accessible SFTP file browser and native editor
This commit is contained in:
147
lib/ui/views/file_editor_view.dart
Normal file
147
lib/ui/views/file_editor_view.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:accessible_terminal/state/terminal_provider.dart';
|
||||
|
||||
class FileEditorView extends ConsumerStatefulWidget {
|
||||
final String path;
|
||||
const FileEditorView({super.key, required this.path});
|
||||
|
||||
@override
|
||||
ConsumerState<FileEditorView> createState() => _FileEditorViewState();
|
||||
}
|
||||
|
||||
class _FileEditorViewState extends ConsumerState<FileEditorView> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
bool _isLoading = true;
|
||||
bool _isSaving = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadFile();
|
||||
}
|
||||
|
||||
Future<void> _loadFile() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final sshService = ref.read(sshServiceProvider);
|
||||
final content = await sshService.readFile(widget.path);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_controller.text = content;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
String errorMessage = e.toString();
|
||||
if (e is FormatException) {
|
||||
errorMessage = 'This file appears to be binary or uses an unsupported encoding. Cannot edit as text.';
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_error = errorMessage;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveFile() async {
|
||||
setState(() {
|
||||
_isSaving = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final sshService = ref.read(sshServiceProvider);
|
||||
await sshService.writeFile(widget.path, _controller.text);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('File saved successfully')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSaving = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error saving file: $e'), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fileName = widget.path.split('/').last;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Editing: $fileName'),
|
||||
actions: [
|
||||
if (!_isLoading && _error == null)
|
||||
IconButton(
|
||||
icon: _isSaving
|
||||
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Icon(Icons.save),
|
||||
onPressed: _isSaving ? null : _saveFile,
|
||||
tooltip: 'Save File',
|
||||
),
|
||||
],
|
||||
),
|
||||
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: _loadFile, child: const Text('Retry')),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Semantics(
|
||||
label: 'File content editor',
|
||||
multiline: true,
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 16),
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'Enter file content...',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user