This commit is contained in:
2025-10-26 17:53:53 +01:00
parent 348746c0d4
commit 8c1de0f883
8 changed files with 922 additions and 843 deletions

View File

@@ -0,0 +1,43 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shoppinglist/main.dart';
import 'package:shoppinglist/pages/lists.dart';
import 'package:shoppinglist/pages/login.dart';
class AuthGate extends StatefulWidget {
const AuthGate({super.key});
@override
State<AuthGate> createState() => _AuthGateState();
}
class _AuthGateState extends State<AuthGate> {
StreamSubscription? _authSub;
bool _loggedIn = false;
@override
void initState() {
super.initState();
_loggedIn = pb.authStore.isValid;
_authSub = pb.authStore.onChange.listen((_) {
setState(() => _loggedIn = pb.authStore.isValid);
});
}
@override
void dispose() {
_authSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_loggedIn) {
return const LoginPage();
} else {
return const ListsPage();
}
}
}

View File

@@ -0,0 +1,264 @@
import 'package:flutter/material.dart';
import 'package:pocketbase/pocketbase.dart';
import 'package:shoppinglist/main.dart';
import 'package:shoppinglist/models/models.dart';
class MembersDialog extends StatefulWidget {
final Liste list;
const MembersDialog({super.key, required this.list});
@override
State<MembersDialog> createState() => _MembersDialogState();
}
class _MembersDialogState extends State<MembersDialog> {
final TextEditingController _emailCtl = TextEditingController();
bool _isLoading = false;
String? _error;
bool _isUsersLoading = true;
final Map<String, String> _userEmails = {};
bool _showAddMemberField = false;
bool _hasChanges = false;
@override
void initState() {
super.initState();
_loadUsers();
}
Future<void> _loadUsers() async {
setState(() => _isUsersLoading = true);
try {
final List<String> ids = <String>{
widget.list.owner,
...widget.list.members,
}.where((id) => id.isNotEmpty).toList();
if (ids.isEmpty) {
setState(() => _isUsersLoading = false);
return;
}
final List<RecordModel> users = await apiService.getUsersByIds(ids);
if (mounted) {
setState(() {
for (final RecordModel user in users) {
_userEmails[user.id] = user.data['email'] as String;
}
_isUsersLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_error = 'Fehler beim Laden der Benutzer.';
_isUsersLoading = false;
});
}
}
}
Future<void> _addMember() async {
final String email = _emailCtl.text.trim();
if (email.isEmpty) return;
setState(() {
_isLoading = true;
_error = null;
});
try {
final RecordModel? user = await apiService.findUserByEmail(email);
if (user == null) {
throw Exception('Benutzer nicht gefunden.');
}
if (widget.list.owner == user.id ||
widget.list.members.contains(user.id)) {
throw Exception('Benutzer ist bereits Mitglied.');
}
final List<String> newMembers = [...widget.list.members, user.id];
await apiService.updateList(widget.list.id, members: newMembers);
setState(() {
widget.list.members = newMembers;
_userEmails[user.id] = user.data['email'] as String;
_emailCtl.clear();
_hasChanges = true;
});
} catch (e) {
setState(() => _error = e.toString());
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _removeMember(String memberId) async {
final String? memberEmail = _userEmails[memberId];
final bool? ok = await showDialog<bool>(
context: context,
builder: (c) => AlertDialog(
title: const Text('Mitglied entfernen?'),
content: Text(
'Möchtest du "${memberEmail ?? memberId}" wirklich aus der Liste entfernen?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(c, false),
child: const Text('Abbrechen'),
),
TextButton(
onPressed: () => Navigator.pop(c, true),
child: const Text('Entfernen'),
),
],
),
);
if (ok != true) return;
setState(() {
_isLoading = true;
_error = null;
});
try {
final List<String> newMembers = widget.list.members
.where((id) => id != memberId)
.toList();
await apiService.updateList(widget.list.id, members: newMembers);
setState(() {
widget.list.members = newMembers;
_hasChanges = true;
});
} catch (e) {
setState(() => _error = e.toString());
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
final bool isOwner = widget.list.owner == apiService.userId;
return AlertDialog(
title: const Text('Mitglieder'),
content: SizedBox(
width: double.maxFinite,
child: _isUsersLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.list.members.isEmpty)
const Padding(
padding: EdgeInsets.only(top: 4.0),
child: Text(
'Niemand',
style: TextStyle(fontStyle: FontStyle.italic),
),
)
else
...widget.list.members.map(
(id) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"${_userEmails[id] ?? 'Unbekannt'}${_userEmails[widget.list.owner] == (_userEmails[id] ?? id) ? " (Besitzer)" : ""}",
),
),
if (isOwner)
IconButton(
icon: const Icon(Icons.remove_circle_outline),
tooltip: 'Entfernen',
onPressed:
_isLoading ||
_userEmails[widget.list.owner] ==
(_userEmails[id] ?? id)
? null
: () => _removeMember(id),
),
],
),
),
if (isOwner) ...[
const SizedBox(height: 12),
Row(
children: [
...[
_showAddMemberField
? Expanded(
child: TextField(
controller: _emailCtl,
decoration: const InputDecoration(
labelText: 'Email des Mitglieds',
isDense: true,
),
keyboardType: TextInputType.emailAddress,
onSubmitted: (_) => _addMember(),
autofocus: true,
),
)
: Expanded(child: SizedBox.shrink()),
],
IconButton(
icon: Icon(
_showAddMemberField
? Icons.close
: Icons.add_circle_outline,
),
tooltip: _showAddMemberField
? 'Schließen'
: 'Mitglied hinzufügen',
onPressed: () {
setState(() {
_showAddMemberField = !_showAddMemberField;
_error = null;
if (!_showAddMemberField) {
_emailCtl.clear();
}
});
},
),
],
),
if (_error != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
_error!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
],
],
),
),
),
actions: [
if (isOwner && _showAddMemberField)
TextButton(
onPressed: _isLoading ? null : _addMember,
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Hinzufügen'),
),
TextButton(
onPressed: () => Navigator.pop(context, _hasChanges),
child: const Text('Schließen'),
),
],
);
}
}