hwhub/lib/main.dart
Lukas Müllner 1be830b16f feat(scanner): polish detection feedback and main collection behavior
- Replace noisy no-id snackbars with in-screen red status feedback
- Keep success state green and reduce interruption during scan flow
- Make scan-tab collection switching session-only (does not overwrite main)
- Keep main collection preselected from collections tab preference
- Apply recent UX text/layout polish for scan and detail actions
2026-03-04 13:44:55 +01:00

235 lines
6.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'theme/app_theme.dart';
import 'services/collection_service.dart';
import 'screens/login_screen.dart';
import 'screens/home_shell.dart';
// Re-export so other files can `import '../main.dart'` for these.
export 'package:supabase_flutter/supabase_flutter.dart'
show AuthException, UserAttributes;
// ── Supabase credentials ──────────────────────────────────────────────
const _supabaseUrl = 'https://yaopcyubateifnicpywp.supabase.co';
const _supabaseAnonKey = 'sb_publishable_a7czIl7-TGeBJvid9z2XZA_3ElImliL';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: _supabaseUrl,
anonKey: _supabaseAnonKey,
authOptions: const FlutterAuthClientOptions(
authFlowType: AuthFlowType.implicit,
),
);
runApp(const Car64App());
}
/// Convenience accessor used throughout the app.
final supabase = Supabase.instance.client;
/// Global keys so dialogs & snackbars survive widget-tree rebuilds.
final navigatorKey = GlobalKey<NavigatorState>();
final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
/// Show a snackbar safely through the global key.
void showGlobalSnackBar(String message, {bool isError = false}) {
final messenger = scaffoldMessengerKey.currentState;
if (messenger == null) return;
messenger
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red : null,
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.fromLTRB(16, 0, 16, 96),
duration: const Duration(seconds: 3),
),
);
}
/// Show a dialog safely through the global navigator key.
Future<T?> showGlobalDialog<T>({required WidgetBuilder builder}) {
return showDialog<T>(
context: navigatorKey.currentContext!,
builder: builder,
);
}
// ── Root App Widget ───────────────────────────────────────────────────
class Car64App extends StatelessWidget {
const Car64App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'car64',
debugShowCheckedModeBanner: false,
navigatorKey: navigatorKey,
scaffoldMessengerKey: scaffoldMessengerKey,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: ThemeMode.system,
home: const AuthGate(),
);
}
}
// ── Auth Gate ─────────────────────────────────────────────────────────
class AuthGate extends StatefulWidget {
const AuthGate({super.key});
@override
State<AuthGate> createState() => _AuthGateState();
}
class _AuthGateState extends State<AuthGate> {
bool _isLoading = true;
bool _isInPasswordRecoveryFlow = false;
Session? _session;
String? _lastEnsuredUserId;
@override
void initState() {
super.initState();
_session = supabase.auth.currentSession;
_ensureDefaultCollectionIfNeeded();
supabase.auth.onAuthStateChange.listen(
(AuthState authState) {
if (!mounted) return;
final wasLoggedIn = _session != null;
final isLoggedIn = authState.session != null;
_session = authState.session;
if (wasLoggedIn != isLoggedIn) {
setState(() {});
}
_ensureDefaultCollectionIfNeeded();
if (authState.event == AuthChangeEvent.passwordRecovery) {
setState(() => _isInPasswordRecoveryFlow = true);
_showResetPasswordDialog();
}
},
onError: (error) {
showGlobalSnackBar('Auth error: $error', isError: true);
},
);
setState(() => _isLoading = false);
}
Future<void> _ensureDefaultCollectionIfNeeded() async {
final userId = _session?.user.id;
if (userId == null || userId == _lastEnsuredUserId) return;
_lastEnsuredUserId = userId;
try {
await CollectionService.ensureDefaultCollection();
} catch (e) {
showGlobalSnackBar('Collection setup failed: $e', isError: true);
}
}
Future<void> _showResetPasswordDialog() async {
await showDialog<void>(
context: navigatorKey.currentContext!,
barrierDismissible: false,
builder: (_) => const _ResetPasswordDialog(),
);
if (mounted) {
setState(() => _isInPasswordRecoveryFlow = false);
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (_isInPasswordRecoveryFlow) return const LoginScreen();
return _session != null ? const HomeShell() : const LoginScreen();
}
}
// ── Reset Password Dialog ─────────────────────────────────────────────
class _ResetPasswordDialog extends StatefulWidget {
const _ResetPasswordDialog();
@override
State<_ResetPasswordDialog> createState() => _ResetPasswordDialogState();
}
class _ResetPasswordDialogState extends State<_ResetPasswordDialog> {
final _passwordController = TextEditingController();
bool _isSaving = false;
@override
void dispose() {
_passwordController.dispose();
super.dispose();
}
Future<void> _save() async {
final newPassword = _passwordController.text.trim();
if (newPassword.length < 6) {
showGlobalSnackBar('Password must be at least 6 characters.');
return;
}
setState(() => _isSaving = true);
try {
await supabase.auth.updateUser(
UserAttributes(password: newPassword),
);
if (!mounted) return;
Navigator.of(context).pop();
showGlobalSnackBar('Password updated successfully!');
} on AuthException catch (e) {
if (!mounted) return;
setState(() => _isSaving = false);
showGlobalSnackBar(e.message, isError: true);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Set New Password'),
content: TextField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(labelText: 'New password'),
onSubmitted: (_) => _save(),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: _isSaving ? null : _save,
child: _isSaving
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Save'),
),
],
);
}
}