Refactor codebase resolving warnings and remove self-package imports

This commit is contained in:
2023-06-03 11:27:14 +02:00
parent 6d0898d6ac
commit f0da6f2596
79 changed files with 204 additions and 193 deletions

120
lib/view/login/login.dart Normal file
View File

@ -0,0 +1,120 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_login/flutter_login.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../api/apiError.dart';
import '../../api/webuntis/queries/authenticate/authenticateParams.dart';
import '../../api/webuntis/queries/authenticate/authenticate.dart';
import '../../api/webuntis/webuntisError.dart';
import '../../model/accountModel.dart';
class Login extends StatefulWidget {
const Login({Key? key}) : super(key: key);
@override
State<Login> createState() => _LoginState();
}
class _LoginState extends State<Login> {
bool displayDisclaimerText = true;
String? _checkInput(value){
return (value ?? "").length == 0 ? "Eingabe erforderlich" : null;
}
Future<String?> _login(LoginData data) async {
SharedPreferences preferences = await SharedPreferences.getInstance();
preferences.setBool("loggedIn", false);
try {
await Authenticate(
AuthenticateParams(
user: data.name,
password: data.password,
)
).run().then((value) => {
log(value.sessionId)
});
} on WebuntisError catch(e) {
return e.toString();
} on ApiError catch(e) {
return e.toString();
}
setState(() {
displayDisclaimerText = false;
});
preferences.setBool("loggedIn", true);
preferences.setString("username", data.name);
preferences.setString("password", data.password);
return null;
}
Future<String> _resetPassword(String name) {
return Future.delayed(Duration.zero).then((_) {
return "Diese Funktion steht nicht zur Verfügung!";
});
}
@override
Widget build(BuildContext context) {
return FlutterLogin(
logo: Image.asset("assets/logo/icon.png").image,
userValidator: _checkInput,
passwordValidator: _checkInput,
onSubmitAnimationCompleted: () => Provider.of<AccountModel>(context, listen: false).login(),
onLogin: _login,
onSignup: null,
onRecoverPassword: _resetPassword,
hideForgotPasswordButton: true,
theme: LoginTheme(
primaryColor: Theme.of(context).primaryColor,
accentColor: Colors.white,
errorColor: Theme.of(context).primaryColor,
footerBottomPadding: 10,
textFieldStyle: const TextStyle(
fontWeight: FontWeight.w500
),
cardTheme: const CardTheme(
elevation: 10,
),
),
messages: LoginMessages(
loginButton: "Anmelden",
userHint: "Nutzername",
passwordHint: "Passwort",
),
disableCustomPageTransformer: true,
headerWidget: Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Center(
child: Visibility(
visible: displayDisclaimerText,
child: const Text(
"Dies ist ein Inoffizieller Nextcloud & Webuntis Client und wird nicht vom Marianum selbst betrieben.\nKeinerlei Gewähr für Vollständigkeit, Richtigkeit und Aktualität!",
textAlign: TextAlign.center,
),
),
),
),
footer: "Marianum Fulda - Die persönliche Schule",
title: "Marianum Fulda",
userType: LoginUserType.name,
);
}
}

View File

@ -0,0 +1,185 @@
import 'dart:developer';
import 'dart:io';
import 'package:better_open_file/better_open_file.dart';
import 'package:filesize/filesize.dart';
import 'package:flowder/flowder.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:path_provider/path_provider.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
import '../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../widget/confirmDialog.dart';
import '../../../widget/unimplementedDialog.dart';
import 'files.dart';
class FileElement extends StatefulWidget {
final CacheableFile file;
final List<String> path;
final void Function() refetch;
const FileElement(this.file, this.path, this.refetch, {Key? key}) : super(key: key);
static Future<DownloaderCore> download(String remotePath, String name, Function(double) onProgress, Function(OpenResult) onDone) async {
Directory paths = await getApplicationDocumentsDirectory();
String local = paths.path + Platform.pathSeparator + name;
DownloaderUtils options = DownloaderUtils(
progressCallback: (current, total) {
final progress = (current / total) * 100;
onProgress(progress);
},
file: File(local),
progress: ProgressImplementation(),
deleteOnCancel: true,
onDone: () {
Future<OpenResult> result = OpenFile.open(local);
result.then((value) => {
onDone(value)
});
},
);
return await Flowder.download(
"${await WebdavApi.webdavConnectString}$remotePath",
options,
);
}
@override
State<FileElement> createState() => _FileElementState();
}
class _FileElementState extends State<FileElement> {
double percent = 0;
Future<DownloaderCore>? downloadCore;
Widget getSubtitle() {
if(widget.file.currentlyDownloading) {
return Row(
children: [
Container(
margin: const EdgeInsets.only(right: 10),
child: const Text("Download:"),
),
Expanded(
child: LinearProgressIndicator(value: percent/100),
),
Container(
margin: const EdgeInsets.only(left: 10),
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()}");
}
@override
Widget build(BuildContext context) {
return ListTile(
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Icon(widget.file.isDirectory ? Icons.folder_outlined : Icons.description_outlined)],
),
title: Text(widget.file.name),
subtitle: getSubtitle(),
trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null),
onTap: () {
if(widget.file.isDirectory) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) {
return Files(widget.path.toList()..add(widget.file.name));
},
));
} else {
if(widget.file.currentlyDownloading) {
showDialog(context: context, builder: (context) {
return AlertDialog(
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")),
TextButton(onPressed: () {
downloadCore?.then((value) {
if(!value.isCancelled) value.cancel();
Navigator.of(context).pop();
});
setState(() {
widget.file.currentlyDownloading = false;
percent = 0;
downloadCore = null;
});
}, child: const Text("Ja, Abbrechen"))
],
);
});
return;
}
setState(() {
widget.file.currentlyDownloading = true;
});
log("Download: ${widget.file.path} to ${widget.file.name}");
downloadCore = FileElement.download(widget.file.path, widget.file.name, (progress) {
setState(() => percent = progress);
}, (result) {
if(result.type != ResultType.done) {
showDialog(context: context, builder: (context) {
return AlertDialog(
title: const Text("Download"),
content: Text(result.message),
);
});
}
setState(() {
widget.file.currentlyDownloading = false;
percent = 0;
});
});
}
},
onLongPress: () {
showDialog(context: context, builder: (context) {
return SimpleDialog(
children: [
ListTile(
leading: const Icon(Icons.delete_outline),
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.",
onConfirm: () {
WebdavApi.webdav
.then((value) => value.delete(widget.file.path))
.then((value) => widget.refetch());
}
));
},
),
ListTile(
leading: const Icon(Icons.share_outlined),
title: const Text("Teilen"),
onTap: () {
Navigator.of(context).pop();
UnimplementedDialog.show(context);
},
)
],
);
});
},
);
}
}

View File

