124 lines
4.9 KiB
Dart
124 lines
4.9 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:gimnasio_soma/core/config/app_env.dart';
|
|
import 'package:gimnasio_soma/core/config/env_banner.dart';
|
|
import 'package:gimnasio_soma/core/config/supabase_config.dart';
|
|
import 'package:gimnasio_soma/core/router/app_router.dart';
|
|
import 'package:gimnasio_soma/core/services/soma_logger.dart';
|
|
import 'package:gimnasio_soma/core/theme/soma_theme.dart';
|
|
|
|
/// Si se compiló para prod y no vino marcada por `soma_pro_updater`, relanza
|
|
/// a través de él en vez de arrancar directo. Evita que alguien se quede en
|
|
/// una versión vieja por abrir el .exe de la app sin pasar por el updater.
|
|
/// Se salta en dev porque ahí no existe `soma_pro_updater` al lado.
|
|
Future<bool> _redirectToUpdaterIfNeeded(List<String> args) async {
|
|
if (!AppEnv.isProd || args.contains('--from-updater')) return false;
|
|
|
|
// En Windows/Linux esta app vive en una subcarpeta `app/` al lado del
|
|
// updater (evita que sus carpetas `data/` se pisen); en macOS son
|
|
// hermanos porque cada `.app` ya es autocontenido.
|
|
final exeDir = File(Platform.resolvedExecutable).parent;
|
|
// En macOS el exe vive en .app/Contents/MacOS/ — subimos 3 niveles para llegar
|
|
// a la carpeta que contiene ambos .app bundles (donde está soma_pro_updater.app).
|
|
// En Windows/Linux la app vive en app/ — subimos 1 nivel para llegar al updater.
|
|
final updaterDir = Platform.isMacOS ? exeDir.parent.parent.parent : exeDir.parent;
|
|
final sep = Platform.pathSeparator;
|
|
final updaterName = Platform.isMacOS ? 'soma_pro_updater.app' : 'soma_pro_updater.exe';
|
|
final updaterPath = '${updaterDir.path}$sep$updaterName';
|
|
|
|
final exists =
|
|
Platform.isMacOS ? Directory(updaterPath).existsSync() : File(updaterPath).existsSync();
|
|
if (!exists) return false;
|
|
|
|
// Freno de rebote: si ya redirigimos hace poco, el updater ya nos relanzó
|
|
// pero este proceso arrancó de nuevo sin `--from-updater` (en macOS, `open`
|
|
// sin `-n` puede reactivar una instancia existente en vez de crear una
|
|
// nueva, y ahí se pierde el `--args`). En vez de rebotar para siempre,
|
|
// cortamos el ciclo y arrancamos directo.
|
|
final bounceFile = File('${updaterDir.path}$sep.soma_redirect_bounce');
|
|
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
|
if (bounceFile.existsSync()) {
|
|
final lastMs = int.tryParse(bounceFile.readAsStringSync().trim()) ?? 0;
|
|
if (nowMs - lastMs < 8000) {
|
|
try {
|
|
bounceFile.deleteSync();
|
|
} catch (_) {}
|
|
return false;
|
|
}
|
|
}
|
|
bounceFile.writeAsStringSync('$nowMs');
|
|
|
|
if (Platform.isMacOS) {
|
|
// `-n` fuerza una instancia nueva del updater en vez de reactivar una
|
|
// existente, así el `open` de abajo siempre entrega los args frescos.
|
|
await Process.start('open', ['-n', updaterPath], mode: ProcessStartMode.detached);
|
|
} else {
|
|
await Process.start(updaterPath, const [], mode: ProcessStartMode.detached);
|
|
}
|
|
// Mismo respiro que del lado del updater: si este proceso sale demasiado
|
|
// rápido, macOS puede no llegar a transferirle el foco al proceso recién
|
|
// lanzado y su ventana queda sin renderizar el primer frame.
|
|
await Future.delayed(const Duration(milliseconds: 600));
|
|
return true;
|
|
}
|
|
|
|
void main(List<String> args) async {
|
|
if (await _redirectToUpdaterIfNeeded(args)) {
|
|
exit(0);
|
|
}
|
|
|
|
runZonedGuarded(() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
|
|
final log = SomaLogger.instance;
|
|
log.detectPlatform();
|
|
log.info('App', 'Iniciando SOMA PRO...');
|
|
|
|
// Config de entorno horneada al compilar (--dart-define-from-file=config/<env>.json).
|
|
// Corta el arranque con un mensaje claro si se compiló sin pasar el archivo.
|
|
AppEnv.assertConfigured();
|
|
log.info('App', 'Entorno: ${AppEnv.env}');
|
|
|
|
try {
|
|
await SupabaseConfig.initialize();
|
|
log.info('App', 'Supabase inicializado');
|
|
} catch (e) {
|
|
log.error('App', 'Error inicializando Supabase', detail: e.toString());
|
|
}
|
|
|
|
// Captura errores de Flutter (widgets, rendering, etc.)
|
|
FlutterError.onError = (details) {
|
|
log.error('Flutter', details.exceptionAsString(),
|
|
detail: details.stack?.toString().split('\n').take(5).join('\n'));
|
|
};
|
|
|
|
runApp(const ProviderScope(child: MainApp()));
|
|
}, (error, stack) {
|
|
// Captura errores no manejados fuera del framework
|
|
SomaLogger.instance.error('Zone', error.toString(),
|
|
detail: stack.toString().split('\n').take(5).join('\n'));
|
|
});
|
|
}
|
|
|
|
class MainApp extends ConsumerWidget {
|
|
const MainApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final router = ref.watch(routerProvider);
|
|
|
|
return MaterialApp.router(
|
|
title: 'SOMA PRO',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: SomaTheme.dark,
|
|
routerConfig: router,
|
|
// Cinta "DEV" en cualquier build que no sea prod (ver EnvBanner).
|
|
builder: (context, child) =>
|
|
EnvBanner(child: child ?? const SizedBox.shrink()),
|
|
);
|
|
}
|
|
}
|