CPD_Abgabe/lib/main.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),
// };
//}
}