PACKET v1.0.0 - Initial release
App móvil Flutter para capturar contenido multimedia, etiquetarlo con hashes y enviarlo a backends configurables. Features: - Captura de fotos, audio, video y archivos - Sistema de etiquetas con bibliotecas externas (HST) - Packs de etiquetas predefinidos - Cola de reintentos (hasta 20 contenedores) - Soporte GPS - Hash SHA-256 auto-generado por contenedor - Persistencia SQLite local - Múltiples destinos configurables Stack: Flutter 3.38.5, flutter_bloc, sqflite, dio 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
106
lib/presentation/widgets/audio_recorder.dart
Normal file
106
lib/presentation/widgets/audio_recorder.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:record/record.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class AudioRecorderButton extends StatefulWidget {
|
||||
final void Function(Uint8List bytes, String nombre) onRecorded;
|
||||
|
||||
const AudioRecorderButton({super.key, required this.onRecorded});
|
||||
|
||||
@override
|
||||
State<AudioRecorderButton> createState() => _AudioRecorderButtonState();
|
||||
}
|
||||
|
||||
class _AudioRecorderButtonState extends State<AudioRecorderButton> {
|
||||
final _recorder = AudioRecorder();
|
||||
bool _isRecording = false;
|
||||
int _seconds = 0;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_recorder.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _toggleRecording() async {
|
||||
if (_isRecording) {
|
||||
await _stopRecording();
|
||||
} else {
|
||||
await _startRecording();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startRecording() async {
|
||||
final hasPermission = await _recorder.hasPermission();
|
||||
if (!hasPermission) return;
|
||||
|
||||
final dir = await getTemporaryDirectory();
|
||||
final path = '${dir.path}/audio_${DateTime.now().millisecondsSinceEpoch}.m4a';
|
||||
|
||||
await _recorder.start(
|
||||
const RecordConfig(encoder: AudioEncoder.aacLc),
|
||||
path: path,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_isRecording = true;
|
||||
_seconds = 0;
|
||||
});
|
||||
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (t) {
|
||||
setState(() => _seconds++);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _stopRecording() async {
|
||||
_timer?.cancel();
|
||||
final path = await _recorder.stop();
|
||||
|
||||
setState(() {
|
||||
_isRecording = false;
|
||||
_seconds = 0;
|
||||
});
|
||||
|
||||
if (path != null) {
|
||||
final file = File(path);
|
||||
final bytes = await file.readAsBytes();
|
||||
final nombre = 'audio_${DateTime.now().millisecondsSinceEpoch}.m4a';
|
||||
widget.onRecorded(bytes, nombre);
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDuration(int seconds) {
|
||||
final mins = seconds ~/ 60;
|
||||
final secs = seconds % 60;
|
||||
return '${mins.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FilledButton.tonal(
|
||||
onPressed: _toggleRecording,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor:
|
||||
_isRecording ? Theme.of(context).colorScheme.errorContainer : null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
_isRecording ? Icons.stop : Icons.mic,
|
||||
size: 18,
|
||||
color: _isRecording ? Theme.of(context).colorScheme.error : null,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(_isRecording ? _formatDuration(_seconds) : 'Audio'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user