Compare commits

...

4 commits
v0.1.0 ... main

10 changed files with 82 additions and 106 deletions

View file

@ -6,7 +6,7 @@ plugins {
android { android {
namespace = "com.slrpg.app" namespace = "com.slrpg.app"
compileSdk = 36 compileSdk = 35
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
@ -21,7 +21,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "com.slrpg.app" applicationId = "com.slrpg.app"
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
targetSdk = 36 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "1.0.0" versionName = "1.0.0"
} }

View file

@ -23,23 +23,30 @@ void main() async {
DeviceOrientation.portraitDown, DeviceOrientation.portraitDown,
]); ]);
final notificationService = NotificationService();
await notificationService.init();
final database = AppDatabase(); final database = AppDatabase();
final authStore = PbAuthStore(); final authStore = PbAuthStore();
await authStore.loadFromStorage(); 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}"); log("Auth loaded. Valid? ${authStore.isValid}");
runApp( runApp(
ProviderScope( UncontrolledProviderScope(
overrides: [ container: container,
appDatabaseProvider.overrideWithValue(database),
apiClientProvider
.overrideWith((ref) => ApiClient(authStore: authStore)),
],
child: const SLRPGApp(), child: const SLRPGApp(),
), ),
); );

View file

@ -31,7 +31,8 @@ class ErrorHandler {
return l10n.errorEntryNotUnique; return l10n.errorEntryNotUnique;
} }
// PocketBase specific error for failed login // 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.errorAuthenticationFailed;
} }
return l10n.errorIllegalRequest; return l10n.errorIllegalRequest;
@ -41,6 +42,7 @@ class ErrorHandler {
} }
static void showErrorSnackBar(BuildContext context, Object error) { static void showErrorSnackBar(BuildContext context, Object error) {
if (!context.mounted) return;
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);
scaffoldMessenger.hideCurrentSnackBar(); scaffoldMessenger.hideCurrentSnackBar();
@ -50,8 +52,8 @@ class ErrorHandler {
backgroundColor: AppTheme.errorColor, backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 4), duration: const Duration(seconds: 4),
dismissDirection: DismissDirection.horizontal, // Easier to swipe away dismissDirection: DismissDirection.horizontal,
margin: const EdgeInsets.fromLTRB(16, 16, 16, 60), // Move it up to not block navigation margin: const EdgeInsets.fromLTRB(16, 16, 16, 60),
action: SnackBarAction( action: SnackBarAction(
label: 'OK', label: 'OK',
textColor: Colors.white, textColor: Colors.white,

View file

@ -19,7 +19,7 @@ class NotificationService {
tz.initializeTimeZones(); tz.initializeTimeZones();
const AndroidInitializationSettings initializationSettingsAndroid = const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher'); AndroidInitializationSettings('ic_launcher');
const DarwinInitializationSettings initializationSettingsDarwin = const DarwinInitializationSettings initializationSettingsDarwin =
DarwinInitializationSettings( DarwinInitializationSettings(
@ -47,8 +47,17 @@ class NotificationService {
final androidImplementation = final androidImplementation =
_notifications.resolvePlatformSpecificImplementation< _notifications.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>(); 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();
}
} }
} }

View file

@ -286,19 +286,19 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (dialogContext) => AlertDialog(
title: Text(title, style: const TextStyle(color: AppTheme.errorColor)), title: Text(title, style: const TextStyle(color: AppTheme.errorColor)),
content: Text(content), content: Text(content),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(dialogContext),
child: Text(l10n.commonCancel), child: Text(l10n.commonCancel),
), ),
ElevatedButton( ElevatedButton(
style: style:
ElevatedButton.styleFrom(backgroundColor: AppTheme.errorColor), ElevatedButton.styleFrom(backgroundColor: AppTheme.errorColor),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(dialogContext);
onConfirm(); onConfirm();
}, },
child: Text(l10n.commonConfirm), child: Text(l10n.commonConfirm),

View file

@ -253,13 +253,13 @@ class _HubScreenState extends ConsumerState<HubScreen> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (dialogContext) => AlertDialog(
title: Text(l10n.multiplayerTitle), title: Text(l10n.multiplayerTitle),
content: Text(l10n.multiplayerDescription), content: Text(l10n.multiplayerDescription),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(dialogContext);
_showJoinCodeDialog(); _showJoinCodeDialog();
}, },
child: Text(l10n.multiplayerJoinButton), child: Text(l10n.multiplayerJoinButton),
@ -268,14 +268,14 @@ class _HubScreenState extends ConsumerState<HubScreen> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor), backgroundColor: AppTheme.primaryColor),
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(dialogContext);
try { try {
final party = final party =
await ref.read(partyRepositoryProvider).createParty(); await ref.read(partyRepositoryProvider).createParty();
if (mounted) context.go('/lobby/${party.id}'); if (mounted) context.go('/lobby/${party.id}');
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
if (mounted) ErrorHandler.showErrorSnackBar(context, e); ErrorHandler.showErrorSnackBar(context, e);
} }
} }
}, },
@ -293,7 +293,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
final controller = TextEditingController(); final controller = TextEditingController();
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (dialogContext) => AlertDialog(
title: Text(l10n.multiplayerEnterCodeTitle), title: Text(l10n.multiplayerEnterCodeTitle),
content: TextField( content: TextField(
controller: controller, controller: controller,
@ -305,21 +305,21 @@ class _HubScreenState extends ConsumerState<HubScreen> {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(dialogContext),
child: Text(l10n.multiplayerCancelAction), child: Text(l10n.multiplayerCancelAction),
), ),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
final code = controller.text.trim().toUpperCase(); final code = controller.text.trim().toUpperCase();
if (code.isNotEmpty) { if (code.isNotEmpty) {
Navigator.pop(context); Navigator.pop(dialogContext);
try { try {
final party = final party =
await ref.read(partyRepositoryProvider).joinParty(code); await ref.read(partyRepositoryProvider).joinParty(code);
if (mounted) context.go('/lobby/${party.id}'); if (mounted) context.go('/lobby/${party.id}');
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
if (mounted) ErrorHandler.showErrorSnackBar(context, e); ErrorHandler.showErrorSnackBar(context, e);
} }
} }
} }

