import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pocketbase/pocketbase.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; import 'package:shoppinglist/services/api.dart'; import 'package:shoppinglist/models/models.dart'; import 'package:shoppinglist/providers/theme.dart'; late final PocketBase pb; late final ApiService apiService; void main() async { WidgetsFlutterBinding.ensureInitialized(); if (Platform.isAndroid || Platform.isIOS) { await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); } if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { await windowManager.ensureInitialized(); const WindowOptions windowOptions = WindowOptions( size: Size(400, 720), minimumSize: Size(380, 600), center: true, title: 'Einkaufsliste', ); windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.show(); await windowManager.focus(); }); } final SharedPreferences prefs = await SharedPreferences.getInstance(); final AsyncAuthStore store = AsyncAuthStore( save: (String data) async => prefs.setString('pb_auth', data), initial: prefs.getString('pb_auth'), ); pb = PocketBase('https://pb.flumm.io', authStore: store); apiService = ApiService(pb); runApp( ChangeNotifierProvider( create: (_) => ThemeProvider(prefs), child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return Consumer( builder: (context, themeProvider, child) { return MaterialApp( title: 'Einkaufsliste', debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: Colors.deepPurple, brightness: Brightness.light, ), useMaterial3: true, ), darkTheme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: Colors.deepPurple, brightness: Brightness.dark, ), useMaterial3: true, ), themeMode: themeProvider.themeMode, home: const AuthGate(), ); }, ); } } class AuthGate extends StatefulWidget { const AuthGate({super.key}); @override State createState() => _AuthGateState(); } class _AuthGateState extends State { 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(); } } } class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State createState() => _LoginPageState(); } class _LoginPageState extends State { final TextEditingController _emailCtl = TextEditingController(); final TextEditingController _pwCtl = TextEditingController(); bool _isRegister = false; bool _loading = false; String? _error; Future _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', ), ), ], ), ), ); } } class ListsPage extends StatefulWidget { const ListsPage({super.key}); @override State createState() => _ListsPageState(); } class _ListsPageState extends State { late Future> _listsFuture; @override void initState() { super.initState(); _loadLists(); } void _loadLists() { setState(() => _listsFuture = apiService.getLists()); } Future _createList() async { final TextEditingController titleCtl = TextEditingController(); final bool? ok = await showDialog( 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( context, listen: false, ); return Scaffold( appBar: AppBar( title: const Text('Deine Listen'), actions: [ PopupMenuButton( 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>( 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)); }, ); }, ), floatingActionButton: FloatingActionButton( onPressed: _createList, child: const Icon(Icons.add), ), ); } } class ListDetailPage extends StatefulWidget { final Liste list; const ListDetailPage({super.key, required this.list}); @override State createState() => _ListDetailPageState(); } class _ListDetailPageState extends State { late Future> _itemsFuture; @override void initState() { super.initState(); _loadItems(); } void _loadItems() { setState(() => _itemsFuture = apiService.getItems(widget.list.id)); } Future _addItem() async { final TextEditingController ctl = TextEditingController(); final bool? ok = await showDialog( 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 List items = await _itemsFuture; final int maxPos = items.fold( -1, (max, item) => item.position > max ? item.position : max, ); await apiService.createItem(widget.list.id, ctl.text.trim(), maxPos + 1); _loadItems(); } catch (e) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Fehler: $e'))); } } } Future _toggleItem(Item item) async { try { setState(() => item.checked = !item.checked); 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)), body: FutureBuilder>( 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 items = snapshot.data ?? []; return ListView.builder( itemCount: items.length, itemBuilder: (c, i) { final it = items[i]; return CheckboxListTile( title: Text(it.name), value: it.checked, onChanged: (_) => _toggleItem(it), ); }, ); }, ), floatingActionButton: FloatingActionButton( onPressed: _addItem, child: const Icon(Icons.add), ), ); } }