Compare commits

2 Commits

41 changed files with 407 additions and 1285 deletions

View File

@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-03-24T11:30:25.894049082Z"> <DropdownSelection timestamp="2025-09-29T10:52:30.282144200Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=R52T605XE0L" /> <DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\danie\.android\avd\Medium_Phone.avd" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

View File

@ -41,11 +41,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.DevSettingsActivity"
android:exported="false"
android:parentActivityName=".MainActivity" />
</application> </application>
</manifest> </manifest>

View File

@ -11,16 +11,14 @@ import android.widget.EditText
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import com.dano.test1.auth.LoginManager
import androidx.core.view.WindowInsetsCompat import com.dano.test1.auth.TokenStore
import com.dano.test1.network.DatabaseDownloader import com.dano.test1.network.DatabaseDownloader
import com.dano.test1.network.LoginManager
import com.dano.test1.network.TokenStore
import com.dano.test1.questionnaire.QuestionnaireBase import com.dano.test1.questionnaire.QuestionnaireBase
import com.dano.test1.ui.HandlerOpeningScreen import com.dano.test1.ui.HandlerOpeningScreen
import com.dano.test1.util.LanguageManager
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@ -39,7 +37,6 @@ class MainActivity : AppCompatActivity() {
private fun t(key: String): String = LanguageManager.getText(bootLanguageId, key) private fun t(key: String): String = LanguageManager.getText(bootLanguageId, key)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// === Offline-Start ermöglichen === // === Offline-Start ermöglichen ===
@ -209,16 +206,6 @@ class MainActivity : AppCompatActivity() {
} }
// --- /LIVE NETZSTATUS --- // --- /LIVE NETZSTATUS ---
override fun onContentChanged() {
super.onContentChanged()
val content = findViewById<View>(android.R.id.content) ?: return
ViewCompat.setOnApplyWindowInsetsListener(content) { v, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(bars.left, bars.top, bars.right, bars.bottom)
WindowInsetsCompat.CONSUMED
}
}
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
} }

View File

@ -1,4 +1,4 @@
package com.dano.test1.network package com.dano.test1.auth
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context import android.content.Context

View File

@ -1,4 +1,4 @@
package com.dano.test1.network package com.dano.test1.auth
import android.content.Context import android.content.Context

View File

