This commit is contained in:
otsmr 2025-07-11 19:27:40 +02:00
parent 94017615c5
commit 9a5114eb5d
16 changed files with 824 additions and 72 deletions

View file

@ -5,6 +5,8 @@ PODS:
- Flutter - Flutter
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- device_info_plus (0.0.1):
- Flutter
- Firebase (11.10.0): - Firebase (11.10.0):
- Firebase/Core (= 11.10.0) - Firebase/Core (= 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`) - background_downloader (from `.symlinks/plugins/background_downloader/ios`)
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Firebase - Firebase
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
@ -291,6 +294,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/camera_avfoundation/ios" :path: ".symlinks/plugins/camera_avfoundation/ios"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
firebase_core: firebase_core:
:path: ".symlinks/plugins/firebase_core/ios" :path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging: firebase_messaging:
@ -344,6 +349,7 @@ SPEC CHECKSUMS:
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
firebase_core: ba71b44041571da878cb624ce0d80250bcbe58ad firebase_core: ba71b44041571da878cb624ce0d80250bcbe58ad
firebase_messaging: 13129fe2ca166d1ed2d095062d76cee88943d067 firebase_messaging: 13129fe2ca166d1ed2d095062d76cee88943d067

View file

@ -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.", "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", "settingsHelp": "Hilfe",
"settingsHelpFAQ": "FAQ", "settingsHelpFAQ": "FAQ",
"feedbackTooltip": "Feedback zur Verbesserung von twonly geben.",
"settingsHelpContactUs": "Kontaktiere uns", "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", "settingsHelpDiagnostics": "Diagnoseprotokoll",
"settingsHelpVersion": "Version", "settingsHelpVersion": "Version",
"settingsHelpLicenses": "Lizenzen (Source-Code)", "settingsHelpLicenses": "Lizenzen (Source-Code)",
@ -153,6 +171,7 @@
"undo": "Rückgängig", "undo": "Rückgängig",
"redo": "Wiederholen", "redo": "Wiederholen",
"next": "Weiter", "next": "Weiter",
"submit": "Abschicken",
"close": "Schließen", "close": "Schließen",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"ok": "Ok", "ok": "Ok",

View file

@ -213,6 +213,7 @@
"@settingsHelpDiagnostics": {}, "@settingsHelpDiagnostics": {},
"settingsHelpFAQ": "FAQ", "settingsHelpFAQ": "FAQ",
"@settingsHelpFAQ": {}, "@settingsHelpFAQ": {},
"feedbackTooltip": "Give Feedback to improve twonly.",
"settingsHelpContactUs": "Contact us", "settingsHelpContactUs": "Contact us",
"@settingsHelpContactUs": {}, "@settingsHelpContactUs": {},
"settingsHelpVersion": "Version", "settingsHelpVersion": "Version",
@ -222,6 +223,23 @@
"settingsHelpCredits": "Licenses (Images)", "settingsHelpCredits": "Licenses (Images)",
"@settingsHelpCredits": {}, "@settingsHelpCredits": {},
"settingsHelpImprint": "Imprint & Privacy Policy", "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", "settingsHelpTerms": "Terms of Service",
"settingsAppearanceTheme": "Theme", "settingsAppearanceTheme": "Theme",
"@settingsAppearanceTheme": {}, "@settingsAppearanceTheme": {},
@ -276,6 +294,7 @@
"undo": "Undo", "undo": "Undo",
"redo": "Redo", "redo": "Redo",
"next": "Next", "next": "Next",
"submit": "Submit",
"close": "Close", "close": "Close",
"disable": "Disable", "disable": "Disable",
"enable": "Enable", "enable": "Enable",

View file

@ -734,6 +734,12 @@ abstract class AppLocalizations {
/// **'FAQ'** /// **'FAQ'**
String get settingsHelpFAQ; 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. /// No description provided for @settingsHelpContactUs.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -764,6 +770,108 @@ abstract class AppLocalizations {
/// **'Imprint & Privacy Policy'** /// **'Imprint & Privacy Policy'**
String get settingsHelpImprint; 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. /// No description provided for @settingsHelpTerms.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -920,6 +1028,12 @@ abstract class AppLocalizations {
/// **'Next'** /// **'Next'**
String get next; String get next;
/// No description provided for @submit.
///
/// In en, this message translates to:
/// **'Submit'**
String get submit;
/// No description provided for @close. /// No description provided for @close.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -357,6 +357,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settingsHelpFAQ => 'FAQ'; String get settingsHelpFAQ => 'FAQ';
@override
String get feedbackTooltip => 'Feedback zur Verbesserung von twonly geben.';
@override @override
String get settingsHelpContactUs => 'Kontaktiere uns'; String get settingsHelpContactUs => 'Kontaktiere uns';
@ -372,6 +375,59 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settingsHelpImprint => 'Impressum & Datenschutzrichtlinie'; 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 @override
String get settingsHelpTerms => 'Nutzungsbedingungen'; String get settingsHelpTerms => 'Nutzungsbedingungen';
@ -465,6 +521,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get next => 'Weiter'; String get next => 'Weiter';
@override
String get submit => 'Abschicken';
@override @override
String get close => 'Schließen'; String get close => 'Schließen';

View file

@ -352,6 +352,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settingsHelpFAQ => 'FAQ'; String get settingsHelpFAQ => 'FAQ';
@override
String get feedbackTooltip => 'Give Feedback to improve twonly.';
@override @override
String get settingsHelpContactUs => 'Contact us'; String get settingsHelpContactUs => 'Contact us';
@ -367,6 +370,59 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settingsHelpImprint => 'Imprint & Privacy Policy'; 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 @override
String get settingsHelpTerms => 'Terms of Service'; String get settingsHelpTerms => 'Terms of Service';
@ -460,6 +516,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get next => 'Next'; String get next => 'Next';
@override
String get submit => 'Submit';
@override @override
String get close => 'Close'; String get close => 'Close';

View file

@ -44,6 +44,9 @@ class UserData {
@JsonKey(defaultValue: true) @JsonKey(defaultValue: true)
bool useHighQuality = true; bool useHighQuality = true;
@JsonKey(defaultValue: true)
bool showFeedbackShortcut = true;
List<String>? preSelectedEmojies; List<String>? preSelectedEmojies;
Map<String, List<String>>? autoDownloadOptions; Map<String, List<String>>? autoDownloadOptions;

View file

@ -25,6 +25,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
ThemeMode.system ThemeMode.system
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt() ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
..useHighQuality = json['useHighQuality'] as bool? ?? true ..useHighQuality = json['useHighQuality'] as bool? ?? true
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
..preSelectedEmojies = (json['preSelectedEmojies'] as List<dynamic>?) ..preSelectedEmojies = (json['preSelectedEmojies'] as List<dynamic>?)
?.map((e) => e as String) ?.map((e) => e as String)
.toList() .toList()
@ -77,6 +78,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'defaultShowTime': instance.defaultShowTime, 'defaultShowTime': instance.defaultShowTime,
'useHighQuality': instance.useHighQuality, 'useHighQuality': instance.useHighQuality,
'showFeedbackShortcut': instance.showFeedbackShortcut,
'preSelectedEmojies': instance.preSelectedEmojies, 'preSelectedEmojies': instance.preSelectedEmojies,
'autoDownloadOptions': instance.autoDownloadOptions, 'autoDownloadOptions': instance.autoDownloadOptions,
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery, 'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,

View file

@ -32,6 +32,17 @@ class Log {
Mutex writeToLogGuard = Mutex(); Mutex writeToLogGuard = Mutex();
Future<String> 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<void> _writeLogToFile(LogRecord record) async { Future<void> _writeLogToFile(LogRecord record) async {
final directory = await getApplicationSupportDirectory(); final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log'); final logFile = File('${directory.path}/app.log');

View file

@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/services/api/media_download.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/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/connection_info.comp.dart';
import 'package:twonly/src/views/chats/chat_list_components/demo_user.card.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/chat_messages.view.dart';
import 'package:twonly/src/views/chats/media_viewer.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/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/settings/settings_main.view.dart';
import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -40,6 +42,7 @@ class _ChatListViewState extends State<ChatListView> {
GlobalKey firstUserListItemKey = GlobalKey(); GlobalKey firstUserListItemKey = GlobalKey();
GlobalKey searchForOtherUsers = GlobalKey(); GlobalKey searchForOtherUsers = GlobalKey();
Timer? tutorial; Timer? tutorial;
bool showFeedbackShortcut = false;
@override @override
void initState() { void initState() {
@ -65,6 +68,13 @@ class _ChatListViewState extends State<ChatListView> {
await showChatListTutorialContextMenu(context, firstUserListItemKey); await showChatListTutorialContextMenu(context, firstUserListItemKey);
} }
}); });
final user = await getUser();
if (user != null) {
setState(() {
showFeedbackShortcut = user.showFeedbackShortcut;
});
}
} }
@override @override
@ -105,6 +115,20 @@ class _ChatListViewState extends State<ChatListView> {
), ),
]), ]),
actions: [ 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( StreamBuilder(
stream: twonlyDB.contactsDao.watchContactsRequested(), stream: twonlyDB.contactsDao.watchContactsRequested(),
builder: (context, snapshot) { builder: (context, snapshot) {

View file

@ -1,12 +1,34 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.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/views/components/radio_button.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
class AppearanceView extends StatelessWidget { class AppearanceView extends StatefulWidget {
const AppearanceView({super.key}); const AppearanceView({super.key});
@override
State<AppearanceView> createState() => _AppearanceViewState();
}
class _AppearanceViewState extends State<AppearanceView> {
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 { void _showSelectThemeMode(BuildContext context) async {
ThemeMode? selectedValue = context.read<SettingsChangeProvider>().themeMode; ThemeMode? selectedValue = context.read<SettingsChangeProvider>().themeMode;
@ -55,6 +77,14 @@ class AppearanceView extends StatelessWidget {
} }
} }
void toggleShowFeedbackIcon() async {
await updateUserdata((u) {
u.showFeedbackShortcut = !u.showFeedbackShortcut;
return u;
});
initAsync();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ThemeMode? selectedTheme = ThemeMode? selectedTheme =
@ -73,6 +103,14 @@ class AppearanceView extends StatelessWidget {
_showSelectThemeMode(context); _showSelectThemeMode(context);
}, },
), ),
ListTile(
title: Text(context.lang.contactUsShortcut),
onTap: toggleShowFeedbackIcon,
trailing: Checkbox(
value: !showFeedbackShortcut,
onChanged: (a) => toggleShowFeedbackIcon(),
),
),
], ],
), ),
); );

View file

@ -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/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http; 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/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'; import 'package:url_launcher/url_launcher.dart';
class ContactUsView extends StatefulWidget { class ContactUsView extends StatefulWidget {
@ -14,94 +26,235 @@ class ContactUsView extends StatefulWidget {
class _ContactUsState extends State<ContactUsView> { class _ContactUsState extends State<ContactUsView> {
final TextEditingController _controller = TextEditingController(); final TextEditingController _controller = TextEditingController();
bool isLoading = false; bool isLoading = false;
bool includeDebugLog = true;
int? _selectedFeedback;
String? _selectedReason;
String? debugLogDownloadToken;
Future<void> _submitFeedback() async { Future<String?> uploadDebugLog() async {
final String feedback = _controller.text; 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<String> _getFeedbackText() async {
setState(() { setState(() {
isLoading = true; 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) { if (!mounted) return "";
// Show a message if the text field is empty
ScaffoldMessenger.of(context).showSnackBar( // Get device information
SnackBar(content: Text('Please enter your message.')), if (Theme.of(context).platform == TargetPlatform.android) {
); AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
return; 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 (includeDebugLog) {
if (user == null) return; try {
final token = await uploadDebugLog();
final feedbackFull = "${user.username}\n\n$feedback"; if (token != null) {
debugLogToken =
final response = await http.post( "Debug Log: https://api.twonly.eu/api/download/$token";
Uri.parse('https://twonly.theconnectapp.de/subscribe.twonly.php'), }
headers: <String, String>{ } catch (e) {
'Content-Type': 'application/x-www-form-urlencoded', ScaffoldMessenger.of(context).showSnackBar(
}, SnackBar(content: Text('Could not upload the debug log!')),
body: {
'feedback': feedbackFull,
},
); );
if (!mounted) return; }
}
setState(() { setState(() {
isLoading = false; isLoading = false;
}); });
if (response.statusCode == 200) { return """
// Handle successful response $feedback
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Feedback submitted successfully!')), ----------
); Reason: ${_selectedReason ?? "Other"}
Navigator.pop(context); Locale: $locale
} else { Emoji: ${FeedbackEmojiRow.getEmoji(_selectedFeedback)}
// Handle error response Device info: $phoneModel
ScaffoldMessenger.of(context).showSnackBar( twonly Version: $appVersion
SnackBar(content: Text('Failed to submit feedback.')), twonly Package: $packageName
); $osVersion
} $debugLogToken
""";
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<String> _reasons = [
context.lang.contactUsReasonNotWorking,
context.lang.contactUsReasonFeatureRequest,
context.lang.contactUsReasonQuestion,
context.lang.contactUsReasonFeedback,
context.lang.contactUsReasonOther,
];
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.settingsHelpContactUs), title: Text(context.lang.settingsHelpContactUs),
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: ListView(
children: [ children: [
Text(context.lang.contactUsMessageTitle),
SizedBox(height: 5),
TextField( TextField(
controller: _controller, controller: _controller,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Your Feedback.', hintText: context.lang.contactUsYourMessage,
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
minLines: 5,
maxLines: 10, maxLines: 10,
), ),
Padding( SizedBox(height: 5),
padding: EdgeInsets.all(20), Text(
context.lang.contactUsMessage,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 10,
),
),
SizedBox(height: 20),
Text(context.lang.contactUsReason),
SizedBox(height: 5),
DropdownButton<String>(
hint: Text(context.lang.contactUsSelectOption),
underline: SizedBox.shrink(),
value: _selectedReason,
onChanged: (String? newValue) {
setState(() {
_selectedReason = newValue;
});
},
items: _reasons.map<DropdownMenuItem<String>>((String reason) {
return DropdownMenuItem<String>(
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( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
GestureDetector( GestureDetector(
onTap: () { onTap: () {
launchUrl(Uri.parse("https://twonly.eu/support")); launchUrl(Uri.parse("https://twonly.eu/en/faq/"));
}, },
child: Text( child: Text(
'Have you read our FAQ yet?', context.lang.contactUsFaq,
style: TextStyle( style: TextStyle(
color: Colors.blue, color: Colors.blue,
), ),
), ),
), ),
ElevatedButton( ElevatedButton(
onPressed: (isLoading) ? null : _submitFeedback, onPressed: (isLoading)
child: Text('Submit'), ? 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),
), ),
], ],
), ),
@ -109,3 +262,128 @@ class _ContactUsState extends State<ContactUsView> {
); );
} }
} }
class IncludeDebugLog extends StatefulWidget {
final bool isChecked;
final Function(bool) onChanged;
const IncludeDebugLog({
super.key,
required this.isChecked,
required this.onChanged,
});
@override
State<IncludeDebugLog> createState() => _IncludeDebugLogState();
}
class _IncludeDebugLogState extends State<IncludeDebugLog> {
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<int?> 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);
}
}
}

View file

@ -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<SubmitMessage> createState() => _ContactUsState();
}
class _ContactUsState extends State<SubmitMessage> {
final TextEditingController _controller = TextEditingController();
bool isLoading = false;
@override
void initState() {
super.initState();
_controller.text = widget.fullMessage;
}
Future<void> _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: <String, String>{
'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),
),
],
),
),
);
}
}

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:io';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
class DiagnosticsView extends StatefulWidget { class DiagnosticsView extends StatefulWidget {
@ -29,7 +29,7 @@ class _DiagnosticsViewState extends State<DiagnosticsView> {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Diagnostics')), appBar: AppBar(title: const Text('Diagnostics')),
body: FutureBuilder<String>( body: FutureBuilder<String>(
future: _loadLogFile(), future: loadLogFile(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@ -109,15 +109,4 @@ class _DiagnosticsViewState extends State<DiagnosticsView> {
), ),
); );
} }
Future<String> _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.';
}
}
} }

View file

@ -337,6 +337,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.11" 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: dots_indicator:
dependency: transitive dependency: transitive
description: description:
@ -1879,6 +1895,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.13.0" 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: x25519:
dependency: transitive dependency: transitive
description: description:

View file

@ -70,6 +70,7 @@ dependencies:
background_downloader: ^9.2.2 background_downloader: ^9.2.2
hashlib: ^2.0.0 hashlib: ^2.0.0
video_thumbnail: ^0.5.6 video_thumbnail: ^0.5.6
device_info_plus: ^11.5.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: