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,77 @@
import '../datasources/local_database.dart';
import '../../domain/entities/destino.dart';
import '../../domain/entities/biblioteca.dart';
class ConfigRepository {
// Destinos
Future<List<Destino>> getDestinos() async {
final db = await LocalDatabase.database;
final results = await db.query('destinos');
return results.map((m) => Destino.fromMap(m)).toList();
}
Future<Destino?> getDestinoActivo() async {
final db = await LocalDatabase.database;
final results = await db.query(
'destinos',
where: 'activo = ?',
whereArgs: [1],
limit: 1,
);
if (results.isEmpty) return null;
return Destino.fromMap(results.first);
}
Future<int> insertDestino(Destino destino) async {
final db = await LocalDatabase.database;
return db.insert('destinos', destino.toMap());
}
Future<void> updateDestino(Destino destino) async {
final db = await LocalDatabase.database;
await db.update(
'destinos',
destino.toMap(),
where: 'id = ?',
whereArgs: [destino.id],
);
}
Future<void> deleteDestino(int id) async {
final db = await LocalDatabase.database;
await db.delete('destinos', where: 'id = ?', whereArgs: [id]);
}
Future<void> setDestinoActivo(int id) async {
final db = await LocalDatabase.database;
await db.update('destinos', {'activo': 0});
await db.update('destinos', {'activo': 1}, where: 'id = ?', whereArgs: [id]);
}
// Bibliotecas
Future<List<Biblioteca>> getBibliotecas() async {
final db = await LocalDatabase.database;
final results = await db.query('bibliotecas');
return results.map((m) => Biblioteca.fromMap(m)).toList();
}
Future<int> insertBiblioteca(Biblioteca biblioteca) async {
final db = await LocalDatabase.database;
return db.insert('bibliotecas', biblioteca.toMap());
}
Future<void> updateBiblioteca(Biblioteca biblioteca) async {
final db = await LocalDatabase.database;
await db.update(
'bibliotecas',
biblioteca.toMap(),
where: 'id = ?',
whereArgs: [biblioteca.id],
);
}
Future<void> deleteBiblioteca(int id) async {
final db = await LocalDatabase.database;
await db.delete('bibliotecas', where: 'id = ?', whereArgs: [id]);
}
}

View File

