From 9a5114eb5df060e531c6626742b6898354590748 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 11 Jul 2025 19:27:40 +0200 Subject: [PATCH] fix #228 --- ios/Podfile.lock | 6 + lib/src/localization/app_de.arb | 19 + lib/src/localization/app_en.arb | 19 + .../generated/app_localizations.dart | 114 +++++ .../generated/app_localizations_de.dart | 59 +++ .../generated/app_localizations_en.dart | 59 +++ lib/src/model/json/userdata.dart | 3 + lib/src/model/json/userdata.g.dart | 2 + lib/src/utils/log.dart | 11 + lib/src/views/chats/chat_list.view.dart | 24 ++ lib/src/views/settings/appearance.view.dart | 40 +- .../views/settings/help/contact_us.view.dart | 394 +++++++++++++++--- .../help/contact_us/submit_message.view.dart | 106 +++++ .../views/settings/help/diagnostics.view.dart | 15 +- pubspec.lock | 24 ++ pubspec.yaml | 1 + 16 files changed, 824 insertions(+), 72 deletions(-) create mode 100644 lib/src/views/settings/help/contact_us/submit_message.view.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5aa465a..c4d6f7d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -5,6 +5,8 @@ PODS: - Flutter - connectivity_plus (0.0.1): - Flutter + - device_info_plus (0.0.1): + - Flutter - Firebase (11.10.0): - Firebase/Core (= 11.10.0) - Firebase/Core (11.10.0): @@ -232,6 +234,7 @@ DEPENDENCIES: - background_downloader (from `.symlinks/plugins/background_downloader/ios`) - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Firebase - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) @@ -291,6 +294,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/camera_avfoundation/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" firebase_messaging: @@ -344,6 +349,7 @@ SPEC CHECKSUMS: background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 firebase_core: ba71b44041571da878cb624ce0d80250bcbe58ad firebase_messaging: 13129fe2ca166d1ed2d095062d76cee88943d067 diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 4d7aa71..49ba0a7 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -121,7 +121,25 @@ "settingsNotifyTroubleshootingNoProblemDesc": "Klicke auf OK, um eine Testbenachrichtigung zu erhalten. Wenn du auch nach 10 Minuten warten keine Nachricht erhältst, sende uns bitte dein Diagnoseprotokoll unter Einstellungen > Hilfe > Diagnoseprotokoll, damit wir uns das Problem ansehen können.", "settingsHelp": "Hilfe", "settingsHelpFAQ": "FAQ", + "feedbackTooltip": "Feedback zur Verbesserung von twonly geben.", "settingsHelpContactUs": "Kontaktiere uns", + "contactUsFaq": "FAQ schon gelesen?", + "contactUsEmojis": "Wie fühlst du dich? (optional)", + "contactUsSelectOption": "Bitte wähle eine Option", + "contactUsReason": "Sag uns, warum du uns kontaktierst", + "contactUsMessage": "Wenn du eine Antwort erhalten möchtest, füge bitte deine E-Mail-Adresse hinzu, damit wir dich kontaktieren können.", + "contactUsYourMessage": "Deine Nachricht", + "contactUsMessageTitle": "Erzähl uns, was los ist", + "contactUsReasonNotWorking": "Etwas funktioniert nicht", + "contactUsReasonFeatureRequest": "Funktionsanfrage", + "contactUsReasonQuestion": "Frage", + "contactUsReasonFeedback": "Feedback", + "contactUsReasonOther": "Sonstiges", + "contactUsIncludeLog": "Debug-Protokoll anhängen.", + "contactUsWhatsThat": "Was ist das?", + "contactUsLastWarning": "Dies sind die Informationen, die an uns gesendet werden. Bitte prüfen Sie sie und klicke dann auf „Abschicken“.", + "contactUsSuccess": "Feedback erfolgreich übermittelt!", + "contactUsShortcut": "Feedback-Symbol ausblenden", "settingsHelpDiagnostics": "Diagnoseprotokoll", "settingsHelpVersion": "Version", "settingsHelpLicenses": "Lizenzen (Source-Code)", @@ -153,6 +171,7 @@ "undo": "Rückgängig", "redo": "Wiederholen", "next": "Weiter", + "submit": "Abschicken", "close": "Schließen", "cancel": "Abbrechen", "ok": "Ok", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 0081d7b..ef55dd8 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -213,6 +213,7 @@ "@settingsHelpDiagnostics": {}, "settingsHelpFAQ": "FAQ", "@settingsHelpFAQ": {}, + "feedbackTooltip": "Give Feedback to improve twonly.", "settingsHelpContactUs": "Contact us", "@settingsHelpContactUs": {}, "settingsHelpVersion": "Version", @@ -222,6 +223,23 @@ "settingsHelpCredits": "Licenses (Images)", "@settingsHelpCredits": {}, "settingsHelpImprint": "Imprint & Privacy Policy", + "contactUsFaq": "Have you read our FAQ yet?", + "contactUsEmojis": "How do you feel? (optional)", + "contactUsSelectOption": "Please select an option", + "contactUsReason": "Tell us why you're reaching out", + "contactUsMessage": "If you want to receive an answer, please add your e-mail address so we can contact you.", + "contactUsYourMessage": "Your message", + "contactUsMessageTitle": "Tell us what's going on", + "contactUsReasonNotWorking": "Something's not working", + "contactUsReasonFeatureRequest": "Feature request", + "contactUsReasonQuestion": "Question", + "contactUsReasonFeedback": "Feedback", + "contactUsReasonOther": "Other", + "contactUsIncludeLog": "Include debug log", + "contactUsWhatsThat": "What's that?", + "contactUsLastWarning": "This are the information's which will be send to us. Please verify them and then press submit.", + "contactUsSuccess": "Feedback submitted successfully!", + "contactUsShortcut": "Hide Feedback Icon", "settingsHelpTerms": "Terms of Service", "settingsAppearanceTheme": "Theme", "@settingsAppearanceTheme": {}, @@ -276,6 +294,7 @@ "undo": "Undo", "redo": "Redo", "next": "Next", + "submit": "Submit", "close": "Close", "disable": "Disable", "enable": "Enable", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 6d39214..1b01f30 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -734,6 +734,12 @@ abstract class AppLocalizations { /// **'FAQ'** String get settingsHelpFAQ; + /// No description provided for @feedbackTooltip. + /// + /// In en, this message translates to: + /// **'Give Feedback to improve twonly.'** + String get feedbackTooltip; + /// No description provided for @settingsHelpContactUs. /// /// In en, this message translates to: @@ -764,6 +770,108 @@ abstract class AppLocalizations { /// **'Imprint & Privacy Policy'** String get settingsHelpImprint; + /// No description provided for @contactUsFaq. + /// + /// In en, this message translates to: + /// **'Have you read our FAQ yet?'** + String get contactUsFaq; + + /// No description provided for @contactUsEmojis. + /// + /// In en, this message translates to: + /// **'How do you feel? (optional)'** + String get contactUsEmojis; + + /// No description provided for @contactUsSelectOption. + /// + /// In en, this message translates to: + /// **'Please select an option'** + String get contactUsSelectOption; + + /// No description provided for @contactUsReason. + /// + /// In en, this message translates to: + /// **'Tell us why you\'re reaching out'** + String get contactUsReason; + + /// No description provided for @contactUsMessage. + /// + /// In en, this message translates to: + /// **'If you want to receive an answer, please add your e-mail address so we can contact you.'** + String get contactUsMessage; + + /// No description provided for @contactUsYourMessage. + /// + /// In en, this message translates to: + /// **'Your message'** + String get contactUsYourMessage; + + /// No description provided for @contactUsMessageTitle. + /// + /// In en, this message translates to: + /// **'Tell us what\'s going on'** + String get contactUsMessageTitle; + + /// No description provided for @contactUsReasonNotWorking. + /// + /// In en, this message translates to: + /// **'Something\'s not working'** + String get contactUsReasonNotWorking; + + /// No description provided for @contactUsReasonFeatureRequest. + /// + /// In en, this message translates to: + /// **'Feature request'** + String get contactUsReasonFeatureRequest; + + /// No description provided for @contactUsReasonQuestion. + /// + /// In en, this message translates to: + /// **'Question'** + String get contactUsReasonQuestion; + + /// No description provided for @contactUsReasonFeedback. + /// + /// In en, this message translates to: + /// **'Feedback'** + String get contactUsReasonFeedback; + + /// No description provided for @contactUsReasonOther. + /// + /// In en, this message translates to: + /// **'Other'** + String get contactUsReasonOther; + + /// No description provided for @contactUsIncludeLog. + /// + /// In en, this message translates to: + /// **'Include debug log'** + String get contactUsIncludeLog; + + /// No description provided for @contactUsWhatsThat. + /// + /// In en, this message translates to: + /// **'What\'s that?'** + String get contactUsWhatsThat; + + /// No description provided for @contactUsLastWarning. + /// + /// In en, this message translates to: + /// **'This are the information\'s which will be send to us. Please verify them and then press submit.'** + String get contactUsLastWarning; + + /// No description provided for @contactUsSuccess. + /// + /// In en, this message translates to: + /// **'Feedback submitted successfully!'** + String get contactUsSuccess; + + /// No description provided for @contactUsShortcut. + /// + /// In en, this message translates to: + /// **'Hide Feedback Icon'** + String get contactUsShortcut; + /// No description provided for @settingsHelpTerms. /// /// In en, this message translates to: @@ -920,6 +1028,12 @@ abstract class AppLocalizations { /// **'Next'** String get next; + /// No description provided for @submit. + /// + /// In en, this message translates to: + /// **'Submit'** + String get submit; + /// No description provided for @close. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 6730920..18e7f5b 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -357,6 +357,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settingsHelpFAQ => 'FAQ'; + @override + String get feedbackTooltip => 'Feedback zur Verbesserung von twonly geben.'; + @override String get settingsHelpContactUs => 'Kontaktiere uns'; @@ -372,6 +375,59 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settingsHelpImprint => 'Impressum & Datenschutzrichtlinie'; + @override + String get contactUsFaq => 'FAQ schon gelesen?'; + + @override + String get contactUsEmojis => 'Wie fühlst du dich? (optional)'; + + @override + String get contactUsSelectOption => 'Bitte wähle eine Option'; + + @override + String get contactUsReason => 'Sag uns, warum du uns kontaktierst'; + + @override + String get contactUsMessage => + 'Wenn du eine Antwort erhalten möchtest, füge bitte deine E-Mail-Adresse hinzu, damit wir dich kontaktieren können.'; + + @override + String get contactUsYourMessage => 'Deine Nachricht'; + + @override + String get contactUsMessageTitle => 'Erzähl uns, was los ist'; + + @override + String get contactUsReasonNotWorking => 'Etwas funktioniert nicht'; + + @override + String get contactUsReasonFeatureRequest => 'Funktionsanfrage'; + + @override + String get contactUsReasonQuestion => 'Frage'; + + @override + String get contactUsReasonFeedback => 'Feedback'; + + @override + String get contactUsReasonOther => 'Sonstiges'; + + @override + String get contactUsIncludeLog => 'Debug-Protokoll anhängen.'; + + @override + String get contactUsWhatsThat => 'Was ist das?'; + + @override + String get contactUsLastWarning => + 'Dies sind die Informationen, die an uns gesendet werden. Bitte prüfen Sie sie und klicke dann auf „Abschicken“.'; + + @override + String get contactUsSuccess => 'Feedback erfolgreich übermittelt!'; + + @override + String get contactUsShortcut => 'Feedback-Symbol ausblenden'; + @override String get settingsHelpTerms => 'Nutzungsbedingungen'; @@ -465,6 +521,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get next => 'Weiter'; + @override + String get submit => 'Abschicken'; + @override String get close => 'Schließen'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index cb2639f..007ba96 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -352,6 +352,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsHelpFAQ => 'FAQ'; + @override + String get feedbackTooltip => 'Give Feedback to improve twonly.'; + @override String get settingsHelpContactUs => 'Contact us'; @@ -367,6 +370,59 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsHelpImprint => 'Imprint & Privacy Policy'; + @override + String get contactUsFaq => 'Have you read our FAQ yet?'; + + @override + String get contactUsEmojis => 'How do you feel? (optional)'; + + @override + String get contactUsSelectOption => 'Please select an option'; + + @override + String get contactUsReason => 'Tell us why you\'re reaching out'; + + @override + String get contactUsMessage => + 'If you want to receive an answer, please add your e-mail address so we can contact you.'; + + @override + String get contactUsYourMessage => 'Your message'; + + @override + String get contactUsMessageTitle => 'Tell us what\'s going on'; + + @override + String get contactUsReasonNotWorking => 'Something\'s not working'; + + @override + String get contactUsReasonFeatureRequest => 'Feature request'; + + @override + String get contactUsReasonQuestion => 'Question'; + + @override + String get contactUsReasonFeedback => 'Feedback'; + + @override + String get contactUsReasonOther => 'Other'; + + @override + String get contactUsIncludeLog => 'Include debug log'; + + @override + String get contactUsWhatsThat => 'What\'s that?'; + + @override + String get contactUsLastWarning => + 'This are the information\'s which will be send to us. Please verify them and then press submit.'; + + @override + String get contactUsSuccess => 'Feedback submitted successfully!'; + + @override + String get contactUsShortcut => 'Hide Feedback Icon'; + @override String get settingsHelpTerms => 'Terms of Service'; @@ -460,6 +516,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get next => 'Next'; + @override + String get submit => 'Submit'; + @override String get close => 'Close'; diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index b66c9cb..c222ac1 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -44,6 +44,9 @@ class UserData { @JsonKey(defaultValue: true) bool useHighQuality = true; + @JsonKey(defaultValue: true) + bool showFeedbackShortcut = true; + List? preSelectedEmojies; Map>? autoDownloadOptions; diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 75a915d..8a8cad7 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -25,6 +25,7 @@ UserData _$UserDataFromJson(Map json) => UserData( ThemeMode.system ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt() ..useHighQuality = json['useHighQuality'] as bool? ?? true + ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true ..preSelectedEmojies = (json['preSelectedEmojies'] as List?) ?.map((e) => e as String) .toList() @@ -77,6 +78,7 @@ Map _$UserDataToJson(UserData instance) => { 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'defaultShowTime': instance.defaultShowTime, 'useHighQuality': instance.useHighQuality, + 'showFeedbackShortcut': instance.showFeedbackShortcut, 'preSelectedEmojies': instance.preSelectedEmojies, 'autoDownloadOptions': instance.autoDownloadOptions, 'storeMediaFilesInGallery': instance.storeMediaFilesInGallery, diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index c81c125..36b3054 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -32,6 +32,17 @@ class Log { Mutex writeToLogGuard = Mutex(); +Future loadLogFile() async { + final directory = await getApplicationSupportDirectory(); + final logFile = File('${directory.path}/app.log'); + + if (await logFile.exists()) { + return await logFile.readAsString(); + } else { + return 'Log file does not exist.'; + } +} + Future _writeLogToFile(LogRecord record) async { final directory = await getApplicationSupportDirectory(); final logFile = File('${directory.path}/app.log'); diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index 400b250..7ec44aa 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/chats/chat_list_components/backup_notice.card.dart'; import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart'; import 'package:twonly/src/views/chats/chat_list_components/demo_user.card.dart'; @@ -20,6 +21,7 @@ import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/chats/media_viewer.view.dart'; import 'package:twonly/src/views/chats/start_new_chat.view.dart'; +import 'package:twonly/src/views/settings/help/contact_us.view.dart'; import 'package:twonly/src/views/settings/settings_main.view.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:flutter/material.dart'; @@ -40,6 +42,7 @@ class _ChatListViewState extends State { GlobalKey firstUserListItemKey = GlobalKey(); GlobalKey searchForOtherUsers = GlobalKey(); Timer? tutorial; + bool showFeedbackShortcut = false; @override void initState() { @@ -65,6 +68,13 @@ class _ChatListViewState extends State { await showChatListTutorialContextMenu(context, firstUserListItemKey); } }); + + final user = await getUser(); + if (user != null) { + setState(() { + showFeedbackShortcut = user.showFeedbackShortcut; + }); + } } @override @@ -105,6 +115,20 @@ class _ChatListViewState extends State { ), ]), actions: [ + if (showFeedbackShortcut) + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ContactUsView(), + ), + ); + }, + color: Colors.grey, + tooltip: context.lang.feedbackTooltip, + icon: FaIcon(FontAwesomeIcons.commentDots, size: 19), + ), StreamBuilder( stream: twonlyDB.contactsDao.watchContactsRequested(), builder: (context, snapshot) { diff --git a/lib/src/views/settings/appearance.view.dart b/lib/src/views/settings/appearance.view.dart index 723c194..7c7843c 100644 --- a/lib/src/views/settings/appearance.view.dart +++ b/lib/src/views/settings/appearance.view.dart @@ -1,12 +1,34 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/radio_button.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/utils/misc.dart'; -class AppearanceView extends StatelessWidget { +class AppearanceView extends StatefulWidget { const AppearanceView({super.key}); + @override + State createState() => _AppearanceViewState(); +} + +class _AppearanceViewState extends State { + bool showFeedbackShortcut = false; + + @override + void initState() { + super.initState(); + initAsync(); + } + + Future initAsync() async { + final user = await getUser(); + if (user == null) return; + setState(() { + showFeedbackShortcut = user.showFeedbackShortcut; + }); + } + void _showSelectThemeMode(BuildContext context) async { ThemeMode? selectedValue = context.read().themeMode; @@ -55,6 +77,14 @@ class AppearanceView extends StatelessWidget { } } + void toggleShowFeedbackIcon() async { + await updateUserdata((u) { + u.showFeedbackShortcut = !u.showFeedbackShortcut; + return u; + }); + initAsync(); + } + @override Widget build(BuildContext context) { ThemeMode? selectedTheme = @@ -73,6 +103,14 @@ class AppearanceView extends StatelessWidget { _showSelectThemeMode(context); }, ), + ListTile( + title: Text(context.lang.contactUsShortcut), + onTap: toggleShowFeedbackIcon, + trailing: Checkbox( + value: !showFeedbackShortcut, + onChanged: (a) => toggleShowFeedbackIcon(), + ), + ), ], ), ); diff --git a/lib/src/views/settings/help/contact_us.view.dart b/lib/src/views/settings/help/contact_us.view.dart index 98eb1ff..cebb8d4 100644 --- a/lib/src/views/settings/help/contact_us.view.dart +++ b/lib/src/views/settings/help/contact_us.view.dart @@ -1,7 +1,19 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/constants/secure_storage_keys.dart'; +import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; +import 'package:twonly/src/services/api/media_upload.dart' + show createDownloadTokens, uint8ListToHex; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/views/settings/help/contact_us/submit_message.view.dart'; import 'package:url_launcher/url_launcher.dart'; class ContactUsView extends StatefulWidget { @@ -14,98 +26,364 @@ class ContactUsView extends StatefulWidget { class _ContactUsState extends State { final TextEditingController _controller = TextEditingController(); bool isLoading = false; + bool includeDebugLog = true; + int? _selectedFeedback; + String? _selectedReason; + String? debugLogDownloadToken; - Future _submitFeedback() async { - final String feedback = _controller.text; + Future uploadDebugLog() async { + if (debugLogDownloadToken != null) return debugLogDownloadToken; + Uint8List downloadToken = createDownloadTokens(1)[0]; + + String debugLog = await loadLogFile(); + + var messageOnSuccess = TextMessage() + ..body = [] + ..userId = Int64(0); + + final uploadRequest = UploadRequest( + messagesOnSuccess: [messageOnSuccess], + downloadTokens: [downloadToken], + encryptedData: debugLog.codeUnits, + ); + + final uploadRequestBytes = uploadRequest.writeToBuffer(); + + String? apiAuthTokenRaw = + await FlutterSecureStorage().read(key: SecureStorageKeys.apiAuthToken); + if (apiAuthTokenRaw == null) { + Log.error("api auth token not defined."); + return null; + } + String apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); + + String apiUrl = + "http${apiService.apiSecure}://${apiService.apiHost}/api/upload"; + + var requestMultipart = http.MultipartRequest( + "POST", + Uri.parse(apiUrl), + ); + requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken; + + requestMultipart.files.add(http.MultipartFile.fromBytes( + "file", + uploadRequestBytes, + filename: "upload", + )); + + final response = await requestMultipart.send(); + if (response.statusCode == 200) { + setState(() { + debugLogDownloadToken = uint8ListToHex(downloadToken); + }); + return debugLogDownloadToken; + } + return null; + } + + Future _getFeedbackText() async { setState(() { isLoading = true; }); + String osVersion = ''; + String locale = context.lang.localeName; + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + String phoneModel = ""; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + String appVersion = packageInfo.version; + String packageName = packageInfo.packageName; + final String feedback = _controller.text; + String debugLogToken = ""; - if (feedback.isEmpty) { - // Show a message if the text field is empty - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Please enter your message.')), - ); - return; + if (!mounted) return ""; + + // Get device information + if (Theme.of(context).platform == TargetPlatform.android) { + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + osVersion = "Android version: ${androidInfo.version.release}"; + phoneModel = " ${androidInfo.model} (${androidInfo.brand})"; + } else if (Theme.of(context).platform == TargetPlatform.iOS) { + IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + osVersion = "iOS version: ${iosInfo.utsname.release}"; + phoneModel = " ${iosInfo.name}"; } - final user = await getUser(); - if (user == null) return; + if (includeDebugLog) { + try { + final token = await uploadDebugLog(); + if (token != null) { + debugLogToken = + "Debug Log: https://api.twonly.eu/api/download/$token"; + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Could not upload the debug log!')), + ); + } + } - final feedbackFull = "${user.username}\n\n$feedback"; - - final response = await http.post( - Uri.parse('https://twonly.theconnectapp.de/subscribe.twonly.php'), - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: { - 'feedback': feedbackFull, - }, - ); - if (!mounted) return; setState(() { isLoading = false; }); - if (response.statusCode == 200) { - // Handle successful response - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Feedback submitted successfully!')), - ); - Navigator.pop(context); - } else { - // Handle error response - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to submit feedback.')), - ); - } + return """ +$feedback + +---------- +Reason: ${_selectedReason ?? "Other"} +Locale: $locale +Emoji: ${FeedbackEmojiRow.getEmoji(_selectedFeedback)} +Device info: $phoneModel +twonly Version: $appVersion +twonly Package: $packageName +$osVersion +$debugLogToken +"""; } @override Widget build(BuildContext context) { + final List _reasons = [ + context.lang.contactUsReasonNotWorking, + context.lang.contactUsReasonFeatureRequest, + context.lang.contactUsReasonQuestion, + context.lang.contactUsReasonFeedback, + context.lang.contactUsReasonOther, + ]; return Scaffold( appBar: AppBar( title: Text(context.lang.settingsHelpContactUs), ), body: Padding( padding: const EdgeInsets.all(16.0), - child: Column( + child: ListView( children: [ + Text(context.lang.contactUsMessageTitle), + SizedBox(height: 5), TextField( controller: _controller, decoration: InputDecoration( - hintText: 'Your Feedback.', + hintText: context.lang.contactUsYourMessage, border: OutlineInputBorder(), ), + minLines: 5, maxLines: 10, ), - Padding( - padding: EdgeInsets.all(20), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: () { - launchUrl(Uri.parse("https://twonly.eu/support")); - }, - child: Text( - 'Have you read our FAQ yet?', - style: TextStyle( - color: Colors.blue, - ), - ), - ), - ElevatedButton( - onPressed: (isLoading) ? null : _submitFeedback, - child: Text('Submit'), - ), - ], + SizedBox(height: 5), + Text( + context.lang.contactUsMessage, + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 10, ), ), + SizedBox(height: 20), + Text(context.lang.contactUsReason), + SizedBox(height: 5), + DropdownButton( + hint: Text(context.lang.contactUsSelectOption), + underline: SizedBox.shrink(), + value: _selectedReason, + onChanged: (String? newValue) { + setState(() { + _selectedReason = newValue; + }); + }, + items: _reasons.map>((String reason) { + return DropdownMenuItem( + value: reason, + child: Text(reason), + ); + }).toList(), + ), + SizedBox(height: 20), + Text(context.lang.contactUsEmojis), + SizedBox(height: 5), + FeedbackEmojiRow( + selectedFeedback: _selectedFeedback, + onFeedbackChanged: (int? newValue) { + setState(() { + _selectedFeedback = newValue; + }); + }, + ), + SizedBox(height: 20), + IncludeDebugLog( + isChecked: includeDebugLog, + onChanged: (value) { + setState(() { + includeDebugLog = value; + }); + }, + ), + ], + ), + ), + bottomNavigationBar: Padding( + padding: EdgeInsets.symmetric(vertical: 40, horizontal: 40), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + launchUrl(Uri.parse("https://twonly.eu/en/faq/")); + }, + child: Text( + context.lang.contactUsFaq, + style: TextStyle( + color: Colors.blue, + ), + ), + ), + ElevatedButton( + onPressed: (isLoading) + ? null + : () async { + final fullMessage = await _getFeedbackText(); + if (!context.mounted) return; + + bool? feedbackSend = await Navigator.push(context, + MaterialPageRoute(builder: (context) { + return SubmitMessage( + fullMessage: fullMessage, + ); + })); + + if (feedbackSend == true && context.mounted) { + Navigator.pop(context); + } + }, + child: Text(context.lang.next), + ), ], ), ), ); } } + +class IncludeDebugLog extends StatefulWidget { + final bool isChecked; + final Function(bool) onChanged; + + const IncludeDebugLog({ + super.key, + required this.isChecked, + required this.onChanged, + }); + + @override + State createState() => _IncludeDebugLogState(); +} + +class _IncludeDebugLogState extends State { + void _launchURL() async { + const url = 'https://twonly.eu/en/faq/troubleshooting/debug-log.html'; + if (await launchUrl(Uri.parse(url))) { + } else { + throw 'Could not launch $url'; + } + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Checkbox( + value: widget.isChecked, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onChanged: (bool? value) { + if (value != null) { + widget.onChanged(value); + } + }, + ), + Text(context.lang.contactUsIncludeLog), + SizedBox(width: 20), + GestureDetector( + onTap: _launchURL, + child: Text( + context.lang.contactUsWhatsThat, + style: TextStyle( + color: Colors.blue, + ), + ), + ), + ], + ); + } +} + +class FeedbackEmojiRow extends StatelessWidget { + final int? selectedFeedback; + final ValueChanged onFeedbackChanged; + + const FeedbackEmojiRow({ + super.key, + required this.selectedFeedback, + required this.onFeedbackChanged, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _buildEmojiButton(5, Icons.sentiment_very_satisfied), + _buildEmojiButton(4, Icons.sentiment_satisfied), + _buildEmojiButton(3, Icons.sentiment_neutral), + _buildEmojiButton(2, Icons.sentiment_dissatisfied), + _buildEmojiButton(1, Icons.sentiment_very_dissatisfied), + ], + ); + } + + static String getEmoji(int? value) { + if (value == null) return ""; + switch (value) { + case 5: + return '😄'; + case 4: + return '😊'; + case 3: + return '😐'; + case 2: + return '😕'; + case 1: + return '😞'; + default: + return '❓'; + } + } + + Widget _buildEmojiButton(int value, IconData icon) { + bool isSelected = selectedFeedback == value; + + return GestureDetector( + onTap: () { + onFeedbackChanged(value); + }, + child: Icon( + icon, + size: 40, + color: _getColorForValue(value, isSelected), + ), + ); + } + + Color _getColorForValue(int value, bool isSelected) { + if (isSelected) { + if (value == 5) { + return Colors.greenAccent; + } else if (value > 1) { + return Colors.yellow; + } else { + return Colors.red; + } + } else { + return const Color.fromARGB(155, 134, 134, 134); + } + } +} diff --git a/lib/src/views/settings/help/contact_us/submit_message.view.dart b/lib/src/views/settings/help/contact_us/submit_message.view.dart new file mode 100644 index 0000000..da95427 --- /dev/null +++ b/lib/src/views/settings/help/contact_us/submit_message.view.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:twonly/src/utils/misc.dart'; + +class SubmitMessage extends StatefulWidget { + const SubmitMessage({super.key, required this.fullMessage}); + + final String fullMessage; + + @override + State createState() => _ContactUsState(); +} + +class _ContactUsState extends State { + final TextEditingController _controller = TextEditingController(); + bool isLoading = false; + + @override + void initState() { + super.initState(); + _controller.text = widget.fullMessage; + } + + Future _submitFeedback() async { + final String feedback = _controller.text; + setState(() { + isLoading = true; + }); + + if (feedback.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Please enter your message.')), + ); + return; + } + + final response = await http.post( + Uri.parse('https://twonly.theconnectapp.de/subscribe.twonly.php'), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: { + 'feedback': feedback, + }, + ); + if (!mounted) return; + setState(() { + isLoading = false; + }); + + if (response.statusCode == 200) { + // Handle successful response + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.lang.contactUsSuccess)), + ); + Navigator.pop(context, true); + } else { + // Handle error response + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to submit feedback.')), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.lang.settingsHelpContactUs), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + children: [ + Text( + context.lang.contactUsLastWarning, + textAlign: TextAlign.center, + ), + SizedBox(height: 10), + TextField( + controller: _controller, + decoration: InputDecoration( + hintText: context.lang.contactUsYourMessage, + border: OutlineInputBorder(), + ), + minLines: 5, + maxLines: 20, + ), + ], + ), + ), + bottomNavigationBar: Padding( + padding: EdgeInsets.symmetric(vertical: 40, horizontal: 40), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: (isLoading) ? null : _submitFeedback, + child: Text(context.lang.submit), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/views/settings/help/diagnostics.view.dart b/lib/src/views/settings/help/diagnostics.view.dart index c6f277f..b1f2810 100644 --- a/lib/src/views/settings/help/diagnostics.view.dart +++ b/lib/src/views/settings/help/diagnostics.view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:flutter/services.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; class DiagnosticsView extends StatefulWidget { @@ -29,7 +29,7 @@ class _DiagnosticsViewState extends State { return Scaffold( appBar: AppBar(title: const Text('Diagnostics')), body: FutureBuilder( - future: _loadLogFile(), + future: loadLogFile(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -109,15 +109,4 @@ class _DiagnosticsViewState extends State { ), ); } - - Future _loadLogFile() async { - final directory = await getApplicationSupportDirectory(); - final logFile = File('${directory.path}/app.log'); - - if (await logFile.exists()) { - return await logFile.readAsString(); - } else { - return 'Log file does not exist.'; - } - } } diff --git a/pubspec.lock b/pubspec.lock index 605b79e..bd32bbf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -337,6 +337,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" + url: "https://pub.dev" + source: hosted + version: "11.5.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" dots_indicator: dependency: transitive description: @@ -1879,6 +1895,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" x25519: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b7e164d..231e0d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: background_downloader: ^9.2.2 hashlib: ^2.0.0 video_thumbnail: ^0.5.6 + device_info_plus: ^11.5.0 dev_dependencies: flutter_test: