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:
tzzrgit
2025-12-21 18:10:27 +01:00
commit dac0c51483
163 changed files with 8603 additions and 0 deletions

View 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'),
],
),
);
}
}