@@ -0,0 +1,129 @@
import 'dart:convert';
import 'dart:typed_data';
import '../datasources/local_database.dart';
import '../datasources/backend_api.dart';
import '../../core/constants/app_constants.dart';
import '../../core/errors/exceptions.dart';
import '../../core/utils/retry_utils.dart';
import '../../domain/entities/contenedor.dart';
import '../../domain/entities/destino.dart';
import '../../domain/entities/pendiente.dart';
class ContenedorRepository {
final BackendApi _api = BackendApi();
Future<Map<String, dynamic>> enviar(Contenedor contenedor, Destino destino) async {
try {
final result = await _api.enviarContenedor(contenedor, destino);
await _registrarEnvio(contenedor, destino, result);
return result;
} catch (e) {
await _agregarAPendientes(contenedor, destino);
rethrow;
}
}
Future<void> _registrarEnvio(
Contenedor contenedor,
Destino destino,
Map<String, dynamic> response,
) async {
final db = await LocalDatabase.database;
await db.insert('registro', {
'hash': contenedor.hash,
'titulo': contenedor.titulo,
'hora_envio': DateTime.now().toIso8601String(),
'primera_conf': response['received_at'],
'ultima_conf': response['received_at'],
'destino_id': destino.id,
});
}
// Pendientes
Future<void> _agregarAPendientes(Contenedor contenedor, Destino destino) async {
final count = await getPendientesCount();
if (count >= AppConstants.maxPendientes) {
throw QueueFullException();
}
final db = await LocalDatabase.database;
final contenidoJson = jsonEncode(contenedor.toJson());
await db.insert('pendientes', {
'hash': contenedor.hash,
'titulo': contenedor.titulo,
'contenido': Uint8List.fromList(utf8.encode(contenidoJson)),
'intentos': 1,
'ultimo_intento': DateTime.now().toIso8601String(),
'proximo_intento': RetryUtils.calculateNextRetry(1).toIso8601String(),
'destino_id': destino.id,
});
}
Future<int> getPendientesCount() async {
final db = await LocalDatabase.database;
final result = await db.rawQuery('SELECT COUNT(*) as count FROM pendientes');
return result.first['count'] as int;
}
Future<List<Pendiente>> getPendientes() async {
final db = await LocalDatabase.database;
final results = await db.query('pendientes', orderBy: 'ultimo_intento DESC');
return results.map((m) => Pendiente.fromMap(m)).toList();
}
Future<void> reintentar(Pendiente pendiente, Destino destino) async {
if (!pendiente.puedeReintentar) return;
final db = await LocalDatabase.database;
final contenidoStr = utf8.decode(pendiente.contenido);
final contenidoJson = jsonDecode(contenidoStr) as Map<String, dynamic>;
// Reconstruct basic contenedor for retry
final contenedor = Contenedor(
hash: contenidoJson['hash'] as String,
titulo: contenidoJson['titulo'] as String?,
descripcion: contenidoJson['descripcion'] as String?,
etiquetas: (contenidoJson['etiquetas'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
);
try {
await _api.enviarContenedor(contenedor, destino);
await db.delete('pendientes', where: 'hash = ?', whereArgs: [pendiente.hash]);
} catch (e) {
final nuevoIntento = pendiente.intentos + 1;
if (nuevoIntento >= AppConstants.maxReintentos) {
// Keep in queue but mark as exhausted
await db.update(
'pendientes',
{
'intentos': nuevoIntento,
'ultimo_intento': DateTime.now().toIso8601String(),
'proximo_intento': null,
},
where: 'hash = ?',
whereArgs: [pendiente.hash],
);
} else {
await db.update(
'pendientes',
{
'intentos': nuevoIntento,
'ultimo_intento': DateTime.now().toIso8601String(),
'proximo_intento': RetryUtils.calculateNextRetry(nuevoIntento).toIso8601String(),
},
where: 'hash = ?',
whereArgs: [pendiente.hash],
);
}
}
}
Future<void> eliminarPendiente(String hash) async {
final db = await LocalDatabase.database;
await db.delete('pendientes', where: 'hash = ?', whereArgs: [hash]);
}
}

View File

@@ -0,0 +1,97 @@
import '../datasources/local_database.dart';
import '../datasources/biblioteca_api.dart';
import '../../domain/entities/etiqueta.dart';
import '../../domain/entities/biblioteca.dart';
import '../../domain/entities/pack.dart';
class EtiquetaRepository {
final BibliotecaApi _api = BibliotecaApi();
// Etiquetas Cache
Future<List<Etiqueta>> getEtiquetasByBiblioteca(int bibliotecaId) async {
final db = await LocalDatabase.database;
final results = await db.query(
'etiquetas_cache',
where: 'biblioteca_id = ?',
whereArgs: [bibliotecaId],
);
return results.map((m) => Etiqueta(
hMaestro: m['h_maestro'] as String,
hGlobal: m['h_global'] as String? ?? '',
mrf: m['mrf'] as String?,
ref: m['ref'] as String?,
nombreEs: m['nombre_es'] as String?,
nombreEn: m['nombre_en'] as String?,
grupo: m['grupo'] as String? ?? 'default',
bibliotecaId: bibliotecaId,
)).toList();
}
Future<void> syncBiblioteca(Biblioteca biblioteca) async {
final etiquetas = await _api.fetchEtiquetas(biblioteca);
final db = await LocalDatabase.database;
// Clear old cache
await db.delete(
'etiquetas_cache',
where: 'biblioteca_id = ?',
whereArgs: [biblioteca.id],
);
// Insert new
for (final etiqueta in etiquetas) {
await db.insert('etiquetas_cache', {
...etiqueta.toMap(),
'biblioteca_id': biblioteca.id,
});
}
}
Future<Etiqueta?> resolveHash(String hash) async {
final db = await LocalDatabase.database;
final results = await db.query(
'etiquetas_cache',
where: 'h_maestro = ? OR h_global = ?',
whereArgs: [hash, hash],
);
if (results.isEmpty) return null;
final m = results.first;
return Etiqueta(
hMaestro: m['h_maestro'] as String,
hGlobal: m['h_global'] as String? ?? '',
mrf: m['mrf'] as String?,
ref: m['ref'] as String?,
nombreEs: m['nombre_es'] as String?,
nombreEn: m['nombre_en'] as String?,
grupo: m['grupo'] as String? ?? 'default',
bibliotecaId: m['biblioteca_id'] as int?,
);
}
// Packs
Future<List<Pack>> getPacks() async {
final db = await LocalDatabase.database;
final results = await db.query('packs');
return results.map((m) => Pack.fromMap(m)).toList();
}
Future<int> insertPack(Pack pack) async {
final db = await LocalDatabase.database;
return db.insert('packs', pack.toMap());
}
Future<void> updatePack(Pack pack) async {
final db = await LocalDatabase.database;
await db.update(
'packs',
pack.toMap(),
where: 'id = ?',
whereArgs: [pack.id],
);
}
Future<void> deletePack(int id) async {
final db = await LocalDatabase.database;
await db.delete('packs', where: 'id = ?', whereArgs: [id]);
}
}