@ -1,4 +1,4 @@
package com.dano.test1.data package com.dano.test1.export
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
@ -7,7 +7,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import com.dano.test1.LanguageManager import com.dano.test1.util.LanguageManager
import com.dano.test1.MyApp import com.dano.test1.MyApp
import org.apache.poi.ss.usermodel.Row import org.apache.poi.ss.usermodel.Row
import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.apache.poi.xssf.usermodel.XSSFWorkbook
@ -41,8 +41,8 @@ class ExcelExportService(
val orderedIds = headerRepo.loadOrderedIds() val orderedIds = headerRepo.loadOrderedIds()
if (orderedIds.isEmpty()) return null if (orderedIds.isEmpty()) return null
val clients = MyApp.database.clientDao().getAllClients() val clients = MyApp.Companion.database.clientDao().getAllClients()
val questionnaires = MyApp.database.questionnaireDao().getAll() val questionnaires = MyApp.Companion.database.questionnaireDao().getAll()
val questionnaireIdSet = questionnaires.map { it.id }.toSet() val questionnaireIdSet = questionnaires.map { it.id }.toSet()
val wb = XSSFWorkbook() val wb = XSSFWorkbook()
@ -71,9 +71,9 @@ class ExcelExportService(
var c = 0 var c = 0
row.createCell(c++).setCellValue((rowIdx + 1).toDouble()) row.createCell(c++).setCellValue((rowIdx + 1).toDouble())
val completedForClient = MyApp.database.completedQuestionnaireDao().getAllForClient(client.clientCode) val completedForClient = MyApp.Companion.database.completedQuestionnaireDao().getAllForClient(client.clientCode)
val statusMap = completedForClient.associate { it.questionnaireId to it.isDone } val statusMap = completedForClient.associate { it.questionnaireId to it.isDone }
val answers = MyApp.database.answerDao().getAnswersForClient(client.clientCode) val answers = MyApp.Companion.database.answerDao().getAnswersForClient(client.clientCode)
val answerMap = answers.associate { it.questionId to it.answerValue } val answerMap = answers.associate { it.questionId to it.answerValue }
orderedIds.forEach { id -> orderedIds.forEach { id ->

View File

@ -1,9 +1,9 @@
package com.dano.test1.data package com.dano.test1.export
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import com.dano.test1.LanguageManager import com.dano.test1.util.LanguageManager
import org.apache.poi.ss.usermodel.CellType import org.apache.poi.ss.usermodel.CellType
import org.apache.poi.ss.usermodel.DateUtil import org.apache.poi.ss.usermodel.DateUtil
import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.apache.poi.xssf.usermodel.XSSFWorkbook

View File

@ -1,4 +1,4 @@
package com.dano.test1 package com.dano.test1.network
import java.io.File import java.io.File
import java.security.SecureRandom import java.security.SecureRandom

View File

@ -2,7 +2,6 @@ package com.dano.test1.network
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.dano.test1.AES256Helper
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@ -8,7 +8,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import android.database.Cursor import android.database.Cursor
import com.dano.test1.AES256Helper
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody

View File

@ -1,15 +1,12 @@
package com.dano.test1.questionnaire.handlers package com.dano.test1.questionnaire
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import com.dano.test1.questionnaire.GlobalValues import com.dano.test1.util.LanguageManager
import com.dano.test1.LanguageManager import com.dano.test1.util.setTextSizePercentOfScreenHeight
import com.dano.test1.MyApp import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R import com.dano.test1.R
import com.dano.test1.network.TokenStore import com.dano.test1.auth.TokenStore
import com.dano.test1.utils.ViewUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -44,10 +41,10 @@ class HandlerClientCoachCode(
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: "" questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
ViewUtils.setTextSizePercentOfScreenHeight(titleTextView, 0.03f) titleTextView.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f) questionTextView.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(clientCodeField, 0.025f) clientCodeField.setTextSizePercentOfScreenHeight(0.025f)
ViewUtils.setTextSizePercentOfScreenHeight(coachCodeField, 0.025f) coachCodeField.setTextSizePercentOfScreenHeight(0.025f)
// Client-Code: nur verwenden, wenn bereits geladen // Client-Code: nur verwenden, wenn bereits geladen
val loadedClientCode = GlobalValues.LOADED_CLIENT_CODE val loadedClientCode = GlobalValues.LOADED_CLIENT_CODE
@ -63,7 +60,7 @@ class HandlerClientCoachCode(
val coachFromLogin = TokenStore.getUsername(layout.context) val coachFromLogin = TokenStore.getUsername(layout.context)
if (!coachFromLogin.isNullOrBlank()) { if (!coachFromLogin.isNullOrBlank()) {
coachCodeField.setText(coachFromLogin) coachCodeField.setText(coachFromLogin)
ViewUtils.lockEditField(coachCodeField) // optisch & technisch gesperrt lockCoachField(coachCodeField) // optisch & technisch gesperrt
} else { } else {
// Falls (theoretisch) kein Login-Username vorhanden ist, verhalten wie bisher // Falls (theoretisch) kein Login-Username vorhanden ist, verhalten wie bisher
coachCodeField.setText(answers["coach_code"] as? String ?: "") coachCodeField.setText(answers["coach_code"] as? String ?: "")
@ -96,7 +93,7 @@ class HandlerClientCoachCode(
val dbExistedBefore = dbPath.exists() val dbExistedBefore = dbPath.exists()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val existingClient = MyApp.database.clientDao().getClientByCode(clientCode) val existingClient = MyApp.Companion.database.clientDao().getClientByCode(clientCode)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (existingClient != null && clientCodeField.isEnabled) { if (existingClient != null && clientCodeField.isEnabled) {
@ -107,7 +104,7 @@ class HandlerClientCoachCode(
goToNextQuestion() goToNextQuestion()
if (!dbExistedBefore) { if (!dbExistedBefore) {
MyApp.database.close() MyApp.Companion.database.close()
dbPath.delete() dbPath.delete()
val journalFile = layout.context.getDatabasePath("questionnaire_database-journal") val journalFile = layout.context.getDatabasePath("questionnaire_database-journal")
journalFile.delete() journalFile.delete()
@ -142,4 +139,19 @@ class HandlerClientCoachCode(
// Not used // Not used
} }
private fun lockCoachField(field: EditText) {
field.isFocusable = false
field.isFocusableInTouchMode = false
field.isCursorVisible = false
field.keyListener = null
field.isLongClickable = false
field.isClickable = false
field.setBackgroundResource(R.drawable.bg_field_locked)
field.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_lock_24, 0)
field.compoundDrawablePadding = dp(8)
field.alpha = 0.95f
}
private fun dp(v: Int): Int =
(v * layout.resources.displayMetrics.density).toInt()
} }

View File

@ -1,10 +1,8 @@
package com.dano.test1.questionnaire.handlers package com.dano.test1.questionnaire
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import com.dano.test1.LanguageManager import com.dano.test1.util.LanguageManager
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R import com.dano.test1.R
/* /*

View File

@ -1,4 +1,4 @@
package com.dano.test1.questionnaire.handlers package com.dano.test1.questionnaire
import android.content.Context import android.content.Context
import android.view.View import android.view.View
@ -6,16 +6,13 @@ import android.widget.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import com.dano.test1.questionnaire.GlobalValues import com.dano.test1.util.LanguageManager
import com.dano.test1.LanguageManager import com.dano.test1.util.setTextSizePercentOfScreenHeight
import com.dano.test1.questionnaire.MAX_VALUE_YEAR import com.dano.test1.util.setupSpinner
import com.dano.test1.ui.Month import com.dano.test1.util.Month
import com.dano.test1.ui.Months import com.dano.test1.util.Months
import com.dano.test1.MyApp import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
/* /*
Zweck: Zweck:
@ -57,12 +54,11 @@ class HandlerDateSpinner(
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: "" questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: "" textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
// Schriftgrößen pro Bildschirmhöhe textView.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(textView, 0.03f) questionTextView.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f) labelDay.setTextSizePercentOfScreenHeight(0.025f)
ViewUtils.setTextSizePercentOfScreenHeight(labelDay, 0.025f) labelMonth.setTextSizePercentOfScreenHeight(0.025f)
ViewUtils.setTextSizePercentOfScreenHeight(labelMonth, 0.025f) labelYear.setTextSizePercentOfScreenHeight(0.025f)
ViewUtils.setTextSizePercentOfScreenHeight(labelYear, 0.025f)
// gespeicherte Antwort (YYYY-MM-DD) lesen // gespeicherte Antwort (YYYY-MM-DD) lesen
val (savedYear, savedMonthIndex, savedDay) = question.question?.let { val (savedYear, savedMonthIndex, savedDay) = question.question?.let {
@ -79,10 +75,9 @@ class HandlerDateSpinner(
?: months[today.get(Calendar.MONTH)] ?: months[today.get(Calendar.MONTH)]
val defaultYear = savedYear ?: today.get(Calendar.YEAR) val defaultYear = savedYear ?: today.get(Calendar.YEAR)
// Spinner responsiv aufsetzen (Schrift + Zeilenhöhe ohne Abschneiden) spinnerDay.setupSpinner(days, defaultDay)
ViewUtils.setupResponsiveSpinner(context, spinnerDay, days, defaultDay) spinnerMonth.setupSpinner(months, defaultMonth)
ViewUtils.setupResponsiveSpinner(context, spinnerMonth, months, defaultMonth) spinnerYear.setupSpinner(years, defaultYear)
ViewUtils.setupResponsiveSpinner(context, spinnerYear, years, defaultYear)
// DB-Abfrage, falls noch nicht im answers-Map // DB-Abfrage, falls noch nicht im answers-Map
val answerMapKey = question.question ?: (question.id ?: "") val answerMapKey = question.question ?: (question.id ?: "")
@ -92,7 +87,7 @@ class HandlerDateSpinner(
val clientCode = GlobalValues.LAST_CLIENT_CODE val clientCode = GlobalValues.LAST_CLIENT_CODE
if (clientCode.isNullOrBlank()) return@launch if (clientCode.isNullOrBlank()) return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode) val allAnswersForClient = MyApp.Companion.database.answerDao().getAnswersForClient(clientCode)
val myQuestionId = questionnaireMeta + "-" + question.question val myQuestionId = questionnaireMeta + "-" + question.question
val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue

View File

@ -1,16 +1,14 @@
package com.dano.test1.questionnaire.handlers package com.dano.test1.questionnaire
import android.content.Context import android.content.Context
import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import com.dano.test1.questionnaire.GlobalValues import com.dano.test1.util.LanguageManager
import com.dano.test1.LanguageManager import com.dano.test1.util.setTextSizePercentOfScreenHeight
import com.dano.test1.MyApp import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
import kotlinx.coroutines.* import kotlinx.coroutines.*
/* /*
@ -72,8 +70,8 @@ class HandlerGlassScaleQuestion(
titleTv.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: "" titleTv.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
questionTv.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: "" questionTv.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
ViewUtils.setTextSizePercentOfScreenHeight(titleTv, 0.03f) titleTv.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTv, 0.03f) questionTv.setTextSizePercentOfScreenHeight(0.03f)
// Header Icons // Header Icons
val header = layout.findViewById<LinearLayout>(R.id.glass_header) val header = layout.findViewById<LinearLayout>(R.id.glass_header)
@ -107,7 +105,7 @@ class HandlerGlassScaleQuestion(
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val clientCode = GlobalValues.LAST_CLIENT_CODE ?: return@launch val clientCode = GlobalValues.LAST_CLIENT_CODE ?: return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode) val allAnswersForClient = MyApp.Companion.database.answerDao().getAnswersForClient(clientCode)
val answerMap = allAnswersForClient.associateBy({ it.questionId }, { it.answerValue }) val answerMap = allAnswersForClient.associateBy({ it.questionId }, { it.answerValue })
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -155,7 +153,7 @@ class HandlerGlassScaleQuestion(
text = LanguageManager.getText(languageID, symptomKey) text = LanguageManager.getText(languageID, symptomKey)
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 4f) layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 4f)
setPadding(4, 16, 4, 16) setPadding(4, 16, 4, 16)
ViewUtils.setTextSizePercentOfScreenHeight(this, 0.022f) setTextSizePercentOfScreenHeight(0.022f)
} }
row.addView(symptomText) row.addView(symptomText)

View File

@ -1,18 +1,15 @@
package com.dano.test1.questionnaire.handlers package com.dano.test1.questionnaire
import android.util.TypedValue
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import android.text.Html import android.text.Html
import androidx.core.widget.TextViewCompat
import kotlinx.coroutines.* import kotlinx.coroutines.*
import com.dano.test1.questionnaire.GlobalValues import android.util.TypedValue
import com.dano.test1.LanguageManager import androidx.core.widget.TextViewCompat
import com.dano.test1.util.LanguageManager
import com.dano.test1.util.setTextSizePercentOfScreenHeight
import com.dano.test1.MainActivity import com.dano.test1.MainActivity
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
/* /*
@ -63,9 +60,8 @@ class HandlerLastPage(
finishBtn.isAllCaps = false finishBtn.isAllCaps = false
applyResponsiveTextSizing(finishBtn) applyResponsiveTextSizing(finishBtn)
// Überschriften responsiv skalieren (wie zuvor) titleTv.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(titleTv, 0.03f) questionTv.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTv, 0.03f)
// Buttons // Buttons
prevBtn.setOnClickListener { goToPreviousQuestion() } prevBtn.setOnClickListener { goToPreviousQuestion() }

View File

@ -1,18 +1,15 @@
package com.dano.test1.questionnaire.handlers package com.dano.test1.questionnaire
import android.content.Context import android.content.Context
import android.util.TypedValue
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import androidx.core.widget.TextViewCompat
import kotlinx.coroutines.* import kotlinx.coroutines.*
import com.dano.test1.questionnaire.GlobalValues import android.util.TypedValue
import com.dano.test1.LanguageManager import androidx.core.widget.TextViewCompat
import com.dano.test1.util.LanguageManager
import com.dano.test1.util.setTextSizePercentOfScreenHeight
import com.dano.test1.MyApp import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
/* /*
Zweck: Zweck:
@ -44,9 +41,8 @@ class HandlerMultiCheckboxQuestion(
questionTextView.text = this.question.textKey?.let { LanguageManager.getText(languageID, it) } ?: "" questionTextView.text = this.question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
questionTitle.text = this.question.question?.let { LanguageManager.getText(languageID, it) } ?: "" questionTitle.text = this.question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
// Textgrößen pro Bildschirmhöhe (wie bei deinen anderen Handlern) questionTextView.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f) questionTitle.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTitle, 0.03f)
container.removeAllViews() container.removeAllViews()
@ -95,7 +91,7 @@ class HandlerMultiCheckboxQuestion(
val clientCode = GlobalValues.LAST_CLIENT_CODE val clientCode = GlobalValues.LAST_CLIENT_CODE
if (clientCode.isNullOrBlank()) return@launch if (clientCode.isNullOrBlank()) return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode) val allAnswersForClient = MyApp.Companion.database.answerDao().getAnswersForClient(clientCode)
val myQuestionId = questionnaireMeta + "-" + question.question val myQuestionId = questionnaireMeta + "-" + question.question
val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue

View File

@ -1,17 +1,14 @@
package com.dano.test1.questionnaire.handlers package com.dano.test1.questionnaire
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.text.Html import android.text.Html
import android.widget.* import android.widget.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import com.dano.test1.questionnaire.GlobalValues import com.dano.test1.util.LanguageManager
import com.dano.test1.LanguageManager import com.dano.test1.util.setTextSizePercentOfScreenHeight
import com.dano.test1.MyApp import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
/* /*
Zweck: Zweck:
@ -46,8 +43,8 @@ class HandlerRadioQuestion(
Html.fromHtml(LanguageManager.getText(languageID, it), Html.FROM_HTML_MODE_LEGACY) Html.fromHtml(LanguageManager.getText(languageID, it), Html.FROM_HTML_MODE_LEGACY)
} ?: "" } ?: ""
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f) questionTextView.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTitle, 0.03f) questionTitle.setTextSizePercentOfScreenHeight(0.03f)
radioGroup.removeAllViews() radioGroup.removeAllViews()
@ -56,8 +53,7 @@ class HandlerRadioQuestion(
text = LanguageManager.getText(languageID, option.key) text = LanguageManager.getText(languageID, option.key)
tag = option.key tag = option.key
// RadioButton-Text analog zu EditTexts: 2.5% der Bildschirmhöhe setTextSizePercentOfScreenHeight(0.025f)
ViewUtils.setTextSizePercentOfScreenHeight(this, 0.025f)
layoutParams = RadioGroup.LayoutParams( layoutParams = RadioGroup.LayoutParams(
RadioGroup.LayoutParams.MATCH_PARENT, RadioGroup.LayoutParams.MATCH_PARENT,
@ -83,7 +79,7 @@ class HandlerRadioQuestion(
val clientCode = GlobalValues.LAST_CLIENT_CODE val clientCode = GlobalValues.LAST_CLIENT_CODE
if (clientCode.isNullOrBlank()) return@launch if (clientCode.isNullOrBlank()) return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode) val allAnswersForClient = MyApp.Companion.database.answerDao().getAnswersForClient(clientCode)
val myQuestionId = questionnaireMeta + "-" + question.question val myQuestionId = questionnaireMeta + "-" + question.question
val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue

View File

@ -1,17 +1,15 @@
package com.dano.test1.questionnaire.handlers package com.dano.test1.questionnaire
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import com.dano.test1.ui.Countries import com.dano.test1.util.LanguageManager
import com.dano.test1.questionnaire.GlobalValues import com.dano.test1.util.setTextSizePercentOfScreenHeight
import com.dano.test1.LanguageManager import com.dano.test1.util.setupSpinner
import com.dano.test1.MyApp import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R import com.dano.test1.R
import com.dano.test1.utils.ViewUtils import com.dano.test1.util.Countries
/* /*
Zweck: Zweck:
@ -46,17 +44,15 @@ class HandlerStringSpinner(
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: "" questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: "" textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
// Textgrößen prozentual zur Bildschirmhöhe textView.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(textView, 0.03f) questionTextView.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
val options = buildOptionsList() val options = buildOptionsList()
// vorhandene Auswahl (falls vorhanden) // vorhandene Auswahl (falls vorhanden)
val savedSelection = question.question?.let { answers[it] as? String } val savedSelection = question.question?.let { answers[it] as? String }
// Spinner aufsetzen spinner.setupSpinner(options, savedSelection)
ViewUtils.setupResponsiveSpinner(context, spinner, options, savedSelection)
// Falls noch keine Antwort im Map: aus DB laden // Falls noch keine Antwort im Map: aus DB laden
val answerMapKey = question.question ?: (question.id ?: "") val answerMapKey = question.question ?: (question.id ?: "")
@ -66,7 +62,7 @@ class HandlerStringSpinner(
val clientCode = GlobalValues.LAST_CLIENT_CODE val clientCode = GlobalValues.LAST_CLIENT_CODE
if (clientCode.isNullOrBlank()) return@launch if (clientCode.isNullOrBlank()) return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode) val allAnswersForClient = MyApp.Companion.database.answerDao().getAnswersForClient(clientCode)
val myQuestionId = questionnaireMeta + "-" + question.question val myQuestionId = questionnaireMeta + "-" + question.question
val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue

View File

@ -1,16 +1,14 @@
package com.dano.test1.questionnaire.handlers package com.dano.test1.questionnaire
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import com.dano.test1.questionnaire.GlobalValues import com.dano.test1.util.LanguageManager
import com.dano.test1.LanguageManager import com.dano.test1.util.setTextSizePercentOfScreenHeight
import com.dano.test1.util.setupSpinner
import com.dano.test1.MyApp import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
/* /*
Zweck: Zweck:
@ -47,8 +45,8 @@ class HandlerValueSpinner(
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: "" questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: "" textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
ViewUtils.setTextSizePercentOfScreenHeight(textView, 0.03f) textView.setTextSizePercentOfScreenHeight(0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f) questionTextView.setTextSizePercentOfScreenHeight(0.03f)
val prompt = LanguageManager.getText(languageID, "choose_answer") val prompt = LanguageManager.getText(languageID, "choose_answer")
val spinnerItems: List<String> = listOf(prompt) + if (question.range != null) { val spinnerItems: List<String> = listOf(prompt) + if (question.range != null) {
@ -58,7 +56,7 @@ class HandlerValueSpinner(
} }
val savedValue = question.question?.let { answers[it] as? String } val savedValue = question.question?.let { answers[it] as? String }
ViewUtils.setupResponsiveSpinner(context, spinner, spinnerItems, savedValue) spinner.setupSpinner(spinnerItems, savedValue)
//DB-Abfrage falls noch keine Antwort im Map existiert //DB-Abfrage falls noch keine Antwort im Map existiert
val answerMapKey = question.question ?: (question.id ?: "") val answerMapKey = question.question ?: (question.id ?: "")
@ -68,7 +66,7 @@ class HandlerValueSpinner(
val clientCode = GlobalValues.LAST_CLIENT_CODE val clientCode = GlobalValues.LAST_CLIENT_CODE
if (clientCode.isNullOrBlank()) return@launch if (clientCode.isNullOrBlank()) return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode) val allAnswersForClient = MyApp.Companion.database.answerDao().getAnswersForClient(clientCode)
val myQuestionId = questionnaireMeta + "-" + question.question val myQuestionId = questionnaireMeta + "-" + question.question
val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue

View File

@ -1,554 +0,0 @@
package com.dano.test1.questionnaire
import android.graphics.Color
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout
import com.dano.test1.LanguageManager
import com.dano.test1.LocalizationHelper
import com.dano.test1.MainActivity
import com.dano.test1.R
import com.dano.test1.network.TokenStore
import com.dano.test1.utils.ViewUtils
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import kotlinx.coroutines.*
class QuestionnaireAllInOne(private val questionnaireFileName: String) : QuestionnaireBase<Unit>() {
private data class Section(
val index: Int,
val question: QuestionItem,
val card: MaterialCardView,
val sectionView: View,
val handler: QuestionHandler?
)
private val sections = mutableListOf<Section>()
private lateinit var container: LinearLayout
private lateinit var btnSave: MaterialButton
private var setupComplete = false
override fun startQuestionnaire() {
val (meta, questionsList) = loadQuestionnaireFromJson(questionnaireFileName)
questionnaireMeta = meta
questions = questionsList
currentIndex = 0
buildAllInOneUi()
}
override fun showCurrentQuestion() {
// Not used in all-in-one mode
}
private fun buildAllInOneUi() {
sections.clear()
setupComplete = false
context.setContentView(R.layout.questionnaire_all_in_one)
container = context.findViewById(R.id.questionContainer)
btnSave = context.findViewById(R.id.btnSave)
val btnBack = context.findViewById<MaterialButton>(R.id.btnBack)
val progressBar = context.findViewById<ProgressBar>(R.id.progressBar)
btnSave.text = LanguageManager.getText(languageID, "save")
ViewUtils.setTextSizePercentOfScreenHeight(btnSave, 0.025f)
btnSave.isEnabled = false
btnSave.alpha = 0.5f
btnBack.setOnClickListener {
(context as? MainActivity)?.finishQuestionnaire()
}
setupLanguageSpinner(context.findViewById(R.id.langSpinner))
val inflater = LayoutInflater.from(context)
for ((idx, question) in questions.withIndex()) {
if (question is QuestionItem.LastPage) continue
val layoutResId = getLayoutResId(question.layout ?: "default_layout")
if (layoutResId == 0) continue
val sectionView = inflater.inflate(layoutResId, container, false)
adaptLayoutForEmbedding(sectionView)
LocalizationHelper.localizeViewTree(sectionView, languageID)
val card = wrapInCard(sectionView)
container.addView(card)
val handler = createEmbeddedHandler(question)
handler?.bind(sectionView, question)
reduceTextSizes(sectionView)
sections.add(Section(idx, question, card, sectionView, handler))
}
installChangeListeners()
recalculateVisibility()
// Allow DB restores to finish before enabling interaction tracking
container.postDelayed({
setupComplete = true
recalculateVisibility()
updateSaveButtonState()
}, 600)
btnSave.setOnClickListener {
handleSave(progressBar)
}
}
private fun setupLanguageSpinner(spinner: Spinner) {
val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, LANGUAGE_LABELS).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
spinner.adapter = adapter
spinner.setSelection(LANGUAGE_IDS.indexOf(languageID).coerceAtLeast(0))
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, v: View?, position: Int, id: Long) {
val newLang = LANGUAGE_IDS[position]
if (newLang == languageID) return
spinner.post { onLanguageChanged(newLang) }
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
}
private fun onLanguageChanged(newLang: String) {
for (section in sections) {
if (section.card.visibility == View.VISIBLE) {
try { section.handler?.saveAnswer() } catch (_: Exception) {}
}
}
languageID = newLang
buildAllInOneUi()
}
private fun createEmbeddedHandler(question: QuestionItem): QuestionHandler? {
val noop = {}
val noopId: (String) -> Unit = {}
val toast: (String) -> Unit = { showToast(it) }
val metaId = questionnaireMeta.id
return when (question) {
is QuestionItem.RadioQuestion ->
com.dano.test1.questionnaire.handlers.HandlerRadioQuestion(
context, answers, points, languageID, noop, noop, noopId, toast, metaId
)
is QuestionItem.ClientCoachCodeQuestion ->
com.dano.test1.questionnaire.handlers.HandlerClientCoachCode(
answers, languageID, noop, noop, toast
)
is QuestionItem.DateSpinnerQuestion ->
com.dano.test1.questionnaire.handlers.HandlerDateSpinner(
context, answers, languageID, noop, noop, toast, metaId
)
is QuestionItem.ValueSpinnerQuestion ->
com.dano.test1.questionnaire.handlers.HandlerValueSpinner(
context, answers, languageID, noop, noop, noopId, toast, metaId
)
is QuestionItem.GlassScaleQuestion ->
com.dano.test1.questionnaire.handlers.HandlerGlassScaleQuestion(
context, answers, points, languageID, noop, noop, toast, metaId
)
is QuestionItem.ClientNotSigned ->
com.dano.test1.questionnaire.handlers.HandlerClientNotSigned(
answers, languageID, noop, noop, toast
)
is QuestionItem.StringSpinnerQuestion ->
com.dano.test1.questionnaire.handlers.HandlerStringSpinner(
context, answers, languageID, noop, noop, toast, metaId
)
is QuestionItem.MultiCheckboxQuestion ->
com.dano.test1.questionnaire.handlers.HandlerMultiCheckboxQuestion(
context, answers, points, languageID, noop, noop, toast, metaId
)
else -> null
}
}
private fun adaptLayoutForEmbedding(view: View) {
if (view is ConstraintLayout) {
view.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
// Ensure the last field in the card is never clipped at the bottom
view.setPadding(
view.paddingLeft, view.paddingTop,
view.paddingRight, ViewUtils.dp(context, 12)
)
}
view.findViewById<View>(R.id.Qprev)?.visibility = View.GONE
view.findViewById<View>(R.id.Qnext)?.visibility = View.GONE
view.findViewById<View>(R.id.gTop)?.let { guideline ->
if (guideline is androidx.constraintlayout.widget.Guideline) {
val params = guideline.layoutParams as? ConstraintLayout.LayoutParams
params?.guideBegin = ViewUtils.dp(context, 8)
guideline.layoutParams = params
}
}
adaptInnerScrollView(view, R.id.radioScroll)
adaptInnerScrollView(view, R.id.scrollView)
adaptInnerScrollView(view, R.id.glassScroll)
view.findViewById<EditText>(R.id.client_code)?.let { et ->
val params = et.layoutParams as? ConstraintLayout.LayoutParams ?: return@let
if (params.matchConstraintPercentHeight > 0f) {
params.matchConstraintPercentHeight = 0f
params.height = ViewUtils.dp(context, 64)
et.layoutParams = params
}
}
view.findViewById<EditText>(R.id.coach_code)?.let { et ->
val params = et.layoutParams as? ConstraintLayout.LayoutParams ?: return@let
if (params.matchConstraintPercentHeight > 0f) {
params.matchConstraintPercentHeight = 0f
params.height = ViewUtils.dp(context, 64)
et.layoutParams = params
}
}
}
/**
* Scales down all TextView text sizes by [factor] after the handler has set them.
* Spinners manage their own adapter text sizes and are skipped.
*/
private fun reduceTextSizes(view: View, factor: Float = 0.82f) {
if (view is Spinner) return
if (view is TextView) {
val dm = view.context.resources.displayMetrics
val currentSp = view.textSize / dm.scaledDensity
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, currentSp * factor)
}
if (view is ViewGroup) {
for (i in 0 until view.childCount) reduceTextSizes(view.getChildAt(i), factor)
}
}
private fun adaptInnerScrollView(root: View, scrollViewId: Int) {
val sv = root.findViewById<ScrollView>(scrollViewId) ?: return
val params = sv.layoutParams
if (params is ConstraintLayout.LayoutParams) {
params.height = ConstraintLayout.LayoutParams.WRAP_CONTENT
params.bottomToTop = ConstraintLayout.LayoutParams.UNSET
params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
sv.layoutParams = params
}
sv.isNestedScrollingEnabled = false
}
private fun wrapInCard(sectionView: View): MaterialCardView {
val card = MaterialCardView(context).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
val margin = ViewUtils.dp(context, 6)
setMargins(0, margin, 0, margin)
}
radius = ViewUtils.dp(context, 16).toFloat()
cardElevation = ViewUtils.dp(context, 2).toFloat()
setCardBackgroundColor(Color.WHITE)
strokeColor = Color.parseColor("#D8D1F0")
strokeWidth = ViewUtils.dp(context, 1)
setContentPadding(
ViewUtils.dp(context, 4),
ViewUtils.dp(context, 8),
ViewUtils.dp(context, 4),
ViewUtils.dp(context, 12)
)
}
card.addView(sectionView)
return card
}
// ---- Visibility walk algorithm (reads from UI state, not answers map) ----
private fun recalculateVisibility() {
val visibleIndices = mutableSetOf<Int>()
var i = 0
while (i < questions.size) {
val q = questions[i]
if (q is QuestionItem.LastPage) { i++; continue }
visibleIndices.add(i)
val nextTarget = resolveNextFromUi(q)
when {
nextTarget != null -> {
if (questions.any { it is QuestionItem.LastPage && it.id == nextTarget }) {
break
}
val targetIdx = questions.indexOfFirst { it.id == nextTarget }
i = if (targetIdx != -1) targetIdx else i + 1
}
hasBranchingOptions(q) && !hasAnswerInUi(q) -> break
else -> i++
}
}
for (section in sections) {
val shouldShow = visibleIndices.contains(section.index)
section.card.visibility = if (shouldShow) View.VISIBLE else View.GONE
}
}
private fun resolveNextFromUi(q: QuestionItem): String? {
val section = sections.find { it.question === q } ?: return null
val view = section.sectionView
return when (q) {
is QuestionItem.RadioQuestion -> {
val radioGroup = view.findViewById<RadioGroup>(R.id.RadioGroup) ?: return null
val checkedId = radioGroup.checkedRadioButtonId
if (checkedId == -1) return null
val rb = radioGroup.findViewById<RadioButton>(checkedId) ?: return null
val key = rb.tag?.toString() ?: return null
q.options?.find { it.key == key }?.nextQuestionId
}
is QuestionItem.ValueSpinnerQuestion -> {
val spinner = view.findViewById<Spinner>(R.id.value_spinner) ?: return null
val selected = spinner.selectedItem?.toString() ?: return null
val prompt = LanguageManager.getText(languageID, "choose_answer")
if (selected == prompt) return null
val selectedVal = selected.toIntOrNull()
if (selectedVal != null) {
q.options?.find { it.value == selectedVal }?.nextQuestionId
} else null
}
else -> null
}
}
private fun hasBranchingOptions(q: QuestionItem): Boolean {
return when (q) {
is QuestionItem.RadioQuestion ->
q.options?.any { it.nextQuestionId != null } ?: false
is QuestionItem.ValueSpinnerQuestion ->
q.options?.any { it.nextQuestionId != null } ?: false
else -> false
}
}
private fun hasAnswerInUi(q: QuestionItem): Boolean {
val section = sections.find { it.question === q } ?: return false
val view = section.sectionView
return when (q) {
is QuestionItem.RadioQuestion -> {
val rg = view.findViewById<RadioGroup>(R.id.RadioGroup) ?: return false
rg.checkedRadioButtonId != -1
}
is QuestionItem.ValueSpinnerQuestion -> {
val spinner = view.findViewById<Spinner>(R.id.value_spinner) ?: return false
val selected = spinner.selectedItem?.toString()
val prompt = LanguageManager.getText(languageID, "choose_answer")
!selected.isNullOrEmpty() && selected != prompt
}
else -> true
}
}
// ---- Change listeners ----
private fun installChangeListeners() {
for (section in sections) {
val view = section.sectionView
when (section.question) {
is QuestionItem.RadioQuestion -> {
view.findViewById<RadioGroup>(R.id.RadioGroup)
?.setOnCheckedChangeListener { _, checkedId ->
if (!setupComplete || checkedId == -1) return@setOnCheckedChangeListener
recalculateVisibility()
updateSaveButtonState()
}
}
is QuestionItem.ValueSpinnerQuestion -> {
installSpinnerChangeListener(view, R.id.value_spinner)
}
is QuestionItem.StringSpinnerQuestion -> {
installSpinnerChangeListener(view, R.id.string_spinner)
}
is QuestionItem.DateSpinnerQuestion -> {
val listener = createSimpleSpinnerListener()
view.findViewById<Spinner>(R.id.spinner_value_day)?.onItemSelectedListener = listener
view.findViewById<Spinner>(R.id.spinner_value_month)?.onItemSelectedListener = listener
view.findViewById<Spinner>(R.id.spinner_value_year)?.onItemSelectedListener = listener
}
is QuestionItem.GlassScaleQuestion -> {
installGlassScaleClickListeners(view)
}
is QuestionItem.MultiCheckboxQuestion -> {
installCheckboxClickListeners(view)
}
is QuestionItem.ClientCoachCodeQuestion -> {
installEditTextWatcher(view, R.id.client_code)
installEditTextWatcher(view, R.id.coach_code)
}
is QuestionItem.ClientNotSigned -> {
installEditTextWatcher(view, R.id.coach_code)
}
else -> {}
}
}
}
private fun installSpinnerChangeListener(view: View, spinnerId: Int) {
view.findViewById<Spinner>(spinnerId)?.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?, v: View?, pos: Int, id: Long
) {
if (!setupComplete) return
recalculateVisibility()
updateSaveButtonState()
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
}
private fun createSimpleSpinnerListener(): AdapterView.OnItemSelectedListener {
return object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, v: View?, pos: Int, id: Long) {
if (!setupComplete) return
updateSaveButtonState()
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
}
private fun installGlassScaleClickListeners(view: View) {
val table = view.findViewById<TableLayout>(R.id.glass_table) ?: return
for (r in 0 until table.childCount) {
val row = table.getChildAt(r) as? TableRow ?: continue
val radioGroup = row.getChildAt(1) as? RadioGroup ?: continue
for (c in 0 until radioGroup.childCount) {
val rb = getRadioFromChild(radioGroup.getChildAt(c)) ?: continue
rb.setOnClickListener {
if (setupComplete) updateSaveButtonState()
}
}
}
}
private fun getRadioFromChild(child: View): RadioButton? = when (child) {
is RadioButton -> child
is FrameLayout -> child.getChildAt(0) as? RadioButton
else -> null
}
private fun installCheckboxClickListeners(view: View) {
val cont = view.findViewById<LinearLayout>(R.id.CheckboxContainer) ?: return
for (i in 0 until cont.childCount) {
val cb = cont.getChildAt(i) as? CheckBox ?: continue
cb.setOnClickListener {
if (setupComplete) updateSaveButtonState()
}
}
}
private fun installEditTextWatcher(view: View, editTextId: Int) {
view.findViewById<EditText>(editTextId)?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
if (!setupComplete) return
updateSaveButtonState()
}
})
}
// ---- Save button state ----
private fun updateSaveButtonState() {
val allValid = sections
.filter { it.card.visibility == View.VISIBLE }
.all { section ->
val handler = section.handler ?: return@all true
try { handler.validate() } catch (_: Exception) { false }
}
btnSave.isEnabled = allValid
btnSave.alpha = if (allValid) 1f else 0.5f
}
// ---- Save flow ----
private fun handleSave(progressBar: ProgressBar) {
val visibleSections = sections.filter { it.card.visibility == View.VISIBLE }
for (section in visibleSections) {
val handler = section.handler ?: continue
if (!handler.validate()) {
showToast(LanguageManager.getText(languageID, "fill_all_fields"))
scrollToSection(section)
return
}
}
for (section in visibleSections) {
if (section.question is QuestionItem.ClientCoachCodeQuestion) {
saveClientCoachCodeFromUi(section.sectionView)
}
section.handler?.saveAnswer()
}
showLoading(progressBar, true)
CoroutineScope(Dispatchers.IO).launch {
val startTime = System.currentTimeMillis()
saveAnswersToDatabase(answers, questionnaireMeta.id)
GlobalValues.INTEGRATION_INDEX = points.sum()
val clientCode = answers["client_code"] as? String
if (clientCode != null) {
GlobalValues.LAST_CLIENT_CODE = clientCode
GlobalValues.LOADED_CLIENT_CODE = clientCode
}
val elapsed = System.currentTimeMillis() - startTime
if (elapsed < 1500L) delay(1500L - elapsed)
withContext(Dispatchers.Main) {
showLoading(progressBar, false)
endQuestionnaire()
}
}
}
private fun saveClientCoachCodeFromUi(view: View) {
val clientCode = view.findViewById<EditText>(R.id.client_code)?.text?.toString() ?: ""
val coachCode = view.findViewById<EditText>(R.id.coach_code)?.text?.toString() ?: ""
GlobalValues.LAST_CLIENT_CODE = clientCode
answers["client_code"] = clientCode
answers["coach_code"] = TokenStore.getUsername(context) ?: coachCode
}
private fun showLoading(progressBar: ProgressBar, show: Boolean) {
progressBar.visibility = if (show) View.VISIBLE else View.GONE
btnSave.isEnabled = !show
btnSave.alpha = if (show) 0.5f else 1f
}
private fun scrollToSection(section: Section) {
val scrollView = context.findViewById<androidx.core.widget.NestedScrollView>(R.id.scrollContainer)
scrollView?.post {
scrollView.smoothScrollTo(0, section.card.top)
}
}
}

View File

@ -5,19 +5,10 @@ import android.app.Activity
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import com.dano.test1.LanguageManager import com.dano.test1.util.LanguageManager
import com.dano.test1.MainActivity import com.dano.test1.MainActivity
import com.dano.test1.MyApp import com.dano.test1.MyApp
import com.dano.test1.data.* import com.dano.test1.data.*
import com.dano.test1.questionnaire.handlers.HandlerClientCoachCode
import com.dano.test1.questionnaire.handlers.HandlerClientNotSigned
import com.dano.test1.questionnaire.handlers.HandlerDateSpinner
import com.dano.test1.questionnaire.handlers.HandlerGlassScaleQuestion
import com.dano.test1.questionnaire.handlers.HandlerLastPage
import com.dano.test1.questionnaire.handlers.HandlerMultiCheckboxQuestion
import com.dano.test1.questionnaire.handlers.HandlerRadioQuestion
import com.dano.test1.questionnaire.handlers.HandlerStringSpinner
import com.dano.test1.questionnaire.handlers.HandlerValueSpinner
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonParser import com.google.gson.JsonParser
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -40,17 +31,6 @@ abstract class QuestionnaireBase<T> {
abstract fun startQuestionnaire() abstract fun startQuestionnaire()
abstract fun showCurrentQuestion() abstract fun showCurrentQuestion()
companion object {
val LANGUAGE_IDS = listOf(
"GERMAN", "ENGLISH", "FRENCH", "ROMANIAN", "ARABIC",
"POLISH", "TURKISH", "UKRAINIAN", "RUSSIAN", "SPANISH"
)
val LANGUAGE_LABELS = listOf(
"DE", "EN", "FR", "RO", "AR",
"PL", "TR", "UA", "RU", "ES"
)
}
fun attach(activity: Activity, language: String) { fun attach(activity: Activity, language: String) {
this.context = activity this.context = activity
this.languageID = language this.languageID = language
@ -196,7 +176,7 @@ abstract class QuestionnaireBase<T> {
suspend fun saveAnswersToDatabase(answers: Map<String, Any>, questionnaireId: String) { suspend fun saveAnswersToDatabase(answers: Map<String, Any>, questionnaireId: String) {
Log.d("AnswersMap", answers.toString()) Log.d("AnswersMap", answers.toString())
val db = MyApp.database val db = MyApp.Companion.database
val clientCode = answers["client_code"] as? String ?: return val clientCode = answers["client_code"] as? String ?: return

View File

@ -1,16 +1,8 @@
package com.dano.test1.questionnaire package com.dano.test1.questionnaire
import android.view.Gravity
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button import android.widget.Button
import android.widget.FrameLayout import com.dano.test1.util.LocalizationHelper
import android.widget.Spinner
import androidx.core.content.ContextCompat
import com.dano.test1.LocalizationHelper
import com.dano.test1.R import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
open class QuestionnaireGeneric(private val questionnaireFileName: String) : QuestionnaireBase<Unit>() { open class QuestionnaireGeneric(private val questionnaireFileName: String) : QuestionnaireBase<Unit>() {
@ -47,40 +39,6 @@ open class QuestionnaireGeneric(private val questionnaireFileName: String) : Que
} else { } else {
showEmptyScreen() showEmptyScreen()
} }
injectLanguageSpinner(layout)
}
}
private fun injectLanguageSpinner(layout: View) {
val container = layout as? FrameLayout ?: return
val spinner = Spinner(context)
val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, LANGUAGE_LABELS).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
spinner.adapter = adapter
spinner.background = ContextCompat.getDrawable(context, R.drawable.bg_field_filled)
spinner.setSelection(LANGUAGE_IDS.indexOf(languageID).coerceAtLeast(0))
val margin = ViewUtils.dp(context, 10)
container.addView(
spinner,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.TOP or Gravity.END
).apply { setMargins(0, margin, margin, 0) }
)
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, v: View?, position: Int, id: Long) {
val newLang = LANGUAGE_IDS[position]
if (newLang == languageID) return
languageID = newLang
spinner.post { showCurrentQuestion() }
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
} }
} }
} }

