Merge branch 'develop' into develop-connectedDoubleLessons

This commit is contained in:
Lars Neuhaus 2024-03-30 22:05:54 +01:00
commit 20d7b16ede
130 changed files with 1030 additions and 874 deletions

BIN
.DS_Store vendored

Binary file not shown.

1
.gitignore vendored
View File

@ -349,3 +349,4 @@ hs_err_pid*
# End of https://www.toptal.com/developers/gitignore/api/flutter,intellij,androidstudio,xcode,dart # End of https://www.toptal.com/developers/gitignore/api/flutter,intellij,androidstudio,xcode,dart
*.idea* *.idea*
**/.DS_store

View File

@ -24,6 +24,10 @@ linter:
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
file_names: false
prefer_relative_imports: true
unnecessary_lambdas: true
prefer_single_quotes: true
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@ -5,6 +5,6 @@ class ApiError {
@override @override
String toString() { String toString() {
return "ApiError: $message"; return 'ApiError: $message';
} }
} }

View File

@ -6,11 +6,11 @@ import 'getHolidaysResponse.dart';
class GetHolidays { class GetHolidays {
Future<GetHolidaysResponse> query() async { Future<GetHolidaysResponse> query() async {
String response = (await http.get(Uri.parse("https://ferien-api.de/api/v1/holidays/HE"))).body; String response = (await http.get(Uri.parse('https://ferien-api.de/api/v1/holidays/HE'))).body;
return GetHolidaysResponse( return GetHolidaysResponse(
List<GetHolidaysResponseObject>.from( List<GetHolidaysResponseObject>.from(
jsonDecode(response).map<GetHolidaysResponseObject>( jsonDecode(response).map<GetHolidaysResponseObject>(
(dynamic i) => GetHolidaysResponseObject.fromJson(i) GetHolidaysResponseObject.fromJson
) )
) )
); );

View File

@ -6,7 +6,7 @@ import 'getHolidaysResponse.dart';
class GetHolidaysCache extends RequestCache<GetHolidaysResponse> { class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
GetHolidaysCache({onUpdate, renew}) : super(RequestCache.cacheDay, onUpdate, renew: renew) { GetHolidaysCache({onUpdate, renew}) : super(RequestCache.cacheDay, onUpdate, renew: renew) {
start("MarianumMobile", "state-holidays"); start('MarianumMobile', 'state-holidays');
} }
@override @override
@ -15,6 +15,7 @@ class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
return GetHolidaysResponse( return GetHolidaysResponse(
List<GetHolidaysResponseObject>.from( List<GetHolidaysResponseObject>.from(
parsedListJson.map<GetHolidaysResponseObject>( parsedListJson.map<GetHolidaysResponseObject>(
// ignore: unnecessary_lambdas
(dynamic i) => GetHolidaysResponseObject.fromJson(i) (dynamic i) => GetHolidaysResponseObject.fromJson(i)
) )
) )

View File

@ -11,21 +11,21 @@ import 'autocompleteResponse.dart';
class AutocompleteApi { class AutocompleteApi {
Future<AutocompleteResponse> find(String query) async { Future<AutocompleteResponse> find(String query) async {
Map<String, dynamic> getParameters = { Map<String, dynamic> getParameters = {
"search": query, 'search': query,
"itemType": " ", 'itemType': ' ',
"itemId": " ", 'itemId': ' ',
"shareTypes[]": ["0"], 'shareTypes[]': ['0'],
"limit": "10", 'limit': '10',
}; };
Map<String, String> headers = {}; Map<String, String> headers = {};
headers.putIfAbsent("Accept", () => "application/json"); headers.putIfAbsent('Accept', () => 'application/json');
headers.putIfAbsent("OCS-APIRequest", () => "true"); headers.putIfAbsent('OCS-APIRequest', () => 'true');
Uri endpoint = Uri.https("${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}", "${EndpointData().nextcloud().path}/ocs/v2.php/core/autocomplete/get", getParameters); Uri endpoint = Uri.https('${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}', '${EndpointData().nextcloud().path}/ocs/v2.php/core/autocomplete/get', getParameters);
Response response = await http.get(endpoint, headers: headers); Response response = await http.get(endpoint, headers: headers);
if(response.statusCode != HttpStatus.ok) throw Exception("Api call failed with ${response.statusCode}: ${response.body}"); if(response.statusCode != HttpStatus.ok) throw Exception('Api call failed with ${response.statusCode}: ${response.body}');
String result = response.body; String result = response.body;
return AutocompleteResponse.fromJson(jsonDecode(result)['ocs']); return AutocompleteResponse.fromJson(jsonDecode(result)['ocs']);
} }

View File

@ -10,14 +10,14 @@ import 'fileSharingApiParams.dart';
class FileSharingApi { class FileSharingApi {
Future<void> share(FileSharingApiParams query) async { Future<void> share(FileSharingApiParams query) async {
Map<String, String> headers = {}; Map<String, String> headers = {};
headers.putIfAbsent("Accept", () => "application/json"); headers.putIfAbsent('Accept', () => 'application/json');
headers.putIfAbsent("OCS-APIRequest", () => "true"); headers.putIfAbsent('OCS-APIRequest', () => 'true');
Uri endpoint = Uri.https("${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}", "${EndpointData().nextcloud().path}/ocs/v2.php/apps/files_sharing/api/v1/shares", query.toJson().map((key, value) => MapEntry(key, value.toString()))); Uri endpoint = Uri.https('${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}', '${EndpointData().nextcloud().path}/ocs/v2.php/apps/files_sharing/api/v1/shares', query.toJson().map((key, value) => MapEntry(key, value.toString())));
Response response = await http.post(endpoint, headers: headers); Response response = await http.post(endpoint, headers: headers);
if(response.statusCode != HttpStatus.ok) { if(response.statusCode != HttpStatus.ok) {
throw Exception("Api call failed with ${response.statusCode}: ${response.body}"); throw Exception('Api call failed with ${response.statusCode}: ${response.body}');
} }
} }
} }

View File

@ -11,7 +11,7 @@ class GetChat extends TalkApi<GetChatResponse> {
String chatToken; String chatToken;
GetChatParams params; GetChatParams params;
GetChat(this.chatToken, this.params) : super("v1/chat/$chatToken", null, getParameters: params.toJson()); GetChat(this.chatToken, this.params) : super('v1/chat/$chatToken', null, getParameters: params.toJson());
@override @override
assemble(String raw) { assemble(String raw) {

View File

@ -9,7 +9,7 @@ class GetChatCache extends RequestCache<GetChatResponse> {
String chatToken; String chatToken;
GetChatCache({required onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) { GetChatCache({required onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) {
start("MarianumMobile", "nc-chat-$chatToken"); start('MarianumMobile', 'nc-chat-$chatToken');
} }
@override @override

View File

@ -61,21 +61,21 @@ class GetChatResponseObject {
static GetChatResponseObject getDateDummy(int timestamp) { static GetChatResponseObject getDateDummy(int timestamp) {
DateTime elementDate = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); DateTime elementDate = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
return getTextDummy(Jiffy.parseFromDateTime(elementDate).format(pattern: "dd.MM.yyyy")); return getTextDummy(Jiffy.parseFromDateTime(elementDate).format(pattern: 'dd.MM.yyyy'));
} }
static GetChatResponseObject getTextDummy(String text) { static GetChatResponseObject getTextDummy(String text) {
return GetChatResponseObject( return GetChatResponseObject(
0, 0,
"", '',
GetRoomResponseObjectMessageActorType.user, GetRoomResponseObjectMessageActorType.user,
"", '',
"", '',
0, 0,
"", '',
GetRoomResponseObjectMessageType.system, GetRoomResponseObjectMessageType.system,
false, false,
"", '',
text, text,
null, null,
null, null,
@ -113,10 +113,10 @@ class RichObjectString {
} }
enum RichObjectStringObjectType { enum RichObjectStringObjectType {
@JsonValue("user") user, @JsonValue('user') user,
@JsonValue("group") group, @JsonValue('group') group,
@JsonValue("file") file, @JsonValue('file') file,
@JsonValue("guest") guest, @JsonValue('guest') guest,
@JsonValue("highlight") highlight, @JsonValue('highlight') highlight,
@JsonValue("talk-poll") talkPoll, @JsonValue('talk-poll') talkPoll,
} }

View File

@ -6,7 +6,7 @@ class RichObjectStringProcessor {
if(data == null) return message; if(data == null) return message;
data.forEach((key, value) { data.forEach((key, value) {
message = message.replaceAll(RegExp("{$key}"), value.name); message = message.replaceAll(RegExp('{$key}'), value.name);
}); });
return message; return message;

View File

@ -7,7 +7,7 @@ import 'createRoomParams.dart';
class CreateRoom extends TalkApi { class CreateRoom extends TalkApi {
CreateRoomParams params; CreateRoomParams params;
CreateRoom(this.params) : super("v4/room", params); CreateRoom(this.params) : super('v4/room', params);
@override @override
assemble(String raw) { assemble(String raw) {

View File

@ -7,7 +7,7 @@ import '../talkApi.dart';
class DeleteMessage extends TalkApi { class DeleteMessage extends TalkApi {
String chatToken; String chatToken;
int messageId; int messageId;
DeleteMessage(this.chatToken, this.messageId) : super("v1/chat/$chatToken/$messageId", null); DeleteMessage(this.chatToken, this.messageId) : super('v1/chat/$chatToken/$messageId', null);
@override @override
assemble(String raw) { assemble(String raw) {

View File

@ -8,7 +8,7 @@ import 'deleteReactMessageParams.dart';
class DeleteReactMessage extends TalkApi { class DeleteReactMessage extends TalkApi {
String chatToken; String chatToken;
int messageId; int messageId;
DeleteReactMessage({required this.chatToken, required this.messageId, required DeleteReactMessageParams params}) : super("v1/reaction/$chatToken/$messageId", params); DeleteReactMessage({required this.chatToken, required this.messageId, required DeleteReactMessageParams params}) : super('v1/reaction/$chatToken/$messageId', params);
@override @override
assemble(String raw) { assemble(String raw) {

View File

@ -7,7 +7,7 @@ import 'getParticipantsResponse.dart';
class GetParticipants extends TalkApi<GetParticipantsResponse> { class GetParticipants extends TalkApi<GetParticipantsResponse> {
String token; String token;
GetParticipants(this.token) : super("v4/room/$token/participants", null); GetParticipants(this.token) : super('v4/room/$token/participants', null);
@override @override
GetParticipantsResponse assemble(String raw) { GetParticipantsResponse assemble(String raw) {

View File

@ -8,7 +8,7 @@ class GetParticipantsCache extends RequestCache<GetParticipantsResponse> {
String chatToken; String chatToken;
GetParticipantsCache({required onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) { GetParticipantsCache({required onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) {
start("MarianumMobile", "nc-chat-participants-$chatToken"); start('MarianumMobile', 'nc-chat-participants-$chatToken');
} }
@override @override

View File

@ -10,7 +10,7 @@ import 'getReactionsResponse.dart';
class GetReactions extends TalkApi<GetReactionsResponse> { class GetReactions extends TalkApi<GetReactionsResponse> {
String chatToken; String chatToken;
int messageId; int messageId;
GetReactions({required this.chatToken, required this.messageId}) : super("v1/reaction/$chatToken/$messageId", null); GetReactions({required this.chatToken, required this.messageId}) : super('v1/reaction/$chatToken/$messageId', null);
@override @override
assemble(String raw) { assemble(String raw) {

View File

@ -28,6 +28,6 @@ class GetReactionsResponseObject {
} }
enum GetReactionsResponseObjectActorType { enum GetReactionsResponseObjectActorType {
@JsonValue("guests") guests, @JsonValue('guests') guests,
@JsonValue("users") users, @JsonValue('users') users,
} }

View File

@ -6,7 +6,7 @@ import '../talkApi.dart';
class LeaveRoom extends TalkApi { class LeaveRoom extends TalkApi {
String chatToken; String chatToken;
LeaveRoom(this.chatToken) : super("v4/room/$chatToken/participants/self", null); LeaveRoom(this.chatToken) : super('v4/room/$chatToken/participants/self', null);
@override @override
assemble(String raw) { assemble(String raw) {

View File

@ -8,7 +8,7 @@ import 'reactMessageParams.dart';
class ReactMessage extends TalkApi { class ReactMessage extends TalkApi {
String chatToken; String chatToken;
int messageId; int messageId;
ReactMessage({required this.chatToken, required this.messageId, required ReactMessageParams params}) : super("v1/reaction/$chatToken/$messageId", params); ReactMessage({required this.chatToken, required this.messageId, required ReactMessageParams params}) : super('v1/reaction/$chatToken/$messageId', params);
@override @override
assemble(String raw) { assemble(String raw) {

View File

@ -9,7 +9,7 @@ import 'getRoomResponse.dart';
class GetRoom extends TalkApi<GetRoomResponse> { class GetRoom extends TalkApi<GetRoomResponse> {
GetRoomParams params; GetRoomParams params;
GetRoom(this.params) : super("v4/room", null, getParameters: params.toJson()); GetRoom(this.params) : super('v4/room', null, getParameters: params.toJson());

View File

@ -8,7 +8,7 @@ import 'getRoomResponse.dart';
class GetRoomCache extends RequestCache<GetRoomResponse> { class GetRoomCache extends RequestCache<GetRoomResponse> {
GetRoomCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) { GetRoomCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
start("MarianumMobile", "nc-rooms"); start('MarianumMobile', 'nc-rooms');
} }
@override @override

View File

@ -19,10 +19,10 @@ class GetRoomResponse extends ApiResponse {
final buffer = StringBuffer(); final buffer = StringBuffer();
if(favoritesToTop) { if(favoritesToTop) {
buffer.write(chat.isFavorite ? "b" : "a"); buffer.write(chat.isFavorite ? 'b' : 'a');
} }
if(unreadToTop) { if(unreadToTop) {
buffer.write(chat.unreadMessages > 0 ? "b" : "a"); buffer.write(chat.unreadMessages > 0 ? 'b' : 'a');
} }
buffer.write(chat.lastActivity); buffer.write(chat.lastActivity);
@ -152,16 +152,16 @@ enum GetRoomResponseObjectParticipantNotificationLevel {
// } // }
enum GetRoomResponseObjectMessageActorType { enum GetRoomResponseObjectMessageActorType {
@JsonValue("deleted_users") deletedUsers, @JsonValue('deleted_users') deletedUsers,
@JsonValue("users") user, @JsonValue('users') user,
@JsonValue("guests") guest, @JsonValue('guests') guest,
@JsonValue("bots") bot, @JsonValue('bots') bot,
@JsonValue("bridged") bridge, @JsonValue('bridged') bridge,
} }
enum GetRoomResponseObjectMessageType { enum GetRoomResponseObjectMessageType {
@JsonValue("comment") comment, @JsonValue('comment') comment,
@JsonValue("comment_deleted") deletedComment, @JsonValue('comment_deleted') deletedComment,
@JsonValue("system") system, @JsonValue('system') system,
@JsonValue("command") command, @JsonValue('command') command,
} }

View File

@ -7,7 +7,7 @@ import 'sendMessageParams.dart';
class SendMessage extends TalkApi { class SendMessage extends TalkApi {
String chatToken; String chatToken;
SendMessage(this.chatToken, SendMessageParams params) : super("v1/chat/$chatToken", params); SendMessage(this.chatToken, SendMessageParams params) : super('v1/chat/$chatToken', params);
@override @override
assemble(String raw) { assemble(String raw) {

View File

@ -8,7 +8,7 @@ class SetFavorite extends TalkApi {
String chatToken; String chatToken;
bool favoriteState; bool favoriteState;
SetFavorite(this.chatToken, this.favoriteState) : super("v4/room/$chatToken/favorite", null); SetFavorite(this.chatToken, this.favoriteState) : super('v4/room/$chatToken/favorite', null);
@override @override
assemble(String raw) { assemble(String raw) {

View File

@ -10,7 +10,7 @@ class SetReadMarker extends TalkApi {
bool readState; bool readState;
SetReadMarkerParams? setReadMarkerParams; SetReadMarkerParams? setReadMarkerParams;
SetReadMarker(this.chatToken, this.readState, {this.setReadMarkerParams}) : super("v1/chat/$chatToken/read", null, getParameters: setReadMarkerParams?.toJson()) { SetReadMarker(this.chatToken, this.readState, {this.setReadMarkerParams}) : super('v1/chat/$chatToken/read', null, getParameters: setReadMarkerParams?.toJson()) {
if(readState) assert(setReadMarkerParams?.lastReadMessage != null); if(readState) assert(setReadMarkerParams?.lastReadMessage != null);
} }

View File

@ -34,21 +34,21 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
getParameters?.update(key, (value) => value.toString()); getParameters?.update(key, (value) => value.toString());
}); });
Uri endpoint = Uri.https("${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}", "${EndpointData().nextcloud().path}/ocs/v2.php/apps/spreed/api/$path", getParameters); Uri endpoint = Uri.https('${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}', '${EndpointData().nextcloud().path}/ocs/v2.php/apps/spreed/api/$path', getParameters);
headers ??= {}; headers ??= {};
headers?.putIfAbsent("Accept", () => "application/json"); headers?.putIfAbsent('Accept', () => 'application/json');
headers?.putIfAbsent("OCS-APIRequest", () => "true"); headers?.putIfAbsent('OCS-APIRequest', () => 'true');
http.Response? data; http.Response? data;
try { try {
data = await request(endpoint, body, headers); data = await request(endpoint, body, headers);
if(data == null) throw Exception("No response Data"); if(data == null) throw Exception('No response Data');
if(data.statusCode >= 400 || data.statusCode < 200) throw Exception("Response status code '${data.statusCode}' might indicate an error"); if(data.statusCode >= 400 || data.statusCode < 200) throw Exception("Response status code '${data.statusCode}' might indicate an error");
} catch(e) { } catch(e) {
log(e.toString()); log(e.toString());
throw ApiError("Request $endpoint could not be dispatched: ${e.toString()}"); throw ApiError('Request $endpoint could not be dispatched: ${e.toString()}');
} }
//dynamic jsonData = jsonDecode(data.body); //dynamic jsonData = jsonDecode(data.body);
@ -59,10 +59,10 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
return assembled; return assembled;
} catch (e) { } catch (e) {
// TODO report error // TODO report error
log("Error assembling Talk API ${T.toString()} message: ${e.toString()} response on ${endpoint.path} with request body: $body and request headers: ${headers.toString()}"); log('Error assembling Talk API ${T.toString()} message: ${e.toString()} response on ${endpoint.path} with request body: $body and request headers: ${headers.toString()}');
} }
throw Exception("Error assembling Talk API response"); throw Exception('Error assembling Talk API response');
} }
} }

View File

@ -7,6 +7,6 @@ class TalkError {
@override @override
String toString() { String toString() {
return "Talk - $status - ($code): $message"; return 'Talk - $status - ($code): $message';
} }
} }

View File

@ -24,7 +24,7 @@ class CacheableFile {
CacheableFile.fromDavFile(WebDavFile file) { CacheableFile.fromDavFile(WebDavFile file) {
path = file.path.path; path = file.path.path;
isDirectory = file.isDirectory; isDirectory = file.isDirectory;
name = file.isDirectory ? file.name : file.path.path.split("/").last; name = file.isDirectory ? file.name : file.path.path.split('/').last;
mimeType = file.mimeType; mimeType = file.mimeType;
size = file.size; size = file.size;
eTag = file.etag; eTag = file.etag;

View File

@ -1,7 +1,6 @@
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
import '../../../../../model/endpointData.dart';
import '../../webdavApi.dart'; import '../../webdavApi.dart';
import 'cacheableFile.dart'; import 'cacheableFile.dart';
import 'listFilesParams.dart'; import 'listFilesParams.dart';
@ -15,18 +14,19 @@ class ListFiles extends WebdavApi<ListFilesParams> {
@override @override
Future<ListFilesResponse> run() async { Future<ListFilesResponse> run() async {
List<WebDavFile> davFiles = (await (await WebdavApi.webdav).propfind(PathUri.parse(params.path))).toWebDavFiles(); List<WebDavFile> davFiles = (await (await WebdavApi.webdav).propfind(PathUri.parse(params.path))).toWebDavFiles();
Set<CacheableFile> files = davFiles.map((e) => CacheableFile.fromDavFile(e)).toSet(); Set<CacheableFile> files = davFiles.map(CacheableFile.fromDavFile).toSet();
// webdav handles subdirectories wrong, this is a fix // webdav handles subdirectories wrong, this is a fix
if(EndpointData().getEndpointMode() == EndpointMode.stage) { // currently this fix is not needed anymore
files = files.map((e) { // somehow // if(EndpointData().getEndpointMode() == EndpointMode.stage) {
e.path = e.path.split("mobile/cloud/remote.php/webdav")[1]; // files = files.map((e) { // somehow
return e; // e.path = e.path.split("mobile/cloud/remote.php/webdav")[1];
}).toSet(); // return e;
} // }).toSet();
// }
// somehow the current working folder is also listed, it is filtered here. // somehow the current working folder is also listed, it is filtered here.
files.removeWhere((element) => element.path == "/${params.path}/" || element.path == "/"); files.removeWhere((element) => element.path == '/${params.path}/' || element.path == '/');
return ListFilesResponse(files); return ListFilesResponse(files);
} }

View File

@ -10,9 +10,9 @@ class ListFilesCache extends RequestCache<ListFilesResponse> {
String path; String path;
ListFilesCache({required onUpdate, required this.path}) : super(RequestCache.cacheNothing, onUpdate) { ListFilesCache({required onUpdate, required this.path}) : super(RequestCache.cacheNothing, onUpdate) {
var bytes = utf8.encode("MarianumMobile-$path"); var bytes = utf8.encode('MarianumMobile-$path');
String cacheName = md5.convert(bytes).toString(); String cacheName = md5.convert(bytes).toString();
start("MarianumMobile", "wd-folder-$cacheName"); start('MarianumMobile', 'wd-folder-$cacheName');
} }
@override @override

View File

@ -35,7 +35,7 @@ class ListFilesResponse extends ApiResponse {
switch(sortOption) { switch(sortOption) {
case SortOption.date: case SortOption.date:
buffer.write(Jiffy.parseFromMillisecondsSinceEpoch(file.modifiedAt?.millisecondsSinceEpoch ?? 0).format(pattern: "yyyyMMddhhmmss")); buffer.write(Jiffy.parseFromMillisecondsSinceEpoch(file.modifiedAt?.millisecondsSinceEpoch ?? 0).format(pattern: 'yyyyMMddhhmmss'));
break; break;
case SortOption.name: case SortOption.name:

View File

@ -18,10 +18,10 @@ abstract class WebdavApi<T> extends ApiRequest {
static Future<String> webdavConnectString = buildWebdavConnectString(); static Future<String> webdavConnectString = buildWebdavConnectString();
static Future<WebDavClient> establishWebdavConnection() async { static Future<WebDavClient> establishWebdavConnection() async {
return NextcloudClient(Uri.parse("https://${EndpointData().nextcloud().full()}"), password: AccountData().getPassword(), loginName: AccountData().getUsername()).webdav; return NextcloudClient(Uri.parse('https://${EndpointData().nextcloud().full()}'), password: AccountData().getPassword(), loginName: AccountData().getUsername()).webdav;
} }
static Future<String> buildWebdavConnectString() async { static Future<String> buildWebdavConnectString() async {
return "https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/remote.php/dav/files/${AccountData().getUsername()}/"; return 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/remote.php/dav/files/${AccountData().getUsername()}/';
} }
} }

View File

@ -6,7 +6,7 @@ import '../../mhslApi.dart';
import 'getBreakersResponse.dart'; import 'getBreakersResponse.dart';
class GetBreakers extends MhslApi<GetBreakersResponse> { class GetBreakers extends MhslApi<GetBreakersResponse> {
GetBreakers() : super("breaker/"); GetBreakers() : super('breaker/');
@override @override
GetBreakersResponse assemble(String raw) { GetBreakersResponse assemble(String raw) {

View File

@ -6,7 +6,7 @@ import 'getBreakersResponse.dart';
class GetBreakersCache extends RequestCache<GetBreakersResponse> { class GetBreakersCache extends RequestCache<GetBreakersResponse> {
GetBreakersCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) { GetBreakersCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
start("MarianumMobile", "breakers"); start('MarianumMobile', 'breakers');
} }
@override @override

View File

@ -28,9 +28,9 @@ class GetBreakersReponseObject {
} }
enum BreakerArea { enum BreakerArea {
@JsonValue("GLOBAL") global, @JsonValue('GLOBAL') global,
@JsonValue("TIMETABLE") timetable, @JsonValue('TIMETABLE') timetable,
@JsonValue("TALK") talk, @JsonValue('TALK') talk,
@JsonValue("FILES") files, @JsonValue('FILES') files,
@JsonValue("MORE") more, @JsonValue('MORE') more,
} }

View File

@ -13,6 +13,7 @@ class CustomTimetableEvent {
DateTime startDate; DateTime startDate;
@JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson) @JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson)
DateTime endDate; DateTime endDate;
String? color;
String rrule; String rrule;
@JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson) @JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson)
DateTime createdAt; DateTime createdAt;
@ -20,7 +21,7 @@ class CustomTimetableEvent {
DateTime updatedAt; DateTime updatedAt;
CustomTimetableEvent({required this.id, required this.title, required this.description, required this.startDate, CustomTimetableEvent({required this.id, required this.title, required this.description, required this.startDate,
required this.endDate, required this.rrule, required this.createdAt, required this.updatedAt}); required this.endDate, required this.color, required this.rrule, required this.createdAt, required this.updatedAt});
factory CustomTimetableEvent.fromJson(Map<String, dynamic> json) => _$CustomTimetableEventFromJson(json); factory CustomTimetableEvent.fromJson(Map<String, dynamic> json) => _$CustomTimetableEventFromJson(json);
Map<String, dynamic> toJson() => _$CustomTimetableEventToJson(this); Map<String, dynamic> toJson() => _$CustomTimetableEventToJson(this);

View File

@ -14,6 +14,7 @@ CustomTimetableEvent _$CustomTimetableEventFromJson(
description: json['description'] as String, description: json['description'] as String,
startDate: MhslApi.dateTimeFromJson(json['startDate'] as String), startDate: MhslApi.dateTimeFromJson(json['startDate'] as String),
endDate: MhslApi.dateTimeFromJson(json['endDate'] as String), endDate: MhslApi.dateTimeFromJson(json['endDate'] as String),
color: json['color'] as String?,
rrule: json['rrule'] as String, rrule: json['rrule'] as String,
createdAt: MhslApi.dateTimeFromJson(json['createdAt'] as String), createdAt: MhslApi.dateTimeFromJson(json['createdAt'] as String),
updatedAt: MhslApi.dateTimeFromJson(json['updatedAt'] as String), updatedAt: MhslApi.dateTimeFromJson(json['updatedAt'] as String),
@ -27,6 +28,7 @@ Map<String, dynamic> _$CustomTimetableEventToJson(
'description': instance.description, 'description': instance.description,
'startDate': MhslApi.dateTimeToJson(instance.startDate), 'startDate': MhslApi.dateTimeToJson(instance.startDate),
'endDate': MhslApi.dateTimeToJson(instance.endDate), 'endDate': MhslApi.dateTimeToJson(instance.endDate),
'color': instance.color,
'rrule': instance.rrule, 'rrule': instance.rrule,
'createdAt': MhslApi.dateTimeToJson(instance.createdAt), 'createdAt': MhslApi.dateTimeToJson(instance.createdAt),
'updatedAt': MhslApi.dateTimeToJson(instance.updatedAt), 'updatedAt': MhslApi.dateTimeToJson(instance.updatedAt),

View File

@ -9,11 +9,11 @@ import 'getCustomTimetableEventResponse.dart';
class GetCustomTimetableEvent extends MhslApi<GetCustomTimetableEventResponse> { class GetCustomTimetableEvent extends MhslApi<GetCustomTimetableEventResponse> {
GetCustomTimetableEventParams params; GetCustomTimetableEventParams params;
GetCustomTimetableEvent(this.params) : super("server/timetable/customEvents?user=${params.user}"); GetCustomTimetableEvent(this.params) : super('server/timetable/customEvents?user=${params.user}');
@override @override
GetCustomTimetableEventResponse assemble(String raw) { GetCustomTimetableEventResponse assemble(String raw) {
return GetCustomTimetableEventResponse.fromJson({"events": jsonDecode(raw)}); return GetCustomTimetableEventResponse.fromJson({'events': jsonDecode(raw)});
} }
@override @override

View File

@ -9,7 +9,7 @@ class GetCustomTimetableEventCache extends RequestCache<GetCustomTimetableEventR
GetCustomTimetableEventParams params; GetCustomTimetableEventParams params;
GetCustomTimetableEventCache(this.params, {onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) { GetCustomTimetableEventCache(this.params, {onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
start("MarianumMobile", "customTimetableEvents"); start('MarianumMobile', 'customTimetableEvents');
} }
@override @override

View File

@ -6,7 +6,7 @@ import '../../mhslApi.dart';
import 'getMessagesResponse.dart'; import 'getMessagesResponse.dart';
class GetMessages extends MhslApi<GetMessagesResponse> { class GetMessages extends MhslApi<GetMessagesResponse> {
GetMessages() : super("message/messages.json"); GetMessages() : super('message/messages.json');
@override @override

View File

@ -6,7 +6,7 @@ import 'getMessagesResponse.dart';
class GetMessagesCache extends RequestCache<GetMessagesResponse> { class GetMessagesCache extends RequestCache<GetMessagesResponse> {
GetMessagesCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) { GetMessagesCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
start("MarianumMobile", "message"); start('MarianumMobile', 'message');
} }
@override @override

View File

@ -15,21 +15,21 @@ abstract class MhslApi<T> extends ApiRequest {
T assemble(String raw); T assemble(String raw);
Future<T> run() async { Future<T> run() async {
Uri endpoint = Uri.parse("https://mhsl.eu/marianum/marianummobile/$subpath"); Uri endpoint = Uri.parse('https://mhsl.eu/marianum/marianummobile/$subpath');
http.Response? data = await request(endpoint); http.Response? data = await request(endpoint);
if(data == null) { if(data == null) {
throw ApiError("Request could not be dispatched!"); throw ApiError('Request could not be dispatched!');
} }
if(data.statusCode > 299) { if(data.statusCode > 299) {
throw ApiError("Non 200 Status code from mhsl services: $subpath: ${data.statusCode}"); throw ApiError('Non 200 Status code from mhsl services: $subpath: ${data.statusCode}');
} }
return assemble(utf8.decode(data.bodyBytes)); return assemble(utf8.decode(data.bodyBytes));
} }
static String dateTimeToJson(DateTime time) => Jiffy.parseFromDateTime(time).format(pattern: "yyyy-MM-dd HH:mm:ss"); static String dateTimeToJson(DateTime time) => Jiffy.parseFromDateTime(time).format(pattern: 'yyyy-MM-dd HH:mm:ss');
static DateTime dateTimeFromJson(String time) => DateTime.parse(time); static DateTime dateTimeFromJson(String time) => DateTime.parse(time);
} }

View File

@ -9,7 +9,7 @@ import 'notifyRegisterParams.dart';
class NotifyRegister extends MhslApi<void> { class NotifyRegister extends MhslApi<void> {
NotifyRegisterParams params; NotifyRegisterParams params;
NotifyRegister(this.params) : super("notify/register/"); NotifyRegister(this.params) : super('notify/register/');
@override @override

View File

@ -6,14 +6,12 @@ part 'addFeedbackParams.g.dart';
class AddFeedbackParams { class AddFeedbackParams {
String user; String user;
String feedback; String feedback;
String? screenshot;
int appVersion; int appVersion;
AddFeedbackParams({ AddFeedbackParams({
required this.user, required this.user,
required this.feedback, required this.feedback,
this.screenshot,
required this.appVersion, required this.appVersion,
}); });

View File

@ -10,7 +10,6 @@ AddFeedbackParams _$AddFeedbackParamsFromJson(Map<String, dynamic> json) =>
AddFeedbackParams( AddFeedbackParams(
user: json['user'] as String, user: json['user'] as String,
feedback: json['feedback'] as String, feedback: json['feedback'] as String,
screenshot: json['screenshot'] as String?,
appVersion: json['appVersion'] as int, appVersion: json['appVersion'] as int,
); );
@ -18,6 +17,5 @@ Map<String, dynamic> _$AddFeedbackParamsToJson(AddFeedbackParams instance) =>
<String, dynamic>{ <String, dynamic>{
'user': instance.user, 'user': instance.user,
'feedback': instance.feedback, 'feedback': instance.feedback,
'screenshot': instance.screenshot,
'appVersion': instance.appVersion, 'appVersion': instance.appVersion,
}; };

View File

@ -12,7 +12,7 @@ import 'updateUserIndexParams.dart';
class UpdateUserIndex extends MhslApi<void> { class UpdateUserIndex extends MhslApi<void> {
UpdateUserIndexParams params; UpdateUserIndexParams params;
UpdateUserIndex(this.params) : super("server/userIndex/update"); UpdateUserIndex(this.params) : super('server/userIndex/update');
@override @override
void assemble(String raw) {} void assemble(String raw) {}
@ -20,7 +20,7 @@ class UpdateUserIndex extends MhslApi<void> {
@override @override
Future<http.Response> request(Uri uri) { Future<http.Response> request(Uri uri) {
String data = jsonEncode(params.toJson()); String data = jsonEncode(params.toJson());
log("Updating userindex:\n $data"); log('Updating userindex:\n $data');
return http.post(uri, body: data); return http.post(uri, body: data);
} }

View File

@ -35,8 +35,8 @@ abstract class RequestCache<T extends ApiResponse?> {
onUpdate(newValue); onUpdate(newValue);
Localstore.instance.collection(file).doc(document).set({ Localstore.instance.collection(file).doc(document).set({
"json": jsonEncode(newValue), 'json': jsonEncode(newValue),
"lastupdate": DateTime.now().millisecondsSinceEpoch 'lastupdate': DateTime.now().millisecondsSinceEpoch
}); });
} on WebuntisError catch(e) { } on WebuntisError catch(e) {
onError(e); onError(e);

View File

@ -9,7 +9,7 @@ import 'authenticateResponse.dart';
class Authenticate extends WebuntisApi { class Authenticate extends WebuntisApi {
AuthenticateParams param; AuthenticateParams param;
Authenticate(this.param) : super("authenticate", param, authenticatedResponse: false); Authenticate(this.param) : super('authenticate', param, authenticatedResponse: false);
@override @override
Future<AuthenticateResponse> run() async { Future<AuthenticateResponse> run() async {

View File

@ -4,7 +4,7 @@ import '../../webuntisApi.dart';
import 'getHolidaysResponse.dart'; import 'getHolidaysResponse.dart';
class GetHolidays extends WebuntisApi { class GetHolidays extends WebuntisApi {
GetHolidays() : super("getHolidays", null); GetHolidays() : super('getHolidays', null);
@override @override
Future<GetHolidaysResponse> run() async { Future<GetHolidaysResponse> run() async {

View File

@ -6,7 +6,7 @@ import 'getHolidaysResponse.dart';
class GetHolidaysCache extends RequestCache<GetHolidaysResponse> { class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
GetHolidaysCache({onUpdate}) : super(RequestCache.cacheDay, onUpdate) { GetHolidaysCache({onUpdate}) : super(RequestCache.cacheDay, onUpdate) {
start("MarianumMobile", "wu-holidays"); start('MarianumMobile', 'wu-holidays');
} }
@override @override

View File

@ -5,7 +5,7 @@ import '../../webuntisApi.dart';
import 'getRoomsResponse.dart'; import 'getRoomsResponse.dart';
class GetRooms extends WebuntisApi { class GetRooms extends WebuntisApi {
GetRooms() : super("getRooms", null); GetRooms() : super('getRooms', null);
@override @override
Future<GetRoomsResponse> run() async { Future<GetRoomsResponse> run() async {
@ -14,10 +14,10 @@ class GetRooms extends WebuntisApi {
return finalize(GetRoomsResponse.fromJson(jsonDecode(rawAnswer))); return finalize(GetRoomsResponse.fromJson(jsonDecode(rawAnswer)));
} catch(e, trace) { } catch(e, trace) {
log(trace.toString()); log(trace.toString());
log("Failed to parse getRoom data with server response: $rawAnswer"); log('Failed to parse getRoom data with server response: $rawAnswer');
} }
throw Exception("Failed to parse getRoom server response: $rawAnswer"); throw Exception('Failed to parse getRoom server response: $rawAnswer');
} }
} }

View File

@ -6,7 +6,7 @@ import 'getRoomsResponse.dart';
class GetRoomsCache extends RequestCache<GetRoomsResponse> { class GetRoomsCache extends RequestCache<GetRoomsResponse> {
GetRoomsCache({onUpdate}) : super(RequestCache.cacheHour, onUpdate) { GetRoomsCache({onUpdate}) : super(RequestCache.cacheHour, onUpdate) {
start("MarianumMobile", "wu-rooms"); start('MarianumMobile', 'wu-rooms');
} }
@override @override

View File

@ -4,7 +4,7 @@ import '../../webuntisApi.dart';
import 'getSubjectsResponse.dart'; import 'getSubjectsResponse.dart';
class GetSubjects extends WebuntisApi { class GetSubjects extends WebuntisApi {
GetSubjects() : super("getSubjects", null); GetSubjects() : super('getSubjects', null);
@override @override
Future<GetSubjectsResponse> run() async { Future<GetSubjectsResponse> run() async {

View File

@ -6,7 +6,7 @@ import 'getSubjectsResponse.dart';
class GetSubjectsCache extends RequestCache<GetSubjectsResponse> { class GetSubjectsCache extends RequestCache<GetSubjectsResponse> {
GetSubjectsCache({onUpdate}) : super(RequestCache.cacheHour, onUpdate) { GetSubjectsCache({onUpdate}) : super(RequestCache.cacheHour, onUpdate) {
start("MarianumMobile", "wu-subjects"); start('MarianumMobile', 'wu-subjects');
} }
@override @override

View File

@ -7,7 +7,7 @@ import 'getTimetableResponse.dart';
class GetTimetable extends WebuntisApi { class GetTimetable extends WebuntisApi {
GetTimetableParams params; GetTimetableParams params;
GetTimetable(this.params) : super("getTimetable", params); GetTimetable(this.params) : super('getTimetable', params);
@override @override
Future<GetTimetableResponse> run() async { Future<GetTimetableResponse> run() async {

View File

@ -11,7 +11,7 @@ class GetTimetableCache extends RequestCache<GetTimetableResponse> {
int enddate; int enddate;
GetTimetableCache({required onUpdate, onError, required this.startdate, required this.enddate}) : super(RequestCache.cacheMinute, onUpdate, onError: onError) { GetTimetableCache({required onUpdate, onError, required this.startdate, required this.enddate}) : super(RequestCache.cacheMinute, onUpdate, onError: onError) {
start("MarianumMobile", "wu-timetable-$startdate-$enddate"); start('MarianumMobile', 'wu-timetable-$startdate-$enddate');
} }
@override @override

View File

@ -67,10 +67,10 @@ class GetTimetableParamsOptions {
} }
enum GetTimetableParamsOptionsFields { enum GetTimetableParamsOptionsFields {
@JsonValue("id") id, @JsonValue('id') id,
@JsonValue("name") name, @JsonValue('name') name,
@JsonValue("longname") longname, @JsonValue('longname') longname,
@JsonValue("externalkey") externalkey; @JsonValue('externalkey') externalkey;
static List<GetTimetableParamsOptionsFields> all = [id, name, longname, externalkey]; static List<GetTimetableParamsOptionsFields> all = [id, name, longname, externalkey];
} }
@ -88,7 +88,7 @@ class GetTimetableParamsOptionsElement {
} }
enum GetTimetableParamsOptionsElementKeyType { enum GetTimetableParamsOptionsElementKeyType {
@JsonValue("id") id, @JsonValue('id') id,
@JsonValue("name") name, @JsonValue('name') name,
@JsonValue("externalkey") externalkey @JsonValue('externalkey') externalkey
} }

View File

@ -9,7 +9,7 @@ import 'queries/authenticate/authenticate.dart';
import 'webuntisError.dart'; import 'webuntisError.dart';
abstract class WebuntisApi extends ApiRequest { abstract class WebuntisApi extends ApiRequest {
Uri endpoint = Uri.parse("https://${EndpointData().webuntis().full()}/WebUntis/jsonrpc.do?school=marianum-fulda"); Uri endpoint = Uri.parse('https://${EndpointData().webuntis().full()}/WebUntis/jsonrpc.do?school=marianum-fulda');
String method; String method;
ApiParams? genericParam; ApiParams? genericParam;
http.Response? response; http.Response? response;
@ -22,11 +22,11 @@ abstract class WebuntisApi extends ApiRequest {
Future<String> query(WebuntisApi untis) async { Future<String> query(WebuntisApi untis) async {
String query = '{"id":"ID","method":"$method","params":${untis._body()},"jsonrpc":"2.0"}'; String query = '{"id":"ID","method":"$method","params":${untis._body()},"jsonrpc":"2.0"}';
String sessionId = "0"; String sessionId = '0';
if(authenticatedResponse) { if(authenticatedResponse) {
sessionId = (await Authenticate.getSession()).sessionId; sessionId = (await Authenticate.getSession()).sessionId;
} }
http.Response data = await post(query, {"Cookie": "JSESSIONID=$sessionId"}); http.Response data = await post(query, {'Cookie': 'JSESSIONID=$sessionId'});
response = data; response = data;
dynamic jsonData = jsonDecode(data.body); dynamic jsonData = jsonDecode(data.body);
@ -49,7 +49,7 @@ abstract class WebuntisApi extends ApiRequest {
Future<ApiResponse> run(); Future<ApiResponse> run();
String _body() { String _body() {
return genericParam == null ? "{}" : jsonEncode(genericParam); return genericParam == null ? '{}' : jsonEncode(genericParam);
} }
Future<http.Response> post(String data, Map<String, String>? headers) async { Future<http.Response> post(String data, Map<String, String>? headers) async {
@ -57,7 +57,7 @@ abstract class WebuntisApi extends ApiRequest {
.post(endpoint, body: data, headers: headers) .post(endpoint, body: data, headers: headers)
.timeout( .timeout(
const Duration(seconds: 10), const Duration(seconds: 10),
onTimeout: () => throw WebuntisError("Timeout", 1) onTimeout: () => throw WebuntisError('Timeout', 1)
); );
} }
} }

View File

@ -6,6 +6,6 @@ class WebuntisError implements Exception {
@override @override
String toString() { String toString() {
return "WebUntis ($code): $message"; return 'WebUntis ($code): $message';
} }
} }

View File

@ -5,7 +5,7 @@ import 'dart:developer';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent-tab-view.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
@ -39,14 +39,14 @@ class _AppState extends State<App> with WidgetsBindingObserver {
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
log("AppLifecycle: ${state.toString()}"); log('AppLifecycle: ${state.toString()}');
if(state == AppLifecycleState.resumed) { if(state == AppLifecycleState.resumed) {
EasyThrottle.throttle( EasyThrottle.throttle(
"appLifecycleState", 'appLifecycleState',
const Duration(seconds: 10), const Duration(seconds: 10),
() { () {
log("Refreshing due to LifecycleChange"); log('Refreshing due to LifecycleChange');
NotificationTasks.updateProviders(context); NotificationTasks.updateProviders(context);
Provider.of<TimetableProps>(context, listen: false).run(); Provider.of<TimetableProps>(context, listen: false).run();
} }
@ -93,32 +93,27 @@ class _AppState extends State<App> with WidgetsBindingObserver {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PersistentTabView( return PersistentTabView(
context,
controller: App.bottomNavigator, controller: App.bottomNavigator,
navBarStyle: NavBarStyle.style6, gestureNavigationEnabled: true,
hideNavigationBarWhenKeyboardShows: true, navBarOverlap: const NavBarOverlap.none(),
navBarHeight: MediaQuery.of(context).viewInsets.bottom > 0 ? 0.0 : kBottomNavigationBarHeight, backgroundColor: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(context).colorScheme.surface,
decoration: const NavBarDecoration( screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)),
border: Border(top: BorderSide(width: 1, color: Colors.grey)), tabs: [
), PersistentTabConfig(
screenTransitionAnimation: const ScreenTransitionAnimation(animateTabTransition: true, curve: Curves.ease, duration: Duration(milliseconds: 200)), screen: const Breaker(breaker: BreakerArea.timetable, child: Timetable()),
screens: const [ item: ItemConfig(
Breaker(breaker: BreakerArea.timetable, child: Timetable()), activeForegroundColor: Theme.of(context).primaryColor,
Breaker(breaker: BreakerArea.talk, child: ChatList()), inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
Breaker(breaker: BreakerArea.files, child: Files([])),
Breaker(breaker: BreakerArea.more, child: Overhang()),
],
items: [
PersistentBottomNavBarItem(
activeColorPrimary: Theme.of(context).primaryColor,
inactiveColorPrimary: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.calendar_month), icon: const Icon(Icons.calendar_month),
title: "Vertretung" title: 'Vertretung'
), ),
PersistentBottomNavBarItem( ),
activeColorPrimary: Theme.of(context).primaryColor, PersistentTabConfig(
inactiveColorPrimary: Theme.of(context).colorScheme.secondary, screen: const Breaker(breaker: BreakerArea.talk, child: ChatList()),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: Consumer<ChatListProps>( icon: Consumer<ChatListProps>(
builder: (context, value, child) { builder: (context, value, child) {
if(value.primaryLoading()) return const Icon(Icons.chat); if(value.primaryLoading()) return const Icon(Icons.chat);
@ -132,28 +127,41 @@ class _AppState extends State<App> with WidgetsBindingObserver {
badgeColor: Theme.of(context).primaryColor, badgeColor: Theme.of(context).primaryColor,
elevation: 1, elevation: 1,
), ),
badgeContent: Text("$messages", style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)), badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
child: const Icon(Icons.chat), child: const Icon(Icons.chat),
); );
}, },
), ),
title: "Talk", title: 'Talk',
), ),
PersistentBottomNavBarItem( ),
activeColorPrimary: Theme.of(context).primaryColor, PersistentTabConfig(
inactiveColorPrimary: Theme.of(context).colorScheme.secondary, screen: const Breaker(breaker: BreakerArea.files, child: Files([])),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.folder), icon: const Icon(Icons.folder),
title: "Dateien" title: 'Dateien'
), ),
PersistentBottomNavBarItem( ),
activeColorPrimary: Theme.of(context).primaryColor, PersistentTabConfig(
inactiveColorPrimary: Theme.of(context).colorScheme.secondary, screen: const Breaker(breaker: BreakerArea.more, child: Overhang()),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.apps), icon: const Icon(Icons.apps),
title: "Mehr" title: 'Mehr'
),
), ),
], ],
navBarBuilder: (config) => Style6BottomNavBar(
navBarConfig: config,
navBarDecoration: NavBarDecoration(
border: const Border(top: BorderSide(width: 1, color: Colors.grey)),
color: Theme.of(context).colorScheme.surface,
),
),
); );
} }
@override @override

View File

@ -0,0 +1,5 @@
extension RenderNotNullExt<T> on T? {
R? wrapNullable<R>(R Function(T data) defaultValueCallback) {
return this != null ? defaultValueCallback(this as T) : null;
}
}

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:feedback/feedback.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -30,7 +29,6 @@ import 'storage/base/settingsProvider.dart';
import 'theming/darkAppTheme.dart'; import 'theming/darkAppTheme.dart';
import 'theming/lightAppTheme.dart'; import 'theming/lightAppTheme.dart';
import 'view/login/login.dart'; import 'view/login/login.dart';
import 'view/pages/more/feedback/feedbackForm.dart';
import 'widget/placeholderView.dart'; import 'widget/placeholderView.dart';
Future<void> main() async { Future<void> main() async {
@ -40,7 +38,7 @@ Future<void> main() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
log("Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}"); log("Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}");
} catch (e) { } catch (e) {
log("Error initializing Firebase app!"); log('Error initializing Firebase app!');
} }
ByteData data = await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem'); ByteData data = await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem');
@ -71,12 +69,7 @@ Future<void> main() async {
ChangeNotifierProvider(create: (context) => MessageProps()), ChangeNotifierProvider(create: (context) => MessageProps()),
ChangeNotifierProvider(create: (context) => HolidaysProps()), ChangeNotifierProvider(create: (context) => HolidaysProps()),
], ],
child: BetterFeedback(
themeMode: ThemeMode.dark,
feedbackBuilder: (context, callback, scrollController) => FeedbackForm(callback: callback, scrollController: scrollController),
localeOverride: const Locale('de'),
child: const Main(), child: const Main(),
),
) )
); );
} }
@ -93,7 +86,7 @@ class _MainState extends State<Main> {
@override @override
void initState() { void initState() {
Jiffy.setLocale("de"); Jiffy.setLocale('de');
AccountData().waitForPopulation().then((value) { AccountData().waitForPopulation().then((value) {
Provider.of<AccountModel>(context, listen: false) Provider.of<AccountModel>(context, listen: false)
@ -113,7 +106,12 @@ class _MainState extends State<Main> {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Consumer<SettingsProvider>( child: Consumer<SettingsProvider>(
builder: (context, settings, child) { builder: (context, settings, child) {
var devToolsSettings = settings.val().devToolsSettings;
return MaterialApp( return MaterialApp(
showPerformanceOverlay: devToolsSettings.showPerformanceOverlay,
checkerboardOffscreenLayers: devToolsSettings.checkerboardOffscreenLayers,
checkerboardRasterCacheImages: devToolsSettings.checkerboardRasterCacheImages,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
localizationsDelegates: const [ localizationsDelegates: const [
...GlobalMaterialLocalizations.delegates, ...GlobalMaterialLocalizations.delegates,
@ -138,7 +136,7 @@ class _MainState extends State<Main> {
switch(accountModel.state) { switch(accountModel.state) {
case AccountModelState.loggedIn: return const App(); case AccountModelState.loggedIn: return const App();
case AccountModelState.loggedOut: return const Login(); case AccountModelState.loggedOut: return const Login();
case AccountModelState.undefined: return const PlaceholderView(icon: Icons.timer, text: "Daten werden geladen"); case AccountModelState.undefined: return const PlaceholderView(icon: Icons.timer, text: 'Daten werden geladen');
} }
}, },
) )

View File

@ -10,8 +10,8 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'accountModel.dart'; import 'accountModel.dart';
class AccountData { class AccountData {
static const _usernameField = "username"; static const _usernameField = 'username';
static const _passwordField = "password"; static const _passwordField = 'password';
static final AccountData _instance = AccountData._construct(); static final AccountData _instance = AccountData._construct();
final Future<SharedPreferences> _storage = SharedPreferences.getInstance(); final Future<SharedPreferences> _storage = SharedPreferences.getInstance();
@ -29,21 +29,21 @@ class AccountData {
String? _password; String? _password;
String getUsername() { String getUsername() {
if(_username == null) throw Exception("Username not initialized"); if(_username == null) throw Exception('Username not initialized');
return _username!; return _username!;
} }
String getPassword() { String getPassword() {
if(_password == null) throw Exception("Password not initialized"); if(_password == null) throw Exception('Password not initialized');
return _password!; return _password!;
} }
String getUserSecret() { String getUserSecret() {
return sha512.convert(utf8.encode("${AccountData().getUsername()}:${AccountData().getPassword()}")).toString(); return sha512.convert(utf8.encode('${AccountData().getUsername()}:${AccountData().getPassword()}')).toString();
} }
Future<String> getDeviceId() async { Future<String> getDeviceId() async {
return sha512.convert(utf8.encode("${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}")).toString(); return sha512.convert(utf8.encode('${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}')).toString();
} }
Future<void> setData(String username, String password) async { Future<void> setData(String username, String password) async {
@ -84,7 +84,7 @@ class AccountData {
} }
String buildHttpAuthString() { String buildHttpAuthString() {
if(!isPopulated()) throw Exception("AccountData (e.g. username or password) is not initialized!"); if(!isPopulated()) throw Exception('AccountData (e.g. username or password) is not initialized!');
return "$_username:$_password"; return '$_username:$_password';
} }
} }

View File

@ -26,7 +26,7 @@ class BreakerProps extends DataHolder {
for(var key in breakers.regional.keys) { for(var key in breakers.regional.keys) {
GetBreakersReponseObject value = breakers.regional[key]!; GetBreakersReponseObject value = breakers.regional[key]!;
if(int.parse(key.split("b")[1]) >= selfVersion) { if(int.parse(key.split('b')[1]) >= selfVersion) {
if(value.areas.contains(type)) return value.message; if(value.areas.contains(type)) return value.message;
} }
} }

View File

@ -4,7 +4,7 @@ import '../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../dataHolder.dart'; import '../dataHolder.dart';
class ChatProps extends DataHolder { class ChatProps extends DataHolder {
String _queryToken = ""; String _queryToken = '';
DateTime _lastTokenSet = DateTime.now(); DateTime _lastTokenSet = DateTime.now();
GetChatResponse? _getChatResponse; GetChatResponse? _getChatResponse;

View File

@ -21,7 +21,7 @@ class Endpoint {
String domain; String domain;
String path; String path;
Endpoint({required this.domain, this.path = ""}); Endpoint({required this.domain, this.path = ''});
String full() { String full() {
return domain + path; return domain + path;
@ -40,17 +40,17 @@ class EndpointData {
EndpointMode getEndpointMode() { EndpointMode getEndpointMode() {
late String existingName; late String existingName;
existingName = AccountData().getUsername(); existingName = AccountData().getUsername();
return existingName.startsWith("google") ? EndpointMode.stage : EndpointMode.live; return existingName.startsWith('google') ? EndpointMode.stage : EndpointMode.live;
} }
Endpoint webuntis() { Endpoint webuntis() {
return EndpointOptions( return EndpointOptions(
live: Endpoint( live: Endpoint(
domain: "peleus.webuntis.com", domain: 'peleus.webuntis.com',
), ),
staged: Endpoint( staged: Endpoint(
domain: "mhsl.eu", domain: 'mhsl.eu',
path: "/marianum/marianummobile/webuntis/public/index.php/api" path: '/marianum/marianummobile/webuntis/public/index.php/api'
), ),
).get(getEndpointMode()); ).get(getEndpointMode());
} }
@ -58,11 +58,11 @@ class EndpointData {
Endpoint nextcloud() { Endpoint nextcloud() {
return EndpointOptions( return EndpointOptions(
live: Endpoint( live: Endpoint(
domain: "cloud.marianum-fulda.de", domain: 'cloud.marianum-fulda.de',
), ),
staged: Endpoint( staged: Endpoint(
domain: "mhsl.eu", domain: 'mhsl.eu',
path: "/marianum/marianummobile/cloud", path: '/marianum/marianummobile/cloud',
) )
).get(getEndpointMode()); ).get(getEndpointMode());
} }

View File

@ -12,7 +12,7 @@ extension ExtendedList on List {
class FilesProps extends DataHolder { class FilesProps extends DataHolder {
List<String> folderPath = List<String>.empty(growable: true); List<String> folderPath = List<String>.empty(growable: true);
String currentFolderName = "Home"; String currentFolderName = 'Home';
ListFilesResponse? _listFilesResponse; ListFilesResponse? _listFilesResponse;
ListFilesResponse get listFilesResponse => _listFilesResponse!; ListFilesResponse get listFilesResponse => _listFilesResponse!;
@ -32,7 +32,7 @@ class FilesProps extends DataHolder {
_listFilesResponse = null; _listFilesResponse = null;
notifyListeners(); notifyListeners();
ListFilesCache( ListFilesCache(
path: folderPath.isEmpty ? "/" : folderPath.join("/"), path: folderPath.isEmpty ? '/' : folderPath.join('/'),
onUpdate: (ListFilesResponse data) => { onUpdate: (ListFilesResponse data) => {
_listFilesResponse = data, _listFilesResponse = data,
notifyListeners(), notifyListeners(),
@ -48,7 +48,7 @@ class FilesProps extends DataHolder {
void popFolder() { void popFolder() {
folderPath.removeLast(); folderPath.removeLast();
if(folderPath.isEmpty) currentFolderName = "Home"; if(folderPath.isEmpty) currentFolderName = 'Home';
run(); run();
} }
} }

View File

@ -49,8 +49,8 @@ class TimetableProps extends DataHolder {
@override @override
void run({renew}) { void run({renew}) {
GetTimetableCache( GetTimetableCache(
startdate: int.parse(DateFormat("yyyyMMdd").format(startDate)), startdate: int.parse(DateFormat('yyyyMMdd').format(startDate)),
enddate: int.parse(DateFormat("yyyyMMdd").format(endDate)), enddate: int.parse(DateFormat('yyyyMMdd').format(endDate)),
onUpdate: (GetTimetableResponse data) => { onUpdate: (GetTimetableResponse data) => {
_getTimetableResponse = data, _getTimetableResponse = data,
notifyListeners(), notifyListeners(),

View File

@ -49,11 +49,11 @@ class NotificationController {
DebugTile(context).run(() { DebugTile(context).run(() {
showDialog(context: context, builder: (context) => AlertDialog( showDialog(context: context, builder: (context) => AlertDialog(
title: const Text("Notification report"), title: const Text('Notification report'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Text("Dieser Bericht wird angezeigt, da du den Entwicklermodus aktiviert hast und die App über eine Benachrichtigung geöffnet wurde."), const Text('Dieser Bericht wird angezeigt, da du den Entwicklermodus aktiviert hast und die App über eine Benachrichtigung geöffnet wurde.'),
Text(JsonViewer.format(message.data)), Text(JsonViewer.format(message.data)),
], ],
), ),

View File

@ -10,13 +10,13 @@ import '../widget/confirmDialog.dart';
class NotifyUpdater { class NotifyUpdater {
static ConfirmDialog enableAfterDisclaimer(SettingsProvider settings) { static ConfirmDialog enableAfterDisclaimer(SettingsProvider settings) {
return ConfirmDialog( return ConfirmDialog(
title: "Warnung", title: 'Warnung',
icon: Icons.warning_amber, icon: Icons.warning_amber,
content: "" content: ''
"Die Push-Benachrichtigungen werden durch mhsl.eu versendet.\n\n" 'Die Push-Benachrichtigungen werden durch mhsl.eu versendet.\n\n'
"Durch das aktivieren dieser Funktion wird dein Nutzername, dein Password und eine Geräte-ID von mhsl dauerhaft gespeichert und verarbeitet.\n\n" 'Durch das aktivieren dieser Funktion wird dein Nutzername, dein Password und eine Geräte-ID von mhsl dauerhaft gespeichert und verarbeitet.\n\n'
"Für mehr Informationen drücke lange auf die Einstellungsoption!", 'Für mehr Informationen drücke lange auf die Einstellungsoption!',
confirmButton: "Aktivieren", confirmButton: 'Aktivieren',
onConfirm: () { onConfirm: () {
FirebaseMessaging.instance.requestPermission( FirebaseMessaging.instance.requestPermission(
provisional: false provisional: false
@ -29,7 +29,7 @@ class NotifyUpdater {
static void registerToServer() async { static void registerToServer() async {
String? fcmToken = await FirebaseMessaging.instance.getToken(); String? fcmToken = await FirebaseMessaging.instance.getToken();
if(fcmToken == null) throw Exception("Failed to register push notification because there is no FBC token!"); if(fcmToken == null) throw Exception('Failed to register push notification because there is no FBC token!');
NotifyRegister( NotifyRegister(
NotifyRegisterParams( NotifyRegisterParams(

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import '../devTools/devToolsSettings.dart';
import '../file/fileSettings.dart'; import '../file/fileSettings.dart';
import '../fileView/fileViewSettings.dart'; import '../fileView/fileViewSettings.dart';
import '../gradeAverages/gradeAveragesSettings.dart'; import '../gradeAverages/gradeAveragesSettings.dart';
@ -27,6 +28,7 @@ class Settings {
HolidaysSettings holidaysSettings; HolidaysSettings holidaysSettings;
FileViewSettings fileViewSettings; FileViewSettings fileViewSettings;
NotificationSettings notificationSettings; NotificationSettings notificationSettings;
DevToolsSettings devToolsSettings;
Settings({ Settings({
required this.appTheme, required this.appTheme,
@ -38,6 +40,7 @@ class Settings {
required this.holidaysSettings, required this.holidaysSettings,
required this.fileViewSettings, required this.fileViewSettings,
required this.notificationSettings, required this.notificationSettings,
required this.devToolsSettings,
}); });
static String _themeToJson(ThemeMode m) => m.name; static String _themeToJson(ThemeMode m) => m.name;

View File

@ -23,6 +23,8 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
json['fileViewSettings'] as Map<String, dynamic>), json['fileViewSettings'] as Map<String, dynamic>),
notificationSettings: NotificationSettings.fromJson( notificationSettings: NotificationSettings.fromJson(
json['notificationSettings'] as Map<String, dynamic>), json['notificationSettings'] as Map<String, dynamic>),
devToolsSettings: DevToolsSettings.fromJson(
json['devToolsSettings'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
@ -35,4 +37,5 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'holidaysSettings': instance.holidaysSettings.toJson(), 'holidaysSettings': instance.holidaysSettings.toJson(),
'fileViewSettings': instance.fileViewSettings.toJson(), 'fileViewSettings': instance.fileViewSettings.toJson(),
'notificationSettings': instance.notificationSettings.toJson(), 'notificationSettings': instance.notificationSettings.toJson(),
'devToolsSettings': instance.devToolsSettings.toJson(),
}; };

View File

@ -8,7 +8,7 @@ import '../../view/settings/defaultSettings.dart';
import 'settings.dart'; import 'settings.dart';
class SettingsProvider extends ChangeNotifier { class SettingsProvider extends ChangeNotifier {
static const String _fieldName = "settings"; static const String _fieldName = 'settings';
late SharedPreferences _storage; late SharedPreferences _storage;
late Settings _settings = DefaultSettings.get(); late Settings _settings = DefaultSettings.get();
@ -45,13 +45,13 @@ class SettingsProvider extends ChangeNotifier {
_settings = Settings.fromJson(jsonDecode(_storage.getString(_fieldName)!)); _settings = Settings.fromJson(jsonDecode(_storage.getString(_fieldName)!));
} catch(exception) { } catch(exception) {
try { try {
log("Settings were changed, trying to recover from old Settings: ${exception.toString()}"); log('Settings were changed, trying to recover from old Settings: ${exception.toString()}');
_settings = Settings.fromJson(_mergeSettings(jsonDecode(_storage.getString(_fieldName)!), DefaultSettings.get().toJson())); _settings = Settings.fromJson(_mergeSettings(jsonDecode(_storage.getString(_fieldName)!), DefaultSettings.get().toJson()));
log("Settings recovered successfully: ${_settings.toJson().toString()}"); log('Settings recovered successfully: ${_settings.toJson().toString()}');
} catch(exception) { } catch(exception) {
log("Settings are defective and not recoverable, using defaults: ${exception.toString()}"); log('Settings are defective and not recoverable, using defaults: ${exception.toString()}');
_settings = DefaultSettings.get(); _settings = DefaultSettings.get();
log("Settings were reset to defaults!"); log('Settings were reset to defaults!');
} }
} }

View File

@ -0,0 +1,15 @@
import 'package:json_annotation/json_annotation.dart';
part 'devToolsSettings.g.dart';
@JsonSerializable()
class DevToolsSettings {
bool showPerformanceOverlay;
bool checkerboardOffscreenLayers;
bool checkerboardRasterCacheImages;
DevToolsSettings({required this.showPerformanceOverlay, required this.checkerboardOffscreenLayers, required this.checkerboardRasterCacheImages});
factory DevToolsSettings.fromJson(Map<String, dynamic> json) => _$DevToolsSettingsFromJson(json);
Map<String, dynamic> toJson() => _$DevToolsSettingsToJson(this);
}

View File

@ -0,0 +1,22 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'devToolsSettings.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
DevToolsSettings _$DevToolsSettingsFromJson(Map<String, dynamic> json) =>
DevToolsSettings(
showPerformanceOverlay: json['showPerformanceOverlay'] as bool,
checkerboardOffscreenLayers: json['checkerboardOffscreenLayers'] as bool,
checkerboardRasterCacheImages:
json['checkerboardRasterCacheImages'] as bool,
);
Map<String, dynamic> _$DevToolsSettingsToJson(DevToolsSettings instance) =>
<String, dynamic>{
'showPerformanceOverlay': instance.showPerformanceOverlay,
'checkerboardOffscreenLayers': instance.checkerboardOffscreenLayers,
'checkerboardRasterCacheImages': instance.checkerboardRasterCacheImages,
};

View File

@ -4,13 +4,13 @@ class AppTheme {
static ThemeModeDisplay getDisplayOptions(ThemeMode theme) { static ThemeModeDisplay getDisplayOptions(ThemeMode theme) {
switch(theme) { switch(theme) {
case ThemeMode.system: case ThemeMode.system:
return ThemeModeDisplay(icon: Icons.auto_fix_high_outlined, displayName: "Systemvorgabe"); return ThemeModeDisplay(icon: Icons.auto_fix_high_outlined, displayName: 'Systemvorgabe');
case ThemeMode.light: case ThemeMode.light:
return ThemeModeDisplay(icon: Icons.wb_sunny_outlined, displayName: "Hell"); return ThemeModeDisplay(icon: Icons.wb_sunny_outlined, displayName: 'Hell');
case ThemeMode.dark: case ThemeMode.dark:
return ThemeModeDisplay(icon: Icons.dark_mode_outlined, displayName: "Dunkel"); return ThemeModeDisplay(icon: Icons.dark_mode_outlined, displayName: 'Dunkel');
} }
} }

View File

@ -21,7 +21,7 @@ class _LoginState extends State<Login> {
bool displayDisclaimerText = true; bool displayDisclaimerText = true;
String? _checkInput(value){ String? _checkInput(value){
return (value ?? "").length == 0 ? "Eingabe erforderlich" : null; return (value ?? '').length == 0 ? 'Eingabe erforderlich' : null;
} }
Future<String?> _login(LoginData data) async { Future<String?> _login(LoginData data) async {
@ -42,7 +42,7 @@ class _LoginState extends State<Login> {
} catch(e) { } catch(e) {
await AccountData().removeData(); await AccountData().removeData();
log(e.toString()); log(e.toString());
return "Benutzername oder Password falsch! (${e.toString()})"; return 'Benutzername oder Password falsch! (${e.toString()})';
} }
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
@ -51,14 +51,14 @@ class _LoginState extends State<Login> {
Future<String> _resetPassword(String name) { Future<String> _resetPassword(String name) {
return Future.delayed(Duration.zero).then((_) { return Future.delayed(Duration.zero).then((_) {
return "Diese Funktion steht nicht zur Verfügung!"; return 'Diese Funktion steht nicht zur Verfügung!';
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FlutterLogin( return FlutterLogin(
logo: Image.asset("assets/logo/icon.png").image, logo: Image.asset('assets/logo/icon.png').image,
userValidator: _checkInput, userValidator: _checkInput,
passwordValidator: _checkInput, passwordValidator: _checkInput,
@ -84,9 +84,9 @@ class _LoginState extends State<Login> {
), ),
messages: LoginMessages( messages: LoginMessages(
loginButton: "Anmelden", loginButton: 'Anmelden',
userHint: "Nutzername", userHint: 'Nutzername',
passwordHint: "Passwort", passwordHint: 'Passwort',
), ),
disableCustomPageTransformer: true, disableCustomPageTransformer: true,
@ -97,15 +97,15 @@ class _LoginState extends State<Login> {
child: Visibility( child: Visibility(
visible: displayDisclaimerText, visible: displayDisclaimerText,
child: const Text( 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!", '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, textAlign: TextAlign.center,
), ),
), ),
), ),
), ),
footer: "Marianum Fulda - Die persönliche Schule", footer: 'Marianum Fulda - Die persönliche Schule',
title: "Marianum Fulda", title: 'Marianum Fulda',
userType: LoginUserType.name, userType: LoginUserType.name,
); );

View File

@ -6,11 +6,13 @@ import 'package:flowder/flowder.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import '../../../widget/infoDialog.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart'; import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
import '../../../api/marianumcloud/webdav/webdavApi.dart'; import '../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../model/endpointData.dart';
import '../../../widget/centeredLeading.dart'; import '../../../widget/centeredLeading.dart';
import '../../../widget/confirmDialog.dart'; import '../../../widget/confirmDialog.dart';
import '../../../widget/fileViewer.dart'; import '../../../widget/fileViewer.dart';
@ -27,7 +29,7 @@ class FileElement extends StatefulWidget {
Directory paths = await getTemporaryDirectory(); Directory paths = await getTemporaryDirectory();
var encodedPath = Uri.encodeComponent(remotePath); var encodedPath = Uri.encodeComponent(remotePath);
encodedPath = encodedPath.replaceAll("%2F", "/"); encodedPath = encodedPath.replaceAll('%2F', '/');
String local = paths.path + Platform.pathSeparator + name; String local = paths.path + Platform.pathSeparator + name;
@ -42,7 +44,7 @@ class FileElement extends StatefulWidget {
onDone: () { onDone: () {
//Future<OpenResult> result = OpenFile.open(local); // TODO legacy - refactor: remove onDone parameter //Future<OpenResult> result = OpenFile.open(local); // TODO legacy - refactor: remove onDone parameter
Navigator.of(context).push(MaterialPageRoute(builder: (context) => FileViewer(path: local))); Navigator.of(context).push(MaterialPageRoute(builder: (context) => FileViewer(path: local)));
onDone(OpenResult(message: "File viewer opened", type: ResultType.done)); onDone(OpenResult(message: 'File viewer opened', type: ResultType.done));
// result.then((value) => { // result.then((value) => {
// onDone(value) // onDone(value)
// }); // });
@ -69,21 +71,21 @@ class _FileElementState extends State<FileElement> {
children: [ children: [
Container( Container(
margin: const EdgeInsets.only(right: 10), margin: const EdgeInsets.only(right: 10),
child: const Text("Download:"), child: const Text('Download:'),
), ),
Expanded( Expanded(
child: LinearProgressIndicator(value: percent/100), child: LinearProgressIndicator(value: percent/100),
), ),
Container( Container(
margin: const EdgeInsets.only(left: 10), margin: const EdgeInsets.only(left: 10),
child: Text("${percent.round()}%"), child: Text('${percent.round()}%'),
), ),
], ],
); );
} }
return widget.file.isDirectory return widget.file.isDirectory
? Text("geändert ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}") ? Text('geändert ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}')
: Text("${filesize(widget.file.size)}, ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}"); : Text('${filesize(widget.file.size)}, ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}');
} }
@override @override
@ -103,14 +105,18 @@ class _FileElementState extends State<FileElement> {
}, },
)); ));
} else { } else {
if(EndpointData().getEndpointMode() == EndpointMode.stage) {
InfoDialog.show(context, 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!');
return;
}
if(widget.file.currentlyDownloading) { if(widget.file.currentlyDownloading) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => ConfirmDialog( builder: (context) => ConfirmDialog(
title: "Download abbrechen?", title: 'Download abbrechen?',
content: "Möchtest du den Download abbrechen?", content: 'Möchtest du den Download abbrechen?',
cancelButton: "Nein", cancelButton: 'Nein',
confirmButton: "Ja, Abbrechen", confirmButton: 'Ja, Abbrechen',
onConfirm: () { onConfirm: () {
downloadCore?.then((value) { downloadCore?.then((value) {
if(!value.isCancelled) value.cancel(); if(!value.isCancelled) value.cancel();
@ -137,7 +143,7 @@ class _FileElementState extends State<FileElement> {
if(result.type != ResultType.done) { if(result.type != ResultType.done) {
showDialog(context: context, builder: (context) { showDialog(context: context, builder: (context) {
return AlertDialog( return AlertDialog(
title: const Text("Download"), title: const Text('Download'),
content: Text(result.message), content: Text(result.message),
); );
}); });
@ -157,12 +163,12 @@ class _FileElementState extends State<FileElement> {
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.delete_outline), leading: const Icon(Icons.delete_outline),
title: const Text("Löschen"), title: const Text('Löschen'),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
showDialog(context: context, builder: (context) => ConfirmDialog( showDialog(context: context, builder: (context) => ConfirmDialog(
title: "Element löschen?", title: 'Element löschen?',
content: "Das Element wird unwiederruflich gelöscht.", content: 'Das Element wird unwiederruflich gelöscht.',
onConfirm: () { onConfirm: () {
WebdavApi.webdav WebdavApi.webdav
.then((value) => value.delete(PathUri.parse(widget.file.path))) .then((value) => value.delete(PathUri.parse(widget.file.path)))
@ -175,7 +181,7 @@ class _FileElementState extends State<FileElement> {
visible: !kReleaseMode, visible: !kReleaseMode,
child: ListTile( child: ListTile(
leading: const Icon(Icons.share_outlined), leading: const Icon(Icons.share_outlined),
title: const Text("Teilen"), title: const Text('Teilen'),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
UnimplementedDialog.show(context); UnimplementedDialog.show(context);

View File

@ -43,8 +43,8 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
setState(() { setState(() {
state = FileUploadState.checkConflict; state = FileUploadState.checkConflict;
}); });
List<WebDavResponse> result = (await webdavClient.propfind(PathUri.parse(widget.remotePath.join("/")))).responses; List<WebDavResponse> result = (await webdavClient.propfind(PathUri.parse(widget.remotePath.join('/')))).responses;
if(result.any((element) => element.href!.endsWith("/$targetFileName"))) { if(result.any((element) => element.href!.endsWith('/$targetFileName'))) {
setState(() { setState(() {
state = FileUploadState.conflict; state = FileUploadState.conflict;
}); });
@ -57,7 +57,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
} }
Future<HttpClientResponse> uploadTask = webdavClient.putFile(File(widget.localPath), FileStat.statSync(widget.localPath), PathUri.parse(fullRemotePath)); // TODO use onProgress from putFile Future<HttpClientResponse> uploadTask = webdavClient.putFile(File(widget.localPath), FileStat.statSync(widget.localPath), PathUri.parse(fullRemotePath)); // TODO use onProgress from putFile
uploadTask.then((value) => Future<HttpClientResponse?>.value(value)).catchError((e) { uploadTask.then(Future<HttpClientResponse?>.value).catchError((e) {
setState(() { setState(() {
state = FileUploadState.error; state = FileUploadState.error;
}); });
@ -67,7 +67,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
cancelableOperation = CancelableOperation<HttpClientResponse>.fromFuture( cancelableOperation = CancelableOperation<HttpClientResponse>.fromFuture(
uploadTask, uploadTask,
onCancel: () => log("Upload cancelled"), onCancel: () => log('Upload cancelled'),
); );
cancelableOperation!.then((value) { cancelableOperation!.then((value) {
@ -88,7 +88,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
void initState() { void initState() {
super.initState(); super.initState();
targetFileName = widget.fileName; targetFileName = widget.fileName;
remoteFolderName = widget.remotePath.isNotEmpty ? widget.remotePath.last : "/"; remoteFolderName = widget.remotePath.isNotEmpty ? widget.remotePath.last : '/';
fileNameController.text = widget.fileName; fileNameController.text = widget.fileName;
} }
@ -96,7 +96,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if(state == FileUploadState.naming) { if(state == FileUploadState.naming) {
return AlertDialog( return AlertDialog(
title: const Text("Datei hochladen"), title: const Text('Datei hochladen'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -107,7 +107,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
}, },
autocorrect: false, autocorrect: false,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: "Dateiname", labelText: 'Dateiname',
), ),
), ),
], ],
@ -115,10 +115,10 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
actions: [ actions: [
TextButton(onPressed: () { TextButton(onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, child: const Text("Abbrechen")), }, child: const Text('Abbrechen')),
TextButton(onPressed: () async { TextButton(onPressed: () async {
upload(); upload();
}, child: const Text("Hochladen")), }, child: const Text('Hochladen')),
], ],
); );
@ -127,7 +127,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
if(state == FileUploadState.conflict) { if(state == FileUploadState.conflict) {
return AlertDialog( return AlertDialog(
icon: const Icon(Icons.error_outline), icon: const Icon(Icons.error_outline),
title: const Text("Datei konflikt"), title: const Text('Datei konflikt'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -139,10 +139,10 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
setState(() { setState(() {
state = FileUploadState.naming; state = FileUploadState.naming;
}); });
}, child: const Text("Datei umbenennen")), }, child: const Text('Datei umbenennen')),
TextButton(onPressed: () { TextButton(onPressed: () {
upload(override: true); upload(override: true);
}, child: const Text("Datei überschreiben")), }, child: const Text('Datei überschreiben')),
], ],
); );
@ -151,15 +151,15 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
if(state == FileUploadState.upload || state == FileUploadState.checkConflict) { if(state == FileUploadState.upload || state == FileUploadState.checkConflict) {
return AlertDialog( return AlertDialog(
icon: const Icon(Icons.upload), icon: const Icon(Icons.upload),
title: const Text("Hochladen"), title: const Text('Hochladen'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Visibility( Visibility(
visible: state == FileUploadState.upload, visible: state == FileUploadState.upload,
replacement: const Text("Prüfe auf dateikonflikte..."), 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), child: const Text('Upload läuft!\nDies kann je nach Dateigröße einige Zeit dauern...', textAlign: TextAlign.center),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
const CircularProgressIndicator() const CircularProgressIndicator()
@ -183,7 +183,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
} }
return AlertDialog( return AlertDialog(
icon: const Icon(Icons.done), icon: const Icon(Icons.done),
title: const Text("Upload fertig"), title: const Text('Upload fertig'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -193,7 +193,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
actions: [ actions: [
TextButton(onPressed: () { TextButton(onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, child: const Text("Fertig")), }, child: const Text('Fertig')),
], ],
); );
@ -202,23 +202,23 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
if(state == FileUploadState.error) { if(state == FileUploadState.error) {
return AlertDialog( return AlertDialog(
icon: const Icon(Icons.error_outline), icon: const Icon(Icons.error_outline),
title: const Text("Fehler"), title: const Text('Fehler'),
content: const Column( content: const Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text("Es ist ein Fehler aufgetreten!", textAlign: TextAlign.center), Text('Es ist ein Fehler aufgetreten!', textAlign: TextAlign.center),
], ],
), ),
actions: [ actions: [
TextButton(onPressed: () { TextButton(onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, child: const Text("Schlißen")), }, child: const Text('Schlißen')),
], ],
); );
} }
throw UnimplementedError("Invalid state"); throw UnimplementedError('Invalid state');
} }
} }

View File

@ -43,17 +43,17 @@ enum SortOption {
class SortOptions { class SortOptions {
static Map<SortOption, BetterSortOption> options = { static Map<SortOption, BetterSortOption> options = {
SortOption.name: BetterSortOption( SortOption.name: BetterSortOption(
displayName: "Name", displayName: 'Name',
icon: Icons.sort_by_alpha_outlined, icon: Icons.sort_by_alpha_outlined,
compare: (CacheableFile a, CacheableFile b) => a.name.compareTo(b.name) compare: (CacheableFile a, CacheableFile b) => a.name.compareTo(b.name)
), ),
SortOption.date: BetterSortOption( SortOption.date: BetterSortOption(
displayName: "Datum", displayName: 'Datum',
icon: Icons.history_outlined, icon: Icons.history_outlined,
compare: (CacheableFile a, CacheableFile b) => a.modifiedAt!.compareTo(b.modifiedAt!) compare: (CacheableFile a, CacheableFile b) => a.modifiedAt!.compareTo(b.modifiedAt!)
), ),
SortOption.size: BetterSortOption( SortOption.size: BetterSortOption(
displayName: "Größe", displayName: 'Größe',
icon: Icons.sd_card_outlined, icon: Icons.sd_card_outlined,
compare: (CacheableFile a, CacheableFile b) { compare: (CacheableFile a, CacheableFile b) {
if(a.isDirectory || b.isDirectory) return a.isDirectory ? 1 : 0; if(a.isDirectory || b.isDirectory) return a.isDirectory ? 1 : 0;
@ -88,7 +88,7 @@ class _FilesState extends State<Files> {
void _query() { void _query() {
ListFilesCache( ListFilesCache(
path: widget.path.isEmpty ? "/" : widget.path.join("/"), path: widget.path.isEmpty ? '/' : widget.path.join('/'),
onUpdate: (ListFilesResponse d) { onUpdate: (ListFilesResponse d) {
if(!context.mounted) return; // prevent setState when widget is possibly already disposed if(!context.mounted) return; // prevent setState when widget is possibly already disposed
d.files.removeWhere((element) => element.name.isEmpty || element.name == widget.path.lastOrNull()); d.files.removeWhere((element) => element.name.isEmpty || element.name == widget.path.lastOrNull());
@ -109,7 +109,7 @@ class _FilesState extends State<Files> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(widget.path.isNotEmpty ? widget.path.last : "Dateien"), title: Text(widget.path.isNotEmpty ? widget.path.last : 'Dateien'),
actions: [ actions: [
// IconButton( // IconButton(
// icon: const Icon(Icons.search), // icon: const Icon(Icons.search),
@ -127,7 +127,7 @@ class _FilesState extends State<Files> {
children: [ children: [
Icon(e ? Icons.text_rotate_up : Icons.text_rotation_down, color: Theme.of(context).colorScheme.onSurface), Icon(e ? Icons.text_rotate_up : Icons.text_rotation_down, color: Theme.of(context).colorScheme.onSurface),
const SizedBox(width: 15), const SizedBox(width: 15),
Text(e ? "Aufsteigend" : "Absteigend") Text(e ? 'Aufsteigend' : 'Absteigend')
], ],
) )
)).toList(); )).toList();
@ -164,7 +164,7 @@ class _FilesState extends State<Files> {
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: "uploadFile", heroTag: 'uploadFile',
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
onPressed: () { onPressed: () {
showDialog(context: context, builder: (context) { showDialog(context: context, builder: (context) {
@ -172,29 +172,29 @@ class _FilesState extends State<Files> {
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.create_new_folder_outlined), leading: const Icon(Icons.create_new_folder_outlined),
title: const Text("Ordner erstellen"), title: const Text('Ordner erstellen'),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
showDialog(context: context, builder: (context) { showDialog(context: context, builder: (context) {
var inputController = TextEditingController(); var inputController = TextEditingController();
return AlertDialog( return AlertDialog(
title: const Text("Neuer Ordner"), title: const Text('Neuer Ordner'),
content: TextField( content: TextField(
controller: inputController, controller: inputController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: "Name", labelText: 'Name',
), ),
), ),
actions: [ actions: [
TextButton(onPressed: () { TextButton(onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, child: const Text("Abbrechen")), }, child: const Text('Abbrechen')),
TextButton(onPressed: () { TextButton(onPressed: () {
WebdavApi.webdav.then((webdav) { WebdavApi.webdav.then((webdav) {
webdav.mkcol(PathUri.parse("${widget.path.join("/")}/${inputController.text}")).then((value) => _query()); webdav.mkcol(PathUri.parse("${widget.path.join("/")}/${inputController.text}")).then((value) => _query());
}); });
Navigator.of(context).pop(); Navigator.of(context).pop();
}, child: const Text("Ordner erstellen")), }, child: const Text('Ordner erstellen')),
], ],
); );
}); });
@ -202,12 +202,10 @@ class _FilesState extends State<Files> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.upload_file), leading: const Icon(Icons.upload_file),
title: const Text("Aus Dateien hochladen"), title: const Text('Aus Dateien hochladen'),
onTap: () { onTap: () {
context.loaderOverlay.show(); context.loaderOverlay.show();
FilePick.documentPick().then((value) { FilePick.documentPick().then(mediaUpload);
mediaUpload(value);
});
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
@ -215,7 +213,7 @@ class _FilesState extends State<Files> {
visible: !Platform.isIOS, visible: !Platform.isIOS,
child: ListTile( child: ListTile(
leading: const Icon(Icons.add_a_photo_outlined), leading: const Icon(Icons.add_a_photo_outlined),
title: const Text("Aus Gallerie hochladen"), title: const Text('Aus Gallerie hochladen'),
onTap: () { onTap: () {
context.loaderOverlay.show(); context.loaderOverlay.show();
FilePick.galleryPick().then((value) { FilePick.galleryPick().then((value) {
@ -231,13 +229,14 @@ class _FilesState extends State<Files> {
}, },
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),
body: data == null ? const LoadingSpinner() : data!.files.isEmpty ? const PlaceholderView(icon: Icons.folder_off_rounded, text: "Der Ordner ist leer") : LoaderOverlay( body: data == null ? const LoadingSpinner() : data!.files.isEmpty ? const PlaceholderView(icon: Icons.folder_off_rounded, text: 'Der Ordner ist leer') : LoaderOverlay(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () { onRefresh: () {
_query(); _query();
return Future.delayed(const Duration(seconds: 3)); return Future.delayed(const Duration(seconds: 3));
}, },
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: files.length, itemCount: files.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
CacheableFile file = files.toList()[index]; CacheableFile file = files.toList()[index];
@ -257,6 +256,6 @@ class _FilesState extends State<Files> {
} }
var fileName = path.split(Platform.pathSeparator).last; var fileName = path.split(Platform.pathSeparator).last;
showDialog(context: context, builder: (context) => FileUploadDialog(localPath: path, remotePath: widget.path, fileName: fileName, onUploadFinished: () => _query()), barrierDismissible: false); showDialog(context: context, builder: (context) => FileUploadDialog(localPath: path, remotePath: widget.path, fileName: fileName, onUploadFinished: _query), barrierDismissible: false);
} }
} }

View File

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:package_info/package_info.dart';
import '../../../../api/mhsl/server/feedback/addFeedback.dart';
import '../../../../api/mhsl/server/feedback/addFeedbackParams.dart';
import '../../../../model/accountData.dart';
import '../../../../widget/infoDialog.dart';
class FeedbackDialog extends StatefulWidget {
const FeedbackDialog({super.key});
@override
State<FeedbackDialog> createState() => _FeedbackDialogState();
}
class _FeedbackDialogState extends State<FeedbackDialog> {
final TextEditingController _feedbackInput = TextEditingController();
String? _error;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Feedback'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Feedback, Anregungen, Ideen, Fehler und Verbesserungen'),
const SizedBox(height: 10),
const Text('Bitte gib keine geheimen Daten wie z.B. Passwörter weiter.', style: TextStyle(fontSize: 10)),
const SizedBox(height: 10),
TextField(
controller: _feedbackInput,
autofocus: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Text('Feedback und Verbesserungen')
),
// style: TextStyle(),
// expands: true,
minLines: 3,
maxLines: 5,
),
Visibility(
visible: _error != null,
child: Text('Senden fehlgeschlagen: $_error', style: const TextStyle(color: Colors.red))
)
],
),
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Abbrechen')),
TextButton(
onPressed: () async {
AddFeedback(
AddFeedbackParams(
user: AccountData().getUserSecret(),
feedback: _feedbackInput.text,
appVersion: int.parse((await PackageInfo.fromPlatform()).buildNumber)
)
)
.run()
.then((value) {
Navigator.of(context).pop();
InfoDialog.show(context, 'Danke für dein Feedback!');
})
.catchError((error, trace) {
setState(() {
_error = error.toString();
});
});
},
child: const Text('Senden'),
)
],
);
}
}

View File

@ -1,99 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../theming/darkAppTheme.dart';
import '../../../../widget/loadingSpinner.dart';
class FeedbackForm extends StatefulWidget {
final Future<void> Function(String, {Map<String, dynamic>? extras}) callback;
final ScrollController? scrollController;
const FeedbackForm({required this.scrollController, required this.callback, super.key});
@override
State<FeedbackForm> createState() => _FeedbackFormState();
}
class _FeedbackFormState extends State<FeedbackForm> {
final TextEditingController _feedbackInput = TextEditingController();
bool _textFieldEmpty = false;
bool _isSending = false;
@override
void initState() {
super.initState();
_feedbackInput.addListener(() {
setState(() {
_textFieldEmpty = _feedbackInput.text.isEmpty;
});
});
}
@override
Widget build(BuildContext context) {
return Theme(
data: DarkAppTheme.theme,
child: Visibility(
visible: !_isSending,
replacement: const LoadingSpinner(infoText: "Daten werden ermittelt"),
child: SingleChildScrollView(
controller: widget.scrollController,
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("Bitte gib keine geheimen Daten wie z.B. Passwörter weiter!", style: TextStyle(fontSize: 10)),
const SizedBox(height: 10),
TextField(
controller: _feedbackInput,
autofocus: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
label: const Text("Dein Feedback"),
errorText: _textFieldEmpty ? "Bitte gib eine Beschreibung an" : null
),
minLines: 1,
maxLines: 2,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () async {
if(_isSending) return;
if(_feedbackInput.text.isEmpty) {
setState(() {
_textFieldEmpty = true;
});
return;
}
setState(() {
_isSending = true;
});
widget.callback(_feedbackInput.text);
},
child: const Text("Senden"),
),
],
),
const SizedBox(height: 40),
const Center(
child: Column(
children: [
Text(
"Feedback, mal süß wie Kuchen, mal sauer wie Gurken, doch immer ein Schlüssel fürs Wachsen und Lernen.",
textAlign: TextAlign.center,
),
SizedBox(height: 10),
Icon(Icons.emoji_objects_outlined)
],
)
),
],
),
),
),
);
}
}

View File

@ -1,32 +0,0 @@
import 'dart:convert';
import 'package:feedback/feedback.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:loader_overlay/loader_overlay.dart';
import 'package:package_info/package_info.dart';
import '../../../../api/mhsl/server/feedback/addFeedback.dart';
import '../../../../api/mhsl/server/feedback/addFeedbackParams.dart';
import '../../../../model/accountData.dart';
import '../../../../widget/infoDialog.dart';
class FeedbackSender {
static send(BuildContext context, UserFeedback feedback) async {
BetterFeedback.of(context).hide();
context.loaderOverlay.show();
AddFeedbackParams params = AddFeedbackParams(
user: AccountData().getUserSecret(),
feedback: feedback.text,
screenshot: await compute((message) => base64Encode(message), feedback.screenshot),
appVersion: int.parse((await PackageInfo.fromPlatform()).buildNumber)
);
AddFeedback(params).run().then((value) {
InfoDialog.show(context, "Danke für dein Feedback!");
context.loaderOverlay.hide();
}).catchError((error, trace) {
InfoDialog.show(context, error.toString());
context.loaderOverlay.hide();
});
}
}

View File

@ -24,7 +24,7 @@ class _GradeAverageState extends State<GradeAverage> {
String getGradeDisplay(int grade) { String getGradeDisplay(int grade) {
if(gradeSystem) { if(gradeSystem) {
return "Note $grade"; return 'Note $grade';
} else { } else {
return "$grade Punkt${grade > 1 ? "e" : ""}"; return "$grade Punkt${grade > 1 ? "e" : ""}";
} }
@ -48,22 +48,22 @@ class _GradeAverageState extends State<GradeAverage> {
if(!settings.val().gradeAveragesSettings.askedForPreferredGradeSystem) { if(!settings.val().gradeAveragesSettings.askedForPreferredGradeSystem) {
settings.val(write: true).gradeAveragesSettings.askedForPreferredGradeSystem = true; settings.val(write: true).gradeAveragesSettings.askedForPreferredGradeSystem = true;
showDialog(context: context, builder: (context) => AlertDialog( showDialog(context: context, builder: (context) => AlertDialog(
title: const Text("Notensystem"), title: const Text('Notensystem'),
content: const Text("Wähle dein bevorzugtes Schulnotensystem"), content: const Text('Wähle dein bevorzugtes Schulnotensystem'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
switchSystem(true); switchSystem(true);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: const Text("Realschule"), child: const Text('Realschule'),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
switchSystem(false); switchSystem(false);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: const Text("Oberstufe"), child: const Text('Oberstufe'),
), ),
], ],
)); ));
@ -81,7 +81,7 @@ class _GradeAverageState extends State<GradeAverage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Notendurschnittsrechner"), title: const Text('Notendurschnittsrechner'),
actions: [ actions: [
Visibility( Visibility(
visible: grades.isNotEmpty, visible: grades.isNotEmpty,
@ -89,9 +89,9 @@ class _GradeAverageState extends State<GradeAverage> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => ConfirmDialog( builder: (context) => ConfirmDialog(
title: "Zurücksetzen?", title: 'Zurücksetzen?',
content: "Alle Einträge werden entfernt.", content: 'Alle Einträge werden entfernt.',
confirmButton: "Zurücksetzen", confirmButton: 'Zurücksetzen',
onConfirm: () { onConfirm: () {
grades.clear(); grades.clear();
setState(() {}); setState(() {});
@ -109,7 +109,7 @@ class _GradeAverageState extends State<GradeAverage> {
children: [ children: [
Icon(e ? Icons.calculate_outlined : Icons.school_outlined, color: Theme.of(context).colorScheme.onSurface), Icon(e ? Icons.calculate_outlined : Icons.school_outlined, color: Theme.of(context).colorScheme.onSurface),
const SizedBox(width: 15), const SizedBox(width: 15),
Text(e ? "Notensystem" : "Punktesystem"), Text(e ? 'Notensystem' : 'Punktesystem'),
], ],
), ),
)).toList(), )).toList(),
@ -120,9 +120,9 @@ class _GradeAverageState extends State<GradeAverage> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => ConfirmDialog( builder: (context) => ConfirmDialog(
title: "Notensystem wechseln", title: 'Notensystem wechseln',
content: "Beim wechsel des Notensystems werden alle Einträge zurückgesetzt.", content: 'Beim wechsel des Notensystems werden alle Einträge zurückgesetzt.',
confirmButton: "Fortfahren", confirmButton: 'Fortfahren',
onConfirm: () => switchSystem(e), onConfirm: () => switchSystem(e),
), ),
); );
@ -142,7 +142,7 @@ class _GradeAverageState extends State<GradeAverage> {
const SizedBox(height: 10), const SizedBox(height: 10),
const Divider(), const Divider(),
const SizedBox(height: 10), const SizedBox(height: 10),
Text(gradeSystem ? "Wähle unten die Anzahl deiner jewiligen Noten aus" : "Wähle unten die Anzahl deiner jeweiligen Punkte aus"), Text(gradeSystem ? 'Wähle unten die Anzahl deiner jewiligen Noten aus' : 'Wähle unten die Anzahl deiner jeweiligen Punkte aus'),
const SizedBox(height: 10), const SizedBox(height: 10),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
@ -169,7 +169,7 @@ class _GradeAverageState extends State<GradeAverage> {
icon: const Icon(Icons.remove), icon: const Icon(Icons.remove),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
Text("${grades.where(isThis).length}", style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)), Text('${grades.where(isThis).length}', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
IconButton( IconButton(
onPressed: () { onPressed: () {
setState(() { setState(() {

View File

@ -42,23 +42,23 @@ class _HolidaysState extends State<Holidays> {
} }
String parseString(String enDate) { String parseString(String enDate) {
return Jiffy.parse(enDate).format(pattern: "dd.MM.yyyy"); return Jiffy.parse(enDate).format(pattern: 'dd.MM.yyyy');
} }
void showDisclaimer() { void showDisclaimer() {
showDialog(context: context, builder: (context) { showDialog(context: context, builder: (context) {
return AlertDialog( return AlertDialog(
title: const Text("Richtigkeit und Bereitstellung der Daten"), title: const Text('Richtigkeit und Bereitstellung der Daten'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Text("" const Text(''
"Sämtliche Datumsangaben sind ohne Gewähr.\n" 'Sämtliche Datumsangaben sind ohne Gewähr.\n'
"Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n" 'Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n'
"Die Daten stammen von https://ferien-api.de/"), 'Die Daten stammen von https://ferien-api.de/'),
const SizedBox(height: 30), const SizedBox(height: 30),
ListTile( ListTile(
title: const Text("Diese Meldung nicht mehr anzeigen"), title: const Text('Diese Meldung nicht mehr anzeigen'),
trailing: Checkbox( trailing: Checkbox(
value: settings.val().holidaysSettings.dismissedDisclaimer, value: settings.val().holidaysSettings.dismissedDisclaimer,
onChanged: (value) => settings.val(write: true).holidaysSettings.dismissedDisclaimer = value!, onChanged: (value) => settings.val(write: true).holidaysSettings.dismissedDisclaimer = value!,
@ -67,8 +67,8 @@ class _HolidaysState extends State<Holidays> {
], ],
), ),
actions: [ actions: [
TextButton(child: const Text("ferien-api.de besuchen"), onPressed: () => ConfirmDialog.openBrowser(context, "https://ferien-api.de/")), TextButton(child: const Text('ferien-api.de besuchen'), onPressed: () => ConfirmDialog.openBrowser(context, 'https://ferien-api.de/')),
TextButton(child: const Text("Okay"), onPressed: () => Navigator.of(context).pop()), TextButton(child: const Text('Okay'), onPressed: () => Navigator.of(context).pop()),
], ],
); );
}); });
@ -78,11 +78,11 @@ class _HolidaysState extends State<Holidays> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Schulferien in Hessen"), title: const Text('Schulferien in Hessen'),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.warning_amber_outlined), icon: const Icon(Icons.warning_amber_outlined),
onPressed: () => showDisclaimer(), onPressed: showDisclaimer,
), ),
PopupMenuButton<bool>( PopupMenuButton<bool>(
initialValue: settings.val().holidaysSettings.showPastEvents, initialValue: settings.val().holidaysSettings.showPastEvents,
@ -95,7 +95,7 @@ class _HolidaysState extends State<Holidays> {
children: [ children: [
Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, color: Theme.of(context).colorScheme.onSurface), Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, color: Theme.of(context).colorScheme.onSurface),
const SizedBox(width: 15), const SizedBox(width: 15),
Text(e ? "Alle anzeigen" : "Nur zukünftige anzeigen") Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen')
], ],
) )
)).toList(); )).toList();
@ -115,19 +115,19 @@ class _HolidaysState extends State<Holidays> {
List<GetHolidaysResponseObject> holidays = value.getHolidaysResponse.data; List<GetHolidaysResponseObject> holidays = value.getHolidaysResponse.data;
if(!showPastEvents) holidays = holidays.where((element) => DateTime.parse(element.end).isAfter(DateTime.now())).toList(); if(!showPastEvents) holidays = holidays.where((element) => DateTime.parse(element.end).isAfter(DateTime.now())).toList();
if(holidays.isEmpty) return const PlaceholderView(icon: Icons.search_off, text: "Es wurden keine Ferieneinträge gefunden!"); if(holidays.isEmpty) return const PlaceholderView(icon: Icons.search_off, text: 'Es wurden keine Ferieneinträge gefunden!');
return ListView.builder( return ListView.builder(
itemCount: holidays.length, itemCount: holidays.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
GetHolidaysResponseObject holiday = holidays[index]; GetHolidaysResponseObject holiday = holidays[index];
String holidayType = holiday.name.split(" ").first.capitalize(); String holidayType = holiday.name.split(' ').first.capitalize();
return ListTile( return ListTile(
leading: const CenteredLeading(Icon(Icons.calendar_month)), leading: const CenteredLeading(Icon(Icons.calendar_month)),
title: Text("$holidayType ab ${parseString(holiday.start)}"), title: Text('$holidayType ab ${parseString(holiday.start)}'),
subtitle: Text("bis ${parseString(holiday.end)}"), subtitle: Text('bis ${parseString(holiday.end)}'),
onTap: () => showDialog(context: context, builder: (context) => SimpleDialog( onTap: () => showDialog(context: context, builder: (context) => SimpleDialog(
title: Text("$holidayType ${holiday.year} in Hessen"), title: Text('$holidayType ${holiday.year} in Hessen'),
children: [ children: [
ListTile( ListTile(
leading: const CenteredLeading(Icon(Icons.signpost_outlined)), leading: const CenteredLeading(Icon(Icons.signpost_outlined)),
@ -136,11 +136,11 @@ class _HolidaysState extends State<Holidays> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.arrow_forward), leading: const Icon(Icons.arrow_forward),
title: Text("vom ${parseString(holiday.start)}"), title: Text('vom ${parseString(holiday.start)}'),
), ),
ListTile( ListTile(
leading: const Icon(Icons.arrow_back), leading: const Icon(Icons.arrow_back),
title: Text("bis zum ${parseString(holiday.end)}"), title: Text('bis zum ${parseString(holiday.end)}'),
), ),
Visibility( Visibility(
visible: !DateTime.parse(holiday.start).difference(DateTime.now()).isNegative, visible: !DateTime.parse(holiday.start).difference(DateTime.now()).isNegative,

View File

@ -27,7 +27,7 @@ class _MessageState extends State<Message> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Marianum Message"), title: const Text('Marianum Message'),
), ),
body: Consumer<MessageProps>(builder: (context, value, child) { body: Consumer<MessageProps>(builder: (context, value, child) {
if(value.primaryLoading()) return const LoadingSpinner(); if(value.primaryLoading()) return const LoadingSpinner();
@ -43,7 +43,7 @@ class _MessageState extends State<Message> {
children: [Icon(Icons.newspaper)], children: [Icon(Icons.newspaper)],
), ),
title: Text(message.name, overflow: TextOverflow.ellipsis), title: Text(message.name, overflow: TextOverflow.ellipsis),
subtitle: Text("vom ${message.date}"), subtitle: Text('vom ${message.date}'),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () { onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: value.getMessagesResponse.base, message: message))); Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: value.getMessagesResponse.base, message: message)));

View File

@ -29,12 +29,12 @@ class _MessageViewState extends State<MessageView> {
Navigator.of(context).pop(); Navigator.of(context).pop();
showDialog(context: context, builder: (context) { showDialog(context: context, builder: (context) {
return AlertDialog( return AlertDialog(
title: const Text("Fehler beim öffnen"), title: const Text('Fehler beim öffnen'),
content: Text("Dokument '${widget.message.name}' konnte nicht geladen werden:\n${e.description}"), content: Text("Dokument '${widget.message.name}' konnte nicht geladen werden:\n${e.description}"),
actions: [ actions: [
TextButton(onPressed: () { TextButton(onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, child: const Text("Ok")) }, child: const Text('Ok'))
], ],
); );
}); });
@ -43,9 +43,9 @@ class _MessageViewState extends State<MessageView> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => ConfirmDialog( builder: (context) => ConfirmDialog(
title: "Link öffnen", title: 'Link öffnen',
content: "Möchtest du den folgenden Link öffnen?\n${e.uri}", content: 'Möchtest du den folgenden Link öffnen?\n${e.uri}',
confirmButton: "Öffnen", confirmButton: 'Öffnen',
onConfirm: () => launchUrl(Uri.parse(e.uri), mode: LaunchMode.externalApplication), onConfirm: () => launchUrl(Uri.parse(e.uri), mode: LaunchMode.externalApplication),
), ),
); );

View File

@ -8,10 +8,10 @@ class Roomplan extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Raumplan"), title: const Text('Raumplan'),
), ),
body: PhotoView( body: PhotoView(
imageProvider: Image.asset("assets/img/raumplan.jpg").image, imageProvider: Image.asset('assets/img/raumplan.jpg').image,
minScale: 0.5, minScale: 0.5,
maxScale: 2.0, maxScale: 2.0,
backgroundDecoration: BoxDecoration(color: Theme.of(context).colorScheme.background), backgroundDecoration: BoxDecoration(color: Theme.of(context).colorScheme.background),

View File

@ -16,18 +16,18 @@ class _QrShareViewState extends State<QrShareView> {
length: 2, length: 2,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Teile die App"), title: const Text('Teile die App'),
bottom: const TabBar( bottom: const TabBar(
tabs: [ tabs: [
Tab(icon: Icon(Icons.android_outlined), text: "Android"), Tab(icon: Icon(Icons.android_outlined), text: 'Android'),
Tab(icon: Icon(Icons.apple_outlined), text: "iOS & iPadOS"), Tab(icon: Icon(Icons.apple_outlined), text: 'iOS & iPadOS'),
], ],
), ),
), ),
body: const TabBarView( body: const TabBarView(
children: [ children: [
AppSharePlatformView("Für Android", "https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client"), AppSharePlatformView('Für Android', 'https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client'),
AppSharePlatformView("Für iOS & iPad", "https://apps.apple.com/us/app/marianum-fulda/id6458789560"), AppSharePlatformView('Für iOS & iPad', 'https://apps.apple.com/us/app/marianum-fulda/id6458789560'),
], ],
), ),
), ),

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:loader_overlay/loader_overlay.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../../../../widget/sharePositionOrigin.dart'; import '../../../../widget/sharePositionOrigin.dart';
@ -10,12 +9,11 @@ class SelectShareTypeDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LoaderOverlay( return SimpleDialog(
child: SimpleDialog(
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.qr_code_2_outlined), leading: const Icon(Icons.qr_code_2_outlined),
title: const Text("Per QR-Code"), title: const Text('Per QR-Code'),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const QrShareView())); Navigator.of(context).push(MaterialPageRoute(builder: (context) => const QrShareView()));
@ -23,21 +21,20 @@ class SelectShareTypeDialog extends StatelessWidget {
), ),
ListTile( ListTile(
leading: const Icon(Icons.link_outlined), leading: const Icon(Icons.link_outlined),
title: const Text("Per Link teilen"), title: const Text('Per Link teilen'),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () { onTap: () {
Share.share( Share.share(
sharePositionOrigin: SharePositionOrigin.get(context), sharePositionOrigin: SharePositionOrigin.get(context),
subject: "App Teilen", subject: 'App Teilen',
"Hol dir die für das Marianum maßgeschneiderte App:" 'Hol dir die für das Marianum maßgeschneiderte App:'
"\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client " '\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client '
"\niOS: https://apps.apple.com/us/app/marianum-fulda/id6458789560 " '\niOS: https://apps.apple.com/us/app/marianum-fulda/id6458789560 '
"\n\nViel Spaß!" '\n\nViel Spaß!'
); );
}, },
) )
], ],
),
); );
} }
} }

View File

@ -1,12 +1,16 @@
import 'package:feedback/feedback.dart'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:in_app_review/in_app_review.dart'; import 'package:in_app_review/in_app_review.dart';
import '../../extensions/renderNotNull.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import '../../widget/ListItem.dart'; import '../../widget/ListItem.dart';
import '../../widget/centeredLeading.dart'; import '../../widget/centeredLeading.dart';
import '../../widget/infoDialog.dart'; import '../../widget/infoDialog.dart';
import '../settings/settings.dart'; import '../settings/settings.dart';
import 'more/feedback/feedbackSender.dart'; import 'more/feedback/feedbackDialog.dart';
import 'more/gradeAverages/gradeAverage.dart'; import 'more/gradeAverages/gradeAverage.dart';
import 'more/holidays/holidays.dart'; import 'more/holidays/holidays.dart';
import 'more/message/message.dart'; import 'more/message/message.dart';
@ -21,21 +25,22 @@ class Overhang extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Mehr"), title: const Text('Mehr'),
actions: [ actions: [
IconButton(onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const Settings())), icon: const Icon(Icons.settings)) IconButton(onPressed: () => pushScreen(context, screen: const Settings(), withNavBar: false), icon: const Icon(Icons.settings))
], ],
), ),
body: ListView( body: ListView(
children: [ children: [
const ListItemNavigator(icon: Icons.newspaper, text: "Marianum Message", target: Message()), const ListItemNavigator(icon: Icons.newspaper, text: 'Marianum Message', target: Message()),
const ListItemNavigator(icon: Icons.room, text: "Raumplan", target: Roomplan()), const ListItemNavigator(icon: Icons.room, text: 'Raumplan', target: Roomplan()),
const ListItemNavigator(icon: Icons.calculate, text: "Notendurschnittsrechner", target: GradeAverage()), const ListItemNavigator(icon: Icons.calculate, text: 'Notendurschnittsrechner', target: GradeAverage()),
const ListItemNavigator(icon: Icons.calendar_month, text: "Schulferien", target: Holidays()), const ListItemNavigator(icon: Icons.calendar_month, text: 'Schulferien', target: Holidays()),
const Divider(), const Divider(),
ListTile( ListTile(
leading: const Icon(Icons.share_outlined), leading: const Icon(Icons.share_outlined),
title: const Text("Teile die App"), title: const Text('Teile die App'),
subtitle: const Text('Mit Freunden und deiner Klasse teilen'),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () => showDialog(context: context, builder: (context) => const SelectShareTypeDialog()) onTap: () => showDialog(context: context, builder: (context) => const SelectShareTypeDialog())
), ),
@ -44,30 +49,32 @@ class Overhang extends StatelessWidget {
builder: (context, snapshot) { builder: (context, snapshot) {
if(!snapshot.hasData) return const SizedBox.shrink(); if(!snapshot.hasData) return const SizedBox.shrink();
return Visibility( String? getPlatformStoreName() {
visible: snapshot.requireData, if(Platform.isAndroid) return 'Play store';
child: ListTile( if(Platform.isIOS) return 'App store';
return null;
}
return ListTile(
leading: const CenteredLeading(Icon(Icons.star_rate_outlined)), leading: const CenteredLeading(Icon(Icons.star_rate_outlined)),
title: const Text("App Bewerten"), title: const Text('App bewerten'),
subtitle: getPlatformStoreName().wrapNullable((data) => Text('Im $data')),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () { onTap: () {
InAppReview.instance.openStoreListing(appStoreId: "6458789560").then( InAppReview.instance.openStoreListing(appStoreId: '6458789560').then(
(value) => InfoDialog.show(context, "Vielen Dank!"), (value) => InfoDialog.show(context, 'Vielen Dank!'),
onError: (error) => InfoDialog.show(context, error.toString()) onError: (error) => InfoDialog.show(context, error.toString())
); );
}, },
),
); );
}, },
), ),
ListTile( ListTile(
leading: const CenteredLeading(Icon(Icons.feedback_outlined)), leading: const CenteredLeading(Icon(Icons.feedback_outlined)),
title: const Text("Du hast eine Idee?"), title: const Text('Du hast eine Idee?'),
subtitle: const Text("Fehler und Verbessungsvorschläge"), subtitle: const Text('Fehler und Verbessungsvorschläge'),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () { onTap: () => showDialog(context: context, barrierDismissible: false, builder: (context) => const FeedbackDialog()),
BetterFeedback.of(context).show((UserFeedback feedback) => FeedbackSender.send(context, feedback));
},
), ),
], ],
), ),

View File

@ -66,7 +66,7 @@ class _ChatInfoState extends State<ChatInfo> {
if(participants != null) ...[ if(participants != null) ...[
ListTile( ListTile(
leading: const Icon(Icons.supervised_user_circle), leading: const Icon(Icons.supervised_user_circle),
title: Text("${participants!.data.length} Teilnehmer"), title: Text('${participants!.data.length} Teilnehmer'),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () => TalkNavigator.pushSplitView(context, ParticipantsListView(participants!)), onTap: () => TalkNavigator.pushSplitView(context, ParticipantsListView(participants!)),
), ),

View File

@ -16,7 +16,7 @@ class _ParticipantsListViewState extends State<ParticipantsListView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Teilnehmende"), title: const Text('Teilnehmende'),
), ),
body: ListView( body: ListView(
children: widget.participantsResponse.data.map((participant) { children: widget.participantsResponse.data.map((participant) {

View File

@ -40,9 +40,9 @@ class _ChatListState extends State<ChatList> {
ConfirmDialog( ConfirmDialog(
icon: Icons.notifications_active_outlined, icon: Icons.notifications_active_outlined,
title: "Benachrichtigungen aktivieren", title: 'Benachrichtigungen aktivieren',
content: "Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.", content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.',
confirmButton: "Weiter", confirmButton: 'Weiter',
onConfirm: () { onConfirm: () {
FirebaseMessaging.instance.requestPermission( FirebaseMessaging.instance.requestPermission(
provisional: false provisional: false
@ -53,7 +53,7 @@ class _ChatListState extends State<ChatList> {
break; break;
case AuthorizationStatus.denied: case AuthorizationStatus.denied:
showDialog(context: context, builder: (context) => const AlertDialog( showDialog(context: context, builder: (context) => const AlertDialog(
content: Text("Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren."), content: Text('Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.'),
)); ));
break; break;
default: default:
@ -79,7 +79,7 @@ class _ChatListState extends State<ChatList> {
breakpoint: 1000, breakpoint: 1000,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Talk"), title: const Text('Talk'),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
@ -91,16 +91,16 @@ class _ChatListState extends State<ChatList> {
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: "createChat", heroTag: 'createChat',
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
onPressed: () async { onPressed: () async {
showSearch(context: context, delegate: JoinChat()).then((username) { showSearch(context: context, delegate: JoinChat()).then((username) {
if(username == null) return; if(username == null) return;
ConfirmDialog( ConfirmDialog(
title: "Chat starten", title: 'Chat starten',
content: "Möchtest du einen Chat mit Nutzer '$username' starten?", content: "Möchtest du einen Chat mit Nutzer '$username' starten?",
confirmButton: "Chat starten", confirmButton: 'Chat starten',
onConfirm: () { onConfirm: () {
CreateRoom(CreateRoomParams( CreateRoom(CreateRoomParams(
roomType: 1, roomType: 1,
@ -136,7 +136,10 @@ class _ChatListState extends State<ChatList> {
_query(renew: true); _query(renew: true);
return Future.delayed(const Duration(seconds: 3)); return Future.delayed(const Duration(seconds: 3));
}, },
child: ListView(children: chats), child: ListView(
padding: EdgeInsets.zero,
children: chats
),
); );
}, },
), ),

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:loader_overlay/loader_overlay.dart'; import '../../../extensions/dateTime.dart';
import 'package:marianum_mobile/extensions/dateTime.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../api/marianumcloud/talk/chat/getChatResponse.dart';
@ -52,8 +51,8 @@ class _ChatViewState extends State<ChatView> {
data.getChatResponse.sortByTimestamp().forEach((element) { data.getChatResponse.sortByTimestamp().forEach((element) {
DateTime elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000); DateTime elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000);
if(element.systemMessage.contains("reaction")) return; if(element.systemMessage.contains('reaction')) return;
int commonRead = int.parse(data.getChatResponse.headers?['x-chat-last-common-read'] ?? "0"); int commonRead = int.parse(data.getChatResponse.headers?['x-chat-last-common-read'] ?? '0');
if(!elementDate.isSameDay(lastDate)) { if(!elementDate.isSameDay(lastDate)) {
lastDate = elementDate; lastDate = elementDate;
@ -82,8 +81,8 @@ class _ChatViewState extends State<ChatView> {
context: context, context: context,
isSender: false, isSender: false,
bubbleData: GetChatResponseObject.getTextDummy( bubbleData: GetChatResponseObject.getTextDummy(
"Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. " 'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. '
"Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de" 'Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de'
), ),
chatData: widget.room, chatData: widget.room,
refetch: _query, refetch: _query,
@ -92,6 +91,7 @@ class _ChatViewState extends State<ChatView> {
} }
return Scaffold( return Scaffold(
backgroundColor: const Color(0xffefeae2),
appBar: ClickableAppBar( appBar: ClickableAppBar(
onTap: () { onTap: () {
TalkNavigator.pushSplitView(context, ChatInfo(widget.room)); TalkNavigator.pushSplitView(context, ChatInfo(widget.room));
@ -111,14 +111,13 @@ class _ChatViewState extends State<ChatView> {
body: Container( body: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: const AssetImage("assets/background/chat.png"), image: const AssetImage('assets/background/chat.png'),
scale: 1.5, scale: 1.5,
opacity: 1, opacity: 1,
repeat: ImageRepeat.repeat, repeat: ImageRepeat.repeat,
invertColors: AppTheme.isDarkMode(context) invertColors: AppTheme.isDarkMode(context)
) )
), ),
child: LoaderOverlay(
child: data.primaryLoading() ? const LoadingSpinner() : Column( child: data.primaryLoading() ? const LoadingSpinner() : Column(
children: [ children: [
Expanded( Expanded(
@ -130,11 +129,13 @@ class _ChatViewState extends State<ChatView> {
), ),
Container( Container(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: SafeArea(child: ChatTextfield(widget.room.token)), child: TalkNavigator.isSecondaryVisible(context)
? ChatTextfield(widget.room.token)
: SafeArea(child: ChatTextfield(widget.room.token)
),
) )
], ],
), ),
)
), ),
); );
}, },

View File

@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:marianum_mobile/extensions/text.dart'; import '../../../../extensions/text.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
@ -114,7 +114,7 @@ class _ChatBubbleState extends State<ChatBubble> {
); );
Text timeText = Text( Text timeText = Text(
Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: "HH:mm"), Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: 'HH:mm'),
textAlign: TextAlign.end, textAlign: TextAlign.end,
style: TextStyle(color: widget.timeIconColor, fontSize: widget.timeIconSize), style: TextStyle(color: widget.timeIconColor, fontSize: widget.timeIconSize),
); );
@ -184,7 +184,7 @@ class _ChatBubbleState extends State<ChatBubble> {
), ),
onLongPress: () { onLongPress: () {
showDialog(context: context, builder: (context) { showDialog(context: context, builder: (context) {
List<String> commonReactions = ["👍", "👎", "😆", "❤️", "👀", "🤔"]; List<String> commonReactions = ['👍', '👎', '😆', '❤️', '👀', '🤔'];
bool canReact = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment; bool canReact = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
return SimpleDialog( return SimpleDialog(
children: [ children: [
@ -222,7 +222,7 @@ class _ChatBubbleState extends State<ChatBubble> {
visible: canReact, visible: canReact,
child: ListTile( child: ListTile(
leading: const Icon(Icons.add_reaction_outlined), leading: const Icon(Icons.add_reaction_outlined),
title: const Text("Reaktionen"), title: const Text('Reaktionen'),
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => MessageReactions( Navigator.of(context).push(MaterialPageRoute(builder: (context) => MessageReactions(
token: widget.chatData.token, token: widget.chatData.token,
@ -235,7 +235,7 @@ class _ChatBubbleState extends State<ChatBubble> {
visible: !message.containsFile, visible: !message.containsFile,
child: ListTile( child: ListTile(
leading: const Icon(Icons.copy), leading: const Icon(Icons.copy),
title: const Text("Nachricht kopieren"), title: const Text('Nachricht kopieren'),
onTap: () => { onTap: () => {
Clipboard.setData(ClipboardData(text: widget.bubbleData.message)), Clipboard.setData(ClipboardData(text: widget.bubbleData.message)),
Navigator.of(context).pop(), Navigator.of(context).pop(),
@ -256,7 +256,7 @@ class _ChatBubbleState extends State<ChatBubble> {
visible: widget.isSender && DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).add(const Duration(hours: 6)).isAfter(DateTime.now()), visible: widget.isSender && DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).add(const Duration(hours: 6)).isAfter(DateTime.now()),
child: ListTile( child: ListTile(
leading: const Icon(Icons.delete_outline), leading: const Icon(Icons.delete_outline),
title: const Text("Nachricht löschen"), title: const Text('Nachricht löschen'),
onTap: () { onTap: () {
DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) { DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) {
Provider.of<ChatProps>(context, listen: false).run(); Provider.of<ChatProps>(context, listen: false).run();
@ -276,12 +276,12 @@ class _ChatBubbleState extends State<ChatBubble> {
if(downloadProgress > 0) { if(downloadProgress > 0) {
showDialog(context: context, builder: (context) { showDialog(context: context, builder: (context) {
return AlertDialog( return AlertDialog(
title: const Text("Download abbrechen?"), title: const Text('Download abbrechen?'),
content: const Text("Möchtest du den Download abbrechen?"), content: const Text('Möchtest du den Download abbrechen?'),
actions: [ actions: [
TextButton(onPressed: () { TextButton(onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, child: const Text("Nein")), }, child: const Text('Nein')),
TextButton(onPressed: () { TextButton(onPressed: () {
downloadCore?.then((value) { downloadCore?.then((value) {
if(!value.isCancelled) value.cancel(); if(!value.isCancelled) value.cancel();
@ -291,7 +291,7 @@ class _ChatBubbleState extends State<ChatBubble> {
downloadProgress = 0; downloadProgress = 0;
downloadCore = null; downloadCore = null;
}); });
}, child: const Text("Ja, Abbrechen")) }, child: const Text('Ja, Abbrechen'))
], ],
); );
}); });
@ -336,7 +336,7 @@ class _ChatBubbleState extends State<ChatBubble> {
return Container( return Container(
margin: const EdgeInsets.only(right: 2.5, left: 2.5), margin: const EdgeInsets.only(right: 2.5, left: 2.5),
child: ActionChip( child: ActionChip(
label: Text("${e.key} ${e.value}"), label: Text('${e.key} ${e.value}'),
visualDensity: const VisualDensity(vertical: VisualDensity.minimumDensity, horizontal: VisualDensity.minimumDensity), visualDensity: const VisualDensity(vertical: VisualDensity.minimumDensity, horizontal: VisualDensity.minimumDensity),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
backgroundColor: hasSelfReacted ? Theme.of(context).primaryColor : null, backgroundColor: hasSelfReacted ? Theme.of(context).primaryColor : null,

View File

@ -14,14 +14,14 @@ class ChatMessage {
Map<String, RichObjectString>? originalData; Map<String, RichObjectString>? originalData;
RichObjectString? file; RichObjectString? file;
String content = ""; String content = '';
bool get containsFile => file != null; bool get containsFile => file != null;
ChatMessage({required this.originalMessage, this.originalData}) { ChatMessage({required this.originalMessage, this.originalData}) {
if(originalData?.containsKey("file") ?? false) { if(originalData?.containsKey('file') ?? false) {
file = originalData?['file']; file = originalData?['file'];
content = file?.name ?? "Datei"; content = file?.name ?? 'Datei';
} else { } else {
content = RichObjectStringProcessor.parseToString(originalMessage, originalData); content = RichObjectStringProcessor.parseToString(originalMessage, originalData);
} }
@ -56,7 +56,7 @@ class ChatMessage {
fadeInDuration: Duration.zero, fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero, fadeOutDuration: Duration.zero,
errorListener: (value) {}, errorListener: (value) {},
imageUrl: "https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=100&y=-1&a=1", imageUrl: 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=100&y=-1&a=1',
); );
} }

View File

@ -41,9 +41,9 @@ class _ChatTextfieldState extends State<ChatTextfield> {
} }
String filename = "${path.split("/").last.split(".").first}-${const Uuid().v4()}.${path.split(".").last}"; String filename = "${path.split("/").last.split(".").first}-${const Uuid().v4()}.${path.split(".").last}";
String shareFolder = "MarianumMobile"; String shareFolder = 'MarianumMobile';
WebdavApi.webdav.then((webdav) { WebdavApi.webdav.then((webdav) {
webdav.mkcol(PathUri.parse("/$shareFolder")); webdav.mkcol(PathUri.parse('/$shareFolder'));
}); });
showDialog(context: context, builder: (context) => FileUploadDialog( showDialog(context: context, builder: (context) => FileUploadDialog(
@ -55,7 +55,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
FileSharingApi().share(FileSharingApiParams( FileSharingApi().share(FileSharingApiParams(
shareType: 10, shareType: 10,
shareWith: widget.sendToToken, shareWith: widget.sendToToken,
path: "$shareFolder/$filename", path: '$shareFolder/$filename',
)).then((value) => _query()); )).then((value) => _query());
}, },
), barrierDismissible: false); ), barrierDismissible: false);
@ -77,7 +77,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? ""; _textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? '';
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
@ -95,12 +95,10 @@ class _ChatTextfieldState extends State<ChatTextfield> {
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.file_open), leading: const Icon(Icons.file_open),
title: const Text("Aus Dateien auswählen"), title: const Text('Aus Dateien auswählen'),
onTap: () { onTap: () {
context.loaderOverlay.show(); context.loaderOverlay.show();
FilePick.documentPick().then((value) { FilePick.documentPick().then(mediaUpload);
mediaUpload(value);
});
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
@ -108,7 +106,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
visible: !Platform.isIOS, visible: !Platform.isIOS,
child: ListTile( child: ListTile(
leading: const Icon(Icons.image), leading: const Icon(Icons.image),
title: const Text("Aus Gallerie auswählen"), title: const Text('Aus Gallerie auswählen'),
onTap: () { onTap: () {
context.loaderOverlay.show(); context.loaderOverlay.show();
FilePick.galleryPick().then((value) { FilePick.galleryPick().then((value) {
@ -147,12 +145,12 @@ class _ChatTextfieldState extends State<ChatTextfield> {
maxLines: 7, maxLines: 7,
minLines: 1, minLines: 1,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: "Nachricht schreiben...", hintText: 'Nachricht schreiben...',
border: InputBorder.none, border: InputBorder.none,
), ),
onChanged: (String text) { onChanged: (String text) {
if(text.trim().toLowerCase() == "marbot marbot marbot") { if(text.trim().toLowerCase() == 'marbot marbot marbot') {
var newText = "Roboter sind cool und so, aber Marbots sind besser!"; var newText = 'Roboter sind cool und so, aber Marbots sind besser!';
_textBoxController.text = newText; _textBoxController.text = newText;
text = newText; text = newText;
} }
@ -175,8 +173,8 @@ class _ChatTextfieldState extends State<ChatTextfield> {
setState(() { setState(() {
isLoading = false; isLoading = false;
}); });
_textBoxController.text = ""; _textBoxController.text = '';
setDraft(""); setDraft('');
}); });
}, },
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,

Some files were not shown because too many files have changed in this diff Show More