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
.DS_Store.gitignoreanalysis_options.yaml
lib
api
app.dart
extensions
main.dart
model
notification
storage
theming
view

BIN
.DS_Store vendored

Binary file not shown.

1
.gitignore vendored

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

@ -24,6 +24,10 @@ linter:
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` 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
# https://dart.dev/guides/language/analysis-options

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

@ -6,11 +6,11 @@ import 'getHolidaysResponse.dart';
class GetHolidays {
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(
List<GetHolidaysResponseObject>.from(
jsonDecode(response).map<GetHolidaysResponseObject>(
(dynamic i) => GetHolidaysResponseObject.fromJson(i)
GetHolidaysResponseObject.fromJson
)
)
);

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

@ -11,21 +11,21 @@ import 'autocompleteResponse.dart';
class AutocompleteApi {
Future<AutocompleteResponse> find(String query) async {
Map<String, dynamic> getParameters = {
"search": query,
"itemType": " ",
"itemId": " ",
"shareTypes[]": ["0"],
"limit": "10",
'search': query,
'itemType': ' ',
'itemId': ' ',
'shareTypes[]': ['0'],
'limit': '10',
};
Map<String, String> headers = {};
headers.putIfAbsent("Accept", () => "application/json");
headers.putIfAbsent("OCS-APIRequest", () => "true");
headers.putIfAbsent('Accept', () => 'application/json');
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);
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;
return AutocompleteResponse.fromJson(jsonDecode(result)['ocs']);
}

@ -10,14 +10,14 @@ import 'fileSharingApiParams.dart';
class FileSharingApi {
Future<void> share(FileSharingApiParams query) async {
Map<String, String> headers = {};
headers.putIfAbsent("Accept", () => "application/json");
headers.putIfAbsent("OCS-APIRequest", () => "true");
headers.putIfAbsent('Accept', () => 'application/json');
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);
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}');
}
}
}

@ -11,7 +11,7 @@ class GetChat extends TalkApi<GetChatResponse> {
String chatToken;
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
assemble(String raw) {

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

@ -61,21 +61,21 @@ class GetChatResponseObject {
static GetChatResponseObject getDateDummy(int timestamp) {
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) {
return GetChatResponseObject(
0,
"",
'',
GetRoomResponseObjectMessageActorType.user,
"",
"",
'',
'',
0,
"",
'',
GetRoomResponseObjectMessageType.system,
false,
"",
'',
text,
null,
null,
@ -113,10 +113,10 @@ class RichObjectString {
}
enum RichObjectStringObjectType {
@JsonValue("user") user,
@JsonValue("group") group,
@JsonValue("file") file,
@JsonValue("guest") guest,
@JsonValue("highlight") highlight,
@JsonValue("talk-poll") talkPoll,
@JsonValue('user') user,
@JsonValue('group') group,
@JsonValue('file') file,
@JsonValue('guest') guest,
@JsonValue('highlight') highlight,
@JsonValue('talk-poll') talkPoll,
}

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

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

@ -7,7 +7,7 @@ import '../talkApi.dart';
class DeleteMessage extends TalkApi {
String chatToken;
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
assemble(String raw) {

@ -8,7 +8,7 @@ import 'deleteReactMessageParams.dart';
class DeleteReactMessage extends TalkApi {
String chatToken;
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
assemble(String raw) {

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

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

@ -10,7 +10,7 @@ import 'getReactionsResponse.dart';
class GetReactions extends TalkApi<GetReactionsResponse> {
String chatToken;
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
assemble(String raw) {

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

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

@ -8,7 +8,7 @@ import 'reactMessageParams.dart';
class ReactMessage extends TalkApi {
String chatToken;
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
assemble(String raw) {

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

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

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

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

@ -8,7 +8,7 @@ class SetFavorite extends TalkApi {
String chatToken;
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
assemble(String raw) {

@ -10,7 +10,7 @@ class SetReadMarker extends TalkApi {
bool readState;
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);
}

@ -34,21 +34,21 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
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?.putIfAbsent("Accept", () => "application/json");
headers?.putIfAbsent("OCS-APIRequest", () => "true");
headers?.putIfAbsent('Accept', () => 'application/json');
headers?.putIfAbsent('OCS-APIRequest', () => 'true');
http.Response? data;
try {
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");
} catch(e) {
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);
@ -59,10 +59,10 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
return assembled;
} catch (e) {
// 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');
}
}

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

@ -24,7 +24,7 @@ class CacheableFile {
CacheableFile.fromDavFile(WebDavFile file) {
path = file.path.path;
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;
size = file.size;
eTag = file.etag;

@ -1,7 +1,6 @@
import 'package:nextcloud/nextcloud.dart';
import '../../../../../model/endpointData.dart';
import '../../webdavApi.dart';
import 'cacheableFile.dart';
import 'listFilesParams.dart';
@ -15,18 +14,19 @@ class ListFiles extends WebdavApi<ListFilesParams> {
@override
Future<ListFilesResponse> run() async {
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
if(EndpointData().getEndpointMode() == EndpointMode.stage) {
files = files.map((e) { // somehow
e.path = e.path.split("mobile/cloud/remote.php/webdav")[1];
return e;
}).toSet();
}
// currently this fix is not needed anymore
// if(EndpointData().getEndpointMode() == EndpointMode.stage) {
// files = files.map((e) { // somehow
// e.path = e.path.split("mobile/cloud/remote.php/webdav")[1];
// return e;
// }).toSet();
// }
// 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);
}

@ -10,9 +10,9 @@ class ListFilesCache extends RequestCache<ListFilesResponse> {
String path;
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();
start("MarianumMobile", "wd-folder-$cacheName");
start('MarianumMobile', 'wd-folder-$cacheName');
}
@override

@ -35,7 +35,7 @@ class ListFilesResponse extends ApiResponse {
switch(sortOption) {
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;
case SortOption.name:

@ -18,10 +18,10 @@ abstract class WebdavApi<T> extends ApiRequest {
static Future<String> webdavConnectString = buildWebdavConnectString();
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 {
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()}/';
}
}

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

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

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

@ -13,6 +13,7 @@ class CustomTimetableEvent {
DateTime startDate;
@JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson)
DateTime endDate;
String? color;
String rrule;
@JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson)
DateTime createdAt;
@ -20,7 +21,7 @@ class CustomTimetableEvent {
DateTime updatedAt;
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);
Map<String, dynamic> toJson() => _$CustomTimetableEventToJson(this);

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

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

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

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

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

@ -15,21 +15,21 @@ abstract class MhslApi<T> extends ApiRequest {
T assemble(String raw);
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);
if(data == null) {
throw ApiError("Request could not be dispatched!");
throw ApiError('Request could not be dispatched!');
}
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));
}
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);
}

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

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

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

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

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

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

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

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

@ -5,7 +5,7 @@ import '../../webuntisApi.dart';
import 'getRoomsResponse.dart';
class GetRooms extends WebuntisApi {
GetRooms() : super("getRooms", null);
GetRooms() : super('getRooms', null);
@override
Future<GetRoomsResponse> run() async {
@ -14,10 +14,10 @@ class GetRooms extends WebuntisApi {
return finalize(GetRoomsResponse.fromJson(jsonDecode(rawAnswer)));
} catch(e, trace) {
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');
}
}

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

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

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

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

@ -11,7 +11,7 @@ class GetTimetableCache extends RequestCache<GetTimetableResponse> {
int enddate;
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

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

@ -9,7 +9,7 @@ import 'queries/authenticate/authenticate.dart';
import 'webuntisError.dart';
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;
ApiParams? genericParam;
http.Response? response;
@ -22,11 +22,11 @@ abstract class WebuntisApi extends ApiRequest {
Future<String> query(WebuntisApi untis) async {
String query = '{"id":"ID","method":"$method","params":${untis._body()},"jsonrpc":"2.0"}';
String sessionId = "0";
String sessionId = '0';
if(authenticatedResponse) {
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;
dynamic jsonData = jsonDecode(data.body);
@ -49,7 +49,7 @@ abstract class WebuntisApi extends ApiRequest {
Future<ApiResponse> run();
String _body() {
return genericParam == null ? "{}" : jsonEncode(genericParam);
return genericParam == null ? '{}' : jsonEncode(genericParam);
}
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)
.timeout(
const Duration(seconds: 10),
onTimeout: () => throw WebuntisError("Timeout", 1)
onTimeout: () => throw WebuntisError('Timeout', 1)
);
}
}

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

@ -5,7 +5,7 @@ import 'dart:developer';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:firebase_messaging/firebase_messaging.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:badges/badges.dart' as badges;
@ -39,14 +39,14 @@ class _AppState extends State<App> with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
log("AppLifecycle: ${state.toString()}");
log('AppLifecycle: ${state.toString()}');
if(state == AppLifecycleState.resumed) {
EasyThrottle.throttle(
"appLifecycleState",
'appLifecycleState',
const Duration(seconds: 10),
() {
log("Refreshing due to LifecycleChange");
log('Refreshing due to LifecycleChange');
NotificationTasks.updateProviders(context);
Provider.of<TimetableProps>(context, listen: false).run();
}
@ -93,67 +93,75 @@ class _AppState extends State<App> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
return PersistentTabView(
context,
controller: App.bottomNavigator,
navBarStyle: NavBarStyle.style6,
hideNavigationBarWhenKeyboardShows: true,
navBarHeight: MediaQuery.of(context).viewInsets.bottom > 0 ? 0.0 : kBottomNavigationBarHeight,
backgroundColor: Theme.of(context).colorScheme.surface,
decoration: const NavBarDecoration(
border: Border(top: BorderSide(width: 1, color: Colors.grey)),
),
screenTransitionAnimation: const ScreenTransitionAnimation(animateTabTransition: true, curve: Curves.ease, duration: Duration(milliseconds: 200)),
screens: const [
Breaker(breaker: BreakerArea.timetable, child: Timetable()),
Breaker(breaker: BreakerArea.talk, child: ChatList()),
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),
title: "Vertretung"
),
PersistentBottomNavBarItem(
activeColorPrimary: Theme.of(context).primaryColor,
inactiveColorPrimary: Theme.of(context).colorScheme.secondary,
icon: Consumer<ChatListProps>(
builder: (context, value, child) {
if(value.primaryLoading()) return const Icon(Icons.chat);
int messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b);
return badges.Badge(
showBadge: messages > 0,
position: badges.BadgePosition.topEnd(top: -3, end: -3),
stackFit: StackFit.loose,
badgeStyle: badges.BadgeStyle(
padding: const EdgeInsets.all(3),
badgeColor: Theme.of(context).primaryColor,
elevation: 1,
),
badgeContent: Text("$messages", style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
child: const Icon(Icons.chat),
);
},
),
title: "Talk",
),
PersistentBottomNavBarItem(
activeColorPrimary: Theme.of(context).primaryColor,
inactiveColorPrimary: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.folder),
title: "Dateien"
),
PersistentBottomNavBarItem(
activeColorPrimary: Theme.of(context).primaryColor,
inactiveColorPrimary: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.apps),
title: "Mehr"
),
],
);
gestureNavigationEnabled: true,
navBarOverlap: const NavBarOverlap.none(),
backgroundColor: Theme.of(context).colorScheme.primary,
screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)),
tabs: [
PersistentTabConfig(
screen: const Breaker(breaker: BreakerArea.timetable, child: Timetable()),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.calendar_month),
title: 'Vertretung'
),
),
PersistentTabConfig(
screen: const Breaker(breaker: BreakerArea.talk, child: ChatList()),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: Consumer<ChatListProps>(
builder: (context, value, child) {
if(value.primaryLoading()) return const Icon(Icons.chat);
int messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b);
return badges.Badge(
showBadge: messages > 0,
position: badges.BadgePosition.topEnd(top: -3, end: -3),
stackFit: StackFit.loose,
badgeStyle: badges.BadgeStyle(
padding: const EdgeInsets.all(3),
badgeColor: Theme.of(context).primaryColor,
elevation: 1,
),
badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
child: const Icon(Icons.chat),
);
},
),
title: 'Talk',
),
),
PersistentTabConfig(
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),
title: 'Dateien'
),
),
PersistentTabConfig(
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),
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

@ -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;
}
}

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:feedback/feedback.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
@ -30,7 +29,6 @@ import 'storage/base/settingsProvider.dart';
import 'theming/darkAppTheme.dart';
import 'theming/lightAppTheme.dart';
import 'view/login/login.dart';
import 'view/pages/more/feedback/feedbackForm.dart';
import 'widget/placeholderView.dart';
Future<void> main() async {
@ -40,7 +38,7 @@ Future<void> main() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
log("Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}");
} catch (e) {
log("Error initializing Firebase app!");
log('Error initializing Firebase app!');
}
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) => 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
void initState() {
Jiffy.setLocale("de");
Jiffy.setLocale('de');
AccountData().waitForPopulation().then((value) {
Provider.of<AccountModel>(context, listen: false)
@ -113,7 +106,12 @@ class _MainState extends State<Main> {
textDirection: TextDirection.ltr,
child: Consumer<SettingsProvider>(
builder: (context, settings, child) {
var devToolsSettings = settings.val().devToolsSettings;
return MaterialApp(
showPerformanceOverlay: devToolsSettings.showPerformanceOverlay,
checkerboardOffscreenLayers: devToolsSettings.checkerboardOffscreenLayers,
checkerboardRasterCacheImages: devToolsSettings.checkerboardRasterCacheImages,
debugShowCheckedModeBanner: false,
localizationsDelegates: const [
...GlobalMaterialLocalizations.delegates,
@ -132,16 +130,16 @@ class _MainState extends State<Main> {
darkTheme: DarkAppTheme.theme,
home: LoaderOverlay(
child: Breaker(
breaker: BreakerArea.global,
child: Consumer<AccountModel>(
builder: (context, accountModel, child) {
switch(accountModel.state) {
case AccountModelState.loggedIn: return const App();
case AccountModelState.loggedOut: return const Login();
case AccountModelState.undefined: return const PlaceholderView(icon: Icons.timer, text: "Daten werden geladen");
}
},
)
breaker: BreakerArea.global,
child: Consumer<AccountModel>(
builder: (context, accountModel, child) {
switch(accountModel.state) {
case AccountModelState.loggedIn: return const App();
case AccountModelState.loggedOut: return const Login();
case AccountModelState.undefined: return const PlaceholderView(icon: Icons.timer, text: 'Daten werden geladen');
}
},
)
),
),
);

@ -10,8 +10,8 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'accountModel.dart';
class AccountData {
static const _usernameField = "username";
static const _passwordField = "password";
static const _usernameField = 'username';
static const _passwordField = 'password';
static final AccountData _instance = AccountData._construct();
final Future<SharedPreferences> _storage = SharedPreferences.getInstance();
@ -29,21 +29,21 @@ class AccountData {
String? _password;
String getUsername() {
if(_username == null) throw Exception("Username not initialized");
if(_username == null) throw Exception('Username not initialized');
return _username!;
}
String getPassword() {
if(_password == null) throw Exception("Password not initialized");
if(_password == null) throw Exception('Password not initialized');
return _password!;
}
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 {
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 {
@ -84,7 +84,7 @@ class AccountData {
}
String buildHttpAuthString() {
if(!isPopulated()) throw Exception("AccountData (e.g. username or password) is not initialized!");
return "$_username:$_password";
if(!isPopulated()) throw Exception('AccountData (e.g. username or password) is not initialized!');
return '$_username:$_password';
}
}

@ -26,7 +26,7 @@ class BreakerProps extends DataHolder {
for(var key in breakers.regional.keys) {
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;
}
}

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

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

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

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

@ -49,11 +49,11 @@ class NotificationController {
DebugTile(context).run(() {
showDialog(context: context, builder: (context) => AlertDialog(
title: const Text("Notification report"),
title: const Text('Notification report'),
content: Column(
mainAxisSize: MainAxisSize.min,
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)),
],
),

@ -10,13 +10,13 @@ import '../widget/confirmDialog.dart';
class NotifyUpdater {
static ConfirmDialog enableAfterDisclaimer(SettingsProvider settings) {
return ConfirmDialog(
title: "Warnung",
title: 'Warnung',
icon: Icons.warning_amber,
content: ""
"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"
"Für mehr Informationen drücke lange auf die Einstellungsoption!",
confirmButton: "Aktivieren",
content: ''
'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'
'Für mehr Informationen drücke lange auf die Einstellungsoption!',
confirmButton: 'Aktivieren',
onConfirm: () {
FirebaseMessaging.instance.requestPermission(
provisional: false
@ -29,7 +29,7 @@ class NotifyUpdater {
static void registerToServer() async {
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(
NotifyRegisterParams(

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

@ -23,6 +23,8 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
json['fileViewSettings'] as Map<String, dynamic>),
notificationSettings: NotificationSettings.fromJson(
json['notificationSettings'] as Map<String, dynamic>),
devToolsSettings: DevToolsSettings.fromJson(
json['devToolsSettings'] as Map<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(),
'fileViewSettings': instance.fileViewSettings.toJson(),
'notificationSettings': instance.notificationSettings.toJson(),
'devToolsSettings': instance.devToolsSettings.toJson(),
};

@ -8,7 +8,7 @@ import '../../view/settings/defaultSettings.dart';
import 'settings.dart';
class SettingsProvider extends ChangeNotifier {
static const String _fieldName = "settings";
static const String _fieldName = 'settings';
late SharedPreferences _storage;
late Settings _settings = DefaultSettings.get();
@ -45,13 +45,13 @@ class SettingsProvider extends ChangeNotifier {
_settings = Settings.fromJson(jsonDecode(_storage.getString(_fieldName)!));
} catch(exception) {
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()));
log("Settings recovered successfully: ${_settings.toJson().toString()}");
log('Settings recovered successfully: ${_settings.toJson().toString()}');
} 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();
log("Settings were reset to defaults!");
log('Settings were reset to defaults!');
}
}

@ -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);
}

@ -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,
};

@ -4,13 +4,13 @@ class AppTheme {
static ThemeModeDisplay getDisplayOptions(ThemeMode theme) {
switch(theme) {
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:
return ThemeModeDisplay(icon: Icons.wb_sunny_outlined, displayName: "Hell");
return ThemeModeDisplay(icon: Icons.wb_sunny_outlined, displayName: 'Hell');
case ThemeMode.dark:
return ThemeModeDisplay(icon: Icons.dark_mode_outlined, displayName: "Dunkel");
return ThemeModeDisplay(icon: Icons.dark_mode_outlined, displayName: 'Dunkel');
}
}

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

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

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

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

@ -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'),
)
],
);
}
}

@ -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)
],
)
),
],
),
),
),
);
}
}

@ -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();
});
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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