blah
This commit is contained in:
231
lib/pages/list_detail.dart
Normal file
231
lib/pages/list_detail.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:shoppinglist/main.dart';
|
||||
import 'package:shoppinglist/models/models.dart';
|
||||
import 'package:shoppinglist/widgets/members_dialog.dart';
|
||||
|
||||
class ListDetailPage extends StatefulWidget {
|
||||
final Liste list;
|
||||
const ListDetailPage({super.key, required this.list});
|
||||
|
||||
@override
|
||||
State<ListDetailPage> createState() => _ListDetailPageState();
|
||||
}
|
||||
|
||||
class _ListDetailPageState extends State<ListDetailPage> {
|
||||
late Future<List<Item>> _itemsFuture;
|
||||
StreamSubscription? _itemsSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_itemsFuture = apiService.getItems(widget.list.id);
|
||||
_itemsSubscription = apiService.watchItems(widget.list.id).listen((_) {
|
||||
_loadItems();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_itemsSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _loadItems() {
|
||||
setState(() => _itemsFuture = apiService.getItems(widget.list.id));
|
||||
}
|
||||
|
||||
Future<void> _showMembersDialog() async {
|
||||
final bool? updated = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => MembersDialog(list: widget.list),
|
||||
);
|
||||
if (updated == true && mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _editItem(Item item) async {
|
||||
final TextEditingController ctl = TextEditingController(text: item.name);
|
||||
final bool? ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (c) => AlertDialog(
|
||||
title: const Text('Eintrag bearbeiten'),
|
||||
content: TextField(
|
||||
controller: ctl,
|
||||
decoration: const InputDecoration(labelText: 'Name'),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c, false),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c, true),
|
||||
child: const Text('Speichern'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true || ctl.text.trim().isEmpty) return;
|
||||
|
||||
try {
|
||||
final String newName = ctl.text.trim();
|
||||
await apiService.updateItem(item.id, name: newName);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Fehler: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addItem(List<Item> currentItems) async {
|
||||
final TextEditingController ctl = TextEditingController();
|
||||
final bool? ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (c) => AlertDialog(
|
||||
title: const Text('Eintrag hinzufügen'),
|
||||
content: TextField(
|
||||
controller: ctl,
|
||||
decoration: const InputDecoration(labelText: 'Name'),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c, false),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c, true),
|
||||
child: const Text('Hinzufügen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true || ctl.text.trim().isEmpty) return;
|
||||
|
||||
try {
|
||||
final int maxPos = currentItems.fold<int>(
|
||||
-1,
|
||||
(max, item) => item.position > max ? item.position : max,
|
||||
);
|
||||
await apiService.createItem(widget.list.id, ctl.text.trim(), maxPos + 1);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Fehler: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteItem(Item item) async {
|
||||
try {
|
||||
await apiService.deleteItem(item.id);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('"${item.name}" gelöscht.')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Fehler beim Löschen: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleItem(Item item) async {
|
||||
setState(() => item.checked = !item.checked);
|
||||
try {
|
||||
await apiService.updateItem(item.id, checked: item.checked);
|
||||
} catch (e) {
|
||||
setState(() => item.checked = !item.checked);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Fehler beim Aktualisieren: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.list.title),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.people_alt_outlined),
|
||||
tooltip: 'Mitglieder verwalten',
|
||||
onPressed: _showMembersDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder<List<Item>>(
|
||||
future: _itemsFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('Fehler: ${snapshot.error}'));
|
||||
}
|
||||
|
||||
final List<Item> items = snapshot.data ?? [];
|
||||
|
||||
if (items.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('Füge deinen ersten Eintrag hinzu!'),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final Item item = items[index];
|
||||
return ListTile(
|
||||
leading: Checkbox(
|
||||
value: item.checked,
|
||||
onChanged: (_) => _toggleItem(item),
|
||||
),
|
||||
title: Text(
|
||||
item.name,
|
||||
style: TextStyle(
|
||||
decoration:
|
||||
item.checked ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () => _editItem(item),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => _deleteItem(item),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
final currentItems = await _itemsFuture;
|
||||
_addItem(currentItems);
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
250
lib/pages/lists.dart
Normal file
250
lib/pages/lists.dart
Normal file
@@ -0,0 +1,250 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:shoppinglist/main.dart';
|
||||
import 'package:shoppinglist/models/models.dart';
|
||||
import 'package:shoppinglist/pages/list_detail.dart';
|
||||
import 'package:shoppinglist/providers/theme.dart';
|
||||
|
||||
class ListsPage extends StatefulWidget {
|
||||
const ListsPage({super.key});
|
||||
|
||||
@override
|
||||
State<ListsPage> createState() => _ListsPageState();
|
||||
}
|
||||
|
||||
class _ListsPageState extends State<ListsPage> {
|
||||
late Future<List<Liste>> _listsFuture;
|
||||
StreamSubscription? _listsSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadLists();
|
||||
_listsSubscription = apiService.watchLists().listen((_) {
|
||||
_loadLists();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_listsSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _loadLists() {
|
||||
_listsFuture = apiService.getLists();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _showListMenu(Liste list) async {
|
||||
final String? result = await showModalBottomSheet<String>(
|
||||
context: context,
|
||||
builder: (c) => Wrap(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit),
|
||||
title: const Text('Umbenennen'),
|
||||
onTap: () => Navigator.pop(c, 'edit'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: const Text('Löschen'),
|
||||
onTap: () => Navigator.pop(c, 'delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result == 'edit') {
|
||||
_editList(list);
|
||||
} else if (result == 'delete') {
|
||||
_deleteList(list);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _editList(Liste list) async {
|
||||
final TextEditingController titleCtl = TextEditingController(
|
||||
text: list.title,
|
||||
);
|
||||
final bool? ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (c) => AlertDialog(
|
||||
title: const Text('Liste umbenennen'),
|
||||
content: TextField(
|
||||
controller: titleCtl,
|
||||
decoration: const InputDecoration(labelText: 'Titel'),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c, false),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c, true),
|
||||
child: const Text('Speichern'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true || titleCtl.text.trim().isEmpty) return;
|
||||
|
||||
try {
|
||||
await apiService.updateList(list.id, title: titleCtl.text.trim());
|
||||
_loadLists();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Fehler: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteList(Liste list) async {
|
||||
final bool? ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (c) => AlertDialog(
|
||||
title: const Text('Liste löschen?'),
|
||||
content: Text(
|
||||
'Möchtest du die Liste "${list.title}" wirklich löschen?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c, false),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c, true),
|
||||
child: const Text('Löschen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true) return;
|
||||
|
||||
try {
|
||||
await apiService.deleteList(list.id);
|
||||
_loadLists();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Fehler: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createList() async {
|
||||
final TextEditingController titleCtl = TextEditingController();
|
||||
final bool? ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (c) => AlertDialog(
|
||||
title: const Text('Neue Liste'),
|
||||
content: TextField(
|
||||
controller: titleCtl,
|
||||
decoration: const InputDecoration(labelText: 'Titel'),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c, false),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c, true),
|
||||
child: const Text('Erstellen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true || titleCtl.text.trim().isEmpty) return;
|
||||
|
||||
try {
|
||||
await apiService.createList(titleCtl.text.trim());
|
||||
_loadLists();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Fehler: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _openList(Liste list) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => ListDetailPage(list: list)),
|
||||
).then((_) => _loadLists());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeProvider themeProvider = Provider.of<ThemeProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Deine Listen'),
|
||||
actions: [
|
||||
PopupMenuButton<ThemeMode>(
|
||||
onSelected: (mode) => themeProvider.setThemeMode(mode),
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(value: ThemeMode.light, child: Text('Hell')),
|
||||
const PopupMenuItem(value: ThemeMode.dark, child: Text('Dunkel')),
|
||||
const PopupMenuItem(
|
||||
value: ThemeMode.system,
|
||||
child: Text('System'),
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.brightness_6_outlined),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Ausloggen',
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () {
|
||||
apiService.logout();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder<List<Liste>>(
|
||||
future: _listsFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('Fehler: ${snapshot.error}'));
|
||||
}
|
||||
final lists = snapshot.data ?? [];
|
||||
if (lists.isEmpty) {
|
||||
return const Center(child: Text('Keine Listen gefunden.'));
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: lists.length,
|
||||
itemBuilder: (c, i) {
|
||||
final l = lists[i];
|
||||
return ListTile(
|
||||
title: Text(l.title),
|
||||
onTap: () => _openList(l),
|
||||
onLongPress: () => _showListMenu(l),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _createList,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
88
lib/pages/login.dart
Normal file
88
lib/pages/login.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:shoppinglist/main.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final TextEditingController _emailCtl = TextEditingController();
|
||||
final TextEditingController _pwCtl = TextEditingController();
|
||||
bool _isRegister = false;
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
|
||||
Future<void> _submit() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final String email = _emailCtl.text.trim();
|
||||
final String password = _pwCtl.text.trim();
|
||||
if (_isRegister) {
|
||||
await apiService.register(email, password);
|
||||
} else {
|
||||
await apiService.login(email, password);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _error = e.toString());
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(_isRegister ? 'Registrierung' : 'Login')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _emailCtl,
|
||||
decoration: const InputDecoration(labelText: 'Email'),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _pwCtl,
|
||||
decoration: const InputDecoration(labelText: 'Passwort'),
|
||||
obscureText: true,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (_error != null) ...[
|
||||
Text(
|
||||
_error!,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
ElevatedButton(
|
||||
onPressed: _loading ? null : _submit,
|
||||
child: _loading
|
||||
? const CircularProgressIndicator()
|
||||
: Text(_isRegister ? 'Registrieren' : 'Einloggen'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _loading
|
||||
? null
|
||||
: () => setState(() => _isRegister = !_isRegister),
|
||||
child: Text(
|
||||
_isRegister ? 'Ich habe schon ein Konto' : 'Neu registrieren',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user