@ -0,0 +1,227 @@
import 'dart:developer';
import 'dart:io';
import 'package:async/async.dart';
import 'package:flutter/material.dart';
import 'package:nextcloud/nextcloud.dart';
import '../../../api/marianumcloud/webdav/webdavApi.dart';
class FileUploadDialog extends StatefulWidget {
final String localPath;
final List<String> remotePath;
final String fileName;
final void Function() triggerReload;
const FileUploadDialog({Key? key, required this.localPath, required this.remotePath, required this.fileName, required this.triggerReload}) : super(key: key);
@override
State<FileUploadDialog> createState() => _FileUploadDialogState();
}
class _FileUploadDialogState extends State<FileUploadDialog> {
FileUploadState state = FileUploadState.naming;
CancelableOperation? cancelableOperation;
late String targetFileName;
late String remoteFolderName;
late String fullRemotePath = "${widget.remotePath.join("/")}/$targetFileName";
String? lastError;
TextEditingController fileNameController = TextEditingController();
void upload({bool override = false}) async {
setState(() {
state = FileUploadState.upload;
});
WebDavClient webdavClient = await WebdavApi.webdav;
if(!override) {
setState(() {
state = FileUploadState.checkConflict;
});
List<WebDavResponse> result = (await webdavClient.ls(widget.remotePath.join("/"))).responses;
if(result.any((element) => element.href!.endsWith("/$targetFileName"))) {
setState(() {
state = FileUploadState.conflict;
});
return;
} else {
setState(() {
state = FileUploadState.upload;
});
}
}
Future<HttpClientResponse> uploadTask = webdavClient.upload(File(widget.localPath).readAsBytesSync(), fullRemotePath);
uploadTask.then((value) => Future<HttpClientResponse?>.value(value)).catchError((e) {
setState(() {
state = FileUploadState.error;
});
return null;
});
cancelableOperation = CancelableOperation<HttpClientResponse>.fromFuture(
uploadTask,
onCancel: () => log("Upload cancelled"),
);
cancelableOperation!.then((value) {
log("Upload done!");
setState(() {
state = FileUploadState.done;
});
});
}
void cancel() {
cancelableOperation?.cancel();
setState(() {
state = FileUploadState.naming;
});
}
@override
void initState() {
super.initState();
targetFileName = widget.fileName;
remoteFolderName = widget.remotePath.isNotEmpty ? widget.remotePath.last : "/";
fileNameController.text = widget.fileName;
}
@override
Widget build(BuildContext context) {
if(state == FileUploadState.naming) {
return AlertDialog(
title: const Text("Datei hochladen"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: fileNameController,
onChanged: (input) {
targetFileName = input;
},
autocorrect: false,
decoration: const InputDecoration(
labelText: "Dateiname",
),
),
],
),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop();
}, child: const Text("Abbrechen")),
TextButton(onPressed: () async {
upload();
}, child: const Text("Hochladen")),
],
);
}
if(state == FileUploadState.conflict) {
return AlertDialog(
icon: const Icon(Icons.error_outline),
title: const Text("Datei konflikt"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Es gibt bereits eine Datei mit dem Namen $targetFileName in dem ausgewählten Ordner '$remoteFolderName'", textAlign: TextAlign.center),
],
),
actions: [
TextButton(onPressed: () {
setState(() {
state = FileUploadState.naming;
});
}, child: const Text("Datei umbenennen")),
TextButton(onPressed: () {
upload(override: true);
}, child: const Text("Datei überschreiben")),
],
);
}
if(state == FileUploadState.upload || state == FileUploadState.checkConflict) {
return AlertDialog(
icon: const Icon(Icons.upload),
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),
),
const SizedBox(height: 30),
const CircularProgressIndicator()
],
),
actions: const [
// TODO implement working upload cancelling
// TextButton(onPressed: () {
// cancel();
// }, child: const Text("Abbrechen")),
],
);
}
if(state == FileUploadState.done) {
widget.triggerReload();
return AlertDialog(
icon: const Icon(Icons.upload),
title: const Text("Upload fertig"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Die Datei wurde erfolgreich nach '$remoteFolderName' hochgeladen!", textAlign: TextAlign.center),
],
),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop();
}, child: const Text("Fertig")),
],
);
}
if(state == FileUploadState.error) {
return AlertDialog(
icon: const Icon(Icons.error_outline),
title: const Text("Fehler"),
content: const Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Es ist ein Fehler aufgetreten!", textAlign: TextAlign.center),
],
),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop();
}, child: const Text("Schlißen")),
],
);
}
throw UnimplementedError("Invalid state");
}
}
enum FileUploadState {
naming,
checkConflict,
conflict,
upload,
done,
error
}

View File

@ -0,0 +1,247 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:loader_overlay/loader_overlay.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/listFilesCache.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/listFilesResponse.dart';
import '../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../model/files/filesProps.dart';
import '../../../widget/errorView.dart';
import '../../../widget/filePick.dart';
import 'fileUploadDialog.dart';
import 'fileElement.dart';
class Files extends StatefulWidget {
final List<String> path;
const Files(this.path, {Key? key}) : super(key: key);
@override
State<Files> createState() => _FilesState();
}
class BetterSortOption {
String displayName;
int Function(CacheableFile, CacheableFile) compare;
IconData icon;
BetterSortOption({required this.displayName, required this.icon, required this.compare});
}
enum SortOption {
name,
date,
size
}
class SortOptions {
static Map<SortOption, BetterSortOption> options = {
SortOption.name: BetterSortOption(
displayName: "Name",
icon: Icons.sort_by_alpha_outlined,
compare: (CacheableFile a, CacheableFile b) => a.name.compareTo(b.name)
),
SortOption.date: BetterSortOption(
displayName: "Datum",
icon: Icons.history_outlined,
compare: (CacheableFile a, CacheableFile b) => a.modifiedAt!.compareTo(b.modifiedAt!)
),
SortOption.size: BetterSortOption(
displayName: "Größe",
icon: Icons.sd_card_outlined,
compare: (CacheableFile a, CacheableFile b) {
if(a.isDirectory || b.isDirectory) return a.isDirectory ? 1 : 0;
if(a.size == null) return 0;
if(b.size == null) return 1;
return a.size!.compareTo(b.size!);
}
)
};
static BetterSortOption getOption(SortOption option) {
return options[option]!;
}
}
class _FilesState extends State<Files> {
FilesProps props = FilesProps();
ListFilesResponse? data;
SortOption currentSort = SortOption.name;
bool currentSortDirection = true;
@override
void initState() {
super.initState();
_query();
}
void _query() {
ListFilesCache(
path: widget.path.isEmpty ? "/" : widget.path.join("/"),
onUpdate: (ListFilesResponse d) {
if(!context.mounted) return; // prevent setState when widget is possibly already disposed
setState(() {
data = d;
});
}
);
}
@override
Widget build(BuildContext context) {
List<CacheableFile> files = (data?.files.toList() ?? List.empty())..sort(SortOptions.getOption(currentSort).compare);
if(currentSortDirection) files = files.reversed.toList();
return Scaffold(
appBar: AppBar(
title: Text(widget.path.isNotEmpty ? widget.path.last : "Dateien"),
actions: [
// IconButton(
// icon: const Icon(Icons.search),
// onPressed: () => {
// // TODO implement search
// },
// ),
PopupMenuButton<bool>(
icon: Icon(currentSortDirection ? Icons.text_rotate_up : Icons.text_rotation_down),
itemBuilder: (context) {
return [true, false].map((e) => PopupMenuItem<bool>(
value: e,
enabled: e != currentSortDirection,
child: Row(
children: [
Icon(e ? Icons.text_rotate_up : Icons.text_rotation_down, color: Colors.black),
const SizedBox(width: 15),
Text(e ? "Aufsteigend" : "Absteigend")
],
)
)).toList();
},
onSelected: (e) {
setState(() {
currentSortDirection = e;
});
},
),
PopupMenuButton<SortOption>(
icon: const Icon(Icons.sort),
itemBuilder: (context) {
return SortOptions.options.keys.map((key) => PopupMenuItem<SortOption>(
value: key,
enabled: key != currentSort,
child: Row(
children: [
Icon(SortOptions.getOption(key).icon, color: Colors.black),
const SizedBox(width: 15),
Text(SortOptions.getOption(key).displayName)
],
)
)).toList();
},
onSelected: (e) {
setState(() {
currentSort = e;
});
},
),
],
),
floatingActionButton: FloatingActionButton(
backgroundColor: Theme.of(context).primaryColor,
onPressed: () {
showDialog(context: context, builder: (context) {
return SimpleDialog(
children: [
ListTile(
leading: const Icon(Icons.create_new_folder_outlined),
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"),
content: TextField(
controller: inputController,
decoration: const InputDecoration(
labelText: "Name",
),
),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop();
}, child: const Text("Abbrechen")),
TextButton(onPressed: () {
WebdavApi.webdav.then((webdav) {
webdav.mkdirs("${widget.path.join("/")}/${inputController.text}").then((value) => _query());
});
Navigator.of(context).pop();
}, child: const Text("Ordner erstellen")),
],
);
});
},
),
ListTile(
leading: const Icon(Icons.upload_file),
title: const Text("Aus Dateien hochladen"),
onTap: () {
context.loaderOverlay.show();
FilePick.documentPick().then((value) {
log(value ?? "?");
mediaUpload(value);
});
Navigator.of(context).pop();
},
),
ListTile(
leading: const Icon(Icons.add_a_photo_outlined),
title: const Text("Aus Gallerie hochladen"),
onTap: () {
context.loaderOverlay.show();
FilePick.galleryPick().then((value) {
log(value?.path ?? "?");
mediaUpload(value?.path);
});
Navigator.of(context).pop();
},
),
],
);
});
},
child: const Icon(Icons.add),
),
body: data == null ? const Center(child: CircularProgressIndicator()) : data!.files.isEmpty ? const ErrorView(icon: Icons.folder_off_rounded, text: "Der Ordner ist leer") : LoaderOverlay(
child: RefreshIndicator(
onRefresh: () {
_query();
return Future.delayed(const Duration(seconds: 3));
},
child: ListView.builder(
itemCount: files.length,
itemBuilder: (context, index) {
CacheableFile file = files.toList()[index];
return FileElement(file, widget.path, _query);
},
),
)
)
);
}
void mediaUpload(String? path) async {
context.loaderOverlay.hide();
if(path == null) {
return;
}
var fileName = path.split(Platform.pathSeparator).last;
showDialog(context: context, builder: (context) => FileUploadDialog(localPath: path, remotePath: widget.path, fileName: fileName, triggerReload: () => _query()), barrierDismissible: false);
}
}