View file

@ -83,7 +83,11 @@ class PartyRepository {
}); });
}, },
onCancel: () async { 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'); log('🔌 Unsubscribed from party $partyId');
}, },
); );
@ -120,7 +124,11 @@ class PartyRepository {
}); });
}, },
onCancel: () async { 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'); log('🔌 Unsubscribed from party members $partyId');
}, },
); );

View file

@ -1,5 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart'; 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'; part 'rest_timer_service.g.dart';
@ -75,12 +77,21 @@ class RestTimer extends _$RestTimer {
); );
} }
void complete() { void complete() async {
cancel(); cancel();
state = state.copyWith( state = state.copyWith(
isActive: false, isActive: false,
remainingSeconds: 0, 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() { void cancel() {

View file

@ -254,7 +254,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (context) => AlertDialog( builder: (dialogContext) => AlertDialog(
title: Text(l10n.battleRaidComplete), title: Text(l10n.battleRaidComplete),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -267,7 +267,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'+$xpEarned XP', '+$xpEarned XP',
style: Theme.of(context).textTheme.headlineMedium?.copyWith( style: Theme.of(dialogContext).textTheme.headlineMedium?.copyWith(
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
), ),
), ),
@ -276,7 +276,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
actions: [ actions: [
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(dialogContext).pop();
context.go('/hub'); context.go('/hub');
}, },
child: Text(l10n.battleBackToHub), child: Text(l10n.battleBackToHub),
@ -1079,7 +1079,8 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
children: [ children: [
_InfoBox( _InfoBox(
label: l10n.battleWeight, label: l10n.battleWeight,
value: '${currentSet.targetWeightTotal} kg', value:
'${currentSet.targetWeightTotal} ${l10n.unitKg}',
), ),
_InfoBox( _InfoBox(
label: l10n.battleReps, label: l10n.battleReps,
@ -1193,21 +1194,20 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
void _showAbandonDialog(AppLocalizations l10n) { void _showAbandonDialog(AppLocalizations l10n) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (dialogContext) => AlertDialog(
title: Text(l10n.battleAbandonTitle), title: Text(l10n.battleAbandonTitle),
content: Text(l10n.battleAbandonBody), content: Text(l10n.battleAbandonBody),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(l10n.cancelButton), child: Text(l10n.cancelButton),
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
Navigator.of(context).pop(); Navigator.of(dialogContext).pop();
if (widget.partyId != null) { if (widget.partyId != null) {
final userId = final user = await ref.read(userRepositoryProvider).getLocalUser();
(await ref.read(userRepositoryProvider).getLocalUser()) final userId = user?.serverId;
?.serverId;
if (userId != null) { if (userId != null) {
await ref await ref
.read(partyRepositoryProvider) .read(partyRepositoryProvider)

View file

@ -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<void> save(String newToken, dynamic newRecord) async {
// super.save(newToken, newRecord);
// final encoded = jsonEncode(<String, dynamic>{
// 'token': newToken,
// 'model': newRecord,
// });
// await _storage.write(key: _storageKey, value: encoded);
// }
// @override
// void clear() {
// super.clear();
// _storage.delete(key: _storageKey);
// }
// Future<void> loadFromStorage() async {
// final raw = await _storage.read(key: _storageKey);
// if (raw != null && raw.isNotEmpty) {
// try {
// final decoded = jsonDecode(raw) as Map<String, dynamic>;
// 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 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:pocketbase/pocketbase.dart'; import 'package:pocketbase/pocketbase.dart';
@ -87,7 +30,6 @@ class PbAuthStore extends AuthStore {
await _storage.delete(key: _saveKey); await _storage.delete(key: _saveKey);
} }
// Diese Methode rufen wir VOR App-Start auf!
Future<void> loadFromStorage() async { Future<void> loadFromStorage() async {
final raw = await _storage.read(key: _saveKey); final raw = await _storage.read(key: _saveKey);
if (raw != null && raw.isNotEmpty) { 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); super.save(token, model);
} catch (e) { } catch (e) {
// Daten korrupt? Löschen.
await clear(); await clear();
} }
} }