Merge remote-tracking branch 'origin/develop' into develop
# Conflicts: # .idea/libraries/Flutter_Plugins.xml
This commit is contained in:
@ -14,7 +14,7 @@ class ListFiles extends WebdavApi<ListFilesParams> {
|
||||
|
||||
@override
|
||||
Future<ListFilesResponse> run() async {
|
||||
List<WebDavFile> davFiles = (await (await WebdavApi.webdav).propfind(params.path)).toWebDavFiles();
|
||||
List<WebDavFile> davFiles = (await (await WebdavApi.webdav).propfind(Uri.parse(params.path))).toWebDavFiles();
|
||||
Set<CacheableFile> files = davFiles.map((e) => CacheableFile.fromDavFile(e)).toSet();
|
||||
|
||||
// webdav handles subdirectories wrong, this is a fix
|
||||
|
@ -18,7 +18,7 @@ abstract class WebdavApi<T> extends ApiRequest {
|
||||
static Future<String> webdavConnectString = buildWebdavConnectString();
|
||||
|
||||
static Future<WebDavClient> establishWebdavConnection() async {
|
||||
return NextcloudClient("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 {
|
||||
|
@ -8,12 +8,12 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:marianum_mobile/firebase_options.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
||||
import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
|
||||
import 'app.dart';
|
||||
import 'firebase_options.dart';
|
||||
import 'model/accountData.dart';
|
||||
import 'model/accountModel.dart';
|
||||
import 'model/breakers/Breaker.dart';
|
||||
@ -43,9 +43,9 @@ Future<void> main() async {
|
||||
ByteData data = await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem');
|
||||
SecurityContext.defaultContext.setTrustedCertificatesBytes(data.buffer.asUint8List());
|
||||
|
||||
ErrorWidget.builder = (error) {
|
||||
return PlaceholderView(icon: Icons.phonelink_erase_rounded, text: error.toString());
|
||||
};
|
||||
// ErrorWidget.builder = (error) {
|
||||
// return PlaceholderView(icon: Icons.phonelink_erase_rounded, text: error.toString());
|
||||
// };
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
|
@ -14,6 +14,7 @@ abstract class DataHolder extends ChangeNotifier {
|
||||
List<ApiResponse?> properties();
|
||||
|
||||
bool primaryLoading() {
|
||||
// log("${toString()} ${properties().map((e) => e != null ? "1" : "0").join(", ")}");
|
||||
for(ApiResponse? element in properties()) {
|
||||
if(element == null) return true;
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../api/apiResponse.dart';
|
||||
import '../../api/webuntis/queries/getHolidays/getHolidaysCache.dart';
|
||||
import '../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart';
|
||||
import '../../api/webuntis/queries/getRooms/getRoomsCache.dart';
|
||||
import '../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
|
||||
@ -78,12 +79,16 @@ class TimetableProps extends DataHolder {
|
||||
}
|
||||
);
|
||||
|
||||
GetHolidaysCache(
|
||||
onUpdate: (GetHolidaysResponse data) => {
|
||||
_getHolidaysResponse = data,
|
||||
notifyListeners(),
|
||||
}
|
||||
);
|
||||
// GetHolidaysCache( // TODO is this fixed by webuntis? miese kriese
|
||||
// onUpdate: (GetHolidaysResponse data) => {
|
||||
// _getHolidaysResponse = data,
|
||||
// notifyListeners(),
|
||||
// }
|
||||
// );
|
||||
_getHolidaysResponse = GetHolidaysResponse.fromJson(jsonDecode("""
|
||||
{"jsonrpc":"2.0","id":"ID","result":[]}
|
||||
"""));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
DateTime getDate(DateTime d) => DateTime(d.year, d.month, d.day);
|
||||
|
@ -35,8 +35,7 @@ class LightAppTheme {
|
||||
color: marianumRed,
|
||||
),
|
||||
checkboxTheme: CheckboxThemeData(
|
||||
fillColor: MaterialStateProperty.all(marianumRed),
|
||||
// visualDensity: const VisualDensity(horizontal: VisualDensity.maximumDensity),
|
||||
fillColor: MaterialStateProperty.resolveWith((states) => states.contains(MaterialState.selected) ? marianumRed : Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
@ -161,7 +161,7 @@ class _FileElementState extends State<FileElement> {
|
||||
content: "Das Element wird unwiederruflich gelöscht.",
|
||||
onConfirm: () {
|
||||
WebdavApi.webdav
|
||||
.then((value) => value.delete(widget.file.path))
|
||||
.then((value) => value.delete(Uri.parse(widget.file.path)))
|
||||
.then((value) => widget.refetch());
|
||||
}
|
||||
));
|
||||
|
@ -43,7 +43,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
|
||||
setState(() {
|
||||
state = FileUploadState.checkConflict;
|
||||
});
|
||||
List<WebDavResponse> result = (await webdavClient.propfind(widget.remotePath.join("/"))).responses;
|
||||
List<WebDavResponse> result = (await webdavClient.propfind(Uri.parse(widget.remotePath.join("/")))).responses;
|
||||
if(result.any((element) => element.href!.endsWith("/$targetFileName"))) {
|
||||
setState(() {
|
||||
state = FileUploadState.conflict;
|
||||
@ -56,7 +56,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<HttpClientResponse> uploadTask = webdavClient.putFile(File(widget.localPath), FileStat.statSync(widget.localPath), fullRemotePath); // TODO use onProgress from putFile
|
||||
Future<HttpClientResponse> uploadTask = webdavClient.putFile(File(widget.localPath), FileStat.statSync(widget.localPath), Uri.parse(fullRemotePath)); // TODO use onProgress from putFile
|
||||
uploadTask.then((value) => Future<HttpClientResponse?>.value(value)).catchError((e) {
|
||||
setState(() {
|
||||
state = FileUploadState.error;
|
||||
|
@ -189,7 +189,7 @@ class _FilesState extends State<Files> {
|
||||
}, child: const Text("Abbrechen")),
|
||||
TextButton(onPressed: () {
|
||||
WebdavApi.webdav.then((webdav) {
|
||||
webdav.mkcol("${widget.path.join("/")}/${inputController.text}").then((value) => _query());
|
||||
webdav.mkcol(Uri.parse("${widget.path.join("/")}/${inputController.text}")).then((value) => _query());
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
}, child: const Text("Ordner erstellen")),
|
||||
|
@ -147,7 +147,7 @@ class _HolidaysState extends State<Holidays> {
|
||||
subtitle: Text(Jiffy.parse(holiday.start).fromNow()),
|
||||
),
|
||||
),
|
||||
DebugTile(holiday.toJson()).asTile(context),
|
||||
DebugTile(context).jsonData(holiday.toJson()),
|
||||
],
|
||||
)),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
|
@ -1,8 +1,18 @@
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:fast_rsa/fast_rsa.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:persistent_bottom_nav_bar/persistent_tab_view.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../../model/endpointData.dart';
|
||||
import '../../../widget/ListItem.dart';
|
||||
import '../../../widget/debug/debugTile.dart';
|
||||
import '../../settings/settings.dart';
|
||||
import 'gradeAverages/gradeAverage.dart';
|
||||
import 'holidays/holidays.dart';
|
||||
@ -14,6 +24,7 @@ class Overhang extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Mehr"),
|
||||
@ -22,11 +33,59 @@ class Overhang extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
children: const [
|
||||
ListItemNavigator(icon: Icons.newspaper, text: "Marianum Message", target: Message()),
|
||||
ListItemNavigator(icon: Icons.room, text: "Raumplan", target: Roomplan()),
|
||||
ListItemNavigator(icon: Icons.calculate, text: "Notendurschnittsrechner", target: GradeAverage()),
|
||||
ListItemNavigator(icon: Icons.calendar_month, text: "Schulferien", target: Holidays()),
|
||||
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()),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.share_outlined),
|
||||
title: const Text("Teile die App mit deiner Klasse"),
|
||||
onTap: () {
|
||||
Share.share( // TODO ipad needs position argument
|
||||
subject: "App Teilen",
|
||||
"Hol dir die inoffizielle App für's Marianum:"
|
||||
"\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client "
|
||||
"\nApple: https://apps.apple.com/us/app/marianum-fulda/id6458789560 "
|
||||
"\n\nViel Spaß!"
|
||||
);
|
||||
},
|
||||
),
|
||||
DebugTile(context, onlyInDebug: true).callback(onTab: () async {
|
||||
log("Starting");
|
||||
log("Generate keys");
|
||||
final rsaKey = await RSA.generate(2048);
|
||||
final devicePrivateKey = rsaKey.privateKey.toString();
|
||||
final devicePublicKey = rsaKey.publicKey.toString();
|
||||
log("Private: \n$devicePrivateKey");
|
||||
log("Public: \n$devicePublicKey");
|
||||
final pushToken = await FirebaseMessaging.instance.getToken();
|
||||
log("PushToken: $pushToken}");
|
||||
final pushTokenHash = sha512.convert(utf8.encode(pushToken!));
|
||||
log("PushTokenHash: $pushTokenHash");
|
||||
|
||||
final requestMap = {
|
||||
"format": "json",
|
||||
"pushTokenHash": pushTokenHash.toString(),
|
||||
"devicePublicKey": devicePublicKey.toString(),
|
||||
"proxyServer": "https://push-notifications.nextcloud.com/devices"
|
||||
};
|
||||
|
||||
log(jsonEncode(requestMap));
|
||||
http.post(
|
||||
//${AccountData().buildHttpAuthString()}@
|
||||
Uri.parse("https://${EndpointData().nextcloud().full()}/ocs/v2.php/apps/notifications/api/v2/push"),
|
||||
headers: {
|
||||
"OCS-APIRequest": "true",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer Fv3g7g9jW91FXNjZLaJmyprClfy8pX1jEM3hJGbXjPEFcx4oGIEVcpwEnuT4mPs39D9xT063"
|
||||
},
|
||||
body: jsonEncode(requestMap),
|
||||
).then((response) {
|
||||
log("Response: ${response.statusCode}\n${response.body}");
|
||||
});
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -255,7 +255,7 @@ class _ChatBubbleState extends State<ChatBubble> {
|
||||
},
|
||||
),
|
||||
),
|
||||
DebugTile(widget.bubbleData.toJson()).asTile(context),
|
||||
DebugTile(context).jsonData(widget.bubbleData.toJson()),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
@ -41,7 +41,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
String filename = "${path.split("/").last.split(".").first}-${const Uuid().v4()}.${path.split(".").last}";
|
||||
String shareFolder = "MarianumMobile";
|
||||
WebdavApi.webdav.then((webdav) {
|
||||
webdav.mkcol("/$shareFolder");
|
||||
webdav.mkcol(Uri.parse("/$shareFolder"));
|
||||
});
|
||||
|
||||
showDialog(context: context, builder: (context) => FileUploadDialog(
|
||||
|
@ -172,7 +172,7 @@ class _ChatTileState extends State<ChatTile> {
|
||||
).asDialog(context);
|
||||
},
|
||||
),
|
||||
DebugTile(widget.data.toJson()).asTile(context),
|
||||
DebugTile(context).jsonData(widget.data.toJson()),
|
||||
],
|
||||
));
|
||||
},
|
||||
|
@ -92,7 +92,7 @@ class AppointmentDetails {
|
||||
leading: const Icon(Icons.people),
|
||||
title: Text("Klasse(n): ${timetableData.kl.map((e) => e.name).join(", ")}"),
|
||||
),
|
||||
DebugTile(timetableData.toJson()).asTile(context),
|
||||
DebugTile(context).jsonData(timetableData.toJson()),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -1,4 +1,6 @@
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -145,45 +147,48 @@ class _SettingsState extends State<Settings> {
|
||||
|
||||
const Divider(),
|
||||
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.notifications_active_outlined)),
|
||||
title: const Text("Push-Benachrichtigungen aktivieren"),
|
||||
subtitle: const Text("Lange tippen für mehr Informationen"),
|
||||
trailing: Checkbox(
|
||||
value: settings.val().notificationSettings.enabled,
|
||||
onChanged: (e) {
|
||||
if(e!) {
|
||||
ConfirmDialog(
|
||||
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",
|
||||
onConfirm: () {
|
||||
settings.val(write: true).notificationSettings.enabled = e;
|
||||
NotifyUpdater.registerToServer();
|
||||
},
|
||||
).asDialog(context);
|
||||
} else {
|
||||
settings.val(write: true).notificationSettings.enabled = e;
|
||||
}
|
||||
},
|
||||
),
|
||||
onLongPress: () => showDialog(context: context, builder: (context) => AlertDialog(
|
||||
title: const Text("Info über Push"),
|
||||
content: const SingleChildScrollView(child: Text(""
|
||||
"Aufgrund technischer Limitationen müssen Push-nachrichten über einen Externen Server - hier 'mhsl.eu' (Author dieser App) - erfolgen.\n\n"
|
||||
"Wenn Push aktiviert wird, werden deine Zugangsdaten und ein Token verschlüsselt an den Betreiber gesendet und von ihm unverschlüsselt gespeichert.\n\n"
|
||||
"Der extene Server verwendet die Zugangsdaten um sich maschinell in Nextcloud Talk anzumelden und via Websockets auf neue Nachrichten zu warten.\n\n"
|
||||
"Wenn eine neue Nachricht eintrifft wird dein Telefon via FBC-Messaging (Google Firebase Push) vom Externen Server benachrichtigt.\n\n"
|
||||
"Behalte im Hinterkopf, dass deine Zugangsdaten auf einem Externen Server gespeichert werden und dies trots bester Absichten ein Sicherheitsrisiko sein kann!"
|
||||
Visibility(
|
||||
visible: Platform.isAndroid,
|
||||
child: ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.notifications_active_outlined)),
|
||||
title: const Text("Push-Benachrichtigungen aktivieren"),
|
||||
subtitle: const Text("Lange tippen für mehr Informationen"),
|
||||
trailing: Checkbox(
|
||||
value: settings.val().notificationSettings.enabled,
|
||||
onChanged: (e) {
|
||||
if(e!) {
|
||||
ConfirmDialog(
|
||||
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",
|
||||
onConfirm: () {
|
||||
settings.val(write: true).notificationSettings.enabled = e;
|
||||
NotifyUpdater.registerToServer();
|
||||
},
|
||||
).asDialog(context);
|
||||
} else {
|
||||
settings.val(write: true).notificationSettings.enabled = e;
|
||||
}
|
||||
},
|
||||
),
|
||||
onLongPress: () => showDialog(context: context, builder: (context) => AlertDialog(
|
||||
title: const Text("Info über Push"),
|
||||
content: const SingleChildScrollView(child: Text(""
|
||||
"Aufgrund technischer Limitationen müssen Push-nachrichten über einen Externen Server - hier 'mhsl.eu' (Author dieser App) - erfolgen.\n\n"
|
||||
"Wenn Push aktiviert wird, werden deine Zugangsdaten und ein Token verschlüsselt an den Betreiber gesendet und von ihm unverschlüsselt gespeichert.\n\n"
|
||||
"Der extene Server verwendet die Zugangsdaten um sich maschinell in Nextcloud Talk anzumelden und via Websockets auf neue Nachrichten zu warten.\n\n"
|
||||
"Wenn eine neue Nachricht eintrifft wird dein Telefon via FBC-Messaging (Google Firebase Push) vom Externen Server benachrichtigt.\n\n"
|
||||
"Behalte im Hinterkopf, dass deine Zugangsdaten auf einem Externen Server gespeichert werden und dies trots bester Absichten ein Sicherheitsrisiko sein kann!"
|
||||
)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text("Zurück"))
|
||||
],
|
||||
)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text("Zurück"))
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
@ -1,26 +1,38 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../storage/base/settingsProvider.dart';
|
||||
import '../centeredLeading.dart';
|
||||
import 'jsonViewer.dart';
|
||||
|
||||
class DebugTile {
|
||||
Map<String, dynamic> data;
|
||||
BuildContext context;
|
||||
bool onlyInDebug;
|
||||
DebugTile(this.context, {this.onlyInDebug = false});
|
||||
|
||||
DebugTile(this.data);
|
||||
Widget jsonData(Map<String, dynamic> data, {bool ignoreConfig = false}) {
|
||||
return callback(
|
||||
title: "JSON daten anzeigen",
|
||||
onTab: () => JsonViewer.asDialog(context, data)
|
||||
);
|
||||
}
|
||||
|
||||
Widget asTile(BuildContext context, {bool ignoreConfig = false}) {
|
||||
return Visibility(
|
||||
visible: Provider.of<SettingsProvider>(context).val().devToolsEnabled || ignoreConfig,
|
||||
child: ListTile(
|
||||
leading: const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [Icon(Icons.data_object)],
|
||||
),
|
||||
title: const Text("JSON daten anzeigen"),
|
||||
Widget callback({String title = "Debugaktion", required void Function() onTab}) {
|
||||
return child(
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.developer_mode_outlined)),
|
||||
title: Text(title),
|
||||
subtitle: const Text("Entwicklermodus aktiviert"),
|
||||
onTap: () => JsonViewer.asDialog(context, data),
|
||||
),
|
||||
onTap: onTab,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget child(Widget child) {
|
||||
return Visibility(
|
||||
visible: Provider.of<SettingsProvider>(context).val().devToolsEnabled && (onlyInDebug ? kDebugMode : true),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
@ -3,11 +3,11 @@ import 'dart:math';
|
||||
|
||||
import 'package:better_open_file/better_open_file.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:marianum_mobile/storage/base/settingsProvider.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||
|
||||
import '../storage/base/settingsProvider.dart';
|
||||
import 'placeholderView.dart';
|
||||
|
||||
class FileViewer extends StatefulWidget {
|
||||
|
@ -16,7 +16,7 @@ class _LoadingSpinnerState extends State<LoadingSpinner> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
timer = Timer(const Duration(seconds: 15), () {
|
||||
timer = Timer(const Duration(seconds: 30), () {
|
||||
setState(() {
|
||||
textVisible = true;
|
||||
});
|
||||
@ -33,13 +33,16 @@ class _LoadingSpinnerState extends State<LoadingSpinner> {
|
||||
children: [
|
||||
Visibility(
|
||||
visible: !textVisible,
|
||||
replacement: const Icon(Icons.signal_wifi_connected_no_internet_4_outlined),
|
||||
replacement: const Icon(Icons.sentiment_dissatisfied_outlined),
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Visibility(
|
||||
visible: textVisible,
|
||||
child: const Text("Etwas scheint nicht zu funktionieren!\nBist du mit dem Internet verbunden?\n\nVersuche die App neuzustarten"),
|
||||
child: const Text(
|
||||
textAlign: TextAlign.center,
|
||||
"Irgendetwas funktioniert nicht!\nBist du mit dem Internet verbunden?\n\nVersuche die App neuzustarten"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
Reference in New Issue
Block a user