inou-mobile/lib/features/input/input_screen.dart

293 lines
8.7 KiB
Dart

import 'package:flutter/material.dart';
import '../../core/theme.dart';
import 'ocr_capture_screen.dart';
import 'voice_input_widget.dart';
/// Fancy input screen with OCR, voice, and camera capabilities
class InputScreen extends StatefulWidget {
const InputScreen({super.key});
@override
State<InputScreen> createState() => _InputScreenState();
}
class _InputScreenState extends State<InputScreen> {
final TextEditingController _textController = TextEditingController();
bool _showVoiceInput = false;
bool _continuousDictation = false;
bool _isProcessing = false;
@override
void dispose() {
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Input'),
centerTitle: true,
actions: [
// Continuous dictation toggle
if (_showVoiceInput)
IconButton(
icon: Icon(
_continuousDictation ? Icons.repeat_on : Icons.repeat,
color: _continuousDictation
? AppTheme.primaryColor
: Colors.grey,
),
onPressed: () {
setState(() => _continuousDictation = !_continuousDictation);
},
tooltip: 'Continuous dictation',
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Main text input area
Expanded(
child: Container(
decoration: BoxDecoration(
color: AppTheme.surfaceColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.3),
),
),
child: TextField(
controller: _textController,
maxLines: null,
expands: true,
style: const TextStyle(
color: AppTheme.textColor,
fontSize: 16,
),
decoration: const InputDecoration(
hintText: 'Enter or capture text...',
hintStyle: TextStyle(color: Colors.grey),
border: InputBorder.none,
contentPadding: EdgeInsets.all(16),
),
),
),
),
const SizedBox(height: 16),
// Voice input overlay
if (_showVoiceInput) ...[
_buildVoiceInputSection(),
const SizedBox(height: 16),
],
// Action buttons row
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildActionButton(
icon: Icons.camera_alt,
label: 'Camera',
onTap: _onCameraTap,
),
_buildActionButton(
icon: Icons.document_scanner,
label: 'OCR',
onTap: _onOcrTap,
),
_buildActionButton(
icon: _showVoiceInput ? Icons.keyboard : Icons.mic,
label: _showVoiceInput ? 'Keyboard' : 'Voice',
onTap: _onVoiceTap,
isActive: _showVoiceInput,
),
_buildActionButton(
icon: Icons.send,
label: 'Send',
onTap: _onSendTap,
isPrimary: true,
),
],
),
],
),
),
);
}
Widget _buildVoiceInputSection() {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.surfaceColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.3),
),
),
child: VoiceInputWidget(
continuousMode: _continuousDictation,
showTranscript: false, // We show in main text field
buttonSize: 64,
onTranscript: (text) {
// Update text field with partial results
_textController.text = text;
_textController.selection = TextSelection.fromPosition(
TextPosition(offset: text.length),
);
},
onFinalResult: (text) {
// Insert at cursor position for continuous mode
if (_continuousDictation && _textController.text.isNotEmpty) {
final currentText = _textController.text;
if (!currentText.endsWith(' ') && !text.startsWith(' ')) {
_textController.text = '$currentText $text';
} else {
_textController.text = currentText + text;
}
} else {
_textController.text = text;
}
_textController.selection = TextSelection.fromPosition(
TextPosition(offset: _textController.text.length),
);
},
),
);
}
Widget _buildActionButton({
required IconData icon,
required String label,
required VoidCallback onTap,
bool isPrimary = false,
bool isActive = false,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isPrimary
? AppTheme.primaryColor
: isActive
? AppTheme.primaryColor.withOpacity(0.2)
: AppTheme.surfaceColor,
borderRadius: BorderRadius.circular(12),
border: isActive
? Border.all(color: AppTheme.primaryColor, width: 2)
: null,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isActive ? AppTheme.primaryColor : AppTheme.textColor,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: isActive ? AppTheme.primaryColor : AppTheme.textColor,
fontSize: 12,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
),
),
],
),
),
);
}
void _onCameraTap() {
// Camera tap also opens OCR scanner (same functionality)
_openOcrScanner();
}
void _onOcrTap() {
_openOcrScanner();
}
Future<void> _openOcrScanner() async {
if (_isProcessing) return;
setState(() => _isProcessing = true);
try {
final result = await Navigator.of(context).push<OcrCaptureResult>(
MaterialPageRoute(
builder: (context) => const OcrCaptureScreen(
enableLivePreview: true,
keepImage: false,
),
),
);
if (result != null && result.text.isNotEmpty) {
// Append or replace text based on current content
if (_textController.text.isEmpty) {
_textController.text = result.text;
} else {
// Append with newline separator
_textController.text = '${_textController.text}\n\n${result.text}';
}
// Move cursor to end
_textController.selection = TextSelection.fromPosition(
TextPosition(offset: _textController.text.length),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Extracted ${result.textBlocks?.length ?? 0} text blocks'),
backgroundColor: Colors.green,
),
);
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('OCR failed: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isProcessing = false);
}
}
}
void _onVoiceTap() {
setState(() => _showVoiceInput = !_showVoiceInput);
}
void _onSendTap() {
final text = _textController.text.trim();
if (text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter some text')),
);
return;
}
// TODO: Implement send functionality
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Sending: $text')),
);
}
}