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>
148 lines
4.8 KiB
Dart
148 lines
4.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import '../core/theme/app_theme.dart';
|
|
import 'bloc/app/app_cubit.dart';
|
|
import 'bloc/app/app_state.dart';
|
|
import 'bloc/captura/captura_cubit.dart';
|
|
import 'bloc/etiquetas/etiquetas_cubit.dart';
|
|
import 'bloc/packs/packs_cubit.dart';
|
|
import 'bloc/pendientes/pendientes_cubit.dart';
|
|
import 'pages/captura_page.dart';
|
|
import 'pages/etiquetas_page.dart';
|
|
import 'pages/packs_page.dart';
|
|
import 'pages/pendientes_page.dart';
|
|
import 'pages/config_page.dart';
|
|
|
|
class PacketApp extends StatelessWidget {
|
|
const PacketApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MultiBlocProvider(
|
|
providers: [
|
|
BlocProvider(create: (_) => AppCubit()..init()),
|
|
BlocProvider(create: (_) => CapturaCubit()),
|
|
BlocProvider(create: (_) => EtiquetasCubit()..init()),
|
|
BlocProvider(create: (_) => PacksCubit()..load()),
|
|
BlocProvider(create: (_) => PendientesCubit()..load()),
|
|
],
|
|
child: MaterialApp(
|
|
title: 'Packet',
|
|
theme: AppTheme.lightTheme,
|
|
darkTheme: AppTheme.darkTheme,
|
|
themeMode: ThemeMode.system,
|
|
home: const MainScreen(),
|
|
debugShowCheckedModeBanner: false,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MainScreen extends StatelessWidget {
|
|
const MainScreen({super.key});
|
|
|
|
static const _pages = [
|
|
CapturaPage(),
|
|
EtiquetasPage(),
|
|
PacksPage(),
|
|
PendientesPage(),
|
|
ConfigPage(),
|
|
];
|
|
|
|
static const _titles = [
|
|
'Captura',
|
|
'Etiquetas',
|
|
'Packs',
|
|
'Pendientes',
|
|
'Config',
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocBuilder<AppCubit, AppState>(
|
|
builder: (context, state) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(_titles[state.currentIndex]),
|
|
actions: [
|
|
// Destino selector
|
|
if (state.destinos.isNotEmpty)
|
|
PopupMenuButton<int>(
|
|
onSelected: (id) {
|
|
final destino = state.destinos.firstWhere((d) => d.id == id);
|
|
context.read<AppCubit>().setDestinoActivo(destino);
|
|
},
|
|
itemBuilder: (_) => state.destinos
|
|
.map((d) => PopupMenuItem(
|
|
value: d.id,
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
state.destinoActivo?.id == d.id
|
|
? Icons.check
|
|
: Icons.cloud,
|
|
size: 18,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(d.nombre),
|
|
],
|
|
),
|
|
))
|
|
.toList(),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
state.destinoActivo?.nombre ?? 'Sin destino',
|
|
style: Theme.of(context).textTheme.labelLarge,
|
|
),
|
|
const Icon(Icons.arrow_drop_down),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: IndexedStack(
|
|
index: state.currentIndex,
|
|
children: _pages,
|
|
),
|
|
bottomNavigationBar: NavigationBar(
|
|
selectedIndex: state.currentIndex,
|
|
onDestinationSelected: (i) => context.read<AppCubit>().setIndex(i),
|
|
destinations: const [
|
|
NavigationDestination(
|
|
icon: Icon(Icons.camera_alt_outlined),
|
|
selectedIcon: Icon(Icons.camera_alt),
|
|
label: 'Captura',
|
|
),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.label_outline),
|
|
selectedIcon: Icon(Icons.label),
|
|
label: 'Tags',
|
|
),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.inventory_2_outlined),
|
|
selectedIcon: Icon(Icons.inventory_2),
|
|
label: 'Packs',
|
|
),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.pending_outlined),
|
|
selectedIcon: Icon(Icons.pending),
|
|
label: 'Pend.',
|
|
),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.settings_outlined),
|
|
selectedIcon: Icon(Icons.settings),
|
|
label: 'Config',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|