View File

@ -1,9 +0,0 @@
package com.dano.test1.questionnaire
object QuestionnaireProgressiveCallbacks {
@JvmStatic
fun maybeTrim(questionId: String?) {
// Intentional no-op: handlers call this during user interaction,
// but trimming is handled by each questionnaire variant itself.
}
}

View File

@ -1,46 +0,0 @@
package com.dano.test1.ui
import android.content.Context
/**
* Persists dev-only A/B overrides. Not a security boundary; values are for local QA.
* Use [effectiveVariant] when branching questionnaire or remote-config logic.
*/
object AbTestSettingsStore {
private const val PREF = "dev_ab_settings"
private const val KEY_OVERRIDE = "override_enabled"
private const val KEY_VARIANT = "variant"
const val VARIANT_NONE = "NONE"
const val VARIANT_A = "A"
const val VARIANT_B = "B"
fun isOverrideEnabled(context: Context): Boolean =
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).getBoolean(KEY_OVERRIDE, false)
fun setOverrideEnabled(context: Context, enabled: Boolean) {
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).edit()
.putBoolean(KEY_OVERRIDE, enabled)
.apply()
}
fun getVariant(context: Context): String =
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).getString(KEY_VARIANT, VARIANT_NONE)
?: VARIANT_NONE
fun setVariant(context: Context, variant: String) {
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).edit()
.putString(KEY_VARIANT, variant)
.apply()
}
/** Returns "A" or "B" when override is on and a branch is selected; otherwise null. */
fun effectiveVariant(context: Context): String? {
if (!isOverrideEnabled(context)) return null
return when (getVariant(context)) {
VARIANT_A -> "A"
VARIANT_B -> "B"
else -> null
}
}
}

