Merge branch 'develop' into develop-connectedDoubleLessons

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

BIN
.DS_Store vendored

Binary file not shown.

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

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

View File

@ -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) {

View File

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

View File

@ -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) {

View File

@ -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) {

View File

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

View File

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

View File

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

View File

@ -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) {

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

@ -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),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import 'dart:developer';
import 'package:easy_debounce/easy_throttle.dart';
import 'package: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,32 +93,27 @@ 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,
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"
title: 'Vertretung'
),
PersistentBottomNavBarItem(
activeColorPrimary: Theme.of(context).primaryColor,
inactiveColorPrimary: Theme.of(context).colorScheme.secondary,
),
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);
@ -132,28 +127,41 @@ class _AppState extends State<App> with WidgetsBindingObserver {
badgeColor: Theme.of(context).primaryColor,
elevation: 1,
),
badgeContent: Text("$messages", style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
child: const Icon(Icons.chat),
);
},
),
title: "Talk",
title: 'Talk',
),
PersistentBottomNavBarItem(
activeColorPrimary: Theme.of(context).primaryColor,
inactiveColorPrimary: Theme.of(context).colorScheme.secondary,
),
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"
title: 'Dateien'
),
PersistentBottomNavBarItem(
activeColorPrimary: Theme.of(context).primaryColor,
inactiveColorPrimary: Theme.of(context).colorScheme.secondary,
),
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"
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

View File

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

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:developer';
import 'dart: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(),
),
)
);
}
@ -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,
@ -138,7 +136,7 @@ class _MainState extends State<Main> {
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");
case AccountModelState.undefined: return const PlaceholderView(icon: Icons.timer, text: 'Daten werden geladen');
}
},
)

View File

@ -10,8 +10,8 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'accountModel.dart';
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';
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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(),

View File

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

View File

@ -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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,13 +4,13 @@ class AppTheme {
static ThemeModeDisplay getDisplayOptions(ThemeMode theme) {
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');
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ class _GradeAverageState extends State<GradeAverage> {
String getGradeDisplay(int grade) {
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(() {

View File

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

View File

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

View File

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

View File

@ -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),

View File

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

View File

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

View File

@ -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(
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"),
title: const Text('App bewerten'),
subtitle: getPlatformStoreName().wrapNullable((data) => Text('Im $data')),
trailing: const Icon(Icons.arrow_right),
onTap: () {
InAppReview.instance.openStoreListing(appStoreId: "6458789560").then(
(value) => InfoDialog.show(context, "Vielen Dank!"),
InAppReview.instance.openStoreListing(appStoreId: '6458789560').then(
(value) => InfoDialog.show(context, 'Vielen Dank!'),
onError: (error) => InfoDialog.show(context, error.toString())
);
},
),
);
},
),
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()),
),
],
),

View File

@ -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!)),
),

View File

@ -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) {

View File

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

View File

@ -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,14 +111,13 @@ 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(
@ -130,11 +129,13 @@ class _ChatViewState extends State<ChatView> {
),
Container(
color: Theme.of(context).colorScheme.background,
child: SafeArea(child: ChatTextfield(widget.room.token)),
child: TalkNavigator.isSecondaryVisible(context)
? ChatTextfield(widget.room.token)
: SafeArea(child: ChatTextfield(widget.room.token)
),
)
],
),
)
),
);
},

View File

@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/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,

View File

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

View File

@ -41,9 +41,9 @@ class _ChatTextfieldState extends State<ChatTextfield> {
}
String filename = "${path.split("/").last.split(".").first}-${const Uuid().v4()}.${path.split(".").last}";
String 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