View File

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
class AddTimerDialog extends StatefulWidget {
const AddTimerDialog({Key? key}) : super(key: key);
@override
State<AddTimerDialog> createState() => _AddTimerDialogState();
}
class _AddTimerDialogState extends State<AddTimerDialog> {
DateTime selected = DateTime.now().add(const Duration(days: 1));
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text("Timer hinzufügen"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const TextField(
decoration: InputDecoration(
labelText: "Timer Name"
),
),
TextButton(onPressed: () async {
DateTime? selectedDate = await showDatePicker(context: context, initialDate: DateTime.now(), firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365 * 10)));
if(selectedDate == null) return;
setState(() {
selected = selectedDate;
});
}, child: const Text("Datum auswählen")),
TextButton(onPressed: () async {
TimeOfDay? selectedTime = await showTimePicker(context: context, initialTime: TimeOfDay.fromDateTime(DateTime.now()));
if(selectedTime == null) return;
setState(() {
selected = selected.copyWith(hour: selectedTime.hour, minute: selectedTime.minute);
});
}, child: const Text("Zeit auswählen")),
Text(selected.toString())
],
),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop();
}, child: const Text("Abbrechen")),
TextButton(onPressed: () {
// TODO add timer
}, child: const Text("Hinzufügen")),
],
);
}
}

View File

@ -0,0 +1,53 @@
import 'dart:async';
import 'package:animated_digit/animated_digit.dart';
import 'package:flutter/cupertino.dart';
class AnimatedTime extends StatefulWidget {
final Duration Function() callback;
const AnimatedTime({Key? key, required this.callback}) : super(key: key);
@override
State<AnimatedTime> createState() => _AnimatedTimeState();
}
class _AnimatedTimeState extends State<AnimatedTime> {
Duration current = Duration.zero;
@override
void initState() {
super.initState();
Timer.periodic(const Duration(seconds: 1), (Timer t) => update());
}
void update() {
setState(() {
current = widget.callback();
});
}
@override
Widget build(BuildContext context) {
return Row(
children: [
const Text("Noch "),
buildWidget(current.inDays),
const Text(" Tage, "),
buildWidget(current.inHours > 24 ? current.inHours - current.inDays * 24 : current.inHours),
const Text(":"),
buildWidget(current.inMinutes > 60 ? current.inMinutes - current.inHours * 60 : current.inMinutes),
const Text(":"),
buildWidget(current.inSeconds > 60 ? current.inSeconds - current.inMinutes * 60 : current.inSeconds),
const Text(""),
],
);
}
AnimatedDigitWidget buildWidget(int value) {
return AnimatedDigitWidget(
value: value,
duration: const Duration(milliseconds: 100),
textStyle: const TextStyle(fontSize: 15),
);
}
}

View File

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'addTimerDialog.dart';
import 'timer.dart';
class Countdown extends StatefulWidget {
const Countdown({Key? key}) : super(key: key);
@override
State<Countdown> createState() => _CountdownState();
}
class _CountdownState extends State<Countdown> {
List<Timer> timers = List.empty(growable: true);
@override
void initState() {
super.initState();
timers.add(Timer(key: const Key("1"), target: DateTime.now().add(const Duration(seconds: 20)), label: "Countdown 1"));
timers.add(Timer(key: const Key("2"), author: "goldbaja", target: DateTime.now().add(const Duration(days: 20)), label: "Sommerferien"));
timers.add(Timer(key: const Key("3"), target: DateTime.now().add(const Duration(hours: 20)), label: "Joa"));
timers.sort((a, b) => a.target.compareTo(b.target));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Countdown"),
actions: [
IconButton(onPressed: () {
showDialog(context: context, builder: (context) => const AddTimerDialog());
}, icon: const Icon(Icons.add)),
],
),
body: ReorderableListView(
shrinkWrap: true,
footer: Container(
padding: const EdgeInsets.only(top: 30),
child: Center(
child: Text("Halte und Ziehe ein Element um es umzusortieren.", style: TextStyle(color: Theme.of(context).disabledColor)),
),
),
onReorder: (int oldIndex, int newIndex) { },
children: timers,
),
// body: ListView.separated(
// itemBuilder: (context, index) => timers[index],
// separatorBuilder: (context, index) => const Divider(),
// itemCount: timers.length
// )
);
}
}

View File

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'animatedTime.dart';
class Timer extends StatefulWidget {
final DateTime target;
final String? author;
final String label;
const Timer({Key? key, required this.target, this.author, required this.label}) : super(key: key);
@override
State<Timer> createState() => _TimerState();
}
class _TimerState extends State<Timer> {
late bool isLocal;
@override
void initState() {
super.initState();
isLocal = widget.author == null;
}
@override
Widget build(BuildContext context) {
return ListTile(
leading: const Icon(Icons.timer),
title: AnimatedTime(
callback: () {
if(widget.target.isBefore(DateTime.now())) return Duration.zero;
return widget.target.difference(DateTime.now());
},
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if(!isLocal) Row(
children: [
const Text("5"),
IconButton(onPressed: () {
}, icon: const Icon(Icons.thumb_up_outlined)),
],
),
IconButton(onPressed: () {
}, icon: const Icon(Icons.star_outline))
],
),
subtitle: Text("${widget.label}${!isLocal ? "\ngeteilt von ${widget.author}" : ""}"),
);
}
}

View File

@ -0,0 +1,170 @@
import 'package:flutter/material.dart';
class GradeAverage extends StatefulWidget {
const GradeAverage({Key? key}) : super(key: key);
@override
State<GradeAverage> createState() => _GradeAverageState();
}
class _GradeAverageState extends State<GradeAverage> {
double average = 0;
bool gradeSystem = true;
List<int> grades = List.empty(growable: true);
String getGradeDisplay(int grade) {
if(gradeSystem) {
return "Note $grade";
} else {
return "$grade Punkt${grade > 1 ? "e" : ""}";
}
}
@override
Widget build(BuildContext context) {
if(grades.isNotEmpty) {
average = grades.reduce((a, b) => a + b) / grades.length;
} else {
average = 0;
}
return Scaffold(
appBar: AppBar(
title: const Text("Notendurschnittsrechner"),
actions: [
IconButton(onPressed: () {
showDialog(context: context, builder: (context) {
return AlertDialog(
title: const Text("Zurücksetzen?"),
content: const Text("Alle Einträge werden entfernt."),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop();
}, child: const Text("Abbrechen")),
TextButton(onPressed: () {
grades.clear();
setState(() {});
Navigator.of(context).pop();
}, child: const Text("Zurücksetzen"))
],
);
});
}, icon: const Icon(Icons.delete_forever)),
PopupMenuButton<bool>(
enableFeedback: true,
initialValue: gradeSystem,
icon: const Icon(Icons.more_horiz),
itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>(
value: e,
enabled: e != gradeSystem,
child: Row(
children: [
Text(e ? "Notensystem" : "Punktesystem"),
],
),
)).toList(),
onSelected: (e) {
void switchSystem() => setState(() {
grades.clear();
gradeSystem = e;
});
if(grades.isNotEmpty) {
showDialog(context: context, builder: (context) {
return AlertDialog(
title: const Text("Notensystem wechseln"),
content: const Text("Beim wechsel des Notensystems werden alle Einträge zurückgesetzt."),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop();
}, child: const Text("Abbrechen")),
TextButton(onPressed: () {
switchSystem();
Navigator.of(context).pop();
}, child: const Text("Fortfahren")),
],
);
});
} else {
switchSystem();
}
},
),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 30),
Text(average.toStringAsFixed(2), style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold)),
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"),
const SizedBox(height: 10),
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
var grade = gradeSystem ? index + 1 : 14 - index + 1;
bool isThis(int e) => e == grade;
return Material(
child: ListTile(
tileColor: grade.isEven ? Colors.transparent : Colors.transparent.withAlpha(50),
title: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(getGradeDisplay(grade)),
const SizedBox(width: 30),
IconButton(
onPressed: () {
setState(() {
if(!grades.any(isThis)) return;
grades.removeAt(grades.indexWhere(isThis));
});
},
icon: const Icon(Icons.remove),
color: Theme.of(context).colorScheme.onSurface,
),
Text("${grades.where(isThis).length}", style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
IconButton(
onPressed: () {
setState(() {
grades.add(grade);
});
},
icon: const Icon(Icons.add),
color: Theme.of(context).colorScheme.onSurface,
),
],
),
),
trailing: Visibility(
maintainState: true,
maintainAnimation: true,
maintainSize: true,
visible: grades.any(isThis),
child: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
grades.removeWhere(isThis);
});
},
),
),
),
);
},
itemCount: gradeSystem ? 6 : 15,
),
),
],
),
);
}
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../../api/mhsl/message/getMessages/getMessagesResponse.dart';
import '../../../../model/message/messageProps.dart';
import 'messageView.dart';
class Message extends StatefulWidget {
const Message({Key? key}) : super(key: key);
@override
State<Message> createState() => _MessageState();
}
class _MessageState extends State<Message> {
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<MessageProps>(context, listen: false).run();
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Marianum Message"),
),
body: Consumer<MessageProps>(builder: (context, value, child) {
if(value.primaryLoading()) return const Center(child: CircularProgressIndicator());
return RefreshIndicator(
child: ListView.builder(
itemCount: value.getMessagesResponse.messages.length,
itemBuilder: (context, index) {
GetMessagesResponseObject message = value.getMessagesResponse.messages.toList()[index];
return ListTile(
leading: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Icon(Icons.newspaper)],
),
title: Text(message.name, overflow: TextOverflow.ellipsis),
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)));
},
);
}
),
onRefresh: () {
Provider.of<MessageProps>(context, listen: false).run(renew: true);
return Future.delayed(const Duration(seconds: 3));
},
);
}),
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../api/mhsl/message/getMessages/getMessagesResponse.dart';
class MessageView extends StatefulWidget {
final String basePath;
final GetMessagesResponseObject message;
const MessageView({Key? key, required this.basePath, required this.message}) : super(key: key);
@override
State<MessageView> createState() => _MessageViewState();
}
class _MessageViewState extends State<MessageView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.message.name),
),
body: SfPdfViewer.network(
widget.basePath + widget.message.url,
enableHyperlinkNavigation: true,
onDocumentLoadFailed: (PdfDocumentLoadFailedDetails e) {
Navigator.of(context).pop();
showDialog(context: context, builder: (context) {
return AlertDialog(
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"))
],
);
});
},
onHyperlinkClicked: (PdfHyperlinkClickedDetails e) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Link öffnen"),
content: Text("Möchtest du den folgenden Link öffnen?\n${e.uri}"),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop();
}, child: const Text("Abbrechen")),
TextButton(onPressed: () {
launchUrl(Uri.parse(e.uri), mode: LaunchMode.externalApplication);
}, child: const Text("Öffnen")),
],
);
},
);
},
),
);
}
}

