From 6b70116a639d1e1cdfffbf8099f4cd973fec9665 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Wed, 18 Feb 2026 23:08:44 +0100 Subject: [PATCH 1/4] refactor: perform clean up --- android/app/build.gradle.kts | 4 +-- lib/main.dart | 27 ++++++++++++------- lib/src/core/utils/error_handler.dart | 9 ++++--- lib/src/core/utils/notification_service.dart | 15 ++++++++--- .../application/rest_timer_service.dart | 13 ++++++++- 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 018bbcb..5eff307 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -6,7 +6,7 @@ plugins { android { namespace = "com.slrpg.app" - compileSdk = 36 + compileSdk = 35 compileOptions { sourceCompatibility = JavaVersion.VERSION_17 @@ -21,7 +21,7 @@ android { defaultConfig { applicationId = "com.slrpg.app" minSdk = flutter.minSdkVersion - targetSdk = 36 + targetSdk = 35 versionCode = 1 versionName = "1.0.0" } diff --git a/lib/main.dart b/lib/main.dart index 883e0c0..b707544 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,23 +23,30 @@ 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( - ProviderScope( - overrides: [ - appDatabaseProvider.overrideWithValue(database), - apiClientProvider - .overrideWith((ref) => ApiClient(authStore: authStore)), - ], + UncontrolledProviderScope( + container: container, child: const SLRPGApp(), ), ); diff --git a/lib/src/core/utils/error_handler.dart b/lib/src/core/utils/error_handler.dart index 715ee74..edd6e59 100644 --- a/lib/src/core/utils/error_handler.dart +++ b/lib/src/core/utils/error_handler.dart @@ -31,7 +31,8 @@ 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; @@ -43,15 +44,15 @@ class ErrorHandler { static void showErrorSnackBar(BuildContext context, Object error) { 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, // Easier to swipe away - margin: const EdgeInsets.fromLTRB(16, 16, 16, 60), // Move it up to not block navigation + dismissDirection: DismissDirection.horizontal, + margin: const EdgeInsets.fromLTRB(16, 16, 16, 60), 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 776dde2..0587dfe 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('@mipmap/ic_launcher'); + AndroidInitializationSettings('ic_launcher'); const DarwinInitializationSettings initializationSettingsDarwin = DarwinInitializationSettings( @@ -47,8 +47,17 @@ class NotificationService { final androidImplementation = _notifications.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); - await androidImplementation?.requestNotificationsPermission(); - await androidImplementation?.requestExactAlarmsPermission(); + + 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(); + } } } 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 e6903a8..897d6fc 100644 --- a/lib/src/features/workout_runner/application/rest_timer_service.dart +++ b/lib/src/features/workout_runner/application/rest_timer_service.dart @@ -1,5 +1,7 @@ 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'; @@ -75,12 +77,21 @@ class RestTimer extends _$RestTimer { ); } - void complete() { + void complete() async { 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() { From c290c268e8a29b492a4d0b899617530576307f91 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Thu, 19 Feb 2026 20:45:35 +0100 Subject: [PATCH 2/4] fix: add missing changes --- lib/src/shared/data/remote/pb_auth_store.dart | 61 ------------------- 1 file changed, 61 deletions(-) diff --git a/lib/src/shared/data/remote/pb_auth_store.dart b/lib/src/shared/data/remote/pb_auth_store.dart index 17dc5de..061ecd3 100644 --- a/lib/src/shared/data/remote/pb_auth_store.dart +++ b/lib/src/shared/data/remote/pb_auth_store.dart @@ -1,60 +1,3 @@ -// 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'; @@ -87,7 +30,6 @@ 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) { @@ -106,11 +48,8 @@ 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(); } } From 58e79147c76dd7d0d63afe229be3482028afaded Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Thu, 19 Feb 2026 20:53:35 +0100 Subject: [PATCH 3/4] fix: fix usage of wrong context for multiplayer battle --- lib/src/core/utils/error_handler.dart | 1 + .../presentation/screens/profile_screen.dart | 6 +++--- .../presentation/screens/hub_screen.dart | 16 ++++++++-------- .../data/repositories/party_repository.dart | 12 ++++++++++-- .../presentation/screens/battle_screen.dart | 13 +++++++------ 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/lib/src/core/utils/error_handler.dart b/lib/src/core/utils/error_handler.dart index edd6e59..8e35243 100644 --- a/lib/src/core/utils/error_handler.dart +++ b/lib/src/core/utils/error_handler.dart @@ -42,6 +42,7 @@ class ErrorHandler { } static void showErrorSnackBar(BuildContext context, Object error) { + if (!context.mounted) return; final scaffoldMessenger = ScaffoldMessenger.of(context); scaffoldMessenger.hideCurrentSnackBar(); diff --git a/lib/src/features/authentication/presentation/screens/profile_screen.dart b/lib/src/features/authentication/presentation/screens/profile_screen.dart index e872545..627b381 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: (context) => AlertDialog( + builder: (dialogContext) => AlertDialog( title: Text(title, style: const TextStyle(color: AppTheme.errorColor)), content: Text(content), actions: [ TextButton( - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.pop(dialogContext), child: Text(l10n.commonCancel), ), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: AppTheme.errorColor), onPressed: () { - Navigator.pop(context); + Navigator.pop(dialogContext); 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 75c678d..6407cf7 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: (context) => AlertDialog( + builder: (dialogContext) => AlertDialog( title: Text(l10n.multiplayerTitle), content: Text(l10n.multiplayerDescription), actions: [ TextButton( onPressed: () { - Navigator.pop(context); + Navigator.pop(dialogContext); _showJoinCodeDialog(); }, child: Text(l10n.multiplayerJoinButton), @@ -268,14 +268,14 @@ class _HubScreenState extends ConsumerState { style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryColor), onPressed: () async { - Navigator.pop(context); + Navigator.pop(dialogContext); try { final party = await ref.read(partyRepositoryProvider).createParty(); if (mounted) context.go('/lobby/${party.id}'); } catch (e) { if (mounted) { - if (mounted) ErrorHandler.showErrorSnackBar(context, e); + ErrorHandler.showErrorSnackBar(context, e); } } }, @@ -293,7 +293,7 @@ class _HubScreenState extends ConsumerState { final controller = TextEditingController(); showDialog( context: context, - builder: (context) => AlertDialog( + builder: (dialogContext) => AlertDialog( title: Text(l10n.multiplayerEnterCodeTitle), content: TextField( controller: controller, @@ -305,21 +305,21 @@ class _HubScreenState extends ConsumerState { ), actions: [ TextButton( - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.pop(dialogContext), child: Text(l10n.multiplayerCancelAction), ), ElevatedButton( onPressed: () async { final code = controller.text.trim().toUpperCase(); if (code.isNotEmpty) { - Navigator.pop(context); + Navigator.pop(dialogContext); try { final party = await ref.read(partyRepositoryProvider).joinParty(code); if (mounted) context.go('/lobby/${party.id}'); } catch (e) { if (mounted) { - if (mounted) ErrorHandler.showErrorSnackBar(context, e); + 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 9e8f95d..af37fb3 100644 --- a/lib/src/features/multiplayer/data/repositories/party_repository.dart +++ b/lib/src/features/multiplayer/data/repositories/party_repository.dart @@ -83,7 +83,11 @@ class PartyRepository { }); }, onCancel: () async { - await unsubscribe?.call(); + try { + await unsubscribe?.call(); + } catch (e) { + log('Safe ignore: Unsubscribe error (likely already disconnected): $e'); + } log('🔌 Unsubscribed from party $partyId'); }, ); @@ -120,7 +124,11 @@ class PartyRepository { }); }, onCancel: () async { - await unsubscribe?.call(); + try { + await unsubscribe?.call(); + } catch (e) { + log('Safe ignore: Unsubscribe error (likely already disconnected): $e'); + } log('🔌 Unsubscribed from party members $partyId'); }, ); 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 2cd2f95..3a310d4 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: (context) => AlertDialog( + builder: (dialogContext) => 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(context).textTheme.headlineMedium?.copyWith( + style: Theme.of(dialogContext).textTheme.headlineMedium?.copyWith( color: AppTheme.primaryColor, ), ), @@ -276,7 +276,7 @@ class _BattleScreenState extends ConsumerState { actions: [ ElevatedButton( onPressed: () { - Navigator.of(context).pop(); + Navigator.of(dialogContext).pop(); context.go('/hub'); }, child: Text(l10n.battleBackToHub), @@ -1079,7 +1079,8 @@ class _BattleScreenState extends ConsumerState { children: [ _InfoBox( label: l10n.battleWeight, - value: '${currentSet.targetWeightTotal} kg', + value: + '${currentSet.targetWeightTotal} ${l10n.unitKg}', ), _InfoBox( label: l10n.battleReps, From b772b39be667ede77551f1085223d8a4419fb85d Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Thu, 19 Feb 2026 21:44:27 +0100 Subject: [PATCH 4/4] fix: fix not correctly working multiplayer --- .../presentation/screens/battle_screen.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 3a310d4..51cca59 100644 --- a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart +++ b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart @@ -1194,21 +1194,20 @@ class _BattleScreenState extends ConsumerState { void _showAbandonDialog(AppLocalizations l10n) { showDialog( context: context, - builder: (context) => AlertDialog( + builder: (dialogContext) => AlertDialog( title: Text(l10n.battleAbandonTitle), content: Text(l10n.battleAbandonBody), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => Navigator.of(dialogContext).pop(), child: Text(l10n.cancelButton), ), TextButton( onPressed: () async { - Navigator.of(context).pop(); + Navigator.of(dialogContext).pop(); if (widget.partyId != null) { - final userId = - (await ref.read(userRepositoryProvider).getLocalUser()) - ?.serverId; + final user = await ref.read(userRepositoryProvider).getLocalUser(); + final userId = user?.serverId; if (userId != null) { await ref .read(partyRepositoryProvider)