hwhub/lib/main.dart
Lukas Müllner 91f25e0cef feat(v1.1): complete UI overhaul with branded theme, garage, and image uploads
- Custom Material 3 theme with Hot Wheels branding (orange/red gradient, navy accents)
- Locally bundled Poppins font (Regular, Medium, SemiBold, Bold)
- Redesigned login screen with full-bleed background image and frosted glass form
- Bottom navigation shell: My Garage / Scan / Profile tabs
- My Garage screen with grid view, search, stats, detail bottom sheet
- Edit and delete car details from the detail sheet
- Skip button for quick-add with minimal info
- Camera photo upload to Supabase Storage (UUID-based unguessable paths)
- Change/add photo from car detail view
- Profile screen with change password, about dialog, sign out
- Scan tab with camera scanner and manual entry
- Styled scanner screen with crosshair overlay and gradient buttons
- Custom app icon with transparent background and adaptive icon support
- Native splash screen with brand colors
- Auto-refresh garage on tab switch
2026-02-24 07:46:06 +01:00

200 lines
5.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'theme/app_theme.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,
);
runApp(const HWHubApp());
}
/// 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}) {
scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red : null,
),
);
}
/// 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 HWHubApp extends StatelessWidget {
const HWHubApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'HW Collector Hub',
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;
Session? _session;
@override
void initState() {
super.initState();
_session = supabase.auth.currentSession;
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(() {});
}
if (authState.event == AuthChangeEvent.passwordRecovery) {
_showResetPasswordDialog();
}
},
onError: (error) {
showGlobalSnackBar('Auth error: $error', isError: true);
},
);
setState(() => _isLoading = false);
}
Future<void> _showResetPasswordDialog() async {
await showDialog<void>(
context: navigatorKey.currentContext!,
barrierDismissible: false,
builder: (_) => const _ResetPasswordDialog(),
);
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
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'),
),
],
);
}
}