View File

@ -0,0 +1,36 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:persistent_bottom_nav_bar/persistent_tab_view.dart';
import '../../../widget/ListItem.dart';
import '../../settings/settings.dart';
import 'countdown/countdown.dart';
import 'gradeAverages/gradeAverage.dart';
import 'message/message.dart';
import 'roomplan/roomplan.dart';
class Overhang extends StatelessWidget {
const Overhang({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Mehr"),
actions: [
IconButton(onPressed: () => PersistentNavBarNavigator.pushNewScreen(context, screen: const Settings(), withNavBar: false), icon: const Icon(Icons.settings))
],
),
body: ListView(
children: const [
ListItemNavigator(icon: Icons.newspaper, text: "Marianum Message", target: Message()),
ListItemNavigator(icon: Icons.room, text: "Raumplan", target: Roomplan()),
ListItemNavigator(icon: Icons.calculate, text: "Notendurschnitts rechner", target: GradeAverage()),
if(!kReleaseMode) ListItemNavigator(icon: Icons.calendar_month, text: "Schulferien", target: Roomplan()),
if(!kReleaseMode) ListItemNavigator(icon: Icons.timer, text: "Countdown", target: Countdown()),
],
),
);
}
}

View File

@ -0,0 +1,23 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
class Roomplan extends StatelessWidget {
const Roomplan({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Raumplan"),
),
backgroundColor: Colors.white,
body: PhotoView(
imageProvider: Image.asset("assets/img/raumplan.jpg").image,
minScale: 0.5,
maxScale: 2.0,
backgroundDecoration: const BoxDecoration(color: Colors.white60),
)
);
}
}

View File

@ -0,0 +1,253 @@
import 'package:better_open_file/better_open_file.dart';
import 'package:bubble/bubble.dart';
import 'package:flowder/flowder.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:jiffy/jiffy.dart';
import '../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../model/appTheme.dart';
import '../../settings/debug/jsonViewer.dart';
import '../files/fileElement.dart';
import 'chatMessage.dart';
class ChatBubble extends StatefulWidget {
final BuildContext context;
final bool isSender;
final GetChatResponseObject bubbleData;
final GetRoomResponseObject chatData;
const ChatBubble({
required this.context,
required this.isSender,
required this.bubbleData,
required this.chatData,
Key? key}) : super(key: key);
@override
State<ChatBubble> createState() => _ChatBubbleState();
}
class _ChatBubbleState extends State<ChatBubble> {
// late BubbleStyle styleSystem;
// late BubbleStyle Function(bool) styleRemote;
// late BubbleStyle Function(bool) styleSelf;
BubbleStyle getSystemStyle() {
return BubbleStyle(
color: AppTheme.isDarkMode(context) ? const Color(0xff182229) : Colors.white,
borderWidth: 1,
elevation: 2,
margin: const BubbleEdges.only(bottom: 20, top: 10),
alignment: Alignment.center,
);
}
BubbleStyle getRemoteStyle(bool seamless) {
var color = AppTheme.isDarkMode(context) ? const Color(0xff202c33) : Colors.white;
return BubbleStyle(
nip: BubbleNip.leftTop,
color: seamless ? Colors.transparent : color,
borderWidth: seamless ? 0 : 1,
elevation: seamless ? 0 : 1,
margin: const BubbleEdges.only(bottom: 10, left: 10, right: 50),
alignment: Alignment.topLeft,
);
}
BubbleStyle getSelfStyle(bool seamless) {
return BubbleStyle(
nip: BubbleNip.rightBottom,
color: seamless ? Colors.transparent : const Color(0xff005c4b),
borderWidth: seamless ? 0 : 1,
elevation: seamless ? 0 : 1,
margin: const BubbleEdges.only(bottom: 10, right: 10, left: 50),
alignment: Alignment.topRight,
);
}
late ChatMessage message;
double downloadProgress = 0;
Future<DownloaderCore>? downloadCore;
Size _textSize(String text, TextStyle style) {
final TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: style),
maxLines: 1,
textDirection: TextDirection.ltr)
..layout(minWidth: 0, maxWidth: double.infinity);
return textPainter.size;
}
BubbleStyle getStyle() {
if(widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment) {
if(widget.isSender) {
return getSelfStyle(message.containsFile);
} else {
return getRemoteStyle(message.containsFile);
}
} else {
return getSystemStyle();
}
}
@override
Widget build(BuildContext context) {
message = ChatMessage(originalMessage: widget.bubbleData.message, originalData: widget.bubbleData.messageParameters);
bool showActorDisplayName = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne;
bool showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system;
var actorTextStyle = TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold);
return GestureDetector(
child: Bubble(
style: getStyle(),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.9,
minWidth: showActorDisplayName ? _textSize(widget.bubbleData.actorDisplayName, actorTextStyle).width : 30,
),
child: Stack(
children: [
Padding(
padding: EdgeInsets.only(bottom: showBubbleTime ? 18 : 0, top: showActorDisplayName ? 18 : 0),
child: FutureBuilder(
future: message.getWidget(),
builder: (context, snapshot) {
if(!snapshot.hasData) return const CircularProgressIndicator();
return snapshot.data ?? const Icon(Icons.error);
},
)
),
Visibility(
visible: showActorDisplayName,
child: Positioned(
top: 0,
left: 0,
child: Text(
widget.bubbleData.actorDisplayName,
textAlign: TextAlign.start,
style: actorTextStyle,
),
),
),
Visibility(
visible: showBubbleTime,
child: Positioned(
bottom: 0,
right: 0,
child: Text(
Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: "HH:mm"),
textAlign: TextAlign.end,
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
),
),
Visibility(
visible: downloadProgress > 0,
child: Positioned(
top: 0,
left: 0,
right: 0,
bottom: 0,
child: Stack(
children: [
const Center(child: Icon(Icons.download)),
const Center(child: CircularProgressIndicator(color: Colors.white)),
Center(child: CircularProgressIndicator(value: downloadProgress/100)),
],
)
),
),
],
),
),
),
onLongPress: () {
showDialog(context: context, builder: (context) {
return SimpleDialog(
children: [
Visibility(
visible: !message.containsFile && widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment,
child: ListTile(
leading: const Icon(Icons.copy),
title: const Text("Nachricht kopieren"),
onTap: () => {
Clipboard.setData(ClipboardData(text: widget.bubbleData.message)),
Navigator.of(context).pop(),
},
),
),
Visibility(
visible: !widget.isSender && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne,
child: ListTile(
leading: const Icon(Icons.sms_outlined),
title: Text("Private Nachricht an '${widget.bubbleData.actorDisplayName}'"),
onTap: () => {},
),
),
ListTile(
leading: const Icon(Icons.bug_report_outlined),
title: const Text("Debugdaten anzeigen"),
onTap: () => JsonViewer.asDialog(context, widget.bubbleData.toJson()),
)
],
);
});
},
onTap: () {
if(message.file == null) return;
if(downloadProgress > 0) {
showDialog(context: context, builder: (context) {
return AlertDialog(
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")),
TextButton(onPressed: () {
downloadCore?.then((value) {
if(!value.isCancelled) value.cancel();
Navigator.of(context).pop();
});
setState(() {
downloadProgress = 0;
downloadCore = null;
});
}, child: const Text("Ja, Abbrechen"))
],
);
});
return;
}
downloadProgress = 1;
downloadCore = FileElement.download(message.file!.path!, message.file!.name, (progress) {
if(progress > 1) {
setState(() {
downloadProgress = progress;
});
}
}, (result) {
setState(() {
downloadProgress = 0;
});
if(result.type != ResultType.done) {
showDialog(context: context, builder: (context) {
return AlertDialog(
content: Text(result.message),
);
});
}
});
},
);
}
}

View File

@ -0,0 +1,119 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:persistent_bottom_nav_bar/persistent_tab_view.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../model/chatList/chatListProps.dart';
import '../../../widget/unimplementedDialog.dart';
import 'chatView.dart';
class ChatList extends StatefulWidget {
const ChatList({Key? key}) : super(key: key);
@override
State<ChatList> createState() => _ChatListState();
}
class _ChatListState extends State<ChatList> {
late String username;
@override
void initState() {
super.initState();
SharedPreferences.getInstance().then((value) => {
username = value.getString("username")!
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<ChatListProps>(context, listen: false).run();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Talk"),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => {
UnimplementedDialog.show(context)
},
)
],
),
body: Consumer<ChatListProps>(
builder: (context, data, child) {
if(data.primaryLoading()) {
return const Center(child: CircularProgressIndicator());
}
List<ListTile> chats = List<ListTile>.empty(growable: true);
for (var chatRoom in data.getRoomsResponse.sortByLastActivity()) {
CircleAvatar circleAvatar = CircleAvatar(
foregroundImage: chatRoom.type == GetRoomResponseObjectConversationType.oneToOne ? Image.network("https://cloud.marianum-fulda.de/avatar/${chatRoom.name}/128").image : null,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
child: chatRoom.type == GetRoomResponseObjectConversationType.group ? const Icon(Icons.group) : const Icon(Icons.person),
);
chats.add(ListTile(
title: Text(chatRoom.displayName),
subtitle: Text("${Jiffy.parseFromMillisecondsSinceEpoch(chatRoom.lastMessage.timestamp * 1000).fromNow()}: ${RichObjectStringProcessor.parseToString(chatRoom.lastMessage.message.replaceAll("\n", " "), chatRoom.lastMessage.messageParameters)}", overflow: TextOverflow.ellipsis),
trailing: Visibility(
visible: chatRoom.unreadMessages > 0,
child: Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: Text(
"${chatRoom.unreadMessages}",
style: const TextStyle(
color: Colors.white,
fontSize: 15,
),
textAlign: TextAlign.center,
),
),
),
leading: circleAvatar,
onTap: () async {
PersistentNavBarNavigator.pushNewScreen(
context,
screen: ChatView(room: chatRoom, selfId: username, avatar: circleAvatar),
withNavBar: false
);
},
));
}
return RefreshIndicator(
color: Theme.of(context).primaryColor,
onRefresh: () {
Provider.of<ChatListProps>(context, listen: false).run(renew: true);
return Future.delayed(const Duration(seconds: 3));
},
child: ListView(children: chats),
);
},
),
);
}
}

View File

@ -0,0 +1,68 @@
import 'dart:convert';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
class ChatMessage {
String originalMessage;
Map<String, RichObjectString>? originalData;
RichObjectString? file;
String content = "";
bool get containsFile => file != null;
ChatMessage({required this.originalMessage, this.originalData}) {
if(originalData?.containsKey("file") ?? false) {
file = originalData?['file'];
content = file?.name ?? "Datei";
} else {
content = RichObjectStringProcessor.parseToString(originalMessage, originalData);
}
}
Future<Widget> getWidget() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
if(file == null) {
return SelectableLinkify(
text: content,
onOpen: onOpen,
);
}
return CachedNetworkImage(
errorWidget: (context, url, error) {
return Column(
children: [
const Icon(Icons.image_not_supported_outlined, size: 35),
Text("Keine Dateivorschau:\n${file!.name}", style: const TextStyle(fontWeight: FontWeight.bold))
],
);
},
alignment: Alignment.center,
placeholder: (context, url) {
return const Padding(padding: EdgeInsets.all(10), child: CircularProgressIndicator());
},
fadeInDuration: const Duration(seconds: 1),
imageUrl: "https://cloud.marianum-fulda.de/core/preview?fileId=${file!.id}&x=100&y=-1&a=1",
httpHeaders: {
"Authorization": "Basic ${base64.encode(utf8.encode("${preferences.getString("username")}:${preferences.getString("password")}"))}" // TODO move authentication
},
);
}
void onOpen(LinkableElement link) async {
if(await canLaunchUrlString(link.url)) {
await launchUrlString(link.url);
} else {
}
}
}

View File

@ -0,0 +1,145 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:loader_overlay/loader_overlay.dart';
import 'package:provider/provider.dart';
import '../../../api/marianumcloud/talk/sendMessage/sendMessage.dart';
import '../../../api/marianumcloud/talk/sendMessage/sendMessageParams.dart';
import '../../../model/chatList/chatProps.dart';
import '../../../widget/filePick.dart';
class ChatTextfield extends StatefulWidget {
final String sendToToken;
const ChatTextfield(this.sendToToken, {Key? key}) : super(key: key);
@override
State<ChatTextfield> createState() => _ChatTextfieldState();
}
class _ChatTextfieldState extends State<ChatTextfield> {
final TextEditingController _textBoxController = TextEditingController();
bool sending = false;
bool isLoading = false;
void mediaUpload(String? path) {
if(path == null) {
context.loaderOverlay.hide();
return;
}
showDialog(context: context, builder: (context) {
return AlertDialog(
title: const Text("Datei senden"),
content: Image.file(File(path)),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop();
context.loaderOverlay.hide();
}, child: const Text("Abbrechen")),
TextButton(onPressed: () {
context.loaderOverlay.hide();
}, child: const Text("Senden")),
],
);
});
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Align(
alignment: Alignment.bottomLeft,
child: Container(
padding: const EdgeInsets.only(left: 10, bottom: 10, top: 10),
height: 60,
width: double.infinity,
color: Theme.of(context).colorScheme.secondary,
child: Row(
children: <Widget>[
GestureDetector(
onTap: (){
showDialog(context: context, builder: (context) {
return SimpleDialog(
children: [
ListTile(
leading: const Icon(Icons.file_open),
title: const Text("Aus Dateien auswählen"),
onTap: () {
context.loaderOverlay.show();
FilePick.documentPick().then((value) {
log(value ?? "?");
mediaUpload(value);
});
Navigator.of(context).pop();
},
),
ListTile(
leading: const Icon(Icons.image),
title: const Text("Aus Gallerie auswählen"),
onTap: () {
context.loaderOverlay.show();
FilePick.galleryPick().then((value) {
log(value?.path ?? "?");
mediaUpload(value?.path);
});
Navigator.of(context).pop();
},
),
],
);
});
},
child: Container(
height: 30,
width: 30,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: const Icon(Icons.add, color: Colors.white, size: 20, ),
),
),
const SizedBox(width: 15),
Expanded(
child: TextField(
controller: _textBoxController,
readOnly: sending,
maxLines: 10,
decoration: InputDecoration(
hintText: "Nachricht schreiben...",
hintStyle: TextStyle(color: Theme.of(context).colorScheme.onSecondary),
border: InputBorder.none
),
),
),
const SizedBox(width: 15),
FloatingActionButton(
onPressed: (){
if(_textBoxController.text.isEmpty) return;
setState(() {
sending = true;
});
SendMessage(widget.sendToToken, SendMessageParams(_textBoxController.text)).run().then((value) => {
Provider.of<ChatProps>(context, listen: false).run(),
_textBoxController.text = "",
setState(() {
sending = false;
}),
});
},
backgroundColor: Theme.of(context).primaryColor,
elevation: 0,
child: const Icon(Icons.send, color: Colors.white, size: 18),
),
],
),
),
),
],
);
}
}

