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

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.",
"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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,6 +25,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> 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<dynamic>?)
?.map((e) => e as String)
.toList()
@ -77,6 +78,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'defaultShowTime': instance.defaultShowTime,
'useHighQuality': instance.useHighQuality,
'showFeedbackShortcut': instance.showFeedbackShortcut,
'preSelectedEmojies': instance.preSelectedEmojies,
'autoDownloadOptions': instance.autoDownloadOptions,
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,

View file

@ -32,6 +32,17 @@ class Log {
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 {
final directory = await getApplicationSupportDirectory();
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: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<ChatListView> {
GlobalKey firstUserListItemKey = GlobalKey();
GlobalKey searchForOtherUsers = GlobalKey();
Timer? tutorial;
bool showFeedbackShortcut = false;
@override
void initState() {
@ -65,6 +68,13 @@ class _ChatListViewState extends State<ChatListView> {
await showChatListTutorialContextMenu(context, firstUserListItemKey);
}
});
final user = await getUser();
if (user != null) {
setState(() {
showFeedbackShortcut = user.showFeedbackShortcut;
});
}
}
@override
@ -105,6 +115,20 @@ class _ChatListViewState extends State<ChatListView> {
),
]),
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) {

View file

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

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_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,94 +26,235 @@ class ContactUsView extends StatefulWidget {
class _ContactUsState extends State<ContactUsView> {
final TextEditingController _controller = TextEditingController();
bool isLoading = false;
bool includeDebugLog = true;
int? _selectedFeedback;
String? _selectedReason;
String? debugLogDownloadToken;
Future<void> _submitFeedback() async {
final String feedback = _controller.text;
Future<String?> 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<String> _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;
final feedbackFull = "${user.username}\n\n$feedback";
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': feedbackFull,
},
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!')),
);
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<String> _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),
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<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(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () {
launchUrl(Uri.parse("https://twonly.eu/support"));
launchUrl(Uri.parse("https://twonly.eu/en/faq/"));
},
child: Text(
'Have you read our FAQ yet?',
context.lang.contactUsFaq,
style: TextStyle(
color: Colors.blue,
),
),
),
ElevatedButton(
onPressed: (isLoading) ? null : _submitFeedback,
child: Text('Submit'),
),
],
),
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),
),
],
),
@ -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 '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<DiagnosticsView> {
return Scaffold(
appBar: AppBar(title: const Text('Diagnostics')),
body: FutureBuilder<String>(
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<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"
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:

View file

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