diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 5eff307..018bbcb 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -6,7 +6,7 @@ plugins { android { namespace = "com.slrpg.app" - compileSdk = 35 + compileSdk = 36 compileOptions { sourceCompatibility = JavaVersion.VERSION_17 @@ -21,7 +21,7 @@ android { defaultConfig { applicationId = "com.slrpg.app" minSdk = flutter.minSdkVersion - targetSdk = 35 + targetSdk = 36 versionCode = 1 versionName = "1.0.0" } diff --git a/lib/main.dart b/lib/main.dart index b707544..883e0c0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,30 +23,23 @@ void main() async { DeviceOrientation.portraitDown, ]); + final notificationService = NotificationService(); + await notificationService.init(); + final database = AppDatabase(); + final authStore = PbAuthStore(); await authStore.loadFromStorage(); - final container = ProviderContainer( - overrides: [ - appDatabaseProvider.overrideWithValue(database), - apiClientProvider - .overrideWith((ref) => ApiClient(authStore: authStore)), - ], - ); - - try { - log('Initializing NotificationService...'); - container.read(notificationServiceProvider).init(); - } catch (e) { - log('Error triggering NotificationService: $e'); - } - log("Auth loaded. Valid? ${authStore.isValid}"); runApp( - UncontrolledProviderScope( - container: container, + ProviderScope( + overrides: [ + appDatabaseProvider.overrideWithValue(database), + apiClientProvider + .overrideWith((ref) => ApiClient(authStore: authStore)), + ], child: const SLRPGApp(), ), ); diff --git a/lib/src/core/utils/error_handler.dart b/lib/src/core/utils/error_handler.dart index 8e35243..715ee74 100644 --- a/lib/src/core/utils/error_handler.dart +++ b/lib/src/core/utils/error_handler.dart @@ -31,8 +31,7 @@ class ErrorHandler { return l10n.errorEntryNotUnique; } // PocketBase specific error for failed login - if (e.contains('Failed to authenticate') || - e.contains('identity or password')) { + if (e.contains('Failed to authenticate') || e.contains('identity or password')) { return l10n.errorAuthenticationFailed; } return l10n.errorIllegalRequest; @@ -42,18 +41,17 @@ class ErrorHandler { } static void showErrorSnackBar(BuildContext context, Object error) { - if (!context.mounted) return; final scaffoldMessenger = ScaffoldMessenger.of(context); scaffoldMessenger.hideCurrentSnackBar(); - + scaffoldMessenger.showSnackBar( SnackBar( content: Text(getReadableError(context, error)), backgroundColor: AppTheme.errorColor, behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 4), - dismissDirection: DismissDirection.horizontal, - margin: const EdgeInsets.fromLTRB(16, 16, 16, 60), + dismissDirection: DismissDirection.horizontal, // Easier to swipe away + margin: const EdgeInsets.fromLTRB(16, 16, 16, 60), // Move it up to not block navigation action: SnackBarAction( label: 'OK', textColor: Colors.white, diff --git a/lib/src/core/utils/notification_service.dart b/lib/src/core/utils/notification_service.dart index 0587dfe..776dde2 100644 --- a/lib/src/core/utils/notification_service.dart +++ b/lib/src/core/utils/notification_service.dart @@ -19,7 +19,7 @@ class NotificationService { tz.initializeTimeZones(); const AndroidInitializationSettings initializationSettingsAndroid = - AndroidInitializationSettings('ic_launcher'); + AndroidInitializationSettings('@mipmap/ic_launcher'); const DarwinInitializationSettings initializationSettingsDarwin = DarwinInitializationSettings( @@ -47,17 +47,8 @@ class NotificationService { final androidImplementation = _notifications.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); - - if (androidImplementation != null) { - // Request notification permission (Android 13+) - await androidImplementation.requestNotificationsPermission(); - - // requestExactAlarmsPermission is typically for SCHEDULE_EXACT_ALARM. - // Since we use USE_EXACT_ALARM in the manifest for newer versions, - // we only request this if absolutely necessary or on older versions if supported. - // On Android 13+, USE_EXACT_ALARM is granted at install time. - await androidImplementation.requestExactAlarmsPermission(); - } + await androidImplementation?.requestNotificationsPermission(); + await androidImplementation?.requestExactAlarmsPermission(); } } diff --git a/lib/src/features/authentication/presentation/screens/profile_screen.dart b/lib/src/features/authentication/presentation/screens/profile_screen.dart index 627b381..e872545 100644 --- a/lib/src/features/authentication/presentation/screens/profile_screen.dart +++ b/lib/src/features/authentication/presentation/screens/profile_screen.dart @@ -286,19 +286,19 @@ class _ProfileScreenState extends ConsumerState { final l10n = AppLocalizations.of(context)!; showDialog( context: context, - builder: (dialogContext) => AlertDialog( + builder: (context) => AlertDialog( title: Text(title, style: const TextStyle(color: AppTheme.errorColor)), content: Text(content), actions: [ TextButton( - onPressed: () => Navigator.pop(dialogContext), + onPressed: () => Navigator.pop(context), child: Text(l10n.commonCancel), ), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: AppTheme.errorColor), onPressed: () { - Navigator.pop(dialogContext); + Navigator.pop(context); onConfirm(); }, child: Text(l10n.commonConfirm), diff --git a/lib/src/features/dashboard/presentation/screens/hub_screen.dart b/lib/src/features/dashboard/presentation/screens/hub_screen.dart index 6407cf7..75c678d 100644 --- a/lib/src/features/dashboard/presentation/screens/hub_screen.dart +++ b/lib/src/features/dashboard/presentation/screens/hub_screen.dart @@ -253,13 +253,13 @@ class _HubScreenState extends ConsumerState { showDialog( context: context, - builder: (dialogContext) => AlertDialog( + builder: (context) => AlertDialog( title: Text(l10n.multiplayerTitle), content: Text(l10n.multiplayerDescription), actions: [ TextButton( onPressed: () { - Navigator.pop(dialogContext); + Navigator.pop(context); _showJoinCodeDialog(); }, child: Text(l10n.multiplayerJoinButton), @@ -268,14 +268,14 @@ class _HubScreenState extends ConsumerState { style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryColor), onPressed: () async { - Navigator.pop(dialogContext); + Navigator.pop(context); try { final party = await ref.read(partyRepositoryProvider).createParty(); if (mounted) context.go('/lobby/${party.id}'); } catch (e) { if (mounted) { - ErrorHandler.showErrorSnackBar(context, e); + if (mounted) ErrorHandler.showErrorSnackBar(context, e); } } }, @@ -293,7 +293,7 @@ class _HubScreenState extends ConsumerState { final controller = TextEditingController(); showDialog( context: context, - builder: (dialogContext) => AlertDialog( + builder: (context) => AlertDialog( title: Text(l10n.multiplayerEnterCodeTitle), content: TextField( controller: controller, @@ -305,21 +305,21 @@ class _HubScreenState extends ConsumerState { ), actions: [ TextButton( - onPressed: () => Navigator.pop(dialogContext), + onPressed: () => Navigator.pop(context), child: Text(l10n.multiplayerCancelAction), ), ElevatedButton( onPressed: () async { final code = controller.text.trim().toUpperCase(); if (code.isNotEmpty) { - Navigator.pop(dialogContext); + Navigator.pop(context); try { final party = await ref.read(partyRepositoryProvider).joinParty(code); if (mounted) context.go('/lobby/${party.id}'); } catch (e) { if (mounted) { - ErrorHandler.showErrorSnackBar(context, e); + if (mounted) ErrorHandler.showErrorSnackBar(context, e); } } } diff --git a/lib/src/features/multiplayer/data/repositories/party_repository.dart b/lib/src/features/multiplayer/data/repositories/party_repository.dart index af37fb3..9e8f95d 100644 --- a/lib/src/features/multiplayer/data/repositories/party_repository.dart +++ b/lib/src/features/multiplayer/data/repositories/party_repository.dart @@ -83,11 +83,7 @@ class PartyRepository { }); }, onCancel: () async { - try { - await unsubscribe?.call(); - } catch (e) { - log('Safe ignore: Unsubscribe error (likely already disconnected): $e'); - } + await unsubscribe?.call(); log('🔌 Unsubscribed from party $partyId'); }, ); @@ -124,11 +120,7 @@ class PartyRepository { }); }, onCancel: () async { - try { - await unsubscribe?.call(); - } catch (e) { - log('Safe ignore: Unsubscribe error (likely already disconnected): $e'); - } + await unsubscribe?.call(); log('🔌 Unsubscribed from party members $partyId'); }, ); diff --git a/lib/src/features/workout_runner/application/rest_timer_service.dart b/lib/src/features/workout_runner/application/rest_timer_service.dart index 897d6fc..e6903a8 100644 --- a/lib/src/features/workout_runner/application/rest_timer_service.dart +++ b/lib/src/features/workout_runner/application/rest_timer_service.dart @@ -1,7 +1,5 @@ import 'dart:async'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../../../shared/data/repositories/user_repository.dart'; -import '../../../core/utils/notification_service.dart'; part 'rest_timer_service.g.dart'; @@ -77,21 +75,12 @@ class RestTimer extends _$RestTimer { ); } - void complete() async { + void complete() { cancel(); state = state.copyWith( isActive: false, remainingSeconds: 0, ); - - final userRepo = ref.read(userRepositoryProvider); - final user = await userRepo.getLocalUser(); - final settings = user?.notificationSettings ?? {}; - final restEnabled = settings['rest_finished_enabled'] ?? true; - - if (restEnabled) { - ref.read(notificationServiceProvider).showRestFinishedNotification(); - } } void cancel() { diff --git a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart index 51cca59..2cd2f95 100644 --- a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart +++ b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart @@ -141,11 +141,11 @@ class _BattleScreenState extends ConsumerState { final controller = ref.read(battleControllerProvider.notifier); final battleState = ref.read(battleControllerProvider); - + if (battleState.isResting) { controller.tickRest(); final newState = ref.read(battleControllerProvider); - + if (newState.isResting) { _runRestTimer(); } else { @@ -254,7 +254,7 @@ class _BattleScreenState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: (dialogContext) => AlertDialog( + builder: (context) => AlertDialog( title: Text(l10n.battleRaidComplete), content: Column( mainAxisSize: MainAxisSize.min, @@ -267,7 +267,7 @@ class _BattleScreenState extends ConsumerState { const SizedBox(height: 16), Text( '+$xpEarned XP', - style: Theme.of(dialogContext).textTheme.headlineMedium?.copyWith( + style: Theme.of(context).textTheme.headlineMedium?.copyWith( color: AppTheme.primaryColor, ), ), @@ -276,7 +276,7 @@ class _BattleScreenState extends ConsumerState { actions: [ ElevatedButton( onPressed: () { - Navigator.of(dialogContext).pop(); + Navigator.of(context).pop(); context.go('/hub'); }, child: Text(l10n.battleBackToHub), @@ -1079,8 +1079,7 @@ class _BattleScreenState extends ConsumerState { children: [ _InfoBox( label: l10n.battleWeight, - value: - '${currentSet.targetWeightTotal} ${l10n.unitKg}', + value: '${currentSet.targetWeightTotal} kg', ), _InfoBox( label: l10n.battleReps, @@ -1194,20 +1193,21 @@ class _BattleScreenState extends ConsumerState { void _showAbandonDialog(AppLocalizations l10n) { showDialog( context: context, - builder: (dialogContext) => AlertDialog( + builder: (context) => AlertDialog( title: Text(l10n.battleAbandonTitle), content: Text(l10n.battleAbandonBody), actions: [ TextButton( - onPressed: () => Navigator.of(dialogContext).pop(), + onPressed: () => Navigator.of(context).pop(), child: Text(l10n.cancelButton), ), TextButton( onPressed: () async { - Navigator.of(dialogContext).pop(); + Navigator.of(context).pop(); if (widget.partyId != null) { - final user = await ref.read(userRepositoryProvider).getLocalUser(); - final userId = user?.serverId; + final userId = + (await ref.read(userRepositoryProvider).getLocalUser()) + ?.serverId; if (userId != null) { await ref .read(partyRepositoryProvider) diff --git a/lib/src/shared/data/remote/pb_auth_store.dart b/lib/src/shared/data/remote/pb_auth_store.dart index 061ecd3..17dc5de 100644 --- a/lib/src/shared/data/remote/pb_auth_store.dart +++ b/lib/src/shared/data/remote/pb_auth_store.dart @@ -1,3 +1,60 @@ +// import 'dart:convert'; +// import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +// import 'package:pocketbase/pocketbase.dart'; + +// class PbAuthStore extends AuthStore { +// final FlutterSecureStorage _storage; +// final String _storageKey; + +// PbAuthStore({ +// FlutterSecureStorage? storage, +// String key = 'pb_auth', +// }) : _storage = storage ?? const FlutterSecureStorage(), +// _storageKey = key, +// super(); + +// @override +// Future save(String newToken, dynamic newRecord) async { +// super.save(newToken, newRecord); + +// final encoded = jsonEncode({ +// 'token': newToken, +// 'model': newRecord, +// }); + +// await _storage.write(key: _storageKey, value: encoded); +// } + +// @override +// void clear() { +// super.clear(); +// _storage.delete(key: _storageKey); +// } + +// Future loadFromStorage() async { +// final raw = await _storage.read(key: _storageKey); +// if (raw != null && raw.isNotEmpty) { +// try { +// final decoded = jsonDecode(raw) as Map; +// final token = decoded['token'] as String?; +// final model = decoded['model']; + +// if (token != null && token.isNotEmpty) { +// super.save(token, model); +// return; +// } +// } catch (_) { +// clear(); +// } +// } + +// const legacyKey = 'auth_token'; +// final legacyToken = await _storage.read(key: legacyKey); +// if (legacyToken != null && legacyToken.isNotEmpty) { +// super.save(legacyToken, null); +// } +// } +// } import 'dart:convert'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:pocketbase/pocketbase.dart'; @@ -30,6 +87,7 @@ class PbAuthStore extends AuthStore { await _storage.delete(key: _saveKey); } + // Diese Methode rufen wir VOR App-Start auf! Future loadFromStorage() async { final raw = await _storage.read(key: _saveKey); if (raw != null && raw.isNotEmpty) { @@ -48,8 +106,11 @@ class PbAuthStore extends AuthStore { } } + // super.save schreibt nur in den Speicher (RAM) des AuthStores, + // löst aber kein erneutes 'save' (und damit write) aus. super.save(token, model); } catch (e) { + // Daten korrupt? Löschen. await clear(); } }