487 lines
15 KiB
Dart
487 lines
15 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:graphql_flutter/graphql_flutter.dart';
|
|
import 'dart:convert';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'graphql_config.dart';
|
|
|
|
/// Die Hauptfunktion der App, die den Startpunkt der Anwendung darstellt.
|
|
/// Initialisiert die MaterialApp mit dem TodoListScreen als Home-Widget.
|
|
void main() async {
|
|
// Initialisiere GraphQL
|
|
await initHiveForFlutter();
|
|
runApp(const MyApp());
|
|
}
|
|
|
|
/// Das Root-Widget der Anwendung.
|
|
/// Konfiguriert das grundlegende Theme und die MaterialApp-Einstellungen.
|
|
class MyApp extends StatelessWidget {
|
|
const MyApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GraphQLProvider(
|
|
client: ValueNotifier<GraphQLClient>(graphQLClient),
|
|
child: MaterialApp(
|
|
title: 'To-Do Liste',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(
|
|
seedColor: const Color.fromARGB(255, 13, 245, 117),
|
|
),
|
|
useMaterial3: true,
|
|
),
|
|
home: const AuthWrapper(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Wrapper-Widget, das die Authentifizierung überprüft und entsprechend
|
|
/// den LoginScreen oder den TodoListScreen anzeigt.
|
|
class AuthWrapper extends StatefulWidget {
|
|
const AuthWrapper({super.key});
|
|
|
|
@override
|
|
State<AuthWrapper> createState() => _AuthWrapperState();
|
|
}
|
|
|
|
class _AuthWrapperState extends State<AuthWrapper> {
|
|
final _storage = const FlutterSecureStorage();
|
|
bool _isAuthenticated = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_checkAuth();
|
|
}
|
|
|
|
Future<void> _checkAuth() async {
|
|
final token = await _storage.read(key: 'auth_token');
|
|
setState(() {
|
|
_isAuthenticated = token != null;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return _isAuthenticated
|
|
? const TodoListScreen()
|
|
: LoginScreen(
|
|
onLogin: () async {
|
|
await _checkAuth();
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Login-Screen für die Authentifizierung
|
|
class LoginScreen extends StatefulWidget {
|
|
final VoidCallback onLogin;
|
|
|
|
const LoginScreen({super.key, required this.onLogin});
|
|
|
|
@override
|
|
State<LoginScreen> createState() => _LoginScreenState();
|
|
}
|
|
|
|
class _LoginScreenState extends State<LoginScreen> {
|
|
final _storage = const FlutterSecureStorage();
|
|
final _usernameController = TextEditingController();
|
|
final _passwordController = TextEditingController();
|
|
bool _isLoading = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_usernameController.dispose();
|
|
_passwordController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
//if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) {
|
|
//ScaffoldMessenger.of(context).showSnackBar(
|
|
//const SnackBar(content: Text('Bitte füllen Sie alle Felder aus')),
|
|
//);
|
|
|
|
Future<void> _login() async {
|
|
// Feste User anmeldedaten:
|
|
const validUsername = "user";
|
|
const validPassword = "password";
|
|
|
|
if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Bitte füllen Sie alle Felder aus')),
|
|
);
|
|
return;
|
|
} else if (_usernameController.text != validUsername ||
|
|
_passwordController.text != validPassword) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('Falsche Anmeldedaten')));
|
|
return;
|
|
}
|
|
|
|
await _storage.write(
|
|
key: 'auth_token',
|
|
value: 'dummy_token_${_usernameController.text}',
|
|
);
|
|
|
|
widget.onLogin();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Anmeldung'),
|
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
TextField(
|
|
controller: _usernameController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Benutzername',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: _passwordController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Passwort',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
obscureText: true,
|
|
),
|
|
const SizedBox(height: 24),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: _isLoading ? null : _login,
|
|
child: _isLoading
|
|
? const CircularProgressIndicator()
|
|
: const Text('Anmelden'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Das Haupt-Widget der To-Do Liste.
|
|
/// Implementiert als StatefulWidget, um den Zustand der Aufgabenliste zu verwalten.
|
|
class TodoListScreen extends StatefulWidget {
|
|
const TodoListScreen({super.key});
|
|
|
|
@override
|
|
State<TodoListScreen> createState() => _TodoListScreenState();
|
|
}
|
|
|
|
/// Der State des TodoListScreen-Widgets.
|
|
/// Verwaltet die Liste der Aufgaben und die zugehörigen Funktionen.
|
|
class _TodoListScreenState extends State<TodoListScreen> {
|
|
/// Liste der Todo-Items
|
|
final List<TodoItem> _todos = [];
|
|
|
|
/// Controller für das Texteingabefeld
|
|
final TextEditingController _textController = TextEditingController();
|
|
|
|
final _storage = const FlutterSecureStorage();
|
|
DateTime _selectedDate = DateTime.now();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadTodos();
|
|
}
|
|
|
|
Future<void> _loadTodos() async {
|
|
try {
|
|
final String jsonString = await rootBundle.loadString(
|
|
'assets/todos.json',
|
|
);
|
|
final Map<String, dynamic> jsonData = json.decode(jsonString);
|
|
final List<dynamic> todosJson = jsonData['todos'] as List<dynamic>;
|
|
|
|
setState(() {
|
|
_todos.clear();
|
|
_todos.addAll(
|
|
todosJson.map((json) => TodoItem.fromJson(json)).toList(),
|
|
);
|
|
});
|
|
} catch (e) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Fehler beim Laden der Todos: $e')),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _selectDate(BuildContext context) async {
|
|
final DateTime? picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: _selectedDate,
|
|
firstDate: DateTime.now(),
|
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
|
);
|
|
if (picked != null && picked != _selectedDate) {
|
|
setState(() {
|
|
_selectedDate = picked;
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Fügt eine neue Aufgabe zur Liste hinzu.
|
|
/// [title] Der Titel der neuen Aufgabe
|
|
void _addTodo(String title) {
|
|
if (title.isEmpty) return;
|
|
setState(() {
|
|
final newId = _todos.isEmpty
|
|
? 1
|
|
: _todos.map((e) => e.id).reduce((a, b) => a > b ? a : b) + 1;
|
|
_todos.add(TodoItem(id: newId, title: title, deadline: _selectedDate));
|
|
_textController.clear();
|
|
});
|
|
}
|
|
|
|
/// Entfernt eine Aufgabe aus der Liste.
|
|
/// [index] Der Index der zu entfernenden Aufgabe
|
|
void _removeTodo(int index) {
|
|
setState(() {
|
|
_todos.removeAt(index);
|
|
});
|
|
}
|
|
|
|
/// Wechselt den Status einer Aufgabe zwischen erledigt und nicht erledigt.
|
|
/// [index] Der Index der zu ändernden Aufgabe
|
|
void _toggleTodo(int index) {
|
|
setState(() {
|
|
_todos[index].isCompleted = !_todos[index].isCompleted;
|
|
});
|
|
}
|
|
|
|
Future<void> _logout() async {
|
|
await _storage.delete(key: 'auth_token');
|
|
if (mounted) {
|
|
Navigator.of(context).pushReplacement(
|
|
MaterialPageRoute(builder: (context) => const AuthWrapper()),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('To-Do Liste'),
|
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
foregroundColor: Colors.white,
|
|
actions: [
|
|
IconButton(icon: const Icon(Icons.logout), onPressed: _logout),
|
|
],
|
|
),
|
|
body: Query(
|
|
options: QueryOptions(
|
|
document: gql(getTodosQuery),
|
|
pollInterval:
|
|
const Duration(seconds: 5), // Aktualisiere alle 5 Sekunden
|
|
),
|
|
builder: (QueryResult result,
|
|
{VoidCallback? refetch, FetchMore? fetchMore}) {
|
|
if (result.hasException) {
|
|
return Center(
|
|
child: Text('Fehler: ${result.exception.toString()}'),
|
|
);
|
|
}
|
|
|
|
if (result.isLoading) {
|
|
return const Center(
|
|
child: CircularProgressIndicator(),
|
|
);
|
|
}
|
|
|
|
// Aktualisiere die Todos aus dem GraphQL-Ergebnis
|
|
final todos = (result.data?['todos'] as List<dynamic>?)?.map((todo) {
|
|
return TodoItem.fromJson(todo);
|
|
}).toList() ??
|
|
[];
|
|
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _textController,
|
|
decoration: const InputDecoration(
|
|
hintText: 'Neue Aufgabe eingeben',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Mutation(
|
|
options: MutationOptions(
|
|
document: gql(addTodoMutation),
|
|
update: (cache, result) {
|
|
if (result?.data != null) {
|
|
refetch?.call();
|
|
}
|
|
},
|
|
),
|
|
builder:
|
|
(RunMutation runMutation, QueryResult? result) {
|
|
return ElevatedButton(
|
|
onPressed: () {
|
|
if (_textController.text.isEmpty) return;
|
|
runMutation({
|
|
'name': _textController.text,
|
|
'deadline': DateFormat('yyyy-MM-dd')
|
|
.format(_selectedDate),
|
|
});
|
|
_textController.clear();
|
|
},
|
|
child: const Text('Hinzufügen'),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Text(
|
|
'Deadline: ${DateFormat('dd.MM.yyyy').format(_selectedDate)}'),
|
|
IconButton(
|
|
icon: const Icon(Icons.calendar_today),
|
|
onPressed: () => _selectDate(context),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
itemCount: todos.length,
|
|
itemBuilder: (context, index) {
|
|
final todo = todos[index];
|
|
return Mutation(
|
|
options: MutationOptions(
|
|
document: gql(updateTodoStatusMutation),
|
|
update: (cache, result) {
|
|
if (result?.data != null) {
|
|
refetch?.call();
|
|
}
|
|
},
|
|
),
|
|
builder: (RunMutation runMutation, QueryResult? result) {
|
|
return ListTile(
|
|
leading: Checkbox(
|
|
value: todo.isCompleted,
|
|
onChanged: (bool? value) {
|
|
if (value != null) {
|
|
runMutation({
|
|
'id': todo.id,
|
|
'status': value,
|
|
});
|
|
}
|
|
},
|
|
),
|
|
title: Text(
|
|
todo.title,
|
|
style: TextStyle(
|
|
decoration: todo.isCompleted
|
|
? TextDecoration.lineThrough
|
|
: null,
|
|
),
|
|
),
|
|
subtitle: Text(
|
|
'Deadline: ${DateFormat('dd.MM.yyyy').format(todo.deadline)}',
|
|
style: TextStyle(
|
|
color: todo.deadline.isBefore(DateTime.now()) &&
|
|
!todo.isCompleted
|
|
? Colors.red
|
|
: null,
|
|
),
|
|
),
|
|
trailing: Mutation(
|
|
options: MutationOptions(
|
|
document: gql(deleteTodoMutation),
|
|
update: (cache, result) {
|
|
if (result?.data != null) {
|
|
refetch?.call();
|
|
}
|
|
},
|
|
),
|
|
builder:
|
|
(RunMutation runMutation, QueryResult? result) {
|
|
return IconButton(
|
|
icon: const Icon(Icons.delete),
|
|
onPressed: () {
|
|
runMutation({
|
|
'id': todo.id,
|
|
});
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Repräsentiert eine einzelne Aufgabe in der To-Do Liste.
|
|
/// Enthält den Titel der Aufgabe und ihren Erledigungsstatus.
|
|
class TodoItem {
|
|
final int id;
|
|
String title;
|
|
bool isCompleted;
|
|
DateTime deadline;
|
|
|
|
TodoItem({
|
|
required this.id,
|
|
required this.title,
|
|
this.isCompleted = false,
|
|
required this.deadline,
|
|
});
|
|
|
|
factory TodoItem.fromJson(Map<String, dynamic> json) {
|
|
return TodoItem(
|
|
id: json['id'] as int,
|
|
title: json['name'] as String,
|
|
isCompleted: json['status'] as bool,
|
|
deadline: DateTime.parse(json['deadline'] as String),
|
|
);
|
|
}
|
|
|
|
//Map<String, dynamic> toJson() {
|
|
// return {
|
|
// 'id': id,
|
|
// 'name': title,
|
|
// 'status': isCompleted,
|
|
// 'deadline': DateFormat('yyyy-MM-dd').format(deadline),
|
|
// };
|
|
//}
|
|
}
|