Merge branch 'develop' into feature-highEduGraduationCalculator
# Conflicts: # lib/view/pages/overhang.dart
This commit is contained in:
		@@ -6,7 +6,7 @@ import 'package:flowder/flowder.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:jiffy/jiffy.dart';
 | 
			
		||||
import 'package:marianum_mobile/widget/infoDialog.dart';
 | 
			
		||||
import '../../../widget/infoDialog.dart';
 | 
			
		||||
import 'package:nextcloud/nextcloud.dart';
 | 
			
		||||
import 'package:path_provider/path_provider.dart';
 | 
			
		||||
 | 
			
		||||
@@ -29,7 +29,7 @@ class FileElement extends StatefulWidget {
 | 
			
		||||
    Directory paths = await getTemporaryDirectory();
 | 
			
		||||
    
 | 
			
		||||
    var encodedPath = Uri.encodeComponent(remotePath);
 | 
			
		||||
    encodedPath = encodedPath.replaceAll("%2F", "/");
 | 
			
		||||
    encodedPath = encodedPath.replaceAll('%2F', '/');
 | 
			
		||||
 | 
			
		||||
    String local = paths.path + Platform.pathSeparator + name;
 | 
			
		||||
 | 
			
		||||
@@ -44,7 +44,7 @@ class FileElement extends StatefulWidget {
 | 
			
		||||
      onDone: () {
 | 
			
		||||
        //Future<OpenResult> result = OpenFile.open(local); // TODO legacy - refactor: remove onDone parameter
 | 
			
		||||
        Navigator.of(context).push(MaterialPageRoute(builder: (context) => FileViewer(path: local)));
 | 
			
		||||
        onDone(OpenResult(message: "File viewer opened", type: ResultType.done));
 | 
			
		||||
        onDone(OpenResult(message: 'File viewer opened', type: ResultType.done));
 | 
			
		||||
        // result.then((value) => {
 | 
			
		||||
        //   onDone(value)
 | 
			
		||||
        // });
 | 
			
		||||
@@ -71,21 +71,21 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
        children: [
 | 
			
		||||
          Container(
 | 
			
		||||
            margin: const EdgeInsets.only(right: 10),
 | 
			
		||||
            child: const Text("Download:"),
 | 
			
		||||
            child: const Text('Download:'),
 | 
			
		||||
          ),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: LinearProgressIndicator(value: percent/100),
 | 
			
		||||
          ),
 | 
			
		||||
          Container(
 | 
			
		||||
            margin: const EdgeInsets.only(left: 10),
 | 
			
		||||
            child: Text("${percent.round()}%"),
 | 
			
		||||
            child: Text('${percent.round()}%'),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return widget.file.isDirectory
 | 
			
		||||
        ? Text("geändert ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}")
 | 
			
		||||
        : Text("${filesize(widget.file.size)}, ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}");
 | 
			
		||||
        ? Text('geändert ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}')
 | 
			
		||||
        : Text('${filesize(widget.file.size)}, ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -106,17 +106,17 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
          ));
 | 
			
		||||
        } else {
 | 
			
		||||
          if(EndpointData().getEndpointMode() == EndpointMode.stage) {
 | 
			
		||||
            InfoDialog.show(context, "Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!");
 | 
			
		||||
            InfoDialog.show(context, 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!');
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          if(widget.file.currentlyDownloading) {
 | 
			
		||||
            showDialog(
 | 
			
		||||
              context: context,
 | 
			
		||||
              builder: (context) => ConfirmDialog(
 | 
			
		||||
                title: "Download abbrechen?",
 | 
			
		||||
                content: "Möchtest du den Download abbrechen?",
 | 
			
		||||
                cancelButton: "Nein",
 | 
			
		||||
                confirmButton: "Ja, Abbrechen",
 | 
			
		||||
                title: 'Download abbrechen?',
 | 
			
		||||
                content: 'Möchtest du den Download abbrechen?',
 | 
			
		||||
                cancelButton: 'Nein',
 | 
			
		||||
                confirmButton: 'Ja, Abbrechen',
 | 
			
		||||
                onConfirm: () {
 | 
			
		||||
                  downloadCore?.then((value) {
 | 
			
		||||
                    if(!value.isCancelled) value.cancel();
 | 
			
		||||
@@ -143,7 +143,7 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
            if(result.type != ResultType.done) {
 | 
			
		||||
              showDialog(context: context, builder: (context) {
 | 
			
		||||
                return AlertDialog(
 | 
			
		||||
                  title: const Text("Download"),
 | 
			
		||||
                  title: const Text('Download'),
 | 
			
		||||
                  content: Text(result.message),
 | 
			
		||||
                );
 | 
			
		||||
              });
 | 
			
		||||
@@ -163,12 +163,12 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
            children: [
 | 
			
		||||
              ListTile(
 | 
			
		||||
                leading: const Icon(Icons.delete_outline),
 | 
			
		||||
                title: const Text("Löschen"),
 | 
			
		||||
                title: const Text('Löschen'),
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  Navigator.of(context).pop();
 | 
			
		||||
                  showDialog(context: context, builder: (context) => ConfirmDialog(
 | 
			
		||||
                    title: "Element löschen?",
 | 
			
		||||
                    content: "Das Element wird unwiederruflich gelöscht.",
 | 
			
		||||
                    title: 'Element löschen?',
 | 
			
		||||
                    content: 'Das Element wird unwiederruflich gelöscht.',
 | 
			
		||||
                    onConfirm: () {
 | 
			
		||||
                      WebdavApi.webdav
 | 
			
		||||
                          .then((value) => value.delete(PathUri.parse(widget.file.path)))
 | 
			
		||||
@@ -181,7 +181,7 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
                visible: !kReleaseMode,
 | 
			
		||||
                child: ListTile(
 | 
			
		||||
                  leading: const Icon(Icons.share_outlined),
 | 
			
		||||
                  title: const Text("Teilen"),
 | 
			
		||||
                  title: const Text('Teilen'),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
                    UnimplementedDialog.show(context);
 | 
			
		||||
 
 | 
			
		||||
@@ -43,8 +43,8 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        state = FileUploadState.checkConflict;
 | 
			
		||||
      });
 | 
			
		||||
      List<WebDavResponse> result = (await webdavClient.propfind(PathUri.parse(widget.remotePath.join("/")))).responses;
 | 
			
		||||
      if(result.any((element) => element.href!.endsWith("/$targetFileName"))) {
 | 
			
		||||
      List<WebDavResponse> result = (await webdavClient.propfind(PathUri.parse(widget.remotePath.join('/')))).responses;
 | 
			
		||||
      if(result.any((element) => element.href!.endsWith('/$targetFileName'))) {
 | 
			
		||||
        setState(() {
 | 
			
		||||
          state = FileUploadState.conflict;
 | 
			
		||||
        });
 | 
			
		||||
@@ -57,7 +57,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Future<HttpClientResponse> uploadTask = webdavClient.putFile(File(widget.localPath), FileStat.statSync(widget.localPath), PathUri.parse(fullRemotePath)); // TODO use onProgress from putFile
 | 
			
		||||
    uploadTask.then((value) => Future<HttpClientResponse?>.value(value)).catchError((e) {
 | 
			
		||||
    uploadTask.then(Future<HttpClientResponse?>.value).catchError((e) {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        state = FileUploadState.error;
 | 
			
		||||
      });
 | 
			
		||||
@@ -67,7 +67,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
 | 
			
		||||
    cancelableOperation = CancelableOperation<HttpClientResponse>.fromFuture(
 | 
			
		||||
      uploadTask,
 | 
			
		||||
      onCancel: () => log("Upload cancelled"),
 | 
			
		||||
      onCancel: () => log('Upload cancelled'),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    cancelableOperation!.then((value) {
 | 
			
		||||
@@ -88,7 +88,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    targetFileName = widget.fileName;
 | 
			
		||||
    remoteFolderName = widget.remotePath.isNotEmpty ? widget.remotePath.last : "/";
 | 
			
		||||
    remoteFolderName = widget.remotePath.isNotEmpty ? widget.remotePath.last : '/';
 | 
			
		||||
    fileNameController.text = widget.fileName;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
@@ -96,7 +96,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if(state == FileUploadState.naming) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        title: const Text("Datei hochladen"),
 | 
			
		||||
        title: const Text('Datei hochladen'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
@@ -107,7 +107,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
              },
 | 
			
		||||
              autocorrect: false,
 | 
			
		||||
              decoration: const InputDecoration(
 | 
			
		||||
                labelText: "Dateiname",
 | 
			
		||||
                labelText: 'Dateiname',
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
@@ -115,10 +115,10 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            Navigator.of(context).pop();
 | 
			
		||||
          }, child: const Text("Abbrechen")),
 | 
			
		||||
          }, child: const Text('Abbrechen')),
 | 
			
		||||
          TextButton(onPressed: () async {
 | 
			
		||||
            upload();
 | 
			
		||||
          }, child: const Text("Hochladen")),
 | 
			
		||||
          }, child: const Text('Hochladen')),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
@@ -127,7 +127,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
    if(state == FileUploadState.conflict) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        icon: const Icon(Icons.error_outline),
 | 
			
		||||
        title: const Text("Datei konflikt"),
 | 
			
		||||
        title: const Text('Datei konflikt'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
@@ -139,10 +139,10 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
            setState(() {
 | 
			
		||||
              state = FileUploadState.naming;
 | 
			
		||||
            });
 | 
			
		||||
          }, child: const Text("Datei umbenennen")),
 | 
			
		||||
          }, child: const Text('Datei umbenennen')),
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            upload(override: true);
 | 
			
		||||
          }, child: const Text("Datei überschreiben")),
 | 
			
		||||
          }, child: const Text('Datei überschreiben')),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
@@ -151,15 +151,15 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
    if(state == FileUploadState.upload || state == FileUploadState.checkConflict) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        icon: const Icon(Icons.upload),
 | 
			
		||||
        title: const Text("Hochladen"),
 | 
			
		||||
        title: const Text('Hochladen'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
            Visibility(
 | 
			
		||||
              visible: state == FileUploadState.upload,
 | 
			
		||||
              replacement: const Text("Prüfe auf dateikonflikte..."),
 | 
			
		||||
              child: const Text("Upload läuft!\nDies kann je nach Dateigröße einige Zeit dauern...", textAlign: TextAlign.center),
 | 
			
		||||
              replacement: const Text('Prüfe auf dateikonflikte...'),
 | 
			
		||||
              child: const Text('Upload läuft!\nDies kann je nach Dateigröße einige Zeit dauern...', textAlign: TextAlign.center),
 | 
			
		||||
            ),
 | 
			
		||||
            const SizedBox(height: 30),
 | 
			
		||||
            const CircularProgressIndicator()
 | 
			
		||||
@@ -183,7 +183,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
      }
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        icon: const Icon(Icons.done),
 | 
			
		||||
        title: const Text("Upload fertig"),
 | 
			
		||||
        title: const Text('Upload fertig'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
@@ -193,7 +193,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            Navigator.of(context).pop();
 | 
			
		||||
          }, child: const Text("Fertig")),
 | 
			
		||||
          }, child: const Text('Fertig')),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
@@ -202,23 +202,23 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
    if(state == FileUploadState.error) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        icon: const Icon(Icons.error_outline),
 | 
			
		||||
        title: const Text("Fehler"),
 | 
			
		||||
        title: const Text('Fehler'),
 | 
			
		||||
        content: const Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            Text("Es ist ein Fehler aufgetreten!", textAlign: TextAlign.center),
 | 
			
		||||
            Text('Es ist ein Fehler aufgetreten!', textAlign: TextAlign.center),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            Navigator.of(context).pop();
 | 
			
		||||
          }, child: const Text("Schlißen")),
 | 
			
		||||
          }, child: const Text('Schlißen')),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    throw UnimplementedError("Invalid state");
 | 
			
		||||
    throw UnimplementedError('Invalid state');
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -43,17 +43,17 @@ enum SortOption {
 | 
			
		||||
class SortOptions {
 | 
			
		||||
  static Map<SortOption, BetterSortOption> options = {
 | 
			
		||||
    SortOption.name: BetterSortOption(
 | 
			
		||||
      displayName: "Name",
 | 
			
		||||
      displayName: 'Name',
 | 
			
		||||
      icon: Icons.sort_by_alpha_outlined,
 | 
			
		||||
      compare: (CacheableFile a, CacheableFile b) => a.name.compareTo(b.name)
 | 
			
		||||
    ),
 | 
			
		||||
    SortOption.date: BetterSortOption(
 | 
			
		||||
      displayName: "Datum",
 | 
			
		||||
      displayName: 'Datum',
 | 
			
		||||
      icon: Icons.history_outlined,
 | 
			
		||||
      compare: (CacheableFile a, CacheableFile b) => a.modifiedAt!.compareTo(b.modifiedAt!)
 | 
			
		||||
    ),
 | 
			
		||||
    SortOption.size: BetterSortOption(
 | 
			
		||||
      displayName: "Größe",
 | 
			
		||||
      displayName: 'Größe',
 | 
			
		||||
      icon: Icons.sd_card_outlined,
 | 
			
		||||
      compare: (CacheableFile a, CacheableFile b) {
 | 
			
		||||
        if(a.isDirectory || b.isDirectory) return a.isDirectory ? 1 : 0;
 | 
			
		||||
@@ -88,7 +88,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
 | 
			
		||||
  void _query() {
 | 
			
		||||
    ListFilesCache(
 | 
			
		||||
        path: widget.path.isEmpty ? "/" : widget.path.join("/"),
 | 
			
		||||
        path: widget.path.isEmpty ? '/' : widget.path.join('/'),
 | 
			
		||||
        onUpdate: (ListFilesResponse d) {
 | 
			
		||||
          if(!context.mounted) return; // prevent setState when widget is possibly already disposed
 | 
			
		||||
          d.files.removeWhere((element) => element.name.isEmpty || element.name == widget.path.lastOrNull());
 | 
			
		||||
@@ -109,7 +109,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text(widget.path.isNotEmpty ? widget.path.last : "Dateien"),
 | 
			
		||||
        title: Text(widget.path.isNotEmpty ? widget.path.last : 'Dateien'),
 | 
			
		||||
        actions: [
 | 
			
		||||
          // IconButton(
 | 
			
		||||
          //   icon: const Icon(Icons.search),
 | 
			
		||||
@@ -127,7 +127,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Icon(e ? Icons.text_rotate_up : Icons.text_rotation_down, color: Theme.of(context).colorScheme.onSurface),
 | 
			
		||||
                      const SizedBox(width: 15),
 | 
			
		||||
                      Text(e ? "Aufsteigend" : "Absteigend")
 | 
			
		||||
                      Text(e ? 'Aufsteigend' : 'Absteigend')
 | 
			
		||||
                    ],
 | 
			
		||||
                  )
 | 
			
		||||
              )).toList();
 | 
			
		||||
@@ -164,7 +164,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      floatingActionButton: FloatingActionButton(
 | 
			
		||||
        heroTag: "uploadFile",
 | 
			
		||||
        heroTag: 'uploadFile',
 | 
			
		||||
        backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
        onPressed: () {
 | 
			
		||||
          showDialog(context: context, builder: (context) {
 | 
			
		||||
@@ -172,29 +172,29 @@ class _FilesState extends State<Files> {
 | 
			
		||||
              children: [
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  leading: const Icon(Icons.create_new_folder_outlined),
 | 
			
		||||
                  title: const Text("Ordner erstellen"),
 | 
			
		||||
                  title: const Text('Ordner erstellen'),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
                    showDialog(context: context, builder: (context) {
 | 
			
		||||
                      var inputController = TextEditingController();
 | 
			
		||||
                      return AlertDialog(
 | 
			
		||||
                        title: const Text("Neuer Ordner"),
 | 
			
		||||
                        title: const Text('Neuer Ordner'),
 | 
			
		||||
                        content: TextField(
 | 
			
		||||
                          controller: inputController,
 | 
			
		||||
                          decoration: const InputDecoration(
 | 
			
		||||
                            labelText: "Name",
 | 
			
		||||
                            labelText: 'Name',
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        actions: [
 | 
			
		||||
                          TextButton(onPressed: () {
 | 
			
		||||
                            Navigator.of(context).pop();
 | 
			
		||||
                          }, child: const Text("Abbrechen")),
 | 
			
		||||
                          }, child: const Text('Abbrechen')),
 | 
			
		||||
                          TextButton(onPressed: () {
 | 
			
		||||
                            WebdavApi.webdav.then((webdav) {
 | 
			
		||||
                              webdav.mkcol(PathUri.parse("${widget.path.join("/")}/${inputController.text}")).then((value) => _query());
 | 
			
		||||
                            });
 | 
			
		||||
                            Navigator.of(context).pop();
 | 
			
		||||
                          }, child: const Text("Ordner erstellen")),
 | 
			
		||||
                          }, child: const Text('Ordner erstellen')),
 | 
			
		||||
                        ],
 | 
			
		||||
                      );
 | 
			
		||||
                    });
 | 
			
		||||
@@ -202,12 +202,10 @@ class _FilesState extends State<Files> {
 | 
			
		||||
                ),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  leading: const Icon(Icons.upload_file),
 | 
			
		||||
                  title: const Text("Aus Dateien hochladen"),
 | 
			
		||||
                  title: const Text('Aus Dateien hochladen'),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    context.loaderOverlay.show();
 | 
			
		||||
                    FilePick.documentPick().then((value) {
 | 
			
		||||
                      mediaUpload(value);
 | 
			
		||||
                    });
 | 
			
		||||
                    FilePick.documentPick().then(mediaUpload);
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
@@ -215,7 +213,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
                  visible: !Platform.isIOS,
 | 
			
		||||
                  child: ListTile(
 | 
			
		||||
                    leading: const Icon(Icons.add_a_photo_outlined),
 | 
			
		||||
                    title: const Text("Aus Gallerie hochladen"),
 | 
			
		||||
                    title: const Text('Aus Gallerie hochladen'),
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      context.loaderOverlay.show();
 | 
			
		||||
                      FilePick.galleryPick().then((value) {
 | 
			
		||||
@@ -231,7 +229,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
        },
 | 
			
		||||
        child: const Icon(Icons.add),
 | 
			
		||||
      ),
 | 
			
		||||
      body: data == null ? const LoadingSpinner() : data!.files.isEmpty ? const PlaceholderView(icon: Icons.folder_off_rounded, text: "Der Ordner ist leer") : LoaderOverlay(
 | 
			
		||||
      body: data == null ? const LoadingSpinner() : data!.files.isEmpty ? const PlaceholderView(icon: Icons.folder_off_rounded, text: 'Der Ordner ist leer') : LoaderOverlay(
 | 
			
		||||
        child: RefreshIndicator(
 | 
			
		||||
          onRefresh: () {
 | 
			
		||||
            _query();
 | 
			
		||||
@@ -258,6 +256,6 @@ class _FilesState extends State<Files> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var fileName = path.split(Platform.pathSeparator).last;
 | 
			
		||||
    showDialog(context: context, builder: (context) => FileUploadDialog(localPath: path, remotePath: widget.path, fileName: fileName, onUploadFinished: () => _query()), barrierDismissible: false);
 | 
			
		||||
    showDialog(context: context, builder: (context) => FileUploadDialog(localPath: path, remotePath: widget.path, fileName: fileName, onUploadFinished: _query), barrierDismissible: false);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,20 +22,20 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AlertDialog(
 | 
			
		||||
 | 
			
		||||
      title: const Text("Feedback"),
 | 
			
		||||
      title: const Text('Feedback'),
 | 
			
		||||
      content: Column(
 | 
			
		||||
        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
        children: [
 | 
			
		||||
          const Text("Feedback, Anregungen, Ideen, Fehler und Verbesserungen"),
 | 
			
		||||
          const Text('Feedback, Anregungen, Ideen, Fehler und Verbesserungen'),
 | 
			
		||||
          const SizedBox(height: 10),
 | 
			
		||||
          const Text("Bitte gib keine geheimen Daten wie z.B. Passwörter weiter.", style: TextStyle(fontSize: 10)),
 | 
			
		||||
          const Text('Bitte gib keine geheimen Daten wie z.B. Passwörter weiter.', style: TextStyle(fontSize: 10)),
 | 
			
		||||
          const SizedBox(height: 10),
 | 
			
		||||
          TextField(
 | 
			
		||||
            controller: _feedbackInput,
 | 
			
		||||
            autofocus: true,
 | 
			
		||||
            decoration: const InputDecoration(
 | 
			
		||||
              border: OutlineInputBorder(),
 | 
			
		||||
              label: Text("Feedback und Verbesserungen")
 | 
			
		||||
              label: Text('Feedback und Verbesserungen')
 | 
			
		||||
            ),
 | 
			
		||||
            // style: TextStyle(),
 | 
			
		||||
            // expands: true,
 | 
			
		||||
@@ -44,12 +44,12 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
 | 
			
		||||
          ),
 | 
			
		||||
          Visibility(
 | 
			
		||||
            visible: _error != null,
 | 
			
		||||
            child: Text("Senden fehlgeschlagen: $_error", style: const TextStyle(color: Colors.red))
 | 
			
		||||
            child: Text('Senden fehlgeschlagen: $_error', style: const TextStyle(color: Colors.red))
 | 
			
		||||
          )
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      actions: [
 | 
			
		||||
        TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text("Abbrechen")),
 | 
			
		||||
        TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Abbrechen')),
 | 
			
		||||
        TextButton(
 | 
			
		||||
          onPressed: () async {
 | 
			
		||||
            AddFeedback(
 | 
			
		||||
@@ -62,7 +62,7 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
 | 
			
		||||
            .run()
 | 
			
		||||
            .then((value) {
 | 
			
		||||
              Navigator.of(context).pop();
 | 
			
		||||
              InfoDialog.show(context, "Danke für dein Feedback!");
 | 
			
		||||
              InfoDialog.show(context, 'Danke für dein Feedback!');
 | 
			
		||||
            })
 | 
			
		||||
            .catchError((error, trace) {
 | 
			
		||||
                setState(() {
 | 
			
		||||
@@ -70,7 +70,7 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
          child: const Text("Senden"),
 | 
			
		||||
          child: const Text('Senden'),
 | 
			
		||||
        )
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
 | 
			
		||||
  String getGradeDisplay(int grade) {
 | 
			
		||||
    if(gradeSystem) {
 | 
			
		||||
      return "Note $grade";
 | 
			
		||||
      return 'Note $grade';
 | 
			
		||||
    } else {
 | 
			
		||||
      return "$grade Punkt${grade > 1 ? "e" : ""}";
 | 
			
		||||
    }
 | 
			
		||||
@@ -48,22 +48,22 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
      if(!settings.val().gradeAveragesSettings.askedForPreferredGradeSystem) {
 | 
			
		||||
        settings.val(write: true).gradeAveragesSettings.askedForPreferredGradeSystem = true;
 | 
			
		||||
        showDialog(context: context, builder: (context) => AlertDialog(
 | 
			
		||||
          title: const Text("Notensystem"),
 | 
			
		||||
          content: const Text("Wähle dein bevorzugtes Schulnotensystem"),
 | 
			
		||||
          title: const Text('Notensystem'),
 | 
			
		||||
          content: const Text('Wähle dein bevorzugtes Schulnotensystem'),
 | 
			
		||||
          actions: [
 | 
			
		||||
            TextButton(
 | 
			
		||||
              onPressed: () {
 | 
			
		||||
                switchSystem(true);
 | 
			
		||||
                Navigator.of(context).pop();
 | 
			
		||||
              },
 | 
			
		||||
              child: const Text("Realschule"),
 | 
			
		||||
              child: const Text('Realschule'),
 | 
			
		||||
            ),
 | 
			
		||||
            TextButton(
 | 
			
		||||
              onPressed: () {
 | 
			
		||||
                switchSystem(false);
 | 
			
		||||
                Navigator.of(context).pop();
 | 
			
		||||
              },
 | 
			
		||||
              child: const Text("Oberstufe"),
 | 
			
		||||
              child: const Text('Oberstufe'),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ));
 | 
			
		||||
@@ -81,7 +81,7 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Notendurschnittsrechner"),
 | 
			
		||||
        title: const Text('Notendurschnittsrechner'),
 | 
			
		||||
        actions: [
 | 
			
		||||
          Visibility(
 | 
			
		||||
            visible: grades.isNotEmpty,
 | 
			
		||||
@@ -89,9 +89,9 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
              showDialog(
 | 
			
		||||
                context: context,
 | 
			
		||||
                builder: (context) => ConfirmDialog(
 | 
			
		||||
                  title: "Zurücksetzen?",
 | 
			
		||||
                  content: "Alle Einträge werden entfernt.",
 | 
			
		||||
                  confirmButton: "Zurücksetzen",
 | 
			
		||||
                  title: 'Zurücksetzen?',
 | 
			
		||||
                  content: 'Alle Einträge werden entfernt.',
 | 
			
		||||
                  confirmButton: 'Zurücksetzen',
 | 
			
		||||
                  onConfirm: () {
 | 
			
		||||
                    grades.clear();
 | 
			
		||||
                    setState(() {});
 | 
			
		||||
@@ -109,7 +109,7 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
                children: [
 | 
			
		||||
                  Icon(e ? Icons.calculate_outlined : Icons.school_outlined, color: Theme.of(context).colorScheme.onSurface),
 | 
			
		||||
                  const SizedBox(width: 15),
 | 
			
		||||
                  Text(e ? "Notensystem" : "Punktesystem"),
 | 
			
		||||
                  Text(e ? 'Notensystem' : 'Punktesystem'),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            )).toList(),
 | 
			
		||||
@@ -120,9 +120,9 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
                showDialog(
 | 
			
		||||
                  context: context,
 | 
			
		||||
                  builder: (context) => ConfirmDialog(
 | 
			
		||||
                    title: "Notensystem wechseln",
 | 
			
		||||
                    content: "Beim wechsel des Notensystems werden alle Einträge zurückgesetzt.",
 | 
			
		||||
                    confirmButton: "Fortfahren",
 | 
			
		||||
                    title: 'Notensystem wechseln',
 | 
			
		||||
                    content: 'Beim wechsel des Notensystems werden alle Einträge zurückgesetzt.',
 | 
			
		||||
                    confirmButton: 'Fortfahren',
 | 
			
		||||
                    onConfirm: () => switchSystem(e),
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
@@ -142,7 +142,7 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
          const SizedBox(height: 10),
 | 
			
		||||
          const Divider(),
 | 
			
		||||
          const SizedBox(height: 10),
 | 
			
		||||
          Text(gradeSystem ? "Wähle unten die Anzahl deiner jewiligen Noten aus" : "Wähle unten die Anzahl deiner jeweiligen Punkte aus"),
 | 
			
		||||
          Text(gradeSystem ? 'Wähle unten die Anzahl deiner jewiligen Noten aus' : 'Wähle unten die Anzahl deiner jeweiligen Punkte aus'),
 | 
			
		||||
          const SizedBox(height: 10),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: ListView.builder(
 | 
			
		||||
@@ -169,7 +169,7 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
                            icon: const Icon(Icons.remove),
 | 
			
		||||
                            color: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                          ),
 | 
			
		||||
                          Text("${grades.where(isThis).length}", style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
 | 
			
		||||
                          Text('${grades.where(isThis).length}', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
 | 
			
		||||
                          IconButton(
 | 
			
		||||
                            onPressed: () {
 | 
			
		||||
                              setState(() {
 | 
			
		||||
 
 | 
			
		||||
@@ -42,23 +42,23 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String parseString(String enDate) {
 | 
			
		||||
    return Jiffy.parse(enDate).format(pattern: "dd.MM.yyyy");
 | 
			
		||||
    return Jiffy.parse(enDate).format(pattern: 'dd.MM.yyyy');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void showDisclaimer() {
 | 
			
		||||
    showDialog(context: context, builder: (context) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        title: const Text("Richtigkeit und Bereitstellung der Daten"),
 | 
			
		||||
        title: const Text('Richtigkeit und Bereitstellung der Daten'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            const Text(""
 | 
			
		||||
                "Sämtliche Datumsangaben sind ohne Gewähr.\n"
 | 
			
		||||
                "Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n"
 | 
			
		||||
                "Die Daten stammen von https://ferien-api.de/"),
 | 
			
		||||
            const Text(''
 | 
			
		||||
                'Sämtliche Datumsangaben sind ohne Gewähr.\n'
 | 
			
		||||
                'Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n'
 | 
			
		||||
                'Die Daten stammen von https://ferien-api.de/'),
 | 
			
		||||
            const SizedBox(height: 30),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: const Text("Diese Meldung nicht mehr anzeigen"),
 | 
			
		||||
              title: const Text('Diese Meldung nicht mehr anzeigen'),
 | 
			
		||||
              trailing: Checkbox(
 | 
			
		||||
                value: settings.val().holidaysSettings.dismissedDisclaimer,
 | 
			
		||||
                onChanged: (value) => settings.val(write: true).holidaysSettings.dismissedDisclaimer = value!,
 | 
			
		||||
@@ -67,8 +67,8 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(child: const Text("ferien-api.de besuchen"), onPressed: () => ConfirmDialog.openBrowser(context, "https://ferien-api.de/")),
 | 
			
		||||
          TextButton(child: const Text("Okay"), onPressed: () => Navigator.of(context).pop()),
 | 
			
		||||
          TextButton(child: const Text('ferien-api.de besuchen'), onPressed: () => ConfirmDialog.openBrowser(context, 'https://ferien-api.de/')),
 | 
			
		||||
          TextButton(child: const Text('Okay'), onPressed: () => Navigator.of(context).pop()),
 | 
			
		||||
        ],
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
@@ -78,11 +78,11 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Schulferien in Hessen"),
 | 
			
		||||
        title: const Text('Schulferien in Hessen'),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: const Icon(Icons.warning_amber_outlined),
 | 
			
		||||
            onPressed: () => showDisclaimer(),
 | 
			
		||||
            onPressed: showDisclaimer,
 | 
			
		||||
          ),
 | 
			
		||||
          PopupMenuButton<bool>(
 | 
			
		||||
            initialValue: settings.val().holidaysSettings.showPastEvents,
 | 
			
		||||
@@ -95,7 +95,7 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, color: Theme.of(context).colorScheme.onSurface),
 | 
			
		||||
                      const SizedBox(width: 15),
 | 
			
		||||
                      Text(e ? "Alle anzeigen" : "Nur zukünftige anzeigen")
 | 
			
		||||
                      Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen')
 | 
			
		||||
                    ],
 | 
			
		||||
                  )
 | 
			
		||||
              )).toList();
 | 
			
		||||
@@ -115,19 +115,19 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
        List<GetHolidaysResponseObject> holidays = value.getHolidaysResponse.data;
 | 
			
		||||
        if(!showPastEvents) holidays = holidays.where((element) => DateTime.parse(element.end).isAfter(DateTime.now())).toList();
 | 
			
		||||
 | 
			
		||||
        if(holidays.isEmpty) return const PlaceholderView(icon: Icons.search_off, text: "Es wurden keine Ferieneinträge gefunden!");
 | 
			
		||||
        if(holidays.isEmpty) return const PlaceholderView(icon: Icons.search_off, text: 'Es wurden keine Ferieneinträge gefunden!');
 | 
			
		||||
 | 
			
		||||
        return ListView.builder(
 | 
			
		||||
            itemCount: holidays.length,
 | 
			
		||||
            itemBuilder: (context, index) {
 | 
			
		||||
              GetHolidaysResponseObject holiday = holidays[index];
 | 
			
		||||
              String holidayType = holiday.name.split(" ").first.capitalize();
 | 
			
		||||
              String holidayType = holiday.name.split(' ').first.capitalize();
 | 
			
		||||
              return ListTile(
 | 
			
		||||
                leading: const CenteredLeading(Icon(Icons.calendar_month)),
 | 
			
		||||
                title: Text("$holidayType ab ${parseString(holiday.start)}"),
 | 
			
		||||
                subtitle: Text("bis ${parseString(holiday.end)}"),
 | 
			
		||||
                title: Text('$holidayType ab ${parseString(holiday.start)}'),
 | 
			
		||||
                subtitle: Text('bis ${parseString(holiday.end)}'),
 | 
			
		||||
                onTap: () => showDialog(context: context, builder: (context) => SimpleDialog(
 | 
			
		||||
                  title: Text("$holidayType ${holiday.year} in Hessen"),
 | 
			
		||||
                  title: Text('$holidayType ${holiday.year} in Hessen'),
 | 
			
		||||
                  children: [
 | 
			
		||||
                    ListTile(
 | 
			
		||||
                      leading: const CenteredLeading(Icon(Icons.signpost_outlined)),
 | 
			
		||||
@@ -136,11 +136,11 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
                    ),
 | 
			
		||||
                    ListTile(
 | 
			
		||||
                      leading: const Icon(Icons.arrow_forward),
 | 
			
		||||
                      title: Text("vom ${parseString(holiday.start)}"),
 | 
			
		||||
                      title: Text('vom ${parseString(holiday.start)}'),
 | 
			
		||||
                    ),
 | 
			
		||||
                    ListTile(
 | 
			
		||||
                      leading: const Icon(Icons.arrow_back),
 | 
			
		||||
                      title: Text("bis zum ${parseString(holiday.end)}"),
 | 
			
		||||
                      title: Text('bis zum ${parseString(holiday.end)}'),
 | 
			
		||||
                    ),
 | 
			
		||||
                    Visibility(
 | 
			
		||||
                      visible: !DateTime.parse(holiday.start).difference(DateTime.now()).isNegative,
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ class _MessageState extends State<Message> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Marianum Message"),
 | 
			
		||||
        title: const Text('Marianum Message'),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Consumer<MessageProps>(builder: (context, value, child) {
 | 
			
		||||
        if(value.primaryLoading()) return const LoadingSpinner();
 | 
			
		||||
@@ -43,7 +43,7 @@ class _MessageState extends State<Message> {
 | 
			
		||||
                    children: [Icon(Icons.newspaper)],
 | 
			
		||||
                  ),
 | 
			
		||||
                  title: Text(message.name, overflow: TextOverflow.ellipsis),
 | 
			
		||||
                  subtitle: Text("vom ${message.date}"),
 | 
			
		||||
                  subtitle: Text('vom ${message.date}'),
 | 
			
		||||
                  trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: value.getMessagesResponse.base, message: message)));
 | 
			
		||||
 
 | 
			
		||||
@@ -29,12 +29,12 @@ class _MessageViewState extends State<MessageView> {
 | 
			
		||||
          Navigator.of(context).pop();
 | 
			
		||||
          showDialog(context: context, builder: (context) {
 | 
			
		||||
            return AlertDialog(
 | 
			
		||||
              title: const Text("Fehler beim öffnen"),
 | 
			
		||||
              title: const Text('Fehler beim öffnen'),
 | 
			
		||||
              content: Text("Dokument '${widget.message.name}' konnte nicht geladen werden:\n${e.description}"),
 | 
			
		||||
              actions: [
 | 
			
		||||
                TextButton(onPressed: () {
 | 
			
		||||
                  Navigator.of(context).pop();
 | 
			
		||||
                }, child: const Text("Ok"))
 | 
			
		||||
                }, child: const Text('Ok'))
 | 
			
		||||
              ],
 | 
			
		||||
            );
 | 
			
		||||
          });
 | 
			
		||||
@@ -43,9 +43,9 @@ class _MessageViewState extends State<MessageView> {
 | 
			
		||||
          showDialog(
 | 
			
		||||
            context: context,
 | 
			
		||||
            builder: (context) => ConfirmDialog(
 | 
			
		||||
              title: "Link öffnen",
 | 
			
		||||
              content: "Möchtest du den folgenden Link öffnen?\n${e.uri}",
 | 
			
		||||
              confirmButton: "Öffnen",
 | 
			
		||||
              title: 'Link öffnen',
 | 
			
		||||
              content: 'Möchtest du den folgenden Link öffnen?\n${e.uri}',
 | 
			
		||||
              confirmButton: 'Öffnen',
 | 
			
		||||
              onConfirm: () => launchUrl(Uri.parse(e.uri), mode: LaunchMode.externalApplication),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,10 @@ class Roomplan extends StatelessWidget {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Raumplan"),
 | 
			
		||||
        title: const Text('Raumplan'),
 | 
			
		||||
      ),
 | 
			
		||||
      body: PhotoView(
 | 
			
		||||
        imageProvider: Image.asset("assets/img/raumplan.jpg").image,
 | 
			
		||||
        imageProvider: Image.asset('assets/img/raumplan.jpg').image,
 | 
			
		||||
        minScale: 0.5,
 | 
			
		||||
        maxScale: 2.0,
 | 
			
		||||
        backgroundDecoration: BoxDecoration(color: Theme.of(context).colorScheme.background),
 | 
			
		||||
 
 | 
			
		||||
@@ -16,18 +16,18 @@ class _QrShareViewState extends State<QrShareView> {
 | 
			
		||||
      length: 2,
 | 
			
		||||
      child: Scaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          title: const Text("Teile die App"),
 | 
			
		||||
          title: const Text('Teile die App'),
 | 
			
		||||
          bottom: const TabBar(
 | 
			
		||||
            tabs: [
 | 
			
		||||
              Tab(icon: Icon(Icons.android_outlined), text: "Android"),
 | 
			
		||||
              Tab(icon: Icon(Icons.apple_outlined), text: "iOS & iPadOS"),
 | 
			
		||||
              Tab(icon: Icon(Icons.android_outlined), text: 'Android'),
 | 
			
		||||
              Tab(icon: Icon(Icons.apple_outlined), text: 'iOS & iPadOS'),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        body: const TabBarView(
 | 
			
		||||
          children: [
 | 
			
		||||
            AppSharePlatformView("Für Android", "https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client"),
 | 
			
		||||
            AppSharePlatformView("Für iOS & iPad", "https://apps.apple.com/us/app/marianum-fulda/id6458789560"),
 | 
			
		||||
            AppSharePlatformView('Für Android', 'https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client'),
 | 
			
		||||
            AppSharePlatformView('Für iOS & iPad', 'https://apps.apple.com/us/app/marianum-fulda/id6458789560'),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ class SelectShareTypeDialog extends StatelessWidget {
 | 
			
		||||
      children: [
 | 
			
		||||
        ListTile(
 | 
			
		||||
          leading: const Icon(Icons.qr_code_2_outlined),
 | 
			
		||||
          title: const Text("Per QR-Code"),
 | 
			
		||||
          title: const Text('Per QR-Code'),
 | 
			
		||||
          trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            Navigator.of(context).push(MaterialPageRoute(builder: (context) => const QrShareView()));
 | 
			
		||||
@@ -21,16 +21,16 @@ class SelectShareTypeDialog extends StatelessWidget {
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          leading: const Icon(Icons.link_outlined),
 | 
			
		||||
          title: const Text("Per Link teilen"),
 | 
			
		||||
          title: const Text('Per Link teilen'),
 | 
			
		||||
          trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            Share.share(
 | 
			
		||||
                sharePositionOrigin: SharePositionOrigin.get(context),
 | 
			
		||||
                subject: "App Teilen",
 | 
			
		||||
                "Hol dir die für das Marianum maßgeschneiderte App:"
 | 
			
		||||
                    "\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client "
 | 
			
		||||
                    "\niOS: https://apps.apple.com/us/app/marianum-fulda/id6458789560 "
 | 
			
		||||
                    "\n\nViel Spaß!"
 | 
			
		||||
                subject: 'App Teilen',
 | 
			
		||||
                'Hol dir die für das Marianum maßgeschneiderte App:'
 | 
			
		||||
                    '\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client '
 | 
			
		||||
                    '\niOS: https://apps.apple.com/us/app/marianum-fulda/id6458789560 '
 | 
			
		||||
                    '\n\nViel Spaß!'
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:in_app_review/in_app_review.dart';
 | 
			
		||||
import 'package:marianum_mobile/extensions/renderNotNull.dart';
 | 
			
		||||
import '../../extensions/renderNotNull.dart';
 | 
			
		||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
 | 
			
		||||
 | 
			
		||||
import '../../widget/ListItem.dart';
 | 
			
		||||
@@ -26,23 +26,23 @@ class Overhang extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Mehr"),
 | 
			
		||||
        title: const Text('Mehr'),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(onPressed: () => pushScreen(context, screen: const Settings(), withNavBar: false), icon: const Icon(Icons.settings))
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      body: ListView(
 | 
			
		||||
        children: [
 | 
			
		||||
          const ListItemNavigator(icon: Icons.newspaper, text: "Marianum Message", target: Message()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.room, text: "Raumplan", target: Roomplan()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.calculate, text: "Notendurschnittsrechner", target: GradeAverage()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.newspaper, text: 'Marianum Message', target: Message()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.room, text: 'Raumplan', target: Roomplan()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.calculate, text: 'Notendurschnittsrechner', target: GradeAverage()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.school_outlined, text: "Abiturrechner", target: AbiturCalculatorView()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.calendar_month, text: "Schulferien", target: Holidays()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.calendar_month, text: 'Schulferien', target: Holidays()),
 | 
			
		||||
          const Divider(),
 | 
			
		||||
          ListTile(
 | 
			
		||||
            leading: const Icon(Icons.share_outlined),
 | 
			
		||||
            title: const Text("Teile die App"),
 | 
			
		||||
            subtitle: const Text("Mit Freunden und deiner Klasse teilen"),
 | 
			
		||||
            title: const Text('Teile die App'),
 | 
			
		||||
            subtitle: const Text('Mit Freunden und deiner Klasse teilen'),
 | 
			
		||||
            trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
            onTap: () => showDialog(context: context, builder: (context) => const SelectShareTypeDialog())
 | 
			
		||||
          ),
 | 
			
		||||
@@ -52,19 +52,19 @@ class Overhang extends StatelessWidget {
 | 
			
		||||
              if(!snapshot.hasData) return const SizedBox.shrink();
 | 
			
		||||
 | 
			
		||||
              String? getPlatformStoreName() {
 | 
			
		||||
                if(Platform.isAndroid) return "Play store";
 | 
			
		||||
                if(Platform.isIOS) return "App store";
 | 
			
		||||
                if(Platform.isAndroid) return 'Play store';
 | 
			
		||||
                if(Platform.isIOS) return 'App store';
 | 
			
		||||
                return null;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              return ListTile(
 | 
			
		||||
                leading: const CenteredLeading(Icon(Icons.star_rate_outlined)),
 | 
			
		||||
                title: const Text("App Bewerten"),
 | 
			
		||||
                subtitle: getPlatformStoreName().wrapNullable((data) => Text("Im $data")),
 | 
			
		||||
                title: const Text('App bewerten'),
 | 
			
		||||
                subtitle: getPlatformStoreName().wrapNullable((data) => Text('Im $data')),
 | 
			
		||||
                trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  InAppReview.instance.openStoreListing(appStoreId: "6458789560").then(
 | 
			
		||||
                          (value) => InfoDialog.show(context, "Vielen Dank!"),
 | 
			
		||||
                  InAppReview.instance.openStoreListing(appStoreId: '6458789560').then(
 | 
			
		||||
                          (value) => InfoDialog.show(context, 'Vielen Dank!'),
 | 
			
		||||
                      onError: (error) => InfoDialog.show(context, error.toString())
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
@@ -73,8 +73,8 @@ class Overhang extends StatelessWidget {
 | 
			
		||||
          ),
 | 
			
		||||
          ListTile(
 | 
			
		||||
            leading: const CenteredLeading(Icon(Icons.feedback_outlined)),
 | 
			
		||||
            title: const Text("Du hast eine Idee?"),
 | 
			
		||||
            subtitle: const Text("Fehler und Verbessungsvorschläge"),
 | 
			
		||||
            title: const Text('Du hast eine Idee?'),
 | 
			
		||||
            subtitle: const Text('Fehler und Verbessungsvorschläge'),
 | 
			
		||||
            trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
            onTap: () => showDialog(context: context, barrierDismissible: false, builder: (context) => const FeedbackDialog()),
 | 
			
		||||
          ),
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,7 @@ class _ChatInfoState extends State<ChatInfo> {
 | 
			
		||||
            if(participants != null) ...[
 | 
			
		||||
              ListTile(
 | 
			
		||||
                leading: const Icon(Icons.supervised_user_circle),
 | 
			
		||||
                title: Text("${participants!.data.length} Teilnehmer"),
 | 
			
		||||
                title: Text('${participants!.data.length} Teilnehmer'),
 | 
			
		||||
                trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
                onTap: () => TalkNavigator.pushSplitView(context, ParticipantsListView(participants!)),
 | 
			
		||||
              ),
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ class _ParticipantsListViewState extends State<ParticipantsListView> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Teilnehmende"),
 | 
			
		||||
        title: const Text('Teilnehmende'),
 | 
			
		||||
      ),
 | 
			
		||||
      body: ListView(
 | 
			
		||||
        children: widget.participantsResponse.data.map((participant) {
 | 
			
		||||
 
 | 
			
		||||
@@ -40,9 +40,9 @@ class _ChatListState extends State<ChatList> {
 | 
			
		||||
 | 
			
		||||
        ConfirmDialog(
 | 
			
		||||
          icon: Icons.notifications_active_outlined,
 | 
			
		||||
          title: "Benachrichtigungen aktivieren",
 | 
			
		||||
          content: "Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.",
 | 
			
		||||
          confirmButton: "Weiter",
 | 
			
		||||
          title: 'Benachrichtigungen aktivieren',
 | 
			
		||||
          content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.',
 | 
			
		||||
          confirmButton: 'Weiter',
 | 
			
		||||
          onConfirm: () {
 | 
			
		||||
            FirebaseMessaging.instance.requestPermission(
 | 
			
		||||
                provisional: false
 | 
			
		||||
@@ -53,7 +53,7 @@ class _ChatListState extends State<ChatList> {
 | 
			
		||||
                  break;
 | 
			
		||||
                case AuthorizationStatus.denied:
 | 
			
		||||
                  showDialog(context: context, builder: (context) => const AlertDialog(
 | 
			
		||||
                    content: Text("Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren."),
 | 
			
		||||
                    content: Text('Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.'),
 | 
			
		||||
                  ));
 | 
			
		||||
                  break;
 | 
			
		||||
                default:
 | 
			
		||||
@@ -79,7 +79,7 @@ class _ChatListState extends State<ChatList> {
 | 
			
		||||
      breakpoint: 1000,
 | 
			
		||||
      child: Scaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          title: const Text("Talk"),
 | 
			
		||||
          title: const Text('Talk'),
 | 
			
		||||
          actions: [
 | 
			
		||||
            IconButton(
 | 
			
		||||
              icon: const Icon(Icons.search),
 | 
			
		||||
@@ -91,16 +91,16 @@ class _ChatListState extends State<ChatList> {
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        floatingActionButton: FloatingActionButton(
 | 
			
		||||
          heroTag: "createChat",
 | 
			
		||||
          heroTag: 'createChat',
 | 
			
		||||
          backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
          onPressed: () async {
 | 
			
		||||
            showSearch(context: context, delegate: JoinChat()).then((username) {
 | 
			
		||||
              if(username == null) return;
 | 
			
		||||
 | 
			
		||||
              ConfirmDialog(
 | 
			
		||||
                title: "Chat starten",
 | 
			
		||||
                title: 'Chat starten',
 | 
			
		||||
                content: "Möchtest du einen Chat mit Nutzer '$username' starten?",
 | 
			
		||||
                confirmButton: "Chat starten",
 | 
			
		||||
                confirmButton: 'Chat starten',
 | 
			
		||||
                onConfirm: () {
 | 
			
		||||
                  CreateRoom(CreateRoomParams(
 | 
			
		||||
                    roomType: 1,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:marianum_mobile/extensions/dateTime.dart';
 | 
			
		||||
import '../../../extensions/dateTime.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../api/marianumcloud/talk/chat/getChatResponse.dart';
 | 
			
		||||
@@ -51,8 +51,8 @@ class _ChatViewState extends State<ChatView> {
 | 
			
		||||
          data.getChatResponse.sortByTimestamp().forEach((element) {
 | 
			
		||||
            DateTime elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000);
 | 
			
		||||
 | 
			
		||||
            if(element.systemMessage.contains("reaction")) return;
 | 
			
		||||
            int commonRead = int.parse(data.getChatResponse.headers?['x-chat-last-common-read'] ?? "0");
 | 
			
		||||
            if(element.systemMessage.contains('reaction')) return;
 | 
			
		||||
            int commonRead = int.parse(data.getChatResponse.headers?['x-chat-last-common-read'] ?? '0');
 | 
			
		||||
 | 
			
		||||
            if(!elementDate.isSameDay(lastDate)) {
 | 
			
		||||
              lastDate = elementDate;
 | 
			
		||||
@@ -81,8 +81,8 @@ class _ChatViewState extends State<ChatView> {
 | 
			
		||||
              context: context,
 | 
			
		||||
              isSender: false,
 | 
			
		||||
              bubbleData: GetChatResponseObject.getTextDummy(
 | 
			
		||||
                  "Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. "
 | 
			
		||||
                  "Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de"
 | 
			
		||||
                  'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. '
 | 
			
		||||
                  'Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de'
 | 
			
		||||
              ),
 | 
			
		||||
              chatData: widget.room,
 | 
			
		||||
              refetch: _query,
 | 
			
		||||
@@ -111,7 +111,7 @@ class _ChatViewState extends State<ChatView> {
 | 
			
		||||
          body: Container(
 | 
			
		||||
            decoration: BoxDecoration(
 | 
			
		||||
              image: DecorationImage(
 | 
			
		||||
                image: const AssetImage("assets/background/chat.png"),
 | 
			
		||||
                image: const AssetImage('assets/background/chat.png'),
 | 
			
		||||
                scale: 1.5,
 | 
			
		||||
                opacity: 1,
 | 
			
		||||
                repeat: ImageRepeat.repeat,
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:jiffy/jiffy.dart';
 | 
			
		||||
import 'package:marianum_mobile/extensions/text.dart';
 | 
			
		||||
import '../../../../extensions/text.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
 | 
			
		||||
@@ -114,7 +114,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Text timeText = Text(
 | 
			
		||||
      Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: "HH:mm"),
 | 
			
		||||
      Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: 'HH:mm'),
 | 
			
		||||
      textAlign: TextAlign.end,
 | 
			
		||||
      style: TextStyle(color: widget.timeIconColor, fontSize: widget.timeIconSize),
 | 
			
		||||
    );
 | 
			
		||||
@@ -184,7 +184,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
          ),
 | 
			
		||||
          onLongPress: () {
 | 
			
		||||
            showDialog(context: context, builder: (context) {
 | 
			
		||||
              List<String> commonReactions = ["👍", "👎", "😆", "❤️", "👀", "🤔"];
 | 
			
		||||
              List<String> commonReactions = ['👍', '👎', '😆', '❤️', '👀', '🤔'];
 | 
			
		||||
              bool canReact = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
 | 
			
		||||
              return SimpleDialog(
 | 
			
		||||
                children: [
 | 
			
		||||
@@ -222,7 +222,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                    visible: canReact,
 | 
			
		||||
                    child: ListTile(
 | 
			
		||||
                      leading: const Icon(Icons.add_reaction_outlined),
 | 
			
		||||
                      title: const Text("Reaktionen"),
 | 
			
		||||
                      title: const Text('Reaktionen'),
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        Navigator.of(context).push(MaterialPageRoute(builder: (context) => MessageReactions(
 | 
			
		||||
                          token: widget.chatData.token,
 | 
			
		||||
@@ -235,7 +235,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                    visible: !message.containsFile,
 | 
			
		||||
                    child: ListTile(
 | 
			
		||||
                      leading: const Icon(Icons.copy),
 | 
			
		||||
                      title: const Text("Nachricht kopieren"),
 | 
			
		||||
                      title: const Text('Nachricht kopieren'),
 | 
			
		||||
                      onTap: () => {
 | 
			
		||||
                        Clipboard.setData(ClipboardData(text: widget.bubbleData.message)),
 | 
			
		||||
                        Navigator.of(context).pop(),
 | 
			
		||||
@@ -256,7 +256,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                    visible: widget.isSender && DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).add(const Duration(hours: 6)).isAfter(DateTime.now()),
 | 
			
		||||
                    child: ListTile(
 | 
			
		||||
                      leading: const Icon(Icons.delete_outline),
 | 
			
		||||
                      title: const Text("Nachricht löschen"),
 | 
			
		||||
                      title: const Text('Nachricht löschen'),
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) {
 | 
			
		||||
                          Provider.of<ChatProps>(context, listen: false).run();
 | 
			
		||||
@@ -276,12 +276,12 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
            if(downloadProgress > 0) {
 | 
			
		||||
              showDialog(context: context, builder: (context) {
 | 
			
		||||
                return AlertDialog(
 | 
			
		||||
                  title: const Text("Download abbrechen?"),
 | 
			
		||||
                  content: const Text("Möchtest du den Download abbrechen?"),
 | 
			
		||||
                  title: const Text('Download abbrechen?'),
 | 
			
		||||
                  content: const Text('Möchtest du den Download abbrechen?'),
 | 
			
		||||
                  actions: [
 | 
			
		||||
                    TextButton(onPressed: () {
 | 
			
		||||
                      Navigator.of(context).pop();
 | 
			
		||||
                    }, child: const Text("Nein")),
 | 
			
		||||
                    }, child: const Text('Nein')),
 | 
			
		||||
                    TextButton(onPressed: () {
 | 
			
		||||
                      downloadCore?.then((value) {
 | 
			
		||||
                        if(!value.isCancelled) value.cancel();
 | 
			
		||||
@@ -291,7 +291,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                        downloadProgress = 0;
 | 
			
		||||
                        downloadCore = null;
 | 
			
		||||
                      });
 | 
			
		||||
                    }, child: const Text("Ja, Abbrechen"))
 | 
			
		||||
                    }, child: const Text('Ja, Abbrechen'))
 | 
			
		||||
                  ],
 | 
			
		||||
                );
 | 
			
		||||
              });
 | 
			
		||||
@@ -336,7 +336,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                  return Container(
 | 
			
		||||
                    margin: const EdgeInsets.only(right: 2.5, left: 2.5),
 | 
			
		||||
                    child: ActionChip(
 | 
			
		||||
                      label: Text("${e.key} ${e.value}"),
 | 
			
		||||
                      label: Text('${e.key} ${e.value}'),
 | 
			
		||||
                      visualDensity: const VisualDensity(vertical: VisualDensity.minimumDensity, horizontal: VisualDensity.minimumDensity),
 | 
			
		||||
                      padding: EdgeInsets.zero,
 | 
			
		||||
                      backgroundColor: hasSelfReacted ? Theme.of(context).primaryColor : null,
 | 
			
		||||
 
 | 
			
		||||
@@ -14,14 +14,14 @@ class ChatMessage {
 | 
			
		||||
  Map<String, RichObjectString>? originalData;
 | 
			
		||||
 | 
			
		||||
  RichObjectString? file;
 | 
			
		||||
  String content = "";
 | 
			
		||||
  String content = '';
 | 
			
		||||
 | 
			
		||||
  bool get containsFile => file != null;
 | 
			
		||||
 | 
			
		||||
  ChatMessage({required this.originalMessage, this.originalData}) {
 | 
			
		||||
    if(originalData?.containsKey("file") ?? false) {
 | 
			
		||||
    if(originalData?.containsKey('file') ?? false) {
 | 
			
		||||
      file = originalData?['file'];
 | 
			
		||||
      content = file?.name ?? "Datei";
 | 
			
		||||
      content = file?.name ?? 'Datei';
 | 
			
		||||
    } else {
 | 
			
		||||
      content = RichObjectStringProcessor.parseToString(originalMessage, originalData);
 | 
			
		||||
    }
 | 
			
		||||
@@ -56,7 +56,7 @@ class ChatMessage {
 | 
			
		||||
      fadeInDuration: Duration.zero,
 | 
			
		||||
      fadeOutDuration: Duration.zero,
 | 
			
		||||
      errorListener: (value) {},
 | 
			
		||||
      imageUrl: "https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=100&y=-1&a=1",
 | 
			
		||||
      imageUrl: 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=100&y=-1&a=1',
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -41,9 +41,9 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    String filename = "${path.split("/").last.split(".").first}-${const Uuid().v4()}.${path.split(".").last}";
 | 
			
		||||
    String shareFolder = "MarianumMobile";
 | 
			
		||||
    String shareFolder = 'MarianumMobile';
 | 
			
		||||
    WebdavApi.webdav.then((webdav) {
 | 
			
		||||
      webdav.mkcol(PathUri.parse("/$shareFolder"));
 | 
			
		||||
      webdav.mkcol(PathUri.parse('/$shareFolder'));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    showDialog(context: context, builder: (context) => FileUploadDialog(
 | 
			
		||||
@@ -55,7 +55,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
        FileSharingApi().share(FileSharingApiParams(
 | 
			
		||||
          shareType: 10,
 | 
			
		||||
          shareWith: widget.sendToToken,
 | 
			
		||||
          path: "$shareFolder/$filename",
 | 
			
		||||
          path: '$shareFolder/$filename',
 | 
			
		||||
        )).then((value) => _query());
 | 
			
		||||
      },
 | 
			
		||||
    ), barrierDismissible: false);
 | 
			
		||||
@@ -77,7 +77,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    _textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? "";
 | 
			
		||||
    _textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? '';
 | 
			
		||||
 | 
			
		||||
    return Stack(
 | 
			
		||||
      children: <Widget>[
 | 
			
		||||
@@ -95,12 +95,10 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
                        children: [
 | 
			
		||||
                          ListTile(
 | 
			
		||||
                            leading: const Icon(Icons.file_open),
 | 
			
		||||
                            title: const Text("Aus Dateien auswählen"),
 | 
			
		||||
                            title: const Text('Aus Dateien auswählen'),
 | 
			
		||||
                            onTap: () {
 | 
			
		||||
                              context.loaderOverlay.show();
 | 
			
		||||
                              FilePick.documentPick().then((value) {
 | 
			
		||||
                                mediaUpload(value);
 | 
			
		||||
                              });
 | 
			
		||||
                              FilePick.documentPick().then(mediaUpload);
 | 
			
		||||
                              Navigator.of(context).pop();
 | 
			
		||||
                            },
 | 
			
		||||
                          ),
 | 
			
		||||
@@ -108,7 +106,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
                            visible: !Platform.isIOS,
 | 
			
		||||
                            child: ListTile(
 | 
			
		||||
                              leading: const Icon(Icons.image),
 | 
			
		||||
                              title: const Text("Aus Gallerie auswählen"),
 | 
			
		||||
                              title: const Text('Aus Gallerie auswählen'),
 | 
			
		||||
                              onTap: () {
 | 
			
		||||
                                context.loaderOverlay.show();
 | 
			
		||||
                                FilePick.galleryPick().then((value) {
 | 
			
		||||
@@ -147,12 +145,12 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
                    maxLines: 7,
 | 
			
		||||
                    minLines: 1,
 | 
			
		||||
                    decoration: const InputDecoration(
 | 
			
		||||
                      hintText: "Nachricht schreiben...",
 | 
			
		||||
                      hintText: 'Nachricht schreiben...',
 | 
			
		||||
                      border: InputBorder.none,
 | 
			
		||||
                    ),
 | 
			
		||||
                    onChanged: (String text) {
 | 
			
		||||
                      if(text.trim().toLowerCase() == "marbot marbot marbot") {
 | 
			
		||||
                        var newText = "Roboter sind cool und so, aber Marbots sind besser!";
 | 
			
		||||
                      if(text.trim().toLowerCase() == 'marbot marbot marbot') {
 | 
			
		||||
                        var newText = 'Roboter sind cool und so, aber Marbots sind besser!';
 | 
			
		||||
                        _textBoxController.text = newText;
 | 
			
		||||
                        text = newText;
 | 
			
		||||
                      }
 | 
			
		||||
@@ -175,8 +173,8 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
                      setState(() {
 | 
			
		||||
                        isLoading = false;
 | 
			
		||||
                      });
 | 
			
		||||
                      _textBoxController.text = "";
 | 
			
		||||
                      setDraft("");
 | 
			
		||||
                      _textBoxController.text = '';
 | 
			
		||||
                      setDraft('');
 | 
			
		||||
                    });
 | 
			
		||||
                  },
 | 
			
		||||
                  backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ class _ChatTileState extends State<ChatTile> {
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    SharedPreferences.getInstance().then((value) => {
 | 
			
		||||
      username = value.getString("username")!
 | 
			
		||||
      username = value.getString('username')!
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    bool isGroup = widget.data.type != GetRoomResponseObjectConversationType.oneToOne;
 | 
			
		||||
@@ -110,7 +110,7 @@ class _ChatTileState extends State<ChatTile> {
 | 
			
		||||
            minHeight: 20,
 | 
			
		||||
          ),
 | 
			
		||||
          child: Text(
 | 
			
		||||
            "${widget.data.unreadMessages}",
 | 
			
		||||
            '${widget.data.unreadMessages}',
 | 
			
		||||
            style: const TextStyle(
 | 
			
		||||
              color: Colors.white,
 | 
			
		||||
              fontSize: 15,
 | 
			
		||||
@@ -132,7 +132,7 @@ class _ChatTileState extends State<ChatTile> {
 | 
			
		||||
                visible: widget.data.unreadMessages > 0,
 | 
			
		||||
                replacement: ListTile(
 | 
			
		||||
                  leading: const Icon(Icons.mark_chat_unread_outlined),
 | 
			
		||||
                  title: const Text("Als ungelesen markieren"),
 | 
			
		||||
                  title: const Text('Als ungelesen markieren'),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    SetReadMarker(widget.data.token, false).run().then((value) => widget.query(renew: true));
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
@@ -140,7 +140,7 @@ class _ChatTileState extends State<ChatTile> {
 | 
			
		||||
                ),
 | 
			
		||||
                child: ListTile(
 | 
			
		||||
                  leading: const Icon(Icons.mark_chat_read_outlined),
 | 
			
		||||
                  title: const Text("Als gelesen markieren"),
 | 
			
		||||
                  title: const Text('Als gelesen markieren'),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    setCurrentAsRead();
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
@@ -151,7 +151,7 @@ class _ChatTileState extends State<ChatTile> {
 | 
			
		||||
                visible: widget.data.isFavorite,
 | 
			
		||||
                replacement: ListTile(
 | 
			
		||||
                  leading: const Icon(Icons.star_outline),
 | 
			
		||||
                  title: const Text("Zu Favoriten hinzufügen"),
 | 
			
		||||
                  title: const Text('Zu Favoriten hinzufügen'),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    SetFavorite(widget.data.token, true).run().then((value) => widget.query(renew: true));
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
@@ -159,7 +159,7 @@ class _ChatTileState extends State<ChatTile> {
 | 
			
		||||
                ),
 | 
			
		||||
                child: ListTile(
 | 
			
		||||
                  leading: const Icon(Icons.stars_outlined),
 | 
			
		||||
                  title: const Text("Von Favoriten entfernen"),
 | 
			
		||||
                  title: const Text('Von Favoriten entfernen'),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    SetFavorite(widget.data.token, false).run().then((value) => widget.query(renew: true));
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
@@ -168,12 +168,12 @@ class _ChatTileState extends State<ChatTile> {
 | 
			
		||||
              ),
 | 
			
		||||
              ListTile(
 | 
			
		||||
                leading: const Icon(Icons.delete_outline),
 | 
			
		||||
                title: const Text("Konversation verlassen"),
 | 
			
		||||
                title: const Text('Konversation verlassen'),
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  ConfirmDialog(
 | 
			
		||||
                    title: "Chat verlassen",
 | 
			
		||||
                    content: "Du benötigst ggf. eine Einladung um erneut beizutreten.",
 | 
			
		||||
                    confirmButton: "Löschen",
 | 
			
		||||
                    title: 'Chat verlassen',
 | 
			
		||||
                    content: 'Du benötigst ggf. eine Einladung um erneut beizutreten.',
 | 
			
		||||
                    confirmButton: 'Löschen',
 | 
			
		||||
                    onConfirm: () {
 | 
			
		||||
                      LeaveRoom(widget.data.token).run().then((value) => widget.query(renew: true));
 | 
			
		||||
                      Navigator.of(context).pop();
 | 
			
		||||
 
 | 
			
		||||
@@ -17,10 +17,10 @@ class SplitViewPlaceholder extends StatelessWidget {
 | 
			
		||||
              data: MediaQuery.of(context).copyWith(
 | 
			
		||||
                invertColors: !AppTheme.isDarkMode(context),
 | 
			
		||||
              ),
 | 
			
		||||
              child: Image.asset("assets/logo/icon.png", height: 200),
 | 
			
		||||
              child: Image.asset('assets/logo/icon.png', height: 200),
 | 
			
		||||
            ),
 | 
			
		||||
            const SizedBox(height: 30),
 | 
			
		||||
            const Text("Marianum Fulda\nTalk", textAlign: TextAlign.center, style: TextStyle(fontSize: 30)),
 | 
			
		||||
            const Text('Marianum Fulda\nTalk', textAlign: TextAlign.center, style: TextStyle(fontSize: 30)),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      )
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ class JoinChat extends SearchDelegate<String> {
 | 
			
		||||
          return const SizedBox.shrink();
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
      if(query.isNotEmpty) IconButton(onPressed: () => query = "", icon: const Icon(Icons.delete)),
 | 
			
		||||
      if(query.isNotEmpty) IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)),
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -48,7 +48,7 @@ class JoinChat extends SearchDelegate<String> {
 | 
			
		||||
 | 
			
		||||
    if(query.isEmpty) {
 | 
			
		||||
      return const PlaceholderView(
 | 
			
		||||
        text: "Suche nach benutzern",
 | 
			
		||||
        text: 'Suche nach benutzern',
 | 
			
		||||
        icon: Icons.person_search_outlined,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
@@ -63,7 +63,7 @@ class JoinChat extends SearchDelegate<String> {
 | 
			
		||||
            itemBuilder: (context, index) {
 | 
			
		||||
              AutocompleteResponseObject object = snapshot.data!.data[index];
 | 
			
		||||
              CircleAvatar circleAvatar = CircleAvatar(
 | 
			
		||||
                foregroundImage: Image.network("https://${EndpointData().nextcloud().full()}/avatar/${object.id}/128").image,
 | 
			
		||||
                foregroundImage: Image.network('https://${EndpointData().nextcloud().full()}/avatar/${object.id}/128').image,
 | 
			
		||||
                backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
                foregroundColor: Colors.white,
 | 
			
		||||
                child: const Icon(Icons.person),
 | 
			
		||||
@@ -80,7 +80,7 @@ class JoinChat extends SearchDelegate<String> {
 | 
			
		||||
            }
 | 
			
		||||
          );
 | 
			
		||||
        } else if(snapshot.hasError) {
 | 
			
		||||
          return const PlaceholderView(icon: Icons.search_off, text: "Ein fehler ist aufgetreten. Bist du mit dem Internet verbunden?");
 | 
			
		||||
          return const PlaceholderView(icon: Icons.search_off, text: 'Ein fehler ist aufgetreten. Bist du mit dem Internet verbunden?');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return const Center(child: CircularProgressIndicator());
 | 
			
		||||
 
 | 
			
		||||
@@ -33,13 +33,13 @@ class _MessageReactionsState extends State<MessageReactions> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Reaktionen"),
 | 
			
		||||
        title: const Text('Reaktionen'),
 | 
			
		||||
      ),
 | 
			
		||||
      body: FutureBuilder(
 | 
			
		||||
        future: data,
 | 
			
		||||
        builder: (context, snapshot) {
 | 
			
		||||
          if(snapshot.connectionState == ConnectionState.waiting) return const LoadingSpinner();
 | 
			
		||||
          if(snapshot.data == null) return const PlaceholderView(icon: Icons.search_off_outlined, text: "Keine Reaktionen gefunden!");
 | 
			
		||||
          if(snapshot.data == null) return const PlaceholderView(icon: Icons.search_off_outlined, text: 'Keine Reaktionen gefunden!');
 | 
			
		||||
          return ListView(
 | 
			
		||||
            children: [
 | 
			
		||||
              ...snapshot.data!.data.entries.map<Widget>((entry) {
 | 
			
		||||
@@ -49,17 +49,17 @@ class _MessageReactionsState extends State<MessageReactions> {
 | 
			
		||||
                  iconColor: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                  collapsedIconColor: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
 | 
			
		||||
                  subtitle: const Text("Tippe für mehr"),
 | 
			
		||||
                  subtitle: const Text('Tippe für mehr'),
 | 
			
		||||
                  leading: CenteredLeading(Text(entry.key)),
 | 
			
		||||
                  title: Text("${entry.value.length} mal reagiert"),
 | 
			
		||||
                  title: Text('${entry.value.length} mal reagiert'),
 | 
			
		||||
                  children: entry.value.map((e) {
 | 
			
		||||
                    bool isSelf = AccountData().getUsername() == e.actorId;
 | 
			
		||||
                    return ListTile(
 | 
			
		||||
                      leading: UserAvatar(id: e.actorId, isGroup: false),
 | 
			
		||||
                      title: Text(e.actorDisplayName),
 | 
			
		||||
                      subtitle: isSelf
 | 
			
		||||
                        ? const Text("Du")
 | 
			
		||||
                        : e.actorType == GetReactionsResponseObjectActorType.guests ? const Text("Gast") : null,
 | 
			
		||||
                        ? const Text('Du')
 | 
			
		||||
                        : e.actorType == GetReactionsResponseObjectActorType.guests ? const Text('Gast') : null,
 | 
			
		||||
                      trailing: isSelf
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : Visibility(
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class SearchChat extends SearchDelegate {
 | 
			
		||||
  @override
 | 
			
		||||
  List<Widget>? buildActions(BuildContext context) {
 | 
			
		||||
    return [
 | 
			
		||||
      if(query.isNotEmpty) IconButton(onPressed: () => query = "", icon: const Icon(Icons.delete)),
 | 
			
		||||
      if(query.isNotEmpty) IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)),
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,7 @@ class _AppointmentComponentState extends State<AppointmentComponent> {
 | 
			
		||||
                    FittedBox(
 | 
			
		||||
                      fit: BoxFit.fitWidth,
 | 
			
		||||
                      child: Text(
 | 
			
		||||
                        (meeting.location == null || meeting.location!.isEmpty ? " " : meeting.location!),
 | 
			
		||||
                        (meeting.location == null || meeting.location!.isEmpty ? ' ' : meeting.location!),
 | 
			
		||||
                        maxLines: 3,
 | 
			
		||||
                        overflow: TextOverflow.ellipsis,
 | 
			
		||||
                        softWrap: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -27,9 +27,9 @@ import 'customTimetableEventEditDialog.dart';
 | 
			
		||||
 | 
			
		||||
class AppointmentDetails {
 | 
			
		||||
  static String _getEventPrefix(String? code) {
 | 
			
		||||
    if(code == "cancelled") return "Entfällt: ";
 | 
			
		||||
    if(code == "irregular") return "Änderung: ";
 | 
			
		||||
    return code ?? "";
 | 
			
		||||
    if(code == 'cancelled') return 'Entfällt: ';
 | 
			
		||||
    if(code == 'irregular') return 'Änderung: ';
 | 
			
		||||
    return code ?? '';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static void show(BuildContext context, TimetableProps webuntisData, Appointment appointment) {
 | 
			
		||||
@@ -65,13 +65,13 @@ class AppointmentDetails {
 | 
			
		||||
    try {
 | 
			
		||||
      subject = webuntisData.getSubjectsResponse.result.firstWhere((subject) => subject.id == timetableData.su[0].id);
 | 
			
		||||
    } catch(e) {
 | 
			
		||||
      subject = GetSubjectsResponseObject(0, "?", "Unbekannt", "?", true);
 | 
			
		||||
      subject = GetSubjectsResponseObject(0, '?', 'Unbekannt', '?', true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      room = webuntisData.getRoomsResponse.result.firstWhere((room) => room.id == timetableData.ro[0].id);
 | 
			
		||||
    } catch(e) {
 | 
			
		||||
      room = GetRoomsResponseObject(0, "?", "Unbekannt", true, "?");
 | 
			
		||||
      room = GetRoomsResponseObject(0, '?', 'Unbekannt', true, '?');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _bottomSheet(
 | 
			
		||||
@@ -99,7 +99,7 @@ class AppointmentDetails {
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              leading: const Icon(Icons.room),
 | 
			
		||||
              title: Text("Raum: ${room.name} (${room.longName})"),
 | 
			
		||||
              title: Text('Raum: ${room.name} (${room.longName})'),
 | 
			
		||||
              trailing: IconButton(
 | 
			
		||||
                icon: const Icon(Icons.house_outlined),
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
@@ -111,7 +111,7 @@ class AppointmentDetails {
 | 
			
		||||
              leading: const Icon(Icons.person),
 | 
			
		||||
              title: timetableData.te.isNotEmpty
 | 
			
		||||
                  ? Text("Lehrkraft: ${timetableData.te[0].name} ${timetableData.te[0].longname.isNotEmpty ? "(${timetableData.te[0].longname})" : ""}")
 | 
			
		||||
                  : const Text("?"),
 | 
			
		||||
                  : const Text('?'),
 | 
			
		||||
              trailing: Visibility(
 | 
			
		||||
                visible: !kReleaseMode,
 | 
			
		||||
                child: IconButton(
 | 
			
		||||
@@ -124,7 +124,7 @@ class AppointmentDetails {
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              leading: const Icon(Icons.abc),
 | 
			
		||||
              title: Text("Typ: ${timetableData.activityType}"),
 | 
			
		||||
              title: Text('Typ: ${timetableData.activityType}'),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              leading: const Icon(Icons.people),
 | 
			
		||||
@@ -140,9 +140,9 @@ class AppointmentDetails {
 | 
			
		||||
  static Completer deleteCustomEvent(BuildContext context, CustomTimetableEvent appointment) {
 | 
			
		||||
    Completer future = Completer();
 | 
			
		||||
    ConfirmDialog(
 | 
			
		||||
      title: "Termin löschen",
 | 
			
		||||
      title: 'Termin löschen',
 | 
			
		||||
      content: "Der ${appointment.rrule.isEmpty ? "Termin" : "Serientermin"} wird unwiederruflich gelöscht.",
 | 
			
		||||
      confirmButton: "Löschen",
 | 
			
		||||
      confirmButton: 'Löschen',
 | 
			
		||||
      onConfirm: () {
 | 
			
		||||
        RemoveCustomTimetableEvent(
 | 
			
		||||
            RemoveCustomTimetableEventParams(
 | 
			
		||||
@@ -186,14 +186,14 @@ class AppointmentDetails {
 | 
			
		||||
                        builder: (context) => CustomTimetableEventEditDialog(existingEvent: appointment),
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                    label: const Text("Bearbeiten"),
 | 
			
		||||
                    label: const Text('Bearbeiten'),
 | 
			
		||||
                    icon: const Icon(Icons.edit_outlined),
 | 
			
		||||
                  ),
 | 
			
		||||
                  TextButton.icon(
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      deleteCustomEvent(context, appointment).future.then((value) => Navigator.of(context).pop());
 | 
			
		||||
                    },
 | 
			
		||||
                    label: const Text("Löschen"),
 | 
			
		||||
                    label: const Text('Löschen'),
 | 
			
		||||
                    icon: const Icon(Icons.delete_outline),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
@@ -202,7 +202,7 @@ class AppointmentDetails {
 | 
			
		||||
            const Divider(),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              leading: const Icon(Icons.info_outline),
 | 
			
		||||
              title: Text(appointment.description.isEmpty ? "Keine Beschreibung" : appointment.description),
 | 
			
		||||
              title: Text(appointment.description.isEmpty ? 'Keine Beschreibung' : appointment.description),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              leading: const CenteredLeading(Icon(Icons.repeat_outlined)),
 | 
			
		||||
@@ -210,10 +210,10 @@ class AppointmentDetails {
 | 
			
		||||
              subtitle: FutureBuilder(
 | 
			
		||||
                future: RruleL10nEn.create(),
 | 
			
		||||
                builder: (context, snapshot) {
 | 
			
		||||
                  if(appointment.rrule.isEmpty) return const Text("Keine weiteren vorkomnisse");
 | 
			
		||||
                  if(snapshot.data == null) return const Text("...");
 | 
			
		||||
                  if(appointment.rrule.isEmpty) return const Text('Keine weiteren vorkomnisse');
 | 
			
		||||
                  if(snapshot.data == null) return const Text('...');
 | 
			
		||||
                  RecurrenceRule rrule = RecurrenceRule.fromString(appointment.rrule);
 | 
			
		||||
                  if(!rrule.canFullyConvertToText) return const Text("Keine genauere Angabe möglich.");
 | 
			
		||||
                  if(!rrule.canFullyConvertToText) return const Text('Keine genauere Angabe möglich.');
 | 
			
		||||
                  return Text(rrule.toText(l10n: snapshot.data!));
 | 
			
		||||
                },
 | 
			
		||||
              )
 | 
			
		||||
@@ -221,8 +221,8 @@ class AppointmentDetails {
 | 
			
		||||
            DebugTile(context).child(
 | 
			
		||||
              ListTile(
 | 
			
		||||
                leading: const CenteredLeading(Icon(Icons.rule)),
 | 
			
		||||
                title: const Text("RRule"),
 | 
			
		||||
                subtitle: Text(appointment.rrule.isEmpty ? "Keine" : appointment.rrule),
 | 
			
		||||
                title: const Text('RRule'),
 | 
			
		||||
                subtitle: Text(appointment.rrule.isEmpty ? 'Keine' : appointment.rrule),
 | 
			
		||||
              )
 | 
			
		||||
            ),
 | 
			
		||||
            DebugTile(context).jsonData(appointment.toJson()),
 | 
			
		||||
 
 | 
			
		||||
@@ -15,16 +15,16 @@ class TimetableColors {
 | 
			
		||||
  static ColorModeDisplay getDisplayOptions(CustomTimetableColors color) {
 | 
			
		||||
    switch(color) {
 | 
			
		||||
      case CustomTimetableColors.green:
 | 
			
		||||
        return ColorModeDisplay(color: Colors.green, displayName: "Grün");
 | 
			
		||||
        return ColorModeDisplay(color: Colors.green, displayName: 'Grün');
 | 
			
		||||
 | 
			
		||||
      case CustomTimetableColors.blue:
 | 
			
		||||
        return ColorModeDisplay(color: Colors.blue, displayName: "Blau");
 | 
			
		||||
        return ColorModeDisplay(color: Colors.blue, displayName: 'Blau');
 | 
			
		||||
 | 
			
		||||
      case CustomTimetableColors.orange:
 | 
			
		||||
        return ColorModeDisplay(color: Colors.orange.shade800, displayName: "Orange");
 | 
			
		||||
        return ColorModeDisplay(color: Colors.orange.shade800, displayName: 'Orange');
 | 
			
		||||
 | 
			
		||||
      case CustomTimetableColors.red:
 | 
			
		||||
        return ColorModeDisplay(color: DarkAppTheme.marianumRed, displayName: "Rot");
 | 
			
		||||
        return ColorModeDisplay(color: DarkAppTheme.marianumRed, displayName: 'Rot');
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import 'dart:developer';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:jiffy/jiffy.dart';
 | 
			
		||||
import 'package:marianum_mobile/extensions/dateTime.dart';
 | 
			
		||||
import '../../../extensions/dateTime.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:rrule_generator/rrule_generator.dart';
 | 
			
		||||
import 'package:time_range_picker/time_range_picker.dart';
 | 
			
		||||
@@ -32,7 +32,7 @@ class _AddCustomTimetableEventDialogState extends State<CustomTimetableEventEdit
 | 
			
		||||
  late TimeOfDay _endTime = widget.existingEvent?.endDate.toTimeOfDay() ?? const TimeOfDay(hour: 09, minute: 30);
 | 
			
		||||
  late final TextEditingController _eventName = TextEditingController(text: widget.existingEvent?.title);
 | 
			
		||||
  late final TextEditingController _eventDescription = TextEditingController(text: widget.existingEvent?.description);
 | 
			
		||||
  late String _recurringRule = widget.existingEvent?.rrule ?? "";
 | 
			
		||||
  late String _recurringRule = widget.existingEvent?.rrule ?? '';
 | 
			
		||||
  late CustomTimetableColors _customTimetableColor = CustomTimetableColors.values.firstWhere(
 | 
			
		||||
    (element) => element.name == widget.existingEvent?.color,
 | 
			
		||||
    orElse: () => TimetableColors.defaultColor
 | 
			
		||||
@@ -64,7 +64,7 @@ class _AddCustomTimetableEventDialogState extends State<CustomTimetableEventEdit
 | 
			
		||||
                controller: _eventName,
 | 
			
		||||
                autofocus: true,
 | 
			
		||||
                decoration: const InputDecoration(
 | 
			
		||||
                  labelText: "Terminname",
 | 
			
		||||
                  labelText: 'Terminname',
 | 
			
		||||
                  border: OutlineInputBorder()
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
@@ -75,7 +75,7 @@ class _AddCustomTimetableEventDialogState extends State<CustomTimetableEventEdit
 | 
			
		||||
                maxLines: 2,
 | 
			
		||||
                minLines: 2,
 | 
			
		||||
                decoration: const InputDecoration(
 | 
			
		||||
                  labelText: "Beschreibung",
 | 
			
		||||
                  labelText: 'Beschreibung',
 | 
			
		||||
                  border: OutlineInputBorder()
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
@@ -84,7 +84,7 @@ class _AddCustomTimetableEventDialogState extends State<CustomTimetableEventEdit
 | 
			
		||||
            ListTile(
 | 
			
		||||
              leading: const Icon(Icons.date_range_outlined),
 | 
			
		||||
              title: Text(Jiffy.parseFromDateTime(_date).yMMMd),
 | 
			
		||||
              subtitle: const Text("Datum"),
 | 
			
		||||
              subtitle: const Text('Datum'),
 | 
			
		||||
              onTap: () async {
 | 
			
		||||
                final DateTime? pickedDate = await showDatePicker(
 | 
			
		||||
                  context: context,
 | 
			
		||||
@@ -101,8 +101,8 @@ class _AddCustomTimetableEventDialogState extends State<CustomTimetableEventEdit
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              leading: const Icon(Icons.access_time_outlined),
 | 
			
		||||
              title: Text("${_startTime.format(context).toString()} - ${_endTime.format(context).toString()}"),
 | 
			
		||||
              subtitle: const Text("Zeitraum"),
 | 
			
		||||
              title: Text('${_startTime.format(context).toString()} - ${_endTime.format(context).toString()}'),
 | 
			
		||||
              subtitle: const Text('Zeitraum'),
 | 
			
		||||
              onTap: () async {
 | 
			
		||||
                TimeRange timeRange = await showTimeRangePicker(
 | 
			
		||||
                  context: context,
 | 
			
		||||
@@ -112,8 +112,8 @@ class _AddCustomTimetableEventDialogState extends State<CustomTimetableEventEdit
 | 
			
		||||
                  disabledColor: Colors.grey,
 | 
			
		||||
                  paintingStyle: PaintingStyle.fill,
 | 
			
		||||
                  interval: const Duration(minutes: 5),
 | 
			
		||||
                  fromText: "Beginnend",
 | 
			
		||||
                  toText: "Endend",
 | 
			
		||||
                  fromText: 'Beginnend',
 | 
			
		||||
                  toText: 'Endend',
 | 
			
		||||
                  strokeColor: Theme.of(context).colorScheme.secondary,
 | 
			
		||||
                  minDuration: const Duration(minutes: 15),
 | 
			
		||||
                  selectedColor: Theme.of(context).primaryColor,
 | 
			
		||||
@@ -129,7 +129,7 @@ class _AddCustomTimetableEventDialogState extends State<CustomTimetableEventEdit
 | 
			
		||||
            const Divider(),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              leading: const Icon(Icons.color_lens_outlined),
 | 
			
		||||
              title: const Text("Farbgebung"),
 | 
			
		||||
              title: const Text('Farbgebung'),
 | 
			
		||||
              trailing: DropdownButton<CustomTimetableColors>(
 | 
			
		||||
                value: _customTimetableColor,
 | 
			
		||||
                icon: const Icon(Icons.arrow_drop_down),
 | 
			
		||||
@@ -162,7 +162,7 @@ class _AddCustomTimetableEventDialogState extends State<CustomTimetableEventEdit
 | 
			
		||||
              initialRRule: _recurringRule,
 | 
			
		||||
              textDelegate: const GermanRRuleTextDelegate(),
 | 
			
		||||
              onChange: (String newValue) {
 | 
			
		||||
                log("Rule: $newValue");
 | 
			
		||||
                log('Rule: $newValue');
 | 
			
		||||
                setState(() {
 | 
			
		||||
                  _recurringRule = newValue;
 | 
			
		||||
                });
 | 
			
		||||
@@ -183,7 +183,7 @@ class _AddCustomTimetableEventDialogState extends State<CustomTimetableEventEdit
 | 
			
		||||
            if(!validate()) return;
 | 
			
		||||
 | 
			
		||||
            CustomTimetableEvent editedEvent = CustomTimetableEvent(
 | 
			
		||||
              id: "",
 | 
			
		||||
              id: '',
 | 
			
		||||
              title: _eventName.text,
 | 
			
		||||
              description: _eventDescription.text,
 | 
			
		||||
              startDate: _date.withTime(_startTime),
 | 
			
		||||
@@ -210,7 +210,7 @@ class _AddCustomTimetableEventDialogState extends State<CustomTimetableEventEdit
 | 
			
		||||
            } else {
 | 
			
		||||
              UpdateCustomTimetableEvent(
 | 
			
		||||
                UpdateCustomTimetableEventParams(
 | 
			
		||||
                  widget.existingEvent?.id ?? "",
 | 
			
		||||
                  widget.existingEvent?.id ?? '',
 | 
			
		||||
                  editedEvent
 | 
			
		||||
                )
 | 
			
		||||
              ).run().then((value) {
 | 
			
		||||
@@ -224,7 +224,7 @@ class _AddCustomTimetableEventDialogState extends State<CustomTimetableEventEdit
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
          },
 | 
			
		||||
          child: Text(isEditingExisting ? "Speichern" : "Erstellen"),
 | 
			
		||||
          child: Text(isEditingExisting ? 'Speichern' : 'Erstellen'),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -33,12 +33,12 @@ class _TimeRegionComponentState extends State<TimeRegionComponent> {
 | 
			
		||||
          children: [
 | 
			
		||||
            const SizedBox(height: 15),
 | 
			
		||||
            const Icon(Icons.cake),
 | 
			
		||||
            const Text("FREI"),
 | 
			
		||||
            const Text('FREI'),
 | 
			
		||||
            const SizedBox(height: 10),
 | 
			
		||||
            RotatedBox(
 | 
			
		||||
              quarterTurns: 1,
 | 
			
		||||
              child: Text(
 | 
			
		||||
                text.split(":").last,
 | 
			
		||||
                text.split(':').last,
 | 
			
		||||
                maxLines: 1,
 | 
			
		||||
                style: const TextStyle(
 | 
			
		||||
                  fontWeight: FontWeight.bold,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:marianum_mobile/extensions/dateTime.dart';
 | 
			
		||||
import '../../../extensions/dateTime.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:syncfusion_flutter_calendar/calendar.dart';
 | 
			
		||||
 | 
			
		||||
@@ -57,7 +57,7 @@ class _TimetableState extends State<Timetable> {
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Stunden & Vertretungsplan"),
 | 
			
		||||
        title: const Text('Stunden & Vertretungsplan'),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
              icon: const Icon(Icons.home_outlined),
 | 
			
		||||
@@ -74,11 +74,11 @@ class _TimetableState extends State<Timetable> {
 | 
			
		||||
                  Icon icon;
 | 
			
		||||
                  switch(e) {
 | 
			
		||||
                    case CalendarActions.addEvent:
 | 
			
		||||
                      title = "Kalendereintrag hinzufügen";
 | 
			
		||||
                      title = 'Kalendereintrag hinzufügen';
 | 
			
		||||
                      icon = const Icon(Icons.add);
 | 
			
		||||
                    case CalendarActions.viewEvents:
 | 
			
		||||
                    default:
 | 
			
		||||
                      title = "Kalendereinträge anzeigen";
 | 
			
		||||
                      title = 'Kalendereinträge anzeigen';
 | 
			
		||||
                      icon = const Icon(Icons.perm_contact_calendar_outlined);
 | 
			
		||||
                  }
 | 
			
		||||
                  return PopupMenuItem<CalendarActions>(
 | 
			
		||||
@@ -112,9 +112,9 @@ class _TimetableState extends State<Timetable> {
 | 
			
		||||
          if(value.hasError) {
 | 
			
		||||
            return PlaceholderView(
 | 
			
		||||
              icon: Icons.calendar_month,
 | 
			
		||||
              text: "Webuntis error: ${value.error.toString()}",
 | 
			
		||||
              text: 'Webuntis error: ${value.error.toString()}',
 | 
			
		||||
              button: TextButton(
 | 
			
		||||
                child: const Text("Neu laden"),
 | 
			
		||||
                child: const Text('Neu laden'),
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
                  controller.displayDate = DateTime.now().add(const Duration(days: 2));
 | 
			
		||||
                  Provider.of<TimetableProps>(context, listen: false).resetWeek();
 | 
			
		||||
@@ -155,8 +155,8 @@ class _TimetableState extends State<Timetable> {
 | 
			
		||||
                  startHour: 07.5,
 | 
			
		||||
                  endHour: 16.5,
 | 
			
		||||
                  timeInterval: Duration(minutes: 30),
 | 
			
		||||
                  timeFormat: "HH:mm",
 | 
			
		||||
                  dayFormat: "EE",
 | 
			
		||||
                  timeFormat: 'HH:mm',
 | 
			
		||||
                  dayFormat: 'EE',
 | 
			
		||||
                  timeIntervalHeight: 40,
 | 
			
		||||
                ),
 | 
			
		||||
 | 
			
		||||
@@ -259,10 +259,10 @@ class _TimetableState extends State<Timetable> {
 | 
			
		||||
          startTime: startTime,
 | 
			
		||||
          endTime: endTime,
 | 
			
		||||
          subject: subjects.result.firstWhere((subject) => subject.id == element.su[0].id).name,
 | 
			
		||||
          location: ""
 | 
			
		||||
              "${rooms.result.firstWhere((room) => room.id == element.ro[0].id).name}"
 | 
			
		||||
              "\n"
 | 
			
		||||
              "${element.te.first.longname}",
 | 
			
		||||
          location: ''
 | 
			
		||||
              '${rooms.result.firstWhere((room) => room.id == element.ro[0].id).name}'
 | 
			
		||||
              '\n'
 | 
			
		||||
              '${element.te.first.longname}',
 | 
			
		||||
          notes: element.activityType,
 | 
			
		||||
          color: _getEventColor(element, startTime, endTime),
 | 
			
		||||
        );
 | 
			
		||||
@@ -272,7 +272,7 @@ class _TimetableState extends State<Timetable> {
 | 
			
		||||
          id: ArbitraryAppointment(webuntis: element),
 | 
			
		||||
          startTime: _parseWebuntisTimestamp(element.date, element.startTime),
 | 
			
		||||
          endTime: endTime,
 | 
			
		||||
          subject: "Änderung",
 | 
			
		||||
          subject: 'Änderung',
 | 
			
		||||
          notes: element.info,
 | 
			
		||||
          location: 'Unbekannt',
 | 
			
		||||
          color: endTime.isBefore(DateTime.now()) ? Theme.of(context).primaryColor.withAlpha(100) : Theme.of(context).primaryColor,
 | 
			
		||||
@@ -309,10 +309,10 @@ class _TimetableState extends State<Timetable> {
 | 
			
		||||
    int alpha = endTime.isBefore(DateTime.now()) ? 100 : 255;
 | 
			
		||||
 | 
			
		||||
    // Cancelled
 | 
			
		||||
    if(webuntisElement.code == "cancelled") return const Color(0xff000000).withAlpha(alpha);
 | 
			
		||||
    if(webuntisElement.code == 'cancelled') return const Color(0xff000000).withAlpha(alpha);
 | 
			
		||||
 | 
			
		||||
    // Any changes or no teacher at this element
 | 
			
		||||
    if(webuntisElement.code == "irregular" || webuntisElement.te.first.id == 0) return const Color(0xff8F19B3).withAlpha(alpha);
 | 
			
		||||
    if(webuntisElement.code == 'irregular' || webuntisElement.te.first.id == 0) return const Color(0xff8F19B3).withAlpha(alpha);
 | 
			
		||||
 | 
			
		||||
    // Event was in the past
 | 
			
		||||
    if(endTime.isBefore(DateTime.now())) return Theme.of(context).primaryColor.withAlpha(alpha);
 | 
			
		||||
@@ -327,7 +327,7 @@ class _TimetableState extends State<Timetable> {
 | 
			
		||||
  bool _isCrossedOut(CalendarAppointmentDetails calendarEntry) {
 | 
			
		||||
    ArbitraryAppointment appointment = calendarEntry.appointments.first.id as ArbitraryAppointment;
 | 
			
		||||
    if(appointment.hasWebuntis()) {
 | 
			
		||||
      return appointment.webuntis!.code == "cancelled";
 | 
			
		||||
      return appointment.webuntis!.code == 'cancelled';
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ class _ViewCustomTimetableEventsState extends State<ViewCustomTimetableEvents> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Eigene Termine"),
 | 
			
		||||
        title: const Text('Eigene Termine'),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: const Icon(Icons.add),
 | 
			
		||||
@@ -77,10 +77,10 @@ class _ViewCustomTimetableEventsState extends State<ViewCustomTimetableEvents> {
 | 
			
		||||
 | 
			
		||||
        var placeholder = PlaceholderView(
 | 
			
		||||
          icon: Icons.calendar_today_outlined,
 | 
			
		||||
          text: "Keine Einträge vorhanden",
 | 
			
		||||
          text: 'Keine Einträge vorhanden',
 | 
			
		||||
          button: TextButton(
 | 
			
		||||
            onPressed: _openCreateDialog,
 | 
			
		||||
            child: const Text("Termin erstellen"),
 | 
			
		||||
            child: const Text('Termin erstellen'),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user