Refactor codebase resolving warnings and remove self-package imports
This commit is contained in:
185
lib/view/pages/files/fileElement.dart
Normal file
185
lib/view/pages/files/fileElement.dart
Normal 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);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
227
lib/view/pages/files/fileUploadDialog.dart
Normal file
227
lib/view/pages/files/fileUploadDialog.dart
Normal 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
|
||||
}
|
247
lib/view/pages/files/files.dart
Normal file
247
lib/view/pages/files/files.dart
Normal 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);
|
||||
}
|
||||
}
|
59
lib/view/pages/more/countdown/addTimerDialog.dart
Normal file
59
lib/view/pages/more/countdown/addTimerDialog.dart
Normal 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")),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
53
lib/view/pages/more/countdown/animatedTime.dart
Normal file
53
lib/view/pages/more/countdown/animatedTime.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
56
lib/view/pages/more/countdown/countdown.dart
Normal file
56
lib/view/pages/more/countdown/countdown.dart
Normal 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
|
||||
// )
|
||||
);
|
||||
}
|
||||
}
|
54
lib/view/pages/more/countdown/timer.dart
Normal file
54
lib/view/pages/more/countdown/timer.dart
Normal 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}" : ""}"),
|
||||
);
|
||||
}
|
||||
}
|
170
lib/view/pages/more/gradeAverages/gradeAverage.dart
Normal file
170
lib/view/pages/more/gradeAverages/gradeAverage.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
62
lib/view/pages/more/message/message.dart
Normal file
62
lib/view/pages/more/message/message.dart
Normal 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));
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
63
lib/view/pages/more/message/messageView.dart
Normal file
63
lib/view/pages/more/message/messageView.dart
Normal 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")),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
36
lib/view/pages/more/overhang.dart
Normal file
36
lib/view/pages/more/overhang.dart
Normal 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()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
23
lib/view/pages/more/roomplan/roomplan.dart
Normal file
23
lib/view/pages/more/roomplan/roomplan.dart
Normal 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),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
253
lib/view/pages/talk/chatBubble.dart
Normal file
253
lib/view/pages/talk/chatBubble.dart
Normal 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),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
119
lib/view/pages/talk/chatList.dart
Normal file
119
lib/view/pages/talk/chatList.dart
Normal 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),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
68
lib/view/pages/talk/chatMessage.dart
Normal file
68
lib/view/pages/talk/chatMessage.dart
Normal 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 {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
145
lib/view/pages/talk/chatTextfield.dart
Normal file
145
lib/view/pages/talk/chatTextfield.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
117
lib/view/pages/talk/chatView.dart
Normal file
117
lib/view/pages/talk/chatView.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
16
lib/view/pages/timetable/CrossPainter.dart
Normal file
16
lib/view/pages/timetable/CrossPainter.dart
Normal 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;
|
||||
}
|
130
lib/view/pages/timetable/appointmenetComponent.dart
Normal file
130
lib/view/pages/timetable/appointmenetComponent.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
90
lib/view/pages/timetable/appointmentDetails.dart
Normal file
90
lib/view/pages/timetable/appointmentDetails.dart
Normal 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()),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
58
lib/view/pages/timetable/timeRegionComponent.dart
Normal file
58
lib/view/pages/timetable/timeRegionComponent.dart
Normal 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();
|
||||
}
|
||||
}
|
230
lib/view/pages/timetable/timetable.dart
Normal file
230
lib/view/pages/timetable/timetable.dart
Normal 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;
|
||||
}
|
||||
}
|
8
lib/view/pages/timetable/timetableEvents.dart
Normal file
8
lib/view/pages/timetable/timetableEvents.dart
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
||||
|
||||
class TimetableEvents extends CalendarDataSource {
|
||||
TimetableEvents(List<Appointment> source) {
|
||||
appointments = source;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user