View File

@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:loader_overlay/loader_overlay.dart';
import 'package:provider/provider.dart';
import '../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../model/appTheme.dart';
import '../../../model/chatList/chatProps.dart';
import 'chatBubble.dart';
import 'chatTextfield.dart';
class ChatView extends StatefulWidget {
final GetRoomResponseObject room;
final String selfId;
final CircleAvatar avatar;
const ChatView({Key? key, required this.room, required this.selfId, required this.avatar}) : super(key: key);
@override
State<ChatView> createState() => _ChatViewState();
}
class _ChatViewState extends State<ChatView> {
final ScrollController _listController = ScrollController();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<ChatProps>(context, listen: false).setQueryToken(widget.room.token);
});
}
@override
Widget build(BuildContext context) {
return Consumer<ChatProps>(
builder: (context, data, child) {
List<Widget> messages = List<Widget>.empty(growable: true);
if(!data.primaryLoading()) {
DateTime lastDate = DateTime.now();
data.getChatResponse.sortByTimestamp().forEach((element) {
DateTime elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000);
if(elementDate.weekday != lastDate.weekday) {
lastDate = elementDate;
messages.add(ChatBubble(
context: context,
isSender: true,
bubbleData: GetChatResponseObject(
1,
"asd",
GetRoomResponseObjectMessageActorType.bridge,
"system",
"System",
element.timestamp,
elementDate.toIso8601String(),
GetRoomResponseObjectMessageType.system,
false,
"",
Jiffy.parseFromDateTime(elementDate).format(pattern: "dd.MM.yyyy"),
null
),
chatData: widget.room
));
}
messages.add(ChatBubble(context: context, isSender: element.actorId == widget.selfId, bubbleData: element, chatData: widget.room));
});
}
return Scaffold(
backgroundColor: const Color(0xffefeae2),
appBar: AppBar(
title: Row(
children: [
widget.avatar,
const SizedBox(width: 10),
Expanded(
child: Text(widget.room.displayName, overflow: TextOverflow.ellipsis, maxLines: 1),
)
],
),
),
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AppTheme.isDarkMode(context) ? const AssetImage("assets/background/chatDark.png") : const AssetImage("assets/background/chat.png"),
scale: 1.5,
opacity: 0.5,
repeat: ImageRepeat.repeat,
colorFilter: const ColorFilter.linearToSrgbGamma()
)
),
child: LoaderOverlay(
child: data.primaryLoading() ? const Center(child: CircularProgressIndicator()) : Column(
children: [
Expanded(
child: ListView(
reverse: true,
controller: _listController,
children: messages.reversed.toList(),
),
),
ChatTextfield(widget.room.token),
],
),
)
),
);
},
);
}
}

View File

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
class CrossPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.red.withAlpha(200)
..strokeWidth = 2.0;
canvas.drawLine(const Offset(0, 0), Offset(size.width, size.height), paint);
canvas.drawLine(Offset(size.width, 0), Offset(0, size.height), paint);
}
@override
bool shouldRepaint(CrossPainter oldDelegate) => false;
}

View File

@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
import 'CrossPainter.dart';
class AppointmentComponent extends StatefulWidget {
final CalendarAppointmentDetails details;
final bool crossedOut;
const AppointmentComponent({Key? key, required this.details, this.crossedOut = false}) : super(key: key);
@override
State<AppointmentComponent> createState() => _AppointmentComponentState();
}
class _AppointmentComponentState extends State<AppointmentComponent> {
@override
Widget build(BuildContext context) {
final Appointment meeting = widget.details.appointments.first;
final appointmentHeight = widget.details.bounds.height;
double headerHeight = 50;
const double footerHeight = 5;
final double infoHeight = appointmentHeight - (headerHeight + footerHeight);
if (infoHeight < 0) headerHeight += infoHeight;
return Stack(
children: [
Column(
children: [
Container(
padding: const EdgeInsets.all(3),
height: headerHeight,
alignment: Alignment.topLeft,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(5),
topRight: Radius.circular(5),
),
color: meeting.color,
),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
child: Text(
meeting.subject,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w500,
),
maxLines: 1,
softWrap: false,
),
),
FittedBox(
fit: BoxFit.fitWidth,
child: Text(
meeting.location ?? "?",
maxLines: 3,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
)
],
),
),
),
Visibility(
visible: meeting.notes != null && infoHeight > 10,
replacement: Container(
color: meeting.color,
height: infoHeight,
),
child: Container(
height: infoHeight,
padding: const EdgeInsets.fromLTRB(3, 5, 3, 2),
color: meeting.color.withOpacity(0.8),
alignment: Alignment.topLeft,
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
meeting.notes ?? "",
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
)
],
),
),
),
),
Container(
height: footerHeight,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(5),
bottomRight: Radius.circular(5),
),
color: meeting.color,
),
),
],
),
Visibility(
visible: (meeting.id as GetTimetableResponseObject).code == "cancelled",
child: Positioned.fill(
child: CustomPaint(
painter: CrossPainter(),
),
),
),
],
);
}
}

View File

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:persistent_bottom_nav_bar/persistent_tab_view.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
import '../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
import '../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
import '../../../model/timetable/timetableProps.dart';
import '../../../widget/unimplementedDialog.dart';
import '../../settings/debug/jsonViewer.dart';
import '../more/roomplan/roomplan.dart';
class AppointmentDetails {
static String _getEventPrefix(String? code) {
if(code == "cancelled") return "Entfällt: ";
if(code == "irregular") return "Änderung: ";
return code ?? "";
}
static void show(BuildContext context, TimetableProps webuntisData, Appointment appointment) {
GetTimetableResponseObject timetableData = appointment.id as GetTimetableResponseObject;
//GetTimetableResponseObject timetableData = webuntisData.getTimetableResponse.result.firstWhere((element) => element.id == timetableObject.id);
GetSubjectsResponseObject subject = webuntisData.getSubjectsResponse.result.firstWhere((subject) => subject.id == timetableData.su[0]['id']);
GetRoomsResponseObject room = webuntisData.getRoomsResponse.result.firstWhere((room) => room.id == timetableData.ro[0]['id']);
showModalBottomSheet(context: context, builder: (context) => Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 30),
child: Center(
child: Column(
children: [
Icon(Icons.info, color: appointment.color),
const SizedBox(height: 10),
Text("${_getEventPrefix(timetableData.code)}${subject.alternateName} - (${subject.longName})", style: const TextStyle(fontSize: 30)),
Text("${Jiffy.parseFromDateTime(appointment.startTime).format(pattern: "HH:mm")} - ${Jiffy.parseFromDateTime(appointment.endTime).format(pattern: "HH:mm")}", style: const TextStyle(fontSize: 15)),
],
),
),
),
Expanded(
child: ListView(
children: [
ListTile(
leading: const Icon(Icons.notifications_active),
title: Text("Status: ${timetableData.code != null ? "Geändert" : "Regulär"}"),
),
ListTile(
leading: const Icon(Icons.room),
title: Text("Raum: ${room.name}"),
trailing: IconButton(
icon: const Icon(Icons.house_outlined),
onPressed: () {
PersistentNavBarNavigator.pushNewScreen(context, withNavBar: false, screen: const Roomplan());
},
),
),
ListTile(
leading: const Icon(Icons.person),
title: Text("Lehrkraft: (${timetableData.te[0]['name']}) ${timetableData.te[0]['longname']}"),
trailing: IconButton(
icon: const Icon(Icons.textsms_outlined),
onPressed: () {
UnimplementedDialog.show(context);
},
),
),
ListTile(
leading: const Icon(Icons.abc),
title: Text("Typ: ${timetableData.activityType}"),
),
ListTile(
leading: const Icon(Icons.people),
title: Text("Klasse(n): ${timetableData.kl.map((e) => e['name']).join(", ")}"),
),
ListTile(
leading: const Icon(Icons.bug_report_outlined),
title: const Text("Webuntis Rohdaten zeigen"),
onTap: () => JsonViewer.asDialog(context, timetableData.toJson()),
)
],
),
)
],
));
}
}

View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
class TimeRegionComponent extends StatefulWidget {
final TimeRegionDetails details;
const TimeRegionComponent({Key? key, required this.details}) : super(key: key);
@override
State<TimeRegionComponent> createState() => _TimeRegionComponentState();
}
class _TimeRegionComponentState extends State<TimeRegionComponent> {
@override
Widget build(BuildContext context) {
String text = widget.details.region.text!;
Color? color = widget.details.region.color;
if (text == 'centerIcon') {
return Container(
color: color,
alignment: Alignment.center,
child: Icon(
widget.details.region.iconData,
size: 17,
color: Theme.of(context).primaryColor,
),
);
} else if(text.startsWith('holiday')) {
return Container(
color: color,
alignment: Alignment.center,
child: Column(
children: [
const SizedBox(height: 5),
const Icon(Icons.cake),
const Text("FREI"),
const SizedBox(height: 5),
RotatedBox(
quarterTurns: 1,
child: Text(
text.split(":").last,
maxLines: 1,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
decorationStyle: TextDecorationStyle.dashed,
letterSpacing: 2,
),
),
),
],
),
);
}
return const Placeholder();
}
}

View File

@ -0,0 +1,230 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart';
import '../../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
import '../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
import '../../../model/timetable/timetableProps.dart';
import '../../../widget/errorView.dart';
import 'appointmenetComponent.dart';
import 'appointmentDetails.dart';
import 'timeRegionComponent.dart';
import 'timetableEvents.dart';
class Timetable extends StatefulWidget {
const Timetable({Key? key}) : super(key: key);
@override
State<Timetable> createState() => _TimetableState();
}
class _TimetableState extends State<Timetable> {
CalendarController controller = CalendarController();
double elementScale = 40;
double baseElementScale = 40;
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<TimetableProps>(context, listen: false).run();
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Stunden & Vertretungsplan"),
actions: [
IconButton(
icon: const Icon(Icons.today),
onPressed: () {
// controller.displayDate = DateTime.now().jumpToNextWeekDay(DateTime.monday);
// controller.displayDate = DateTime.now().add(Duration(days: 2));
controller.displayDate = DateTime.now();
}
),
],
),
body: Consumer<TimetableProps>(
builder: (context, value, child) {
if(value.primaryLoading()) return const Placeholder();
GetHolidaysResponse holidays = value.getHolidaysResponse;
if(value.hasError) {
return ErrorView(
icon: Icons.calendar_month,
text: "Webuntis error: ${value.error.toString()}",
);
}
return GestureDetector(
onScaleStart: (details) => baseElementScale = elementScale,
onScaleUpdate: (details) {
setState(() {
elementScale = (baseElementScale * details.scale).clamp(40, 80);
});
},
onScaleEnd: (details) {
// TODO save scale for later
},
child: SfCalendar(
view: CalendarView.workWeek,
dataSource: _buildTableEvents(value),
controller: controller,
onViewChanged: (ViewChangedDetails details) {
log(details.visibleDates.toString());
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<TimetableProps>(context, listen: false).updateWeek(details.visibleDates.first, details.visibleDates.last);
});
},
onTap: (calendarTapDetails) {
if(calendarTapDetails.appointments == null) return;
Appointment tapped = calendarTapDetails.appointments!.first;
AppointmentDetails.show(context, value, tapped);
log(tapped.id.toString());
},
firstDayOfWeek: DateTime.monday,
specialRegions: _buildSpecialTimeRegions(holidays),
timeSlotViewSettings: TimeSlotViewSettings(
startHour: 07.5,
endHour: 16.5,
timeInterval: const Duration(minutes: 30),
timeFormat: "HH:mm",
dayFormat: "EE",
timeIntervalHeight: elementScale,
),
timeRegionBuilder: (BuildContext context, TimeRegionDetails timeRegionDetails) => TimeRegionComponent(details: timeRegionDetails),
appointmentBuilder: (BuildContext context, CalendarAppointmentDetails details) => AppointmentComponent(
details: details,
crossedOut: value.getTimetableResponse.result.where((element) => element.id == details.appointments.first.id).firstOrNull?.code == "cancelled"
),
headerHeight: 0,
selectionDecoration: const BoxDecoration(),
allowAppointmentResize: false,
allowDragAndDrop: false,
allowViewNavigation: false,
),
);
},
),
);
}
List<TimeRegion> _buildSpecialTimeRegions(GetHolidaysResponse holidays, ) {
DateTime lastMonday = DateTime.now().subtract(Duration(days: DateTime.now().weekday - 1));
DateTime firstBreak = lastMonday.copyWith(hour: 10, minute: 15);
DateTime secondBreak = lastMonday.copyWith(hour: 13, minute: 50);
DateTime beforeSchool = lastMonday.copyWith(hour: 7, minute: 30);
return [
...holidays.result.map((e) {
return TimeRegion(
startTime: _parseWebuntisTimestamp(e.startDate, 755),
endTime: _parseWebuntisTimestamp(e.startDate, 1630),
text: 'holiday:${e.name}',
color: Theme.of(context).disabledColor.withAlpha(50),
iconData: Icons.holiday_village_outlined
);
}),
TimeRegion(
startTime: firstBreak,
endTime: firstBreak.add(const Duration(minutes: 20)),
recurrenceRule: 'FREQ=DAILY;INTERVAL=1;COUNT=5',
text: 'centerIcon',
color: Theme.of(context).primaryColor.withAlpha(50),
iconData: Icons.restaurant
),
TimeRegion(
startTime: secondBreak,
endTime: secondBreak.add(const Duration(minutes: 15)),
recurrenceRule: 'FREQ=DAILY;INTERVAL=1;COUNT=5',
text: 'centerIcon',
color: Theme.of(context).primaryColor.withAlpha(50),
iconData: Icons.restaurant
),
TimeRegion(
startTime: beforeSchool,
endTime: beforeSchool.add(const Duration(minutes: 25)),
recurrenceRule: 'FREQ=DAILY;INTERVAL=1;COUNT=5',
color: Theme.of(context).disabledColor.withAlpha(50),
text: "centerIcon",
),
];
}
TimetableEvents _buildTableEvents(TimetableProps data) {
List<Appointment> appointments = data.getTimetableResponse.result.map((element) {
GetRoomsResponse rooms = data.getRoomsResponse;
GetSubjectsResponse subjects = data.getSubjectsResponse;
try {
DateTime startTime = _parseWebuntisTimestamp(element.date, element.startTime);
DateTime endTime = _parseWebuntisTimestamp(element.date, element.endTime);
return Appointment(
id: element,
startTime: startTime,
endTime: endTime,
subject: subjects.result.firstWhere((subject) => subject.id == element.su[0]['id']).alternateName,
location: ""
"${rooms.result.firstWhere((room) => room.id == element.ro[0]['id']).name}"
"\n"
"${element.te.first['longname']}",
notes: element.activityType,
color: _getEventColor(element.code, startTime, endTime),
);
} on Error catch(e) {
log(e.toString());
return Appointment(
startTime: _parseWebuntisTimestamp(element.date, element.startTime),
endTime: _parseWebuntisTimestamp(element.date, element.endTime),
subject: "ERROR",
notes: element.info,
location: 'LOCATION',
color: Theme.of(context).primaryColor,
startTimeZone: '',
endTimeZone: '',
);
}
}).toList();
return TimetableEvents(appointments);
}
DateTime _parseWebuntisTimestamp(int date, int time) {
String timeString = time.toString().padLeft(4, '0');
return DateTime.parse('$date ${timeString.substring(0, 2)}:${timeString.substring(2, 4)}');
}
Color _getEventColor(String? code, DateTime startTime, DateTime endTime) {
int opacity = endTime.isBefore(DateTime.now()) ? 100 : 255;
if(code == "cancelled") return const Color(0xff000000).withAlpha(opacity);
if(code == "irregular") return const Color(0xff8F19B3).withAlpha(opacity);
if(endTime.isBefore(DateTime.now())) return Theme.of(context).primaryColor.withAlpha(opacity);
if(endTime.isAfter(DateTime.now()) && startTime.isBefore(DateTime.now())) return Theme.of(context).primaryColor.withRed(100);
return Theme.of(context).primaryColor;
}
}

View File

@ -0,0 +1,8 @@
import 'package:syncfusion_flutter_calendar/calendar.dart';
class TimetableEvents extends CalendarDataSource {
TimetableEvents(List<Appointment> source) {
appointments = source;
}
}

View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
class About extends StatelessWidget {
const About({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Über diese App"),
),
body: const Card(
elevation: 1,
borderOnForeground: true,
child: Text("Marianum Fulda"),
),
);
}
}

View File

@ -0,0 +1,106 @@
import 'dart:convert';
import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:jiffy/jiffy.dart';
import 'package:localstore/localstore.dart';
import 'jsonViewer.dart';
class DebugOverview extends StatefulWidget {
const DebugOverview({Key? key}) : super(key: key);
@override
State<DebugOverview> createState() => _DebugOverviewState();
}
class _DebugOverviewState extends State<DebugOverview> {
final Localstore storage = Localstore.instance;
Future<Map<String, dynamic>?> files = Localstore.instance.collection("MarianumMobile").get();
dynamic data;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Lokaler cache"),
),
body: Column(
children: [
ListTile(
leading: const Icon(Icons.delete_forever),
title: const Text("Cache löschen"),
onTap: () {
PaintingBinding.instance.imageCache.clear();
storage.collection("MarianumMobile").delete().then((value) => {
Navigator.pop(context)
});
},
),
const Divider(),
FutureBuilder(
future: files,
builder: (context, snapshot) {
if(snapshot.hasData) {
List<String> files = snapshot.data?.keys.map((e) => e.toString()).toList() ?? List<String>.empty();
Map<String, dynamic> getValue(int index) {
return snapshot.data?[files[index]];
}
return Expanded(
flex: 5,
child: ListView.builder(
itemCount: files.length,
itemBuilder: (context, index) {
String filename = files[index].split("/").last;
return ListTile(
leading: const Icon(Icons.text_snippet_outlined),
title: Text(filename),
subtitle: Text("${filesize(getValue(index).toString().length * 8)}, ${Jiffy.parseFromMillisecondsSinceEpoch(getValue(index)['lastupdate']).fromNow()}"),
trailing: const Icon(Icons.chevron_right),
textColor: Colors.black,
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return JsonViewer(title: filename, data: {"lastupdate": getValue(index)['lastupdate'], "json": jsonDecode(getValue(index)['json'])});
},));
},
onLongPress: () {
showDialog(context: context, builder: (context) {
return SimpleDialog(
children: [
const ListTile(
leading: Icon(Icons.delete_forever),
title: Text("Diese Datei löschen"),
),
ListTile(
leading: const Icon(Icons.copy),
title: const Text("Dateitext kopieren"),
onTap: () {
Clipboard.setData(ClipboardData(text: getValue(index).toString()));
Navigator.of(context).pop();
},
)
],
);
});
},
);
},
),
);
} else {
return snapshot.data == null ? const Text("No data") : const Center(child: CircularProgressIndicator());
}
},
),
],
),
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pretty_json/pretty_json.dart';
class JsonViewer extends StatelessWidget {
final String title;
final Map<String, dynamic> data;
const JsonViewer({Key? key, required this.title, required this.data}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Text(format(data)),
),
);
}
static String format(Map<String, dynamic> jsonInput) {
return prettyJson(jsonInput, indent: 2);
//return jsonInput.replaceAllMapped(RegExp(r'[{,}]'), (match) => "${match.group(0)}\n ");
}
static void asDialog(BuildContext context, Map<String, dynamic> dataMap) {
showDialog(context: context, builder: (context) {
return AlertDialog(
scrollable: true,
title: const Row(children: [Icon(Icons.bug_report_outlined), Text("Rohdaten")]),
content: Text(JsonViewer.format(dataMap)),
actions: [
TextButton(onPressed: () {
Clipboard.setData(ClipboardData(text: JsonViewer.format(dataMap))).then((value) {
showDialog(context: context, builder: (context) => const AlertDialog(content: Text("Formatiertes JSON wurde erfolgreich in deiner Zwischenlage abgelegt.")));
});
}, child: const Text("Kopieren")),
TextButton(onPressed: () {
Clipboard.setData(ClipboardData(text: dataMap.toString())).then((value) {
showDialog(context: context, builder: (context) => const AlertDialog(content: Text("Unformatiertes JSON wurde erfolgreich in deiner Zwischenablage abgelegt.")));
});
}, child: const Text("Inline Kopieren")),
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text("Schließen"))
],
);
});
}
}

View File

@ -0,0 +1,192 @@
import 'package:flutter/material.dart';
import 'package:package_info/package_info.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../model/accountModel.dart';
import '../../model/appTheme.dart';
import 'debug/debugOverview.dart';
class Settings extends StatefulWidget {
const Settings({Key? key}) : super(key: key);
@override
State<Settings> createState() => _SettingsState();
}
class _SettingsState extends State<Settings> {
@override
void initState() {
super.initState();
}
bool developerMode = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Einstellungen"),
),
body: ListView(
children: [
ListTile(
leading: const Icon(Icons.logout_outlined),
title: const Text("Konto abmelden"),
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Abmelden?"),
content: const Text("Möchtest du dich wirklich abmelden?"),
actions: [
TextButton(
child: const Text("Abmelden"),
onPressed: () {
SharedPreferences.getInstance().then((value) => {
value.clear(),
}).then((value) => {
Provider.of<AccountModel>(context, listen: false).logout(),
Navigator.popUntil(context, (route) => !Navigator.canPop(context)),
});
}
),
TextButton(
child: const Text("Abbrechen"),
onPressed: () {
Navigator.pop(context);
},
),
],
);
},
);
},
),
const Divider(),
Consumer<AppTheme>(
builder: (context, value, child) {
return ListTile(
leading: const Icon(Icons.dark_mode_outlined),
title: const Text("Farbgebung"),
trailing: DropdownButton<ThemeMode>(
value: value.getMode,
icon: const Icon(Icons.arrow_drop_down),
items: ThemeMode.values.map((e) => DropdownMenuItem<ThemeMode>(
value: e,
enabled: e != value.getMode,
child: Row(
children: [
Icon(AppTheme.getDisplayOptions(e).icon),
const SizedBox(width: 10),
Text(AppTheme.getDisplayOptions(e).displayName),
],
),
)).toList(),
onChanged: (e) {
Provider.of<AppTheme>(context, listen: false).setTheme(e ?? ThemeMode.system);
},
),
);
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.live_help_outlined),
title: const Text("Informationen und Lizenzen"),
onTap: () async {
final appInfo = await PackageInfo.fromPlatform();
if(!context.mounted) return; // TODO Fix context used in async
showAboutDialog(
context: context,
applicationIcon: const Icon(Icons.apps),
applicationName: "MarianumMobile",
applicationVersion: "${appInfo.appName}\n\nPackage: ${appInfo.packageName}\n\nVersion: ${appInfo.version}\nBuild: ${appInfo.buildNumber}",
applicationLegalese: "Dies ist ein Inoffizieller Nextcloud & Webuntis Client und wird nicht vom Marianum selbst betrieben.\n"
"Keinerlei Gewähr für Vollständigkeit, Richtigkeit und Aktualität!\n\n"
"Development build\n"
"Marianum Fulda 2023 Elias Müller",
);
},
trailing: const Icon(Icons.arrow_right),
),
ListTile(
leading: const Icon(Icons.policy_outlined),
title: const Text("Datenschutz"),
onTap: () {
launchUrl(Uri.parse("https://mhsl.eu/datenschutz.html"));
},
trailing: const Icon(Icons.open_in_new),
),
ListTile(
leading: const Icon(Icons.badge_outlined),
title: const Text("Impressum"),
onTap: () {
launchUrl(Uri.parse("https://mhsl.eu/id.html"));
},
trailing: const Icon(Icons.open_in_new),
),
const Divider(),
ListTile(
leading: const Icon(Icons.developer_mode_outlined),
title: const Text("Entwicklermodus"),
trailing: Checkbox(
visualDensity: const VisualDensity(horizontal: VisualDensity.minimumDensity),
value: developerMode,
onChanged: (state) {
setState(() {
developerMode = !developerMode;
});
},
),
),
Visibility(
visible: developerMode,
child: ListTile(
leading: const Icon(Icons.data_object),
title: const Text("Storage view"),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return const DebugOverview();
}));
},
trailing: const Icon(Icons.arrow_right),
),
),
Visibility(
visible: developerMode && false, // TODO Implement verbose logging
child: ListTile(
leading: const Icon(Icons.logo_dev),
title: const Text("Logging verbosity"),
trailing: DropdownButton<String>(
value: "1",
items: ["1", "2", "3"].map((e) => DropdownMenuItem<String>(value: e, child: Text(e))).toList(),
onChanged: (e) {
},
),
),
),
],
),
);
}
}