View File

@ -4,22 +4,31 @@ import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.* import android.widget.Button
import com.dano.test1.data.ExcelExportService import android.widget.ProgressBar
import com.dano.test1.utils.ViewUtils import android.widget.TableLayout
import androidx.appcompat.app.AppCompatActivity import android.widget.TableRow
import com.dano.test1.LanguageManager import android.widget.TextView
import android.widget.Toast
import com.dano.test1.export.ExcelExportService
import com.dano.test1.export.HeaderOrderRepository
import com.dano.test1.util.LanguageManager
import com.dano.test1.MainActivity
import com.dano.test1.MyApp import com.dano.test1.MyApp
import com.dano.test1.R import com.dano.test1.R
import com.dano.test1.data.Client import com.dano.test1.data.Client
import com.dano.test1.data.HeaderOrderRepository
import com.dano.test1.data.Question import com.dano.test1.data.Question
import com.dano.test1.data.Questionnaire import com.dano.test1.data.Questionnaire
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import kotlin.math.roundToInt
class DatabaseButtonHandler( class DatabaseButtonHandler(
private val activity: AppCompatActivity, private val activity: MainActivity,
private val databaseButton: Button, private val databaseButton: Button,
private val onClose: () -> Unit, private val onClose: () -> Unit,
private val languageIDProvider: () -> String = { "GERMAN" } private val languageIDProvider: () -> String = { "GERMAN" }
@ -67,7 +76,7 @@ class DatabaseButtonHandler(
uiScope.launch { uiScope.launch {
val clients: List<Client> = withContext(Dispatchers.IO) { val clients: List<Client> = withContext(Dispatchers.IO) {
MyApp.database.clientDao().getAllClients() MyApp.Companion.database.clientDao().getAllClients()
} }
progress.visibility = View.GONE progress.visibility = View.GONE
@ -154,9 +163,11 @@ class DatabaseButtonHandler(
uiScope.launch { uiScope.launch {
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
val allQuestionnairesDb = MyApp.database.questionnaireDao().getAll() val allQuestionnairesDb = MyApp.Companion.database.questionnaireDao().getAll()
val completedForClient = MyApp.database.completedQuestionnaireDao().getAllForClient(clientCode) val completedForClient =
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode) MyApp.Companion.database.completedQuestionnaireDao().getAllForClient(clientCode)
val allAnswersForClient =
MyApp.Companion.database.answerDao().getAnswersForClient(clientCode)
Triple(allQuestionnairesDb, completedForClient, allAnswersForClient) Triple(allQuestionnairesDb, completedForClient, allAnswersForClient)
} }
@ -275,8 +286,9 @@ class DatabaseButtonHandler(
uiScope.launch { uiScope.launch {
val (questions, answersForClient) = withContext(Dispatchers.IO) { val (questions, answersForClient) = withContext(Dispatchers.IO) {
val qs = MyApp.database.questionDao().getQuestionsForQuestionnaire(questionnaireId) val qs = MyApp.Companion.database.questionDao()
val ans = MyApp.database.answerDao() .getQuestionsForQuestionnaire(questionnaireId)
val ans = MyApp.Companion.database.answerDao()
.getAnswersForClientAndQuestionnaire(clientCode, questionnaireId) .getAnswersForClientAndQuestionnaire(clientCode, questionnaireId)
qs to ans qs to ans
} }
@ -420,7 +432,10 @@ class DatabaseButtonHandler(
bgColor?.let { setBackgroundColor(it) } bgColor?.let { setBackgroundColor(it) }
} }
private fun dp(value: Int): Int = ViewUtils.dp(activity, value) private fun dp(value: Int): Int {
val density = activity.resources.displayMetrics.density
return (value * density).roundToInt()
}
private fun <T : View> requireView(id: Int, name: String): T { private fun <T : View> requireView(id: Int, name: String): T {
val v = activity.findViewById<T>(id) val v = activity.findViewById<T>(id)

View File

@ -1,92 +0,0 @@
package com.dano.test1.ui
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.dano.test1.R
/**
* Dev-only A/B settings. Entry: client code [_dev_settings_] on the opening screen Load action.
* Not a security boundary (secret string is in the APK).
*/
class DevSettingsActivity : AppCompatActivity() {
private lateinit var switchOverride: SwitchCompat
private lateinit var radioGroup: RadioGroup
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dev_settings)
findViewById<View>(android.R.id.content)?.let { content ->
ViewCompat.setOnApplyWindowInsetsListener(content) { v, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(bars.left, bars.top, bars.right, bars.bottom)
WindowInsetsCompat.CONSUMED
}
}
val toolbar = findViewById<Toolbar>(R.id.devSettingsToolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
switchOverride = findViewById(R.id.switchAbOverride)
radioGroup = findViewById(R.id.radioGroupVariant)
applyPrefsToUi()
switchOverride.setOnCheckedChangeListener { _, isChecked ->
AbTestSettingsStore.setOverrideEnabled(this, isChecked)
updateRadiosEnabled(isChecked)
}
radioGroup.setOnCheckedChangeListener { _, checkedId ->
val variant = when (checkedId) {
R.id.radioVariantA -> AbTestSettingsStore.VARIANT_A
R.id.radioVariantB -> AbTestSettingsStore.VARIANT_B
else -> AbTestSettingsStore.VARIANT_NONE
}
AbTestSettingsStore.setVariant(this, variant)
}
val databaseButton = findViewById<Button>(R.id.databaseButton)
DatabaseButtonHandler(
activity = this,
databaseButton = databaseButton,
onClose = { recreate() }
).setup()
}
private fun applyPrefsToUi() {
val overrideOn = AbTestSettingsStore.isOverrideEnabled(this)
switchOverride.isChecked = overrideOn
updateRadiosEnabled(overrideOn)
when (AbTestSettingsStore.getVariant(this)) {
AbTestSettingsStore.VARIANT_A -> radioGroup.check(R.id.radioVariantA)
AbTestSettingsStore.VARIANT_B -> radioGroup.check(R.id.radioVariantB)
else -> radioGroup.check(R.id.radioVariantDefault)
}
}
private fun updateRadiosEnabled(enabled: Boolean) {
for (i in 0 until radioGroup.childCount) {
(radioGroup.getChildAt(i) as? RadioButton)?.isEnabled = enabled
}
radioGroup.alpha = if (enabled) 1f else 0.5f
}
override fun onSupportNavigateUp(): Boolean {
finish()
return true
}
}

View File

@ -0,0 +1,97 @@
package com.dano.test1.ui
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import com.dano.test1.util.LanguageManager
import com.dano.test1.MainActivity
import com.dano.test1.MyApp
import kotlinx.coroutines.*
import com.dano.test1.data.CompletedQuestionnaire
import com.dano.test1.questionnaire.GlobalValues
class EditButtonHandler(
private val activity: MainActivity,
private val editButton: Button,
private val editText: EditText,
private val languageIDProvider: () -> String,
private val questionnaireFiles: Map<Button, String>,
private val buttonPoints: MutableMap<String, Int>,
private val updateButtonTexts: () -> Unit,
private val setButtonsEnabled: (List<Button>, Boolean) -> Unit,
private val setUiFreeze: (Boolean) -> Unit,
private val triggerLoad: () -> Unit
) {
fun setup() {
editButton.text = LanguageManager.getText(languageIDProvider(), "edit")
editButton.setOnClickListener { handleEditButtonClick() }
}
private fun handleEditButtonClick() {
val typed = editText.text.toString().trim()
val desiredCode = when {
typed.isNotBlank() -> typed
!GlobalValues.LOADED_CLIENT_CODE.isNullOrBlank() -> GlobalValues.LOADED_CLIENT_CODE!!
else -> ""
}
if (desiredCode.isBlank()) {
val message = LanguageManager.getText(languageIDProvider(), "please_client_code")
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
return
}
GlobalValues.LAST_CLIENT_CODE = desiredCode
val needLoad = GlobalValues.LOADED_CLIENT_CODE?.equals(desiredCode) != true
if (needLoad) {
setUiFreeze(true) // Zwischenzustände unterdrücken
triggerLoad()
}
CoroutineScope(Dispatchers.IO).launch {
val loadedOk = waitUntilClientLoaded(desiredCode, timeoutMs = 2500, stepMs = 50)
if (!loadedOk) {
withContext(Dispatchers.Main) {
val msg = LanguageManager.getText(languageIDProvider(), "open_client_via_load")
Toast.makeText(activity, msg, Toast.LENGTH_LONG).show()
setUiFreeze(false)
}
return@launch
}
val completedEntries: List<CompletedQuestionnaire> =
MyApp.Companion.database.completedQuestionnaireDao().getAllForClient(desiredCode)
val completedFiles = completedEntries.filter { it.isDone }.map { it.questionnaireId.lowercase() }
buttonPoints.clear()
for (entry in completedEntries) {
if (entry.isDone) {
buttonPoints[entry.questionnaireId] = entry.sumPoints ?: 0
}
}
withContext(Dispatchers.Main) {
updateButtonTexts()
val enabledButtons = questionnaireFiles.filter { (_, fileName) ->
completedFiles.any { completedId -> fileName.lowercase().contains(completedId) }
}.keys.toList()
setButtonsEnabled(enabledButtons, true)
setUiFreeze(false)
}
}
}
private suspend fun waitUntilClientLoaded(expectedCode: String, timeoutMs: Long, stepMs: Long): Boolean {
if (GlobalValues.LOADED_CLIENT_CODE?.equals(expectedCode) == true) return true
var waited = 0L
while (waited < timeoutMs) {
delay(stepMs)
waited += stepMs
if (GlobalValues.LOADED_CLIENT_CODE?.equals(expectedCode) == true) return true
}
return GlobalValues.LOADED_CLIENT_CODE?.equals(expectedCode) == true
}
}

View File

@ -10,17 +10,16 @@ import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import com.dano.test1.LanguageManager import com.dano.test1.util.LanguageManager
import com.dano.test1.MainActivity import com.dano.test1.MainActivity
import com.dano.test1.R import com.dano.test1.R
import com.dano.test1.auth.LoginManager
import com.dano.test1.auth.TokenStore
import com.dano.test1.network.DatabaseUploader import com.dano.test1.network.DatabaseUploader
import com.dano.test1.network.LoginManager
import com.dano.test1.network.NetworkUtils import com.dano.test1.network.NetworkUtils
import com.dano.test1.network.TokenStore
import com.dano.test1.questionnaire.GlobalValues import com.dano.test1.questionnaire.GlobalValues
import com.dano.test1.questionnaire.QuestionItem import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.questionnaire.QuestionnaireBase import com.dano.test1.questionnaire.QuestionnaireBase
import com.dano.test1.questionnaire.QuestionnaireAllInOne
import com.dano.test1.questionnaire.QuestionnaireGeneric import com.dano.test1.questionnaire.QuestionnaireGeneric
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import org.json.JSONArray import org.json.JSONArray
@ -28,7 +27,6 @@ import org.json.JSONObject
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.dano.test1.utils.ViewUtils
var RHS_POINTS: Int? = null var RHS_POINTS: Int? = null
@ -43,8 +41,10 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
private lateinit var buttonContainer: LinearLayout private lateinit var buttonContainer: LinearLayout
private lateinit var buttonLoad: Button private lateinit var buttonLoad: Button
private lateinit var saveButton: Button private lateinit var saveButton: Button
private lateinit var editButton: Button
private lateinit var uploadButton: Button private lateinit var uploadButton: Button
private lateinit var downloadButton: Button private lateinit var downloadButton: Button
private lateinit var databaseButton: Button
private lateinit var statusSession: TextView private lateinit var statusSession: TextView
private lateinit var statusOnline: TextView private lateinit var statusOnline: TextView
private val SESSION_WARN_AFTER_MS = 12 * 60 * 60 * 1000L // 12h private val SESSION_WARN_AFTER_MS = 12 * 60 * 60 * 1000L // 12h
@ -93,8 +93,10 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
setupLanguageSpinner() setupLanguageSpinner()
setupLoadButton() setupLoadButton()
setupSaveButton() setupSaveButton()
setupEditButtonHandler()
setupUploadButton() setupUploadButton()
setupDownloadButton() setupDownloadButton()
setupDatabaseButtonHandler()
uiHandler.removeCallbacks(statusTicker) uiHandler.removeCallbacks(statusTicker)
updateStatusStrip() updateStatusStrip()
@ -120,11 +122,13 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
buttonContainer = activity.findViewById(R.id.buttonContainer) buttonContainer = activity.findViewById(R.id.buttonContainer)
buttonLoad = activity.findViewById(R.id.loadButton) buttonLoad = activity.findViewById(R.id.loadButton)
saveButton = activity.findViewById(R.id.saveButton) saveButton = activity.findViewById(R.id.saveButton)
editButton = activity.findViewById(R.id.editButton)
uploadButton = activity.findViewById(R.id.uploadButton) uploadButton = activity.findViewById(R.id.uploadButton)
downloadButton = activity.findViewById(R.id.downloadButton) downloadButton = activity.findViewById(R.id.downloadButton)
downloadButton.visibility = View.GONE downloadButton.visibility = View.GONE
databaseButton = activity.findViewById(R.id.databaseButton)
statusSession = activity.findViewById(R.id.statusSession) statusSession = activity.findViewById(R.id.statusSession)
statusOnline = activity.findViewById(R.id.statusOnline) statusOnline = activity.findViewById(R.id.statusOnline)
val tag = editText.tag as? String ?: "" val tag = editText.tag as? String ?: ""
@ -226,12 +230,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
setOnClickListener { setOnClickListener {
GlobalValues.LAST_CLIENT_CODE = GlobalValues.LOADED_CLIENT_CODE GlobalValues.LAST_CLIENT_CODE = GlobalValues.LOADED_CLIENT_CODE
val fileName = questionnaireFiles[this] ?: return@setOnClickListener val fileName = questionnaireFiles[this] ?: return@setOnClickListener
val variant = AbTestSettingsStore.effectiveVariant(activity) val questionnaire = QuestionnaireGeneric(fileName)
val questionnaire: QuestionnaireBase<*> = if (variant == "B") {
QuestionnaireAllInOne(fileName)
} else {
QuestionnaireGeneric(fileName)
}
startQuestionnaire(questionnaire) startQuestionnaire(questionnaire)
applySetButtonsEnabled(dynamicButtons.filter { it == this }, allowCompleted = false, force = false) applySetButtonsEnabled(dynamicButtons.filter { it == this }, allowCompleted = false, force = false)
} }
@ -331,7 +330,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
dynamicButtonsProvider = { dynamicButtons }, dynamicButtonsProvider = { dynamicButtons },
buttonPoints = buttonPoints, buttonPoints = buttonPoints,
updateButtonTexts = { applyUpdateButtonTexts(force = false) }, updateButtonTexts = { applyUpdateButtonTexts(force = false) },
setButtonsEnabled = { list -> applySetButtonsEnabled(list, allowCompleted = true, force = false) }, setButtonsEnabled = { list -> applySetButtonsEnabled(list, allowCompleted = false, force = false) },
updateMainButtonsState = { updateMainButtonsState(it) }, updateMainButtonsState = { updateMainButtonsState(it) },
).setup() ).setup()
} }
@ -387,8 +386,10 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
} }
buttonLoad.text = t("load") buttonLoad.text = t("load")
saveButton.text = t("save") saveButton.text = t("save")
editButton.text = t("edit")
uploadButton.text = t("upload") uploadButton.text = t("upload")
downloadButton.text = t("download") downloadButton.text = t("download")
databaseButton.text = t("database")
val hintTag = editText.tag as? String ?: "" val hintTag = editText.tag as? String ?: ""
editText.hint = t(hintTag) editText.hint = t(hintTag)
val coachTag = coachEditText.tag as? String ?: "" val coachTag = coachEditText.tag as? String ?: ""
@ -453,6 +454,23 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
).setup() ).setup()
} }
private fun setupEditButtonHandler() {
EditButtonHandler(
activity = activity,
editButton = editButton,
editText = editText,
languageIDProvider = { languageID },
questionnaireFiles = questionnaireFiles,
buttonPoints = buttonPoints,
updateButtonTexts = { applyUpdateButtonTexts(force = true) },
setButtonsEnabled = { list, allowCompleted ->
applySetButtonsEnabled(list, allowCompleted, force = true)
},
setUiFreeze = { freeze -> uiFreeze = freeze },
triggerLoad = { buttonLoad.performClick() }
).setup()
}
private fun setupUploadButton() { private fun setupUploadButton() {
uploadButton.text = t("upload") uploadButton.text = t("upload")
uploadButton.setOnClickListener { uploadButton.setOnClickListener {
@ -514,8 +532,17 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
} }
} }
private fun setupDatabaseButtonHandler() {
DatabaseButtonHandler(
activity = activity,
databaseButton = databaseButton,
onClose = { init() },
languageIDProvider = { languageID }
).setup()
}
private fun updateMainButtonsState(isDatabaseAvailable: Boolean) { private fun updateMainButtonsState(isDatabaseAvailable: Boolean) {
listOf(buttonLoad, saveButton).forEach { b -> listOf(buttonLoad, saveButton, editButton, databaseButton).forEach { b ->
b.isEnabled = isDatabaseAvailable b.isEnabled = isDatabaseAvailable
b.alpha = if (isDatabaseAvailable) 1.0f else 0.5f b.alpha = if (isDatabaseAvailable) 1.0f else 0.5f
} }
@ -545,7 +572,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
} }
} }
private fun dp(v: Int): Int = ViewUtils.dp(activity, v) private fun dp(v: Int): Int = (v * activity.resources.displayMetrics.density).toInt()
private fun isCompleted(button: Button): Boolean { private fun isCompleted(button: Button): Boolean {
val fileName = questionnaireFiles[button] ?: return false val fileName = questionnaireFiles[button] ?: return false
@ -637,7 +664,16 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
} }
private fun lockCoachCodeField() { private fun lockCoachCodeField() {
ViewUtils.lockEditField(coachEditText) coachEditText.isFocusable = false
coachEditText.isFocusableInTouchMode = false
coachEditText.isCursorVisible = false
coachEditText.keyListener = null
coachEditText.isLongClickable = false
coachEditText.isClickable = false
coachEditText.setBackgroundResource(R.drawable.bg_field_locked)
coachEditText.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_lock_24, 0)
coachEditText.compoundDrawablePadding = dp(8)
coachEditText.alpha = 0.95f
} }
private fun applySessionAgeHighlight(ageMs: Long) { private fun applySessionAgeHighlight(ageMs: Long) {

View File

@ -1,10 +1,9 @@
package com.dano.test1.ui package com.dano.test1.ui
import android.content.Intent
import android.widget.Button import android.widget.Button
import android.widget.EditText import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import com.dano.test1.LanguageManager import com.dano.test1.util.LanguageManager
import com.dano.test1.MainActivity import com.dano.test1.MainActivity
import com.dano.test1.MyApp import com.dano.test1.MyApp
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -25,11 +24,6 @@ class LoadButtonHandler(
private val updateMainButtonsState: (Boolean) -> Unit, private val updateMainButtonsState: (Boolean) -> Unit,
) { ) {
companion object {
/** Opening-screen client code that opens dev A/B settings (not a security boundary). */
private const val DEV_SETTINGS_SECRET = "_dev_settings_"
}
fun setup() { fun setup() {
loadButton.text = LanguageManager.getText(languageIDProvider(), "load") loadButton.text = LanguageManager.getText(languageIDProvider(), "load")
loadButton.setOnClickListener { handleLoadButton() } loadButton.setOnClickListener { handleLoadButton() }
@ -43,12 +37,6 @@ class LoadButtonHandler(
return return
} }
if (inputText == DEV_SETTINGS_SECRET) {
editText.text.clear()
activity.startActivity(Intent(activity, DevSettingsActivity::class.java))
return
}
buttonPoints.clear() buttonPoints.clear()
setButtonsEnabled(emptyList()) // temporär sperren setButtonsEnabled(emptyList()) // temporär sperren
updateButtonTexts() // Chips zeigen vorläufig „Gesperrt“ updateButtonTexts() // Chips zeigen vorläufig „Gesperrt“
@ -57,7 +45,7 @@ class LoadButtonHandler(
GlobalValues.LAST_CLIENT_CODE = clientCode GlobalValues.LAST_CLIENT_CODE = clientCode
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val client = MyApp.database.clientDao().getClientByCode(clientCode) val client = MyApp.Companion.database.clientDao().getClientByCode(clientCode)
if (client == null) { if (client == null) {
GlobalValues.LOADED_CLIENT_CODE = null GlobalValues.LOADED_CLIENT_CODE = null
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -97,7 +85,7 @@ class LoadButtonHandler(
} }
} }
is QuestionItem.Condition.QuestionCondition -> { is QuestionItem.Condition.QuestionCondition -> {
val answers = MyApp.database.answerDao().getAnswersForClientAndQuestionnaire(clientCode, condition.questionnaire) val answers = MyApp.Companion.database.answerDao().getAnswersForClientAndQuestionnaire(clientCode, condition.questionnaire)
val relevant = answers.find { it.questionId.endsWith(condition.questionId, ignoreCase = true) } val relevant = answers.find { it.questionId.endsWith(condition.questionId, ignoreCase = true) }
val answerValue = relevant?.answerValue ?: "" val answerValue = relevant?.answerValue ?: ""
when (condition.operator) { when (condition.operator) {
@ -114,7 +102,7 @@ class LoadButtonHandler(
} }
if (!reqOk) return false if (!reqOk) return false
val q = condition.questionCheck ?: return true val q = condition.questionCheck ?: return true
val answers = MyApp.database.answerDao().getAnswersForClientAndQuestionnaire(clientCode, q.questionnaire) val answers = MyApp.Companion.database.answerDao().getAnswersForClientAndQuestionnaire(clientCode, q.questionnaire)
val relevant = answers.find { it.questionId.endsWith(q.questionId, ignoreCase = true) } val relevant = answers.find { it.questionId.endsWith(q.questionId, ignoreCase = true) }
val answerValue = relevant?.answerValue ?: "" val answerValue = relevant?.answerValue ?: ""
when (q.operator) { when (q.operator) {
@ -134,7 +122,7 @@ class LoadButtonHandler(
private suspend fun handleNormalLoad(clientCode: String) { private suspend fun handleNormalLoad(clientCode: String) {
val completedEntries = withContext(Dispatchers.IO) { val completedEntries = withContext(Dispatchers.IO) {
MyApp.database.completedQuestionnaireDao().getAllForClient(clientCode) MyApp.Companion.database.completedQuestionnaireDao().getAllForClient(clientCode)
} }
buttonPoints.clear() buttonPoints.clear()
@ -160,9 +148,10 @@ class LoadButtonHandler(
(completedNorm.contains(targetNorm) || targetNorm.contains(completedNorm)) && completed.isDone (completedNorm.contains(targetNorm) || targetNorm.contains(completedNorm)) && completed.isDone
} }
} }
if (isCompleted) continue
val condMet = evaluateCondition(entry.condition, clientCode, completedEntries) val condMet = evaluateCondition(entry.condition, clientCode, completedEntries)
if (condMet || isCompleted) enabledButtons.add(button) if (condMet) enabledButtons.add(button)
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View File

@ -12,7 +12,7 @@ import android.util.Log
import android.widget.Button import android.widget.Button
import android.widget.EditText import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import com.dano.test1.LanguageManager import com.dano.test1.util.LanguageManager
import com.dano.test1.MainActivity import com.dano.test1.MainActivity
import com.dano.test1.MyApp import com.dano.test1.MyApp
import com.dano.test1.questionnaire.GlobalValues import com.dano.test1.questionnaire.GlobalValues
@ -44,7 +44,7 @@ class SaveButtonHandler(
private fun showCompletedQuestionnaires(clientCode: String) { private fun showCompletedQuestionnaires(clientCode: String) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val actualClientCode = clientCode.removeSuffix("_database") val actualClientCode = clientCode.removeSuffix("_database")
val completedEntries = MyApp.database.completedQuestionnaireDao().getAllForClient(actualClientCode) val completedEntries = MyApp.Companion.database.completedQuestionnaireDao().getAllForClient(actualClientCode)
Log.d("PDF_DEBUG", "Completed entries for client $actualClientCode:") Log.d("PDF_DEBUG", "Completed entries for client $actualClientCode:")
for (entry in completedEntries) { for (entry in completedEntries) {
@ -74,7 +74,7 @@ class SaveButtonHandler(
canvas.drawText("Points: ${entry.sumPoints ?: "N/A"}", 20f, yPosition, paint) canvas.drawText("Points: ${entry.sumPoints ?: "N/A"}", 20f, yPosition, paint)
yPosition += 30f yPosition += 30f
val answers = MyApp.database.answerDao().getAnswersForClientAndQuestionnaire(actualClientCode, entry.questionnaireId) val answers = MyApp.Companion.database.answerDao().getAnswersForClientAndQuestionnaire(actualClientCode, entry.questionnaireId)
for (answer in answers) { for (answer in answers) {
val questionKey = answer.questionId.substringAfter("-") val questionKey = answer.questionId.substringAfter("-")

View File

@ -1,6 +1,6 @@
package com.dano.test1.ui package com.dano.test1.util
import com.dano.test1.LanguageManager import com.dano.test1.util.LanguageManager
object Countries { object Countries {
fun getAllCountries(languageID: String): List<String> { fun getAllCountries(languageID: String): List<String> {

View File

@ -1,4 +1,4 @@
package com.dano.test1 package com.dano.test1.util
import com.dano.test1.questionnaire.MAX_VALUE_AGE import com.dano.test1.questionnaire.MAX_VALUE_AGE
import com.dano.test1.questionnaire.MAX_VALUE_YEAR import com.dano.test1.questionnaire.MAX_VALUE_YEAR

View File

@ -1,4 +1,4 @@
package com.dano.test1 package com.dano.test1.util
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup

View File

@ -1,13 +1,11 @@
package com.dano.test1.ui package com.dano.test1.util
import com.dano.test1.LanguageManager
data class Month(val name: String) { data class Month(val name: String) {
override fun toString(): String = name override fun toString(): String = name
} }
object Months { object Months {
fun getAllMonths(languageID: String): List<Any> { fun getAllMonths(languageID: String): List<Month> {
return listOf( return listOf(
Month(LanguageManager.getText(languageID, "january")), Month(LanguageManager.getText(languageID, "january")),
Month(LanguageManager.getText(languageID, "february")), Month(LanguageManager.getText(languageID, "february")),

View File

@ -0,0 +1,73 @@
package com.dano.test1.util
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.AbsListView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.TextView
import androidx.core.widget.TextViewCompat
fun TextView.setTextSizePercentOfScreenHeight(percentOfHeight: Float) {
val dm = context.resources.displayMetrics
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
TextViewCompat.setAutoSizeTextTypeWithDefaults(this, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
}
fun <T> Spinner.setupSpinner(items: List<T>, selectedItem: T?) {
val dm = context.resources.displayMetrics
fun spFromScreenHeight(percent: Float): Float = (dm.heightPixels * percent) / dm.scaledDensity
fun pxFromSp(sp: Float): Int = (sp * dm.scaledDensity).toInt()
val textSp = spFromScreenHeight(0.0275f)
val textPx = pxFromSp(textSp)
val vPadPx = (textPx * 0.50f).toInt()
val rowHeight = (textPx * 2.20f + 2 * vPadPx).toInt()
val adapter = object : ArrayAdapter<T>(context, android.R.layout.simple_spinner_item, items) {
private fun styleRow(tv: TextView, forceHeight: Boolean) {
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSp)
tv.includeFontPadding = true
tv.setLineSpacing(0f, 1.2f)
tv.gravity = (tv.gravity and Gravity.HORIZONTAL_GRAVITY_MASK) or Gravity.CENTER_VERTICAL
tv.setPadding(tv.paddingLeft, vPadPx, tv.paddingRight, vPadPx)
tv.minHeight = rowHeight
tv.isSingleLine = true
if (forceHeight) {
val lp = tv.layoutParams
if (lp == null || lp.height <= 0) {
tv.layoutParams = AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, rowHeight)
} else {
lp.height = rowHeight
}
}
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val v = super.getView(position, convertView, parent) as TextView
styleRow(v, forceHeight = false)
return v
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val v = super.getDropDownView(position, convertView, parent) as TextView
styleRow(v, forceHeight = true)
return v
}
}
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
this.adapter = adapter
setPadding(paddingLeft, vPadPx, paddingRight, vPadPx)
minimumHeight = rowHeight
requestLayout()
selectedItem?.let {
val index = items.indexOf(it)
if (index >= 0) setSelection(index)
}
}

View File

@ -1,112 +0,0 @@
package com.dano.test1.utils
import android.content.Context
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.AbsListView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import androidx.core.widget.TextViewCompat
import com.dano.test1.R
import kotlin.math.roundToInt
object ViewUtils {
/**
* Sets the text size of a TextView to a percentage of the screen height (in sp).
* Disables auto-sizing to prevent conflicts.
*/
fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
val dm = view.context.resources.displayMetrics
val shortSide = minOf(dm.heightPixels, dm.widthPixels)
val sp = (shortSide * percentOfHeight) / dm.scaledDensity
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
}
/**
* Sets up a Spinner with a responsive, styled adapter.
* Font size and row height are derived from screen height to prevent clipping.
*/
fun <T> setupResponsiveSpinner(context: Context, spinner: Spinner, items: List<T>, selectedItem: T?) {
val dm = context.resources.displayMetrics
val shortSide = minOf(dm.heightPixels, dm.widthPixels)
fun spFromScreenHeight(percent: Float): Float = (shortSide * percent) / dm.scaledDensity
fun pxFromSp(sp: Float): Int = (sp * dm.scaledDensity).toInt()
val textSp = spFromScreenHeight(0.0275f)
val textPx = pxFromSp(textSp)
val vPadPx = (textPx * 0.50f).toInt()
val rowHeight = (textPx * 2.20f + 2 * vPadPx).toInt()
val adapter = object : ArrayAdapter<T>(context, android.R.layout.simple_spinner_item, items) {
private fun styleRow(tv: TextView, forceHeight: Boolean) {
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSp)
tv.includeFontPadding = true
tv.setLineSpacing(0f, 1.2f)
tv.gravity = (tv.gravity and Gravity.HORIZONTAL_GRAVITY_MASK) or Gravity.CENTER_VERTICAL
tv.setPadding(tv.paddingLeft, vPadPx, tv.paddingRight, vPadPx)
tv.minHeight = rowHeight
tv.isSingleLine = true
if (forceHeight) {
val lp = tv.layoutParams
if (lp == null || lp.height <= 0) {
tv.layoutParams = AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, rowHeight)
} else {
lp.height = rowHeight
}
}
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val v = super.getView(position, convertView, parent) as TextView
styleRow(v, forceHeight = false)
return v
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val v = super.getDropDownView(position, convertView, parent) as TextView
styleRow(v, forceHeight = true)
return v
}
}
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
spinner.setPadding(spinner.paddingLeft, vPadPx, spinner.paddingRight, vPadPx)
spinner.minimumHeight = rowHeight
spinner.requestLayout()
selectedItem?.let {
val index = items.indexOf(it)
if (index >= 0) spinner.setSelection(index)
}
}
/**
* Locks an EditText field visually and functionally (e.g. for coach code fields).
*/
fun lockEditField(field: EditText, dpPadding: Int = 8) {
field.isFocusable = false
field.isFocusableInTouchMode = false
field.isCursorVisible = false
field.keyListener = null
field.isLongClickable = false
field.isClickable = false
field.setBackgroundResource(R.drawable.bg_field_locked)
field.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_lock_24, 0)
field.compoundDrawablePadding = dp(field.context, dpPadding)
field.alpha = 0.95f
}
/**
* Converts dp to pixels using the given context.
*/
fun dp(context: Context, value: Int): Int =
(value * context.resources.displayMetrics.density).roundToInt()
}

View File

@ -1,96 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/devSettingsToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:title="A/B testing (dev)"
app:titleTextColor="@android:color/white" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:text="Manual A/B override for local testing. Not a security boundary."
android:textSize="14sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchAbOverride"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:text="Manual A/B override" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="8dp"
android:text="Variant"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/radioGroupVariant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/radioVariantDefault"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Default (no override)" />
<RadioButton
android:id="@+id/radioVariantA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Variant A" />
<RadioButton
android:id="@+id/radioVariantB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Variant B" />
</RadioGroup>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
android:background="#DDDDDD" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:text="Database"
android:textStyle="bold" />
<Button
android:id="@+id/databaseButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Datenbank" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -195,6 +195,21 @@
app:cornerRadius="@dimen/pill_radius" app:cornerRadius="@dimen/pill_radius"
app:backgroundTint="@color/brand_purple"/> app:backgroundTint="@color/brand_purple"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/editButton"
android:layout_width="0dp"
android:layout_height="@dimen/pill_height"
android:layout_weight="1"
android:layout_marginEnd="12dp"
android:textAllCaps="false"
android:textColor="@android:color/white"
app:icon="@drawable/ic_dot_16"
app:iconTint="@android:color/white"
app:iconPadding="8dp"
app:iconGravity="textStart"
app:cornerRadius="@dimen/pill_radius"
app:backgroundTint="@color/brand_purple"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/saveButton" android:id="@+id/saveButton"
android:layout_width="0dp" android:layout_width="0dp"
@ -238,6 +253,19 @@
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/pill_height" android:layout_height="@dimen/pill_height"
android:layout_marginBottom="12dp"
android:textAllCaps="false"
android:textColor="@color/brand_purple"
app:cornerRadius="@dimen/pill_radius"
app:strokeColor="@color/brand_purple"
app:strokeWidth="@dimen/pill_stroke"
app:backgroundTint="@android:color/transparent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/databaseButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="@dimen/pill_height"
android:textAllCaps="false" android:textAllCaps="false"
android:textColor="@color/brand_purple" android:textColor="@color/brand_purple"
app:cornerRadius="@dimen/pill_radius" app:cornerRadius="@dimen/pill_radius"

View File

@ -1,95 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="12dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnBack"
android:layout_width="@dimen/nav_btn_size"
android:layout_height="@dimen/nav_btn_size"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_left"
app:iconTint="@color/btn_nav_left_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_left_tint"
app:rippleColor="@color/btn_nav_left_ripple" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<Spinner
android:id="@+id/langSpinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_field_filled"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingTop="6dp"
android:paddingBottom="6dp" />
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true"
android:clipToPadding="false"
android:paddingBottom="16dp">
<LinearLayout
android:id="@+id/questionContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp" />
</androidx.core.widget.NestedScrollView>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="@android:color/white"
android:elevation="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSave"
android:layout_width="match_parent"
android:layout_height="@dimen/nav_btn_size"
android:textAllCaps="false"
android:minWidth="0dp"
android:insetLeft="0dp"
android:insetRight="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_right_tint"
app:rippleColor="@color/btn_nav_right_ripple" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>