mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-03-03 17:06:47 +00:00
adds user study
This commit is contained in:
parent
f5d4f97c02
commit
8c3ea92b85
9 changed files with 566 additions and 34 deletions
|
|
@ -108,6 +108,19 @@ class UserData {
|
|||
DateTime? nextTimeToShowBackupNotice;
|
||||
BackupServer? backupServer;
|
||||
TwonlySafeBackup? twonlySafeBackup;
|
||||
|
||||
// For my master thesis I want to create a anonymous user study:
|
||||
// - users in the "Tester" Plan can, if they want, take part of the user study
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool askedForUserStudyPermission = false;
|
||||
|
||||
// So update data can be assigned. If set the user choose to participate.
|
||||
String? userStudyParticipantsToken;
|
||||
|
||||
// Once a day the anonymous data is collected and send to the server
|
||||
DateTime? lastUserStudyDataUpload;
|
||||
|
||||
Map<String, dynamic> toJson() => _$UserDataToJson(this);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,14 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
..twonlySafeBackup = json['twonlySafeBackup'] == null
|
||||
? null
|
||||
: TwonlySafeBackup.fromJson(
|
||||
json['twonlySafeBackup'] as Map<String, dynamic>);
|
||||
json['twonlySafeBackup'] as Map<String, dynamic>)
|
||||
..askedForUserStudyPermission =
|
||||
json['askedForUserStudyPermission'] as bool? ?? false
|
||||
..userStudyParticipantsToken =
|
||||
json['userStudyParticipantsToken'] as String?
|
||||
..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastUserStudyDataUpload'] as String);
|
||||
|
||||
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||
'userId': instance.userId,
|
||||
|
|
@ -122,6 +129,10 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
instance.nextTimeToShowBackupNotice?.toIso8601String(),
|
||||
'backupServer': instance.backupServer,
|
||||
'twonlySafeBackup': instance.twonlySafeBackup,
|
||||
'askedForUserStudyPermission': instance.askedForUserStudyPermission,
|
||||
'userStudyParticipantsToken': instance.userStudyParticipantsToken,
|
||||
'lastUserStudyDataUpload':
|
||||
instance.lastUserStudyDataUpload?.toIso8601String(),
|
||||
};
|
||||
|
||||
const _$ThemeModeEnumMap = {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import 'package:twonly/src/utils/keyvalue.dart';
|
|||
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/user_study/user_study_data_collection.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
|
||||
final lockConnecting = Mutex();
|
||||
|
|
@ -100,6 +101,11 @@ class ApiService {
|
|||
unawaited(fetchGroupStatesForUnjoinedGroups());
|
||||
unawaited(fetchMissingGroupPublicKey());
|
||||
unawaited(checkForDeletedUsernames());
|
||||
|
||||
if (gUser.userStudyParticipantsToken != null) {
|
||||
// In case the user participates in the user study, call the handler after authenticated, to be sure there is a internet connection
|
||||
unawaited(handleUserStudyUpload());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,38 +4,41 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
class KeyValueStore {
|
||||
static Future<String> _getFilePath(String key) async {
|
||||
static Future<File> _getFilePath(String key) async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
return '${directory.path}/keyvalue/$key.json';
|
||||
return File('${directory.path}/keyvalue/$key.json');
|
||||
}
|
||||
|
||||
static Future<void> delete(String key) async {
|
||||
try {
|
||||
final file = await _getFilePath(key);
|
||||
if (file.existsSync()) {
|
||||
file.deleteSync();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Error deleting file: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>?> get(String key) async {
|
||||
try {
|
||||
final filePath = await _getFilePath(key);
|
||||
final file = File(filePath);
|
||||
|
||||
// Check if the file exists
|
||||
final file = await _getFilePath(key);
|
||||
if (file.existsSync()) {
|
||||
final contents = await file.readAsString();
|
||||
return jsonDecode(contents) as Map<String, dynamic>;
|
||||
} else {
|
||||
return null; // File does not exist
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Error reading file: $e');
|
||||
Log.warn('Error reading file: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> put(String key, Map<String, dynamic> value) async {
|
||||
try {
|
||||
final filePath = await _getFilePath(key);
|
||||
final file = File(filePath);
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
final file = await _getFilePath(key);
|
||||
await file.parent.create(recursive: true);
|
||||
|
||||
// Write the JSON data to the file
|
||||
await file.writeAsString(jsonEncode(value));
|
||||
} catch (e) {
|
||||
Log.error('Error writing file: $e');
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import 'package:twonly/src/views/settings/help/changelog.view.dart';
|
|||
import 'package:twonly/src/views/settings/profile/profile.view.dart';
|
||||
import 'package:twonly/src/views/settings/settings_main.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
||||
import 'package:twonly/src/views/user_study/user_study_welcome.view.dart';
|
||||
|
||||
class ChatListView extends StatefulWidget {
|
||||
const ChatListView({super.key});
|
||||
|
|
@ -58,6 +59,23 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
});
|
||||
});
|
||||
|
||||
// In case the user is already a Tester, ask him for permission.
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (gUser.subscriptionPlan == SubscriptionPlan.Tester.name &&
|
||||
!gUser.askedForUserStudyPermission) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const UserStudyWelcomeView(
|
||||
wasOpenedAutomatic: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final changeLog = await rootBundle.loadString('CHANGELOG.md');
|
||||
final changeLogHash =
|
||||
(await compute(Sha256().hash, changeLog.codeUnits)).bytes;
|
||||
|
|
@ -83,6 +101,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -295,8 +314,8 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FloatingActionButton.small(
|
||||
backgroundColor: context.color.primary,
|
||||
IconButton.filled(
|
||||
color: context.color.primary,
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
|
@ -307,7 +326,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
),
|
||||
);
|
||||
},
|
||||
child: FaIcon(
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.qrcode,
|
||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
|
@ -10,6 +11,7 @@ import 'package:twonly/src/views/settings/help/contact_us.view.dart';
|
|||
import 'package:twonly/src/views/settings/help/credits.view.dart';
|
||||
import 'package:twonly/src/views/settings/help/diagnostics.view.dart';
|
||||
import 'package:twonly/src/views/settings/help/faq.view.dart';
|
||||
import 'package:twonly/src/views/user_study/user_study_welcome.view.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class HelpView extends StatefulWidget {
|
||||
|
|
@ -109,6 +111,21 @@ class _HelpViewState extends State<HelpView> {
|
|||
},
|
||||
),
|
||||
const Divider(),
|
||||
if (gUser.userStudyParticipantsToken == null || kDebugMode)
|
||||
ListTile(
|
||||
title: const Text('Teilnahme an Nutzerstudie'),
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const UserStudyWelcomeView();
|
||||
},
|
||||
),
|
||||
);
|
||||
setState(() {}); // gUser has changed
|
||||
},
|
||||
),
|
||||
FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snap) {
|
||||
|
|
|
|||
78
lib/src/views/user_study/user_study_data_collection.dart
Normal file
78
lib/src/views/user_study/user_study_data_collection.dart
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// ignore_for_file: avoid_dynamic_calls
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/utils/keyvalue.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
const userStudySurveyKey = 'user_study_survey';
|
||||
|
||||
// LEASE DO NOT SPAM OR TRY SENDING DIRECTLY TO THIS URL!
|
||||
// You're just making my master's thesis more difficult and destroy scientific data. :/
|
||||
const surveyUrlBase = 'https://survey.twonly.org/upload.php';
|
||||
|
||||
Future<void> handleUserStudyUpload() async {
|
||||
try {
|
||||
final token = gUser.userStudyParticipantsToken;
|
||||
if (token == null) return;
|
||||
|
||||
// in case the survey was taken offline try again
|
||||
final userStudySurvey = await KeyValueStore.get(userStudySurveyKey);
|
||||
if (userStudySurvey != null) {
|
||||
final response = await http.post(
|
||||
Uri.parse('$surveyUrlBase/create/$token'),
|
||||
body: jsonEncode(userStudySurvey),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
Log.warn(
|
||||
'Got different status code for survey upload: ${response.statusCode}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
await KeyValueStore.delete(userStudySurveyKey);
|
||||
}
|
||||
|
||||
if (gUser.lastUserStudyDataUpload
|
||||
?.isAfter(DateTime.now().subtract(const Duration(days: 1))) ??
|
||||
false) {
|
||||
// Only send updates once a day.
|
||||
// This enables to see if improvements to actually work.
|
||||
return;
|
||||
}
|
||||
|
||||
final contacts = await twonlyDB.contactsDao.getAllContacts();
|
||||
|
||||
final dataCollection = {
|
||||
'total_contacts': contacts.length,
|
||||
'accepted_contacts': contacts.where((c) => c.accepted).length,
|
||||
'verified_contacts': contacts.where((c) => c.verified).length,
|
||||
};
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('$surveyUrlBase/push/$token'),
|
||||
body: jsonEncode(dataCollection),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
await updateUserdata((u) {
|
||||
u.lastUserStudyDataUpload = DateTime.now();
|
||||
return u;
|
||||
});
|
||||
}
|
||||
if (response.statusCode == 404) {
|
||||
// Token is unknown to the server...
|
||||
await updateUserdata((u) {
|
||||
u
|
||||
..lastUserStudyDataUpload = null
|
||||
..userStudyParticipantsToken = null;
|
||||
return u;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
265
lib/src/views/user_study/user_study_questionnaire.view.dart
Normal file
265
lib/src/views/user_study/user_study_questionnaire.view.dart
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
// ignore_for_file: avoid_dynamic_calls
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/utils/keyvalue.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/user_study/user_study_data_collection.dart';
|
||||
|
||||
class UserStudyQuestionnaire extends StatefulWidget {
|
||||
const UserStudyQuestionnaire({super.key});
|
||||
|
||||
@override
|
||||
State<UserStudyQuestionnaire> createState() => _UserStudyQuestionnaireState();
|
||||
}
|
||||
|
||||
class _UserStudyQuestionnaireState extends State<UserStudyQuestionnaire> {
|
||||
final Map<String, dynamic> _responses = {
|
||||
'gender': null,
|
||||
'gender_free': '',
|
||||
'age': null,
|
||||
'education': null,
|
||||
'education_free': '',
|
||||
'vocational': null,
|
||||
'vocational_free': '',
|
||||
'enrolled': null,
|
||||
'study_program': '',
|
||||
'working': null,
|
||||
'work_field': '',
|
||||
'smartphone_2years': null,
|
||||
'comp_knowledge': null,
|
||||
'security_knowledge': null,
|
||||
'messengers': [],
|
||||
};
|
||||
|
||||
final List<String> _messengerOptions = [
|
||||
'WhatsApp',
|
||||
'Signal',
|
||||
'Telegram',
|
||||
'Facebook Messenger',
|
||||
'iMessage',
|
||||
'Teams',
|
||||
'Viber',
|
||||
'Element',
|
||||
'Andere',
|
||||
];
|
||||
|
||||
Future<void> _submitData() async {
|
||||
await KeyValueStore.put(userStudySurveyKey, _responses);
|
||||
|
||||
await updateUserdata((u) {
|
||||
// generate a random participants id to identify data send later while keeping the user anonym
|
||||
u.userStudyParticipantsToken = getRandomString(25);
|
||||
return u;
|
||||
});
|
||||
|
||||
await handleUserStudyUpload();
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Vielen Dank für deine Teilnahme!')),
|
||||
);
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Befragung')),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_sectionTitle('Demografische Daten'),
|
||||
_questionText('Was ist dein Geschlecht?'),
|
||||
_buildRadioList(
|
||||
['Männlich', 'Weiblich', 'Divers', 'Keine Angabe'],
|
||||
'gender',
|
||||
),
|
||||
_buildTextField(
|
||||
'Freitext (optional)',
|
||||
(val) => _responses['gender_free'] = val,
|
||||
),
|
||||
_questionText('Wie alt bist du?'),
|
||||
_buildRadioList(
|
||||
[
|
||||
'18-22',
|
||||
'23-27',
|
||||
'28-32',
|
||||
'33-37',
|
||||
'38-42',
|
||||
'43-47',
|
||||
'48-52',
|
||||
'53-57',
|
||||
'58-62',
|
||||
'63-67',
|
||||
'68-72',
|
||||
'73-77',
|
||||
'77 oder älter',
|
||||
'Keine Angabe',
|
||||
],
|
||||
'age',
|
||||
),
|
||||
_questionText('Was ist dein höchster Schulabschluss?'),
|
||||
_buildRadioList(
|
||||
[
|
||||
'Noch in der Schule',
|
||||
'Hauptschulabschluss',
|
||||
'POS (Polytechnische Oberschule)',
|
||||
'Realschulabschluss',
|
||||
'Abitur / Hochschulreife',
|
||||
'Kein Abschluss',
|
||||
'Keine Angabe',
|
||||
],
|
||||
'education',
|
||||
),
|
||||
_buildTextField(
|
||||
'Freitext (optional)',
|
||||
(val) => _responses['education_free'] = val,
|
||||
),
|
||||
_questionText('Was ist dein höchster beruflicher Abschluss?'),
|
||||
_buildRadioList(
|
||||
[
|
||||
'Berufsausbildung / Duales System',
|
||||
'Fachschulabschluss',
|
||||
'Fachschulabschluss (ehem. DDR)',
|
||||
'Bachelor',
|
||||
'Master',
|
||||
'Diplom',
|
||||
'Promotion (PhD)',
|
||||
'Kein beruflicher Abschluss',
|
||||
'Keine Angabe',
|
||||
],
|
||||
'vocational',
|
||||
),
|
||||
_buildTextField(
|
||||
'Freitext (optional)',
|
||||
(val) => _responses['vocational_free'] = val,
|
||||
),
|
||||
_questionText(
|
||||
'Bist du derzeit in einem Studiengang eingeschrieben? (Bachelor, Master, Diplom, Staatsexamen, Magister)',
|
||||
),
|
||||
_buildRadioList(['Ja', 'Nein', 'Keine Angabe'], 'enrolled'),
|
||||
_questionText('Wenn ja, welcher Studiengang?'),
|
||||
_buildTextField(
|
||||
'Studiengang eingeben',
|
||||
(val) => _responses['study_program'] = val,
|
||||
),
|
||||
_questionText('Bist du derzeit berufstätig?'),
|
||||
_buildRadioList(['Ja', 'Nein', 'Keine Angabe'], 'working'),
|
||||
_questionText('Wenn ja, in welchem Bereich arbeiten Sie?'),
|
||||
_buildTextField(
|
||||
'Arbeitsbereich eingeben',
|
||||
(val) => _responses['work_field'] = val,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
// const Divider(),
|
||||
_sectionTitle('Technisches Wissen'),
|
||||
_questionText(
|
||||
'Nutzt du seit mehr als zwei Jahren ein Smartphone?',
|
||||
),
|
||||
_buildRadioList(['Ja', 'Nein'], 'smartphone_2years'),
|
||||
_questionText(
|
||||
'Wie schätzt du deine allgemeinen Computerkenntnisse ein?',
|
||||
),
|
||||
_buildRadioList(
|
||||
['Anfänger', 'Mittel', 'Fortgeschritten'],
|
||||
'comp_knowledge',
|
||||
),
|
||||
_questionText(
|
||||
'Wie schätzt du dein Wissen im Bereich IT-Sicherheit ein?',
|
||||
),
|
||||
_buildRadioList(
|
||||
['Anfänger', 'Mittel', 'Fortgeschritten'],
|
||||
'security_knowledge',
|
||||
),
|
||||
_questionText(
|
||||
'Welche der folgenden Messenger hast du schon einmal benutzt?',
|
||||
),
|
||||
..._messengerOptions.map(
|
||||
(m) => CheckboxListTile(
|
||||
title: Text(m),
|
||||
visualDensity: const VisualDensity(horizontal: 0, vertical: -4),
|
||||
value: (_responses['messengers'] as List<dynamic>).contains(m),
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
value!
|
||||
? _responses['messengers'].add(m)
|
||||
: _responses['messengers'].remove(m);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Center(
|
||||
child: FilledButton(
|
||||
onPressed: _submitData,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15),
|
||||
child:
|
||||
Text('Jetzt teilnehmen', style: TextStyle(fontSize: 18)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Hilfsmethoden für das UI
|
||||
Widget _sectionTitle(String title) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _questionText(String text) => Padding(
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 5),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildRadioList(List<String> options, String key) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: DropdownButtonFormField<String>(
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
labelText: 'Bitte wählen...',
|
||||
),
|
||||
initialValue: _responses[key] as String?,
|
||||
items: options.map((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_responses[key] = val;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField(String hint, void Function(String) onChanged) {
|
||||
return TextField(
|
||||
decoration:
|
||||
InputDecoration(hintText: hint, border: const OutlineInputBorder()),
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
120
lib/src/views/user_study/user_study_welcome.view.dart
Normal file
120
lib/src/views/user_study/user_study_welcome.view.dart
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/user_study/user_study_questionnaire.view.dart';
|
||||
|
||||
class UserStudyWelcomeView extends StatefulWidget {
|
||||
const UserStudyWelcomeView({super.key, this.wasOpenedAutomatic = false});
|
||||
|
||||
final bool wasOpenedAutomatic;
|
||||
|
||||
@override
|
||||
State<UserStudyWelcomeView> createState() => _UserStudyWelcomeViewState();
|
||||
}
|
||||
|
||||
class _UserStudyWelcomeViewState extends State<UserStudyWelcomeView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Teilnahme an Nutzerstudie'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 30),
|
||||
const Text(
|
||||
'Es dauert nur ein paar Minuten.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Im Rahmen meiner Masterarbeit möchte ich die Benutzerfreundlichkeit von anonymen und dezentralen Messenger-Diensten verbessern.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Zu diesem Zweck werden in den nächsten Monaten verschiedene Änderungen an der App vorgenommen. Um die Wirksamkeit der Änderungen zu messen, möchte ich einige Daten über deine Nutzung der App sammeln sowie eine kurze Befragung durchführen.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Die Daten bestehen ausschließlich aus Zahlen, z. B. Anzahl deiner Kontakte. Alle Daten werden anonym übermittelt und können nicht mit deinem Benutzerkonto verknüpft werden.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Die Masterarbeit und damit die Nutzerstudie wird bis September durchgeführt. Nach Abschluss erhältst du eine Benachrichtigung und wirst über die Ergebnisse informiert.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Center(
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const UserStudyQuestionnaire();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
|
||||
child: Text(
|
||||
'Weiter zur Befragung',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (widget.wasOpenedAutomatic)
|
||||
Center(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: Text(
|
||||
'Frag mich später noch mal',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (widget.wasOpenedAutomatic)
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
await updateUserdata((u) {
|
||||
u.askedForUserStudyPermission = true;
|
||||
return u;
|
||||
});
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
child: const Text(
|
||||
'Nicht mehr anzeigen',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'PS: twonly ist Open Source, wenn du also genau wissen willst, welche Daten übertragen werden, schau dir einfach die Datei "lib/src/views/user_study/user_study_data_collection.dart" im Repository an :).',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue