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:
33
lib/domain/entities/archivo_adjunto.dart
Normal file
33
lib/domain/entities/archivo_adjunto.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
enum ArchivoTipo { image, audio, video, document }
|
||||
|
||||
class ArchivoAdjunto {
|
||||
final String nombre;
|
||||
final String mimeType;
|
||||
final Uint8List bytes;
|
||||
final ArchivoTipo tipo;
|
||||
final String? hash;
|
||||
|
||||
ArchivoAdjunto({
|
||||
required this.nombre,
|
||||
required this.mimeType,
|
||||
required this.bytes,
|
||||
required this.tipo,
|
||||
this.hash,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'nombre': nombre,
|
||||
'tipo': mimeType,
|
||||
'contenido': bytes,
|
||||
};
|
||||
|
||||
int get sizeBytes => bytes.length;
|
||||
|
||||
String get sizeFormatted {
|
||||
if (sizeBytes < 1024) return '$sizeBytes B';
|
||||
if (sizeBytes < 1024 * 1024) return '${(sizeBytes / 1024).toStringAsFixed(1)} KB';
|
||||
return '${(sizeBytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
}
|
||||
33
lib/domain/entities/biblioteca.dart
Normal file
33
lib/domain/entities/biblioteca.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
class Biblioteca {
|
||||
final int? id;
|
||||
final String nombre;
|
||||
final String url;
|
||||
final String endpoint;
|
||||
final String? hBiblioteca;
|
||||
|
||||
Biblioteca({
|
||||
this.id,
|
||||
required this.nombre,
|
||||
required this.url,
|
||||
required this.endpoint,
|
||||
this.hBiblioteca,
|
||||
});
|
||||
|
||||
String get fullUrl => '$url$endpoint';
|
||||
|
||||
factory Biblioteca.fromMap(Map<String, dynamic> map) => Biblioteca(
|
||||
id: map['id'] as int?,
|
||||
nombre: map['nombre'] as String,
|
||||
url: map['url'] as String,
|
||||
endpoint: map['endpoint'] as String,
|
||||
hBiblioteca: map['h_biblioteca'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
if (id != null) 'id': id,
|
||||
'nombre': nombre,
|
||||
'url': url,
|
||||
'endpoint': endpoint,
|
||||
'h_biblioteca': hBiblioteca,
|
||||
};
|
||||
}
|
||||
55
lib/domain/entities/contenedor.dart
Normal file
55
lib/domain/entities/contenedor.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'archivo_adjunto.dart';
|
||||
import 'gps_location.dart';
|
||||
|
||||
class Contenedor {
|
||||
final String hash;
|
||||
final String? titulo;
|
||||
final String? descripcion;
|
||||
final List<ArchivoAdjunto> archivos;
|
||||
final GpsLocation? gps;
|
||||
final List<String> etiquetas;
|
||||
final DateTime createdAt;
|
||||
|
||||
Contenedor({
|
||||
required this.hash,
|
||||
this.titulo,
|
||||
this.descripcion,
|
||||
List<ArchivoAdjunto>? archivos,
|
||||
this.gps,
|
||||
List<String>? etiquetas,
|
||||
DateTime? createdAt,
|
||||
}) : archivos = archivos ?? [],
|
||||
etiquetas = etiquetas ?? [],
|
||||
createdAt = createdAt ?? DateTime.now();
|
||||
|
||||
Contenedor copyWith({
|
||||
String? hash,
|
||||
String? titulo,
|
||||
String? descripcion,
|
||||
List<ArchivoAdjunto>? archivos,
|
||||
GpsLocation? gps,
|
||||
List<String>? etiquetas,
|
||||
DateTime? createdAt,
|
||||
}) =>
|
||||
Contenedor(
|
||||
hash: hash ?? this.hash,
|
||||
titulo: titulo ?? this.titulo,
|
||||
descripcion: descripcion ?? this.descripcion,
|
||||
archivos: archivos ?? this.archivos,
|
||||
gps: gps ?? this.gps,
|
||||
etiquetas: etiquetas ?? this.etiquetas,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'hash': hash,
|
||||
if (titulo != null) 'titulo': titulo,
|
||||
if (descripcion != null) 'descripcion': descripcion,
|
||||
'etiquetas': etiquetas,
|
||||
if (gps != null) 'gps': gps!.toJson(),
|
||||
'archivos': archivos.map((a) => a.toJson()).toList(),
|
||||
};
|
||||
|
||||
bool get isEmpty => archivos.isEmpty && etiquetas.isEmpty && titulo == null;
|
||||
int get archivoCount => archivos.length;
|
||||
}
|
||||
46
lib/domain/entities/destino.dart
Normal file
46
lib/domain/entities/destino.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
class Destino {
|
||||
final int? id;
|
||||
final String nombre;
|
||||
final String url;
|
||||
final String hash;
|
||||
final bool activo;
|
||||
|
||||
Destino({
|
||||
this.id,
|
||||
required this.nombre,
|
||||
required this.url,
|
||||
required this.hash,
|
||||
this.activo = true,
|
||||
});
|
||||
|
||||
factory Destino.fromMap(Map<String, dynamic> map) => Destino(
|
||||
id: map['id'] as int?,
|
||||
nombre: map['nombre'] as String,
|
||||
url: map['url'] as String,
|
||||
hash: map['hash'] as String,
|
||||
activo: (map['activo'] as int?) == 1,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
if (id != null) 'id': id,
|
||||
'nombre': nombre,
|
||||
'url': url,
|
||||
'hash': hash,
|
||||
'activo': activo ? 1 : 0,
|
||||
};
|
||||
|
||||
Destino copyWith({
|
||||
int? id,
|
||||
String? nombre,
|
||||
String? url,
|
||||
String? hash,
|
||||
bool? activo,
|
||||
}) =>
|
||||
Destino(
|
||||
id: id ?? this.id,
|
||||
nombre: nombre ?? this.nombre,
|
||||
url: url ?? this.url,
|
||||
hash: hash ?? this.hash,
|
||||
activo: activo ?? this.activo,
|
||||
);
|
||||
}
|
||||
49
lib/domain/entities/etiqueta.dart
Normal file
49
lib/domain/entities/etiqueta.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
class Etiqueta {
|
||||
final String hMaestro;
|
||||
final String hGlobal;
|
||||
final String? mrf;
|
||||
final String? ref;
|
||||
final String? nombreEs;
|
||||
final String? nombreEn;
|
||||
final String grupo;
|
||||
final bool activo;
|
||||
final int? bibliotecaId;
|
||||
|
||||
Etiqueta({
|
||||
required this.hMaestro,
|
||||
required this.hGlobal,
|
||||
this.mrf,
|
||||
this.ref,
|
||||
this.nombreEs,
|
||||
this.nombreEn,
|
||||
required this.grupo,
|
||||
this.activo = true,
|
||||
this.bibliotecaId,
|
||||
});
|
||||
|
||||
String get displayName => nombreEs ?? nombreEn ?? ref ?? hMaestro.substring(0, 8);
|
||||
|
||||
String? get imagenUrl => mrf != null ? 'https://tzrtech.org/$mrf.png' : null;
|
||||
|
||||
factory Etiqueta.fromJson(Map<String, dynamic> json) => Etiqueta(
|
||||
hMaestro: json['h_maestro'] as String,
|
||||
hGlobal: json['h_global'] as String,
|
||||
mrf: json['mrf'] as String?,
|
||||
ref: json['ref'] as String?,
|
||||
nombreEs: json['nombre_es'] as String?,
|
||||
nombreEn: json['nombre_en'] as String?,
|
||||
grupo: json['grupo'] as String? ?? 'default',
|
||||
activo: json['activo'] as bool? ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'h_maestro': hMaestro,
|
||||
'h_global': hGlobal,
|
||||
'mrf': mrf,
|
||||
'ref': ref,
|
||||
'nombre_es': nombreEs,
|
||||
'nombre_en': nombreEn,
|
||||
'grupo': grupo,
|
||||
'biblioteca_id': bibliotecaId,
|
||||
};
|
||||
}
|
||||
16
lib/domain/entities/gps_location.dart
Normal file
16
lib/domain/entities/gps_location.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
class GpsLocation {
|
||||
final double lat;
|
||||
final double long;
|
||||
|
||||
GpsLocation({required this.lat, required this.long});
|
||||
|
||||
Map<String, dynamic> toJson() => {'lat': lat, 'long': long};
|
||||
|
||||
factory GpsLocation.fromJson(Map<String, dynamic> json) => GpsLocation(
|
||||
lat: (json['lat'] as num).toDouble(),
|
||||
long: (json['long'] as num).toDouble(),
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => '${lat.toStringAsFixed(4)}, ${long.toStringAsFixed(4)}';
|
||||
}
|
||||
29
lib/domain/entities/pack.dart
Normal file
29
lib/domain/entities/pack.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
class Pack {
|
||||
final int? id;
|
||||
final String nombre;
|
||||
final String icono;
|
||||
final List<String> tags;
|
||||
|
||||
Pack({
|
||||
this.id,
|
||||
required this.nombre,
|
||||
this.icono = '📦',
|
||||
required this.tags,
|
||||
});
|
||||
|
||||
factory Pack.fromMap(Map<String, dynamic> map) => Pack(
|
||||
id: map['id'] as int?,
|
||||
nombre: map['nombre'] as String,
|
||||
icono: map['icono'] as String? ?? '📦',
|
||||
tags: (map['tags'] as String).split(',').where((t) => t.isNotEmpty).toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
if (id != null) 'id': id,
|
||||
'nombre': nombre,
|
||||
'icono': icono,
|
||||
'tags': tags.join(','),
|
||||
};
|
||||
|
||||
int get tagCount => tags.length;
|
||||
}
|
||||
55
lib/domain/entities/pendiente.dart
Normal file
55
lib/domain/entities/pendiente.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
class Pendiente {
|
||||
final String hash;
|
||||
final String? titulo;
|
||||
final Uint8List contenido;
|
||||
final int intentos;
|
||||
final DateTime? ultimoIntento;
|
||||
final DateTime? proximoIntento;
|
||||
final int? destinoId;
|
||||
|
||||
Pendiente({
|
||||
required this.hash,
|
||||
this.titulo,
|
||||
required this.contenido,
|
||||
this.intentos = 0,
|
||||
this.ultimoIntento,
|
||||
this.proximoIntento,
|
||||
this.destinoId,
|
||||
});
|
||||
|
||||
factory Pendiente.fromMap(Map<String, dynamic> map) => Pendiente(
|
||||
hash: map['hash'] as String,
|
||||
titulo: map['titulo'] as String?,
|
||||
contenido: map['contenido'] as Uint8List,
|
||||
intentos: map['intentos'] as int? ?? 0,
|
||||
ultimoIntento: map['ultimo_intento'] != null
|
||||
? DateTime.parse(map['ultimo_intento'] as String)
|
||||
: null,
|
||||
proximoIntento: map['proximo_intento'] != null
|
||||
? DateTime.parse(map['proximo_intento'] as String)
|
||||
: null,
|
||||
destinoId: map['destino_id'] as int?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'hash': hash,
|
||||
'titulo': titulo,
|
||||
'contenido': contenido,
|
||||
'intentos': intentos,
|
||||
'ultimo_intento': ultimoIntento?.toIso8601String(),
|
||||
'proximo_intento': proximoIntento?.toIso8601String(),
|
||||
'destino_id': destinoId,
|
||||
};
|
||||
|
||||
bool get puedeReintentar => intentos < 20;
|
||||
|
||||
String get estado {
|
||||
if (intentos >= 20) return 'agotado';
|
||||
if (proximoIntento != null && proximoIntento!.isAfter(DateTime.now())) {
|
||||
return 'esperando';
|
||||
}
|
||||
return 'listo';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user