From 06b7c18158c60be6e848195b53d53e162d796bf2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Elias=20M=C3=BCller?= <elias@elias-mueller.com>
Date: Tue, 23 May 2023 21:12:56 +0200
Subject: [PATCH] Crossed out cancelled hours, implemented file uploading

---
 lib/screen/pages/files/fileUpload.dart        |  72 ++++++++
 lib/screen/pages/files/files.dart             |  47 +++--
 lib/screen/pages/timetable/CrossPainter.dart  |  17 ++
 .../timetable/appointmenetComponent.dart      | 170 ++++++++++--------
 lib/screen/pages/timetable/timetable.dart     |   5 +-
 pubspec.yaml                                  |   1 +
 6 files changed, 208 insertions(+), 104 deletions(-)
 create mode 100644 lib/screen/pages/files/fileUpload.dart
 create mode 100644 lib/screen/pages/timetable/CrossPainter.dart

diff --git a/lib/screen/pages/files/fileUpload.dart b/lib/screen/pages/files/fileUpload.dart
new file mode 100644
index 0000000..84847ec
--- /dev/null
+++ b/lib/screen/pages/files/fileUpload.dart
@@ -0,0 +1,72 @@
+import 'dart:developer';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:flutter/material.dart';
+import 'package:marianum_mobile/api/marianumcloud/webdav/webdavApi.dart';
+
+class FileUpload extends StatefulWidget {
+  final String localPath;
+  final String remotePath;
+  const FileUpload({Key? key, required this.localPath, required this.remotePath}) : super(key: key);
+
+  @override
+  State<FileUpload> createState() => _FileUploadState();
+}
+
+class _FileUploadState extends State<FileUpload> {
+  CancelableOperation? cancelableOperation;
+  late File localFile;
+
+  @override
+  void initState() {
+    super.initState();
+    localFile = File(widget.localPath);
+  }
+
+  bool isRunning() {
+    return cancelableOperation != null && !(cancelableOperation!.isCompleted);
+  }
+
+  bool isComplete() {
+    return cancelableOperation?.isCompleted ?? false;
+  }
+
+  void start() async {
+    cancelableOperation = CancelableOperation.fromFuture(
+      (await WebdavApi.webdav).upload(localFile.readAsBytesSync(), widget.remotePath).then((value) {
+        log("Upload done!");
+      }),
+      onCancel: () => log("Upload cancelled"),
+    );
+
+    cancelableOperation!.then((e) {
+      setState(() {});
+    });
+    setState(() {});
+  }
+
+  void stop() {
+    cancelableOperation?.cancel();
+    setState(() {});
+    Navigator.of(context).pop();
+  }
+
+
+  @override
+  Widget build(BuildContext context) {
+    List<Widget> actions = List<Widget>.empty(growable: true);
+    if(!isRunning() && !isComplete()) actions.add(TextButton(onPressed: start, child: const Text("Upload starten")));
+    if(isRunning()) actions.add(TextButton(onPressed: stop, child: const Text("Upload Abbrechen")));
+    if(isComplete()) actions.add(TextButton(onPressed: Navigator.of(context).pop, child: const Text("Fertig")));
+
+    return AlertDialog(
+      title: const Text("Hochladen"),
+      content: Center(
+        child: isRunning() ? const CircularProgressIndicator() : const Text("Datei hochladen"),
+      ),
+      actions: actions,
+      icon: const Icon(Icons.upload),
+    );
+  }
+}
diff --git a/lib/screen/pages/files/files.dart b/lib/screen/pages/files/files.dart
index fd5c2b6..b0e2a51 100644
--- a/lib/screen/pages/files/files.dart
+++ b/lib/screen/pages/files/files.dart
@@ -5,9 +5,9 @@ import 'dart:io';
 import 'package:flutter/material.dart';
 import 'package:loader_overlay/loader_overlay.dart';
 import 'package:marianum_mobile/api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
+import 'package:marianum_mobile/screen/pages/files/fileUpload.dart';
 import 'package:marianum_mobile/widget/errorView.dart';
-import 'package:nextcloud/nextcloud.dart';
-import 'package:shared_preferences/shared_preferences.dart';
+import 'package:persistent_bottom_nav_bar/persistent_tab_view.dart';
 
 import '../../../api/marianumcloud/webdav/queries/listFiles/listFilesCache.dart';
 import '../../../api/marianumcloud/webdav/queries/listFiles/listFilesResponse.dart';
@@ -77,7 +77,10 @@ class _FilesState extends State<Files> {
   @override
   void initState() {
     super.initState();
+    _query();
+  }
 
+  void _query() {
     ListFilesCache(
         path: widget.path.isEmpty ? "/" : widget.path.join("/"),
         onUpdate: (ListFilesResponse d) => {
@@ -192,13 +195,19 @@ class _FilesState extends State<Files> {
         child: const Icon(Icons.upload),
       ),
       body: data == null ? const Center(child: CircularProgressIndicator()) : data!.files.isEmpty ? const ErrorView(icon: Icons.folder_off_rounded, text: "Der Ordner ist leer") : LoaderOverlay(
-        child: ListView.builder(
-          itemCount: files.length,
-          itemBuilder: (context, index) {
-            CacheableFile file = files.toList().skip(index).first;
-            return FileElement(file, widget.path);
+        child: RefreshIndicator(
+          onRefresh: () {
+            _query();
+            return Future.delayed(const Duration(seconds: 3));
           },
-        ),
+          child: ListView.builder(
+            itemCount: files.length,
+            itemBuilder: (context, index) {
+              CacheableFile file = files.toList().skip(index).first;
+              return FileElement(file, widget.path);
+            },
+          ),
+        )
       )
     );
   }
@@ -209,26 +218,10 @@ class _FilesState extends State<Files> {
       return;
     }
 
-    SharedPreferences preferences = await SharedPreferences.getInstance();
-    WebDavClient client = NextcloudClient("https://cloud.marianum-fulda.de/", username: preferences.getString("username"), password: preferences.getString("password"), loginName: preferences.getString("username")).webdav;
-
-    log("UPLOAD STARTING: $path");
+    context.loaderOverlay.show();
     File file = File(path);
+    var remotePath = "${widget.path.join("/")}/${file.path.split(Platform.pathSeparator).last}";
+    PersistentNavBarNavigator.pushNewScreen(context, screen: FileUpload(localPath: path, remotePath: remotePath), withNavBar: false);
 
-    showDialog(
-      context: context,
-      builder: (context) {
-        return SimpleDialog(
-          children: [
-            Image.memory(file.readAsBytesSync()),
-          ],
-        );
-      },
-    );
-
-    client.upload(file.readAsBytesSync(), "/${file.path.split(Platform.pathSeparator).last}").then((value) {
-      log("UPLOADED ${value.statusCode}");
-      context.loaderOverlay.hide();
-    });
   }
 }
diff --git a/lib/screen/pages/timetable/CrossPainter.dart b/lib/screen/pages/timetable/CrossPainter.dart
new file mode 100644
index 0000000..c4bb084
--- /dev/null
+++ b/lib/screen/pages/timetable/CrossPainter.dart
@@ -0,0 +1,17 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class CrossPainter extends CustomPainter {
+  @override
+  void paint(Canvas canvas, Size size) {
+    final paint = Paint()
+      ..color = Colors.red.withAlpha(200)
+      ..strokeWidth = 2.0;
+
+    canvas.drawLine(Offset(0, 0), Offset(size.width, size.height), paint);
+    canvas.drawLine(Offset(size.width, 0), Offset(0, size.height), paint);
+  }
+
+  @override
+  bool shouldRepaint(CrossPainter oldDelegate) => false;
+}
\ No newline at end of file
diff --git a/lib/screen/pages/timetable/appointmenetComponent.dart b/lib/screen/pages/timetable/appointmenetComponent.dart
index d553934..be22f9f 100644
--- a/lib/screen/pages/timetable/appointmenetComponent.dart
+++ b/lib/screen/pages/timetable/appointmenetComponent.dart
@@ -1,18 +1,20 @@
 
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
+import 'package:marianum_mobile/api/webuntis/queries/getTimetable/getTimetableResponse.dart';
+import 'package:marianum_mobile/screen/pages/timetable/CrossPainter.dart';
 import 'package:syncfusion_flutter_calendar/calendar.dart';
 
 class AppointmentComponent extends StatefulWidget {
   final CalendarAppointmentDetails details;
-  const AppointmentComponent({Key? key, required this.details}) : super(key: key);
+  final bool crossedOut;
+  const AppointmentComponent({Key? key, required this.details, this.crossedOut = false}) : super(key: key);
 
   @override
   State<AppointmentComponent> createState() => _AppointmentComponentState();
 }
 
 class _AppointmentComponentState extends State<AppointmentComponent> {
-
   @override
   Widget build(BuildContext context) {
     final Appointment meeting = widget.details.appointments.first;
@@ -20,90 +22,106 @@ class _AppointmentComponentState extends State<AppointmentComponent> {
     double headerHeight = 50;
     const double footerHeight = 5;
     final double infoHeight = appointmentHeight - (headerHeight + footerHeight);
-    if(infoHeight < 0) headerHeight += infoHeight;
+    if (infoHeight < 0) headerHeight += infoHeight;
 
-    return Column(
+    return Stack(
       children: [
-        Container(
-          padding: const EdgeInsets.all(3),
-          height: headerHeight,
-          alignment: Alignment.topLeft,
-          decoration: BoxDecoration(
-            shape: BoxShape.rectangle,
-            borderRadius: const BorderRadius.only(
-                topLeft: Radius.circular(5),
-                topRight: Radius.circular(5)),
-            color: meeting.color,
-          ),
-          child: SingleChildScrollView(
-              child: Column(
-                mainAxisAlignment: MainAxisAlignment.start,
-                crossAxisAlignment: CrossAxisAlignment.start,
-                children: [
-                  FittedBox(
-                    fit: BoxFit.fitWidth,
-                    child: Text(
-                      meeting.subject,
-                      style: const TextStyle(
-                        color: Colors.white,
-                        fontSize: 15,
-                        fontWeight: FontWeight.w500,
-                      ),
-                      maxLines: 1,
-                      softWrap: false,
-                    ),
-                  ),
-                  FittedBox(
-                    fit: BoxFit.fitWidth,
-                    child: Text(
-                      meeting.location ?? "?",
-                      maxLines: 3,
-                      overflow: TextOverflow.ellipsis,
-                      softWrap: true,
-                      style: const TextStyle(
-                        color: Colors.white,
-                        fontSize: 10,
-                      ),
-                    ),
-                  )
-                ],
-              )),
-        ),
-        Visibility(
-          visible: meeting.notes != null && infoHeight > 10,
-          replacement: Container(
-            color: meeting.color,
-            height: infoHeight,
-          ),
-          child: Container(
-            height: infoHeight,
-            padding: const EdgeInsets.fromLTRB(3, 5, 3, 2),
-            color: meeting.color.withOpacity(0.8),
-            alignment: Alignment.topLeft,
-            child: SingleChildScrollView(
+        Column(
+          children: [
+            Container(
+              padding: const EdgeInsets.all(3),
+              height: headerHeight,
+              alignment: Alignment.topLeft,
+              decoration: BoxDecoration(
+                shape: BoxShape.rectangle,
+                borderRadius: const BorderRadius.only(
+                  topLeft: Radius.circular(5),
+                  topRight: Radius.circular(5),
+                ),
+                color: meeting.color,
+              ),
+              child: SingleChildScrollView(
                 child: Column(
                   mainAxisAlignment: MainAxisAlignment.start,
                   crossAxisAlignment: CrossAxisAlignment.start,
                   children: [
-                    Text(
-                      meeting.notes ?? "",
-                      style: const TextStyle(
-                        color: Colors.white,
-                        fontSize: 10,
+                    FittedBox(
+                      fit: BoxFit.fitWidth,
+                      child: Text(
+                        meeting.subject,
+                        style: const TextStyle(
+                          color: Colors.white,
+                          fontSize: 15,
+                          fontWeight: FontWeight.w500,
+                        ),
+                        maxLines: 1,
+                        softWrap: false,
+                      ),
+                    ),
+                    FittedBox(
+                      fit: BoxFit.fitWidth,
+                      child: Text(
+                        meeting.location ?? "?",
+                        maxLines: 3,
+                        overflow: TextOverflow.ellipsis,
+                        softWrap: true,
+                        style: const TextStyle(
+                          color: Colors.white,
+                          fontSize: 10,
+                        ),
                       ),
                     )
                   ],
-                )),
-          ),
+                ),
+              ),
+            ),
+            Visibility(
+              visible: meeting.notes != null && infoHeight > 10,
+              replacement: Container(
+                color: meeting.color,
+                height: infoHeight,
+              ),
+              child: Container(
+                height: infoHeight,
+                padding: const EdgeInsets.fromLTRB(3, 5, 3, 2),
+                color: meeting.color.withOpacity(0.8),
+                alignment: Alignment.topLeft,
+                child: SingleChildScrollView(
+                  child: Column(
+                    mainAxisAlignment: MainAxisAlignment.start,
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      Text(
+                        meeting.notes ?? "",
+                        style: const TextStyle(
+                          color: Colors.white,
+                          fontSize: 10,
+                        ),
+                      )
+                    ],
+                  ),
+                ),
+              ),
+            ),
+            Container(
+              height: footerHeight,
+              decoration: BoxDecoration(
+                shape: BoxShape.rectangle,
+                borderRadius: const BorderRadius.only(
+                  bottomLeft: Radius.circular(5),
+                  bottomRight: Radius.circular(5),
+                ),
+                color: meeting.color,
+              ),
+            ),
+          ],
         ),
-        Container(
-          height: footerHeight,
-          decoration: BoxDecoration(
-            shape: BoxShape.rectangle,
-            borderRadius: const BorderRadius.only(
-                bottomLeft: Radius.circular(5),
-                bottomRight: Radius.circular(5)),
-            color: meeting.color,
+        Visibility(
+          visible: (meeting.id as GetTimetableResponseObject).code == "cancelled",
+          child: Positioned.fill(
+            child: CustomPaint(
+              painter: CrossPainter(),
+            ),
           ),
         ),
       ],
diff --git a/lib/screen/pages/timetable/timetable.dart b/lib/screen/pages/timetable/timetable.dart
index ba7faf9..1dcdc5e 100644
--- a/lib/screen/pages/timetable/timetable.dart
+++ b/lib/screen/pages/timetable/timetable.dart
@@ -115,7 +115,10 @@ class _TimetableState extends State<Timetable> {
               ),
 
               timeRegionBuilder: (BuildContext context, TimeRegionDetails timeRegionDetails) => TimeRegionComponent(details: timeRegionDetails),
-              appointmentBuilder: (BuildContext context, CalendarAppointmentDetails details) => AppointmentComponent(details: details),
+              appointmentBuilder: (BuildContext context, CalendarAppointmentDetails details) => AppointmentComponent(
+                  details: details,
+                  crossedOut: value.getTimetableResponse.result.where((element) => element.id == details.appointments.first.id).firstOrNull?.code == "cancelled"
+              ),
 
               headerHeight: 0,
               selectionDecoration: const BoxDecoration(),
diff --git a/pubspec.yaml b/pubspec.yaml
index f764e7c..1c420b2 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -77,6 +77,7 @@ dependencies:
   crypto: ^3.0.3
   package_info: ^2.0.2
   syncfusion_flutter_calendar: ^21.2.4
+  async: ^2.11.0
 
 dev_dependencies:
   flutter_test: