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,45 @@
class AppConstants {
static const int maxPendientes = 20;
static const int maxReintentos = 20;
static const int hashLength = 64;
static const int chunkSize = 512 * 1024; // 512KB
static const String hstBibliotecaHash =
'b7149f9e2106c566032aeb29a26e4c6cdd5f5c16b4421025c58166ee345740d1';
static const String hstApiUrl = 'https://tzrtech.org';
static const String hstApiEndpoint = '/api/tags';
static const Duration httpTimeout = Duration(seconds: 30);
static const Duration retryCheckInterval = Duration(seconds: 30);
}
class RetryDelays {
static const List<Duration> delays = [
Duration(minutes: 1),
Duration(minutes: 2),
Duration(minutes: 5),
Duration(minutes: 10),
Duration(minutes: 20),
Duration(minutes: 30),
Duration(hours: 1),
Duration(hours: 2),
Duration(hours: 3),
Duration(hours: 4),
Duration(hours: 5),
Duration(hours: 6),
Duration(hours: 6),
Duration(hours: 6),
Duration(hours: 8),
Duration(hours: 8),
Duration(hours: 8),
Duration(hours: 8),
Duration(hours: 6),
];
static Duration getDelay(int intento) {
if (intento < 0 || intento >= delays.length) {
return Duration.zero;
}
return delays[intento];
}
}

View File

@@ -0,0 +1,25 @@
class PacketException implements Exception {
final String message;
final String? code;
PacketException(this.message, {this.code});
@override
String toString() => 'PacketException: $message';
}
class NetworkException extends PacketException {
NetworkException(super.message, {super.code});
}
class HashExistsException extends PacketException {
HashExistsException() : super('Hash already exists', code: 'hash_exists');
}
class QueueFullException extends PacketException {
QueueFullException() : super('Queue is full (max 20)', code: 'queue_full');
}
class DatabaseException extends PacketException {
DatabaseException(super.message, {super.code});
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
class AppTheme {
static const Color primaryColor = Color(0xFF2196F3);
static const Color accentColor = Color(0xFF03A9F4);
static const Color errorColor = Color(0xFFE53935);
static const Color successColor = Color(0xFF43A047);
static const Color warningColor = Color(0xFFFFA726);
static ThemeData get lightTheme => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.light,
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(
elevation: 4,
),
);
static ThemeData get darkTheme => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.dark,
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
),
);
}

View File

@@ -0,0 +1,36 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
class HashUtils {
static final _random = Random.secure();
static String generateHash() {
final bytes = List<int>.generate(32, (_) => _random.nextInt(256));
return sha256.convert(bytes).toString();
}
static String hashFromBytes(Uint8List bytes) {
return sha256.convert(bytes).toString();
}
static String hashFromString(String input) {
return sha256.convert(utf8.encode(input)).toString();
}
static bool isValidHash(String hash) {
if (hash.length != 64) return false;
return RegExp(r'^[a-f0-9]{64}$').hasMatch(hash);
}
static List<String> extractHashes(String text) {
final regex = RegExp(r'[a-f0-9]{64}');
return regex.allMatches(text).map((m) => m.group(0)!).toList();
}
static String truncateHash(String hash, {int length = 8}) {
if (hash.length <= length) return hash;
return '${hash.substring(0, length)}...';
}
}

View File

@@ -0,0 +1,20 @@
import '../constants/app_constants.dart';
class RetryUtils {
static DateTime calculateNextRetry(int intentoActual) {
final delay = RetryDelays.getDelay(intentoActual);
return DateTime.now().add(delay);
}
static bool shouldRetry(int intentos) {
return intentos < AppConstants.maxReintentos;
}
static String formatTimeRemaining(DateTime nextRetry) {
final diff = nextRetry.difference(DateTime.now());
if (diff.isNegative) return 'Ahora';
if (diff.inHours > 0) return '${diff.inHours}h ${diff.inMinutes % 60}m';
if (diff.inMinutes > 0) return '${diff.inMinutes}m';
return '${diff.inSeconds}s';
}
}