finished opening screen

This commit is contained in:
oxidiert
2025-09-21 19:49:49 +02:00
parent 1ffd09049e
commit 894823f42a
11 changed files with 420 additions and 175 deletions

View File

@ -4,7 +4,7 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-09-19T19:11:24.523816400Z"> <DropdownSelection timestamp="2025-09-21T10:53:27.572746300Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\danie\.android\avd\Medium_Phone.avd" /> <DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\danie\.android\avd\Medium_Phone.avd" />

View File

@ -13,7 +13,7 @@ android {
minSdk = 29 minSdk = 29
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "1.1" versionName = "1.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@ -48,7 +48,7 @@
}, },
{ {
"file": "questionnaire_6_follow_up_survey.json", "file": "questionnaire_6_follow_up_survey.json",
"showPoints": false, "showPoints": true,
"condition": { "condition": {
"anyOf": [ "anyOf": [
{ {

View File

@ -14,8 +14,8 @@ class EditButtonHandler(
private val questionnaireFiles: Map<Button, String>, private val questionnaireFiles: Map<Button, String>,
private val buttonPoints: MutableMap<String, Int>, private val buttonPoints: MutableMap<String, Int>,
private val updateButtonTexts: () -> Unit, private val updateButtonTexts: () -> Unit,
private val setButtonsEnabled: (List<Button>) -> Unit, private val setButtonsEnabled: (List<Button>, Boolean) -> Unit,
// vor "Bearbeiten" ggf. Laden anstoßen private val setUiFreeze: (Boolean) -> Unit,
private val triggerLoad: () -> Unit private val triggerLoad: () -> Unit
) { ) {
@ -38,29 +38,29 @@ class EditButtonHandler(
return return
} }
// Nutzerwunsch merken (info)
GlobalValues.LAST_CLIENT_CODE = desiredCode GlobalValues.LAST_CLIENT_CODE = desiredCode
// Nur laden, wenn noch nicht/anders geladen
val needLoad = GlobalValues.LOADED_CLIENT_CODE?.equals(desiredCode) != true val needLoad = GlobalValues.LOADED_CLIENT_CODE?.equals(desiredCode) != true
if (needLoad) triggerLoad() if (needLoad) {
// Zwischenzustände aus dem Load-Handler unterdrücken
setUiFreeze(true)
triggerLoad()
}
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val loadedOk = waitUntilClientLoaded(desiredCode, timeoutMs = 2500, stepMs = 50) val loadedOk = waitUntilClientLoaded(desiredCode, timeoutMs = 2500, stepMs = 50)
if (!loadedOk) { if (!loadedOk) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(activity, "Bitte den Klienten über \"Laden\" öffnen.", Toast.LENGTH_LONG).show() Toast.makeText(activity, "Bitte den Klienten über \"Laden\" öffnen.", Toast.LENGTH_LONG).show()
setUiFreeze(false)
} }
return@launch return@launch
} }
// Ab hier: geladen → Bearbeiten-Logik
val completedEntries: List<CompletedQuestionnaire> = val completedEntries: List<CompletedQuestionnaire> =
MyApp.database.completedQuestionnaireDao().getAllForClient(desiredCode) MyApp.database.completedQuestionnaireDao().getAllForClient(desiredCode)
val completedFiles = completedEntries val completedFiles = completedEntries.filter { it.isDone }.map { it.questionnaireId.lowercase() }
.filter { it.isDone }
.map { it.questionnaireId.lowercase() }
buttonPoints.clear() buttonPoints.clear()
for (entry in completedEntries) { for (entry in completedEntries) {
@ -70,21 +70,19 @@ class EditButtonHandler(
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
// nur den finalen Zustand anzeigen
updateButtonTexts() updateButtonTexts()
val enabledButtons = questionnaireFiles.filter { (_, fileName) -> val enabledButtons = questionnaireFiles.filter { (_, fileName) ->
completedFiles.any { completedId -> fileName.lowercase().contains(completedId) } completedFiles.any { completedId -> fileName.lowercase().contains(completedId) }
}.keys.toList() }.keys.toList()
setButtonsEnabled(enabledButtons, true)
setButtonsEnabled(enabledButtons) setUiFreeze(false)
} }
} }
} }
private suspend fun waitUntilClientLoaded(expectedCode: String, timeoutMs: Long, stepMs: Long): Boolean { private suspend fun waitUntilClientLoaded(expectedCode: String, timeoutMs: Long, stepMs: Long): Boolean {
// sofort ok, wenn bereits korrekt geladen
if (GlobalValues.LOADED_CLIENT_CODE?.equals(expectedCode) == true) return true if (GlobalValues.LOADED_CLIENT_CODE?.equals(expectedCode) == true) return true
var waited = 0L var waited = 0L
while (waited < timeoutMs) { while (waited < timeoutMs) {
delay(stepMs) delay(stepMs)

View File

@ -1,9 +1,12 @@
package com.dano.test1 package com.dano.test1
import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
import android.util.TypedValue
import android.view.Gravity
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import kotlinx.coroutines.* import com.google.android.material.button.MaterialButton
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
@ -22,37 +25,46 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
private lateinit var editButton: 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 // <-- NEU private lateinit var databaseButton: Button
private val dynamicButtons = mutableListOf<Button>() private val dynamicButtons = mutableListOf<Button>()
private val questionnaireFiles = mutableMapOf<Button, String>() private val questionnaireFiles = mutableMapOf<Button, String>()
private data class CardParts(
val title: TextView,
val subtitle: TextView,
val chip: TextView
)
private val cardParts = mutableMapOf<Button, CardParts>()
private val buttonPoints: MutableMap<String, Int> = mutableMapOf() private val buttonPoints: MutableMap<String, Int> = mutableMapOf()
private var questionnaireEntries: List<QuestionItem.QuestionnaireEntry> = emptyList() private var questionnaireEntries: List<QuestionItem.QuestionnaireEntry> = emptyList()
private var uiFreeze: Boolean = false
// Feste Standard-Randfarben
private val STROKE_ENABLED = Color.parseColor("#8C79F2") // wenn anklickbar
private val STROKE_DISABLED = Color.parseColor("#D8D3F5") // wenn nicht anklickbar
private fun t(id: String) = LanguageManager.getText(languageID, id)
fun init() { fun init() {
activity.setContentView(R.layout.opening_screen) activity.setContentView(R.layout.opening_screen)
bindViews() bindViews()
loadQuestionnaireOrder() loadQuestionnaireOrder()
createQuestionnaireButtons() createQuestionnaireButtons()
restorePreviousClientCode() restorePreviousClientCode()
setupLanguageSpinner() setupLanguageSpinner()
setupLoadButton() setupLoadButton()
setupSaveButton() setupSaveButton()
setupEditButtonHandler() setupEditButtonHandler()
setupUploadButton() setupUploadButton()
setupDownloadButton() setupDownloadButton()
setupDatabaseButtonHandler() // <-- NEU setupDatabaseButtonHandler()
val dbPath = "/data/data/com.dano.test1/databases/questionnaire_database" val pathExists = File("/data/data/com.dano.test1/databases/questionnaire_database").exists()
val pathExists = File(dbPath).exists()
updateMainButtonsState(pathExists) updateMainButtonsState(pathExists)
if (pathExists && !editText.text.isNullOrBlank()) buttonLoad.performClick()
if (pathExists && !editText.text.isNullOrBlank()) {
buttonLoad.performClick()
}
} }
private fun bindViews() { private fun bindViews() {
@ -65,11 +77,11 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
editButton = activity.findViewById(R.id.editButton) 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)
databaseButton = activity.findViewById(R.id.databaseButton) // <-- NEU databaseButton = activity.findViewById(R.id.databaseButton)
val tag = editText.tag as? String ?: "" val tag = editText.tag as? String ?: ""
editText.hint = LanguageManager.getText(languageID, tag) editText.hint = t(tag)
textView.text = LanguageManager.getText(languageID, "example_text") textView.text = t("example_text")
} }
private fun loadQuestionnaireOrder() { private fun loadQuestionnaireOrder() {
@ -84,11 +96,9 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
val conditionObj = obj.optJSONObject("condition") val conditionObj = obj.optJSONObject("condition")
val condition = parseCondition(conditionObj) val condition = parseCondition(conditionObj)
val showPoints = obj.optBoolean("showPoints", false) val showPoints = obj.optBoolean("showPoints", false)
QuestionItem.QuestionnaireEntry(file, condition, showPoints) QuestionItem.QuestionnaireEntry(file, condition, showPoints)
} }
} catch (e: Exception) { } catch (_: Exception) {
e.printStackTrace()
questionnaireEntries = emptyList() questionnaireEntries = emptyList()
} }
} }
@ -104,17 +114,14 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
} }
return QuestionItem.Condition.AnyOf(conditions) return QuestionItem.Condition.AnyOf(conditions)
} }
if (conditionObj.has("alwaysAvailable")) { if (conditionObj.has("alwaysAvailable") && conditionObj.optBoolean("alwaysAvailable", false)) {
val flag = conditionObj.optBoolean("alwaysAvailable", false) return QuestionItem.Condition.AlwaysAvailable
if (flag) return QuestionItem.Condition.AlwaysAvailable
} }
val requiresList = mutableListOf<String>() val requiresList = mutableListOf<String>()
if (conditionObj.has("requiresCompleted")) { if (conditionObj.has("requiresCompleted")) {
val reqArr = conditionObj.optJSONArray("requiresCompleted") val reqArr = conditionObj.optJSONArray("requiresCompleted")
if (reqArr != null) { if (reqArr != null) {
for (i in 0 until reqArr.length()) { for (i in 0 until reqArr.length()) requiresList.add(reqArr.optString(i))
requiresList.add(reqArr.optString(i))
}
} else { } else {
conditionObj.optString("requiresCompleted")?.let { if (it.isNotBlank()) requiresList.add(it) } conditionObj.optString("requiresCompleted")?.let { if (it.isNotBlank()) requiresList.add(it) }
} }
@ -123,17 +130,15 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
val questionId = conditionObj.optString("questionId", null) val questionId = conditionObj.optString("questionId", null)
val operator = conditionObj.optString("operator", null) val operator = conditionObj.optString("operator", null)
val value = conditionObj.optString("value", null) val value = conditionObj.optString("value", null)
val hasQuestionCheck = !questionnaire.isNullOrBlank() && !questionId.isNullOrBlank() && !operator.isNullOrBlank() && value != null val hasQuestionCheck =
!questionnaire.isNullOrBlank() && !questionId.isNullOrBlank() && !operator.isNullOrBlank() && value != null
return when { return when {
requiresList.isNotEmpty() && hasQuestionCheck -> { requiresList.isNotEmpty() && hasQuestionCheck ->
QuestionItem.Condition.Combined(requiresList, QuestionItem.Condition.QuestionCondition(questionnaire!!, questionId!!, operator!!, value!!)) QuestionItem.Condition.Combined(requiresList, QuestionItem.Condition.QuestionCondition(questionnaire!!, questionId!!, operator!!, value!!))
} hasQuestionCheck ->
hasQuestionCheck -> {
QuestionItem.Condition.QuestionCondition(questionnaire!!, questionId!!, operator!!, value!!) QuestionItem.Condition.QuestionCondition(questionnaire!!, questionId!!, operator!!, value!!)
} requiresList.isNotEmpty() ->
requiresList.isNotEmpty() -> {
QuestionItem.Condition.RequiresCompleted(requiresList) QuestionItem.Condition.RequiresCompleted(requiresList)
}
else -> null else -> null
} }
} }
@ -142,34 +147,95 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
buttonContainer.removeAllViews() buttonContainer.removeAllViews()
dynamicButtons.clear() dynamicButtons.clear()
questionnaireFiles.clear() questionnaireFiles.clear()
for ((index, entry) in questionnaireEntries.withIndex()) { cardParts.clear()
val button = Button(activity).apply {
val vMargin = dp(8)
val startEnabled = mutableListOf<Button>()
questionnaireEntries.forEachIndexed { index, entry ->
val row = FrameLayout(activity).apply {
layoutParams = LinearLayout.LayoutParams( layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT LinearLayout.LayoutParams.WRAP_CONTENT
).apply { setMargins(0, 8, 0, 8) } ).also { it.setMargins(0, vMargin, 0, vMargin) }
text = "Questionnaire ${index + 1}" }
val btn = MaterialButton(activity).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
dp(84)
)
text = ""
isAllCaps = false
setMinimumWidth(0)
setMinimumHeight(0)
cornerRadius = dp(22)
strokeWidth = dp(1)
strokeColor = ColorStateList.valueOf(STROKE_DISABLED)
backgroundTintList = ColorStateList.valueOf(Color.WHITE)
rippleColor = ColorStateList.valueOf(Color.parseColor("#22000000"))
gravity = Gravity.START or Gravity.CENTER_VERTICAL
setPadding(dp(20), 0, dp(20), 0)
id = View.generateViewId() id = View.generateViewId()
setOnClickListener {
GlobalValues.LAST_CLIENT_CODE = GlobalValues.LOADED_CLIENT_CODE
val fileName = questionnaireFiles[this] ?: return@setOnClickListener
val questionnaire = QuestionnaireGeneric(fileName)
startQuestionnaire(questionnaire)
applySetButtonsEnabled(dynamicButtons.filter { it == this }, allowCompleted = false, force = false)
}
} }
buttonContainer.addView(button)
dynamicButtons.add(button)
questionnaireFiles[button] = entry.file
}
updateButtonTexts()
val alwaysButtons = questionnaireEntries.mapIndexedNotNull { idx, entry -> val textColumn = LinearLayout(activity).apply {
val btn = dynamicButtons.getOrNull(idx) orientation = LinearLayout.VERTICAL
if (entry.condition is QuestionItem.Condition.AlwaysAvailable) btn else null layoutParams = FrameLayout.LayoutParams(
} FrameLayout.LayoutParams.WRAP_CONTENT,
setButtonsEnabled(alwaysButtons) FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.START or Gravity.CENTER_VERTICAL
dynamicButtons.forEach { button -> ).also { it.marginStart = dp(24) }
button.setOnClickListener {
GlobalValues.LAST_CLIENT_CODE = GlobalValues.LOADED_CLIENT_CODE
startQuestionnaireForButton(button)
setButtonsEnabled(dynamicButtons.filter { it == button })
} }
val tvTitle = TextView(activity).apply {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
setTextColor(Color.parseColor("#2F2A49"))
setPadding(0, dp(6), 0, dp(2))
}
val tvSubtitle = TextView(activity).apply {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
setTextColor(Color.parseColor("#7B7794"))
visibility = View.GONE
}
textColumn.addView(tvTitle)
textColumn.addView(tvSubtitle)
val chip = TextView(activity).apply {
setPadding(dp(14), dp(8), dp(14), dp(8))
setTextColor(Color.WHITE)
text = t("start")
setBackgroundResource(R.drawable.bg_chip_amber)
setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.END or Gravity.CENTER_VERTICAL
).also { it.marginEnd = dp(16) }
}
row.addView(btn)
row.addView(textColumn)
row.addView(chip)
buttonContainer.addView(row)
dynamicButtons.add(btn)
questionnaireFiles[btn] = entry.file
cardParts[btn] = CardParts(tvTitle, tvSubtitle, chip)
tvTitle.text = "Questionnaire ${index + 1}"
if (entry.condition is QuestionItem.Condition.AlwaysAvailable) startEnabled.add(btn)
} }
applyUpdateButtonTexts(force = false)
applySetButtonsEnabled(startEnabled, allowCompleted = false, force = false)
} }
private fun restorePreviousClientCode() { private fun restorePreviousClientCode() {
@ -186,9 +252,9 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
languageID = languages[position] languageID = languages[position]
updateButtonTexts() applyUpdateButtonTexts(force = false)
val hintTag = editText.tag as? String ?: "" val hintTag = editText.tag as? String ?: ""
editText.hint = LanguageManager.getText(languageID, hintTag) editText.hint = t(hintTag)
} }
override fun onNothingSelected(parent: AdapterView<*>) {} override fun onNothingSelected(parent: AdapterView<*>) {}
} }
@ -203,64 +269,135 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
questionnaireEntriesProvider = { questionnaireEntries }, questionnaireEntriesProvider = { questionnaireEntries },
dynamicButtonsProvider = { dynamicButtons }, dynamicButtonsProvider = { dynamicButtons },
buttonPoints = buttonPoints, buttonPoints = buttonPoints,
updateButtonTexts = { updateButtonTexts() }, updateButtonTexts = { applyUpdateButtonTexts(force = false) },
setButtonsEnabled = { setButtonsEnabled(it) }, setButtonsEnabled = { list -> applySetButtonsEnabled(list, allowCompleted = false, force = false) },
updateMainButtonsState = { updateMainButtonsState(it) }, updateMainButtonsState = { updateMainButtonsState(it) },
).setup() ).setup()
} }
private fun updateButtonTexts() { private fun applyUpdateButtonTexts(force: Boolean) {
// --- dynamische Fragebogen-Buttons wie gehabt --- if (uiFreeze && !force) return
questionnaireFiles.forEach { (button, fileName) -> questionnaireFiles.forEach { (button, fileName) ->
val entry = questionnaireEntries.firstOrNull { it.file == fileName } val entry = questionnaireEntries.firstOrNull { it.file == fileName }
val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json") val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json")
var buttonText = LanguageManager.getText(languageID, key) val titleText = t(key)
val pointsAvailable = buttonPoints.entries.firstOrNull { fileName.contains(it.key, ignoreCase = true) }
val points = pointsAvailable?.value ?: 0
if (entry?.showPoints == true && pointsAvailable != null) { val parts = cardParts[button] ?: return@forEach
buttonText += " (${points} P)" parts.title.text = titleText
}
button.text = buttonText
if (entry?.showPoints == true && pointsAvailable != null) { val points = buttonPoints.entries.firstOrNull { fileName.contains(it.key, ignoreCase = true) }?.value
when { val completed = isCompleted(button)
points in 0..12 -> button.setBackgroundColor(Color.parseColor("#4CAF50")) val enabled = button.isEnabled
points in 13..36 -> button.setBackgroundColor(Color.parseColor("#FFEB3B")) val locked = !enabled && !completed
points in 37..100-> button.setBackgroundColor(Color.parseColor("#F44336"))
else -> button.setBackgroundColor(Color.parseColor("#E0E0E0")) // Rahmenbreite & -farbe je nach Klickbarkeit
} setClickableStroke(button, enabled)
if (locked) {
setLockedAppearance(button, true)
parts.title.setTextColor(Color.WHITE)
parts.subtitle.setTextColor(Color.parseColor("#E0E0E0"))
} else { } else {
button.setBackgroundColor(Color.parseColor("#E0E0E0")) setLockedAppearance(button, false)
parts.title.setTextColor(Color.parseColor("#2F2A49"))
parts.subtitle.setTextColor(Color.parseColor("#7B7794"))
// Tönung nur wenn showPoints == true
applyTintForButton(button, points, emphasize = enabled)
}
if (entry?.showPoints == true && points != null) {
parts.subtitle.visibility = View.VISIBLE
parts.subtitle.text = "${t("points")}: $points"
} else {
parts.subtitle.visibility = View.GONE
parts.subtitle.text = ""
}
when {
completed -> {
parts.chip.text = t("done")
parts.chip.setBackgroundResource(R.drawable.bg_chip_green)
parts.chip.setTextColor(Color.WHITE)
}
enabled -> {
parts.chip.text = t("start")
parts.chip.setBackgroundResource(R.drawable.bg_chip_amber)
parts.chip.setTextColor(Color.WHITE)
}
else -> {
parts.chip.text = t("locked")
parts.chip.setBackgroundResource(R.drawable.bg_chip_grey)
parts.chip.setTextColor(Color.parseColor("#2F2A49"))
}
} }
} }
// --- HIER: alle Hauptbuttons nach Sprache neu setzen --- buttonLoad.text = t("load")
buttonLoad.text = LanguageManager.getText(languageID, "load") saveButton.text = t("save")
saveButton.text = LanguageManager.getText(languageID, "save") editButton.text = t("edit")
editButton.text = LanguageManager.getText(languageID, "edit") uploadButton.text = t("upload")
uploadButton.text = LanguageManager.getText(languageID, "upload") downloadButton.text = t("download")
downloadButton.text= LanguageManager.getText(languageID, "download") databaseButton.text = t("database")
databaseButton.text= LanguageManager.getText(languageID, "database")
// optional: Beispieltext/Hints auch aktualisieren
val hintTag = editText.tag as? String ?: "" val hintTag = editText.tag as? String ?: ""
editText.hint = LanguageManager.getText(languageID, hintTag) editText.hint = t(hintTag)
textView.text = LanguageManager.getText(languageID, "example_text") textView.text = t("example_text")
} }
private fun setButtonsEnabled(enabledButtons: List<Button>) { private fun applySetButtonsEnabled(enabledButtons: List<Button>, allowCompleted: Boolean, force: Boolean) {
if (uiFreeze && !force) return
questionnaireFiles.keys.forEach { button -> questionnaireFiles.keys.forEach { button ->
button.isEnabled = enabledButtons.contains(button) val completed = isCompleted(button)
button.alpha = if (button.isEnabled) 1.0f else 0.5f val isAllowed = enabledButtons.contains(button)
val shouldEnable = if (allowCompleted) isAllowed else isAllowed && !completed
val locked = !shouldEnable && !completed
button.isEnabled = shouldEnable
button.alpha = if (completed || shouldEnable) 1.0f else 0.6f
// Rahmenbreite & -farbe je nach Klickbarkeit
setClickableStroke(button, shouldEnable)
cardParts[button]?.let { parts ->
if (locked) {
setLockedAppearance(button, true)
parts.title.setTextColor(Color.WHITE)
parts.subtitle.setTextColor(Color.parseColor("#E0E0E0"))
} else {
setLockedAppearance(button, false)
parts.title.setTextColor(Color.parseColor("#2F2A49"))
parts.subtitle.setTextColor(Color.parseColor("#7B7794"))
// Tönung nur wenn showPoints == true
applyTintForButton(button, getPointsForButton(button), emphasize = shouldEnable)
}
when {
completed -> {
parts.chip.text = t("done")
parts.chip.setBackgroundResource(R.drawable.bg_chip_green)
parts.chip.setTextColor(Color.WHITE)
}
shouldEnable -> {
parts.chip.text = t("start")
parts.chip.setBackgroundResource(R.drawable.bg_chip_amber)
parts.chip.setTextColor(Color.WHITE)
}
else -> {
parts.chip.text = t("locked")
parts.chip.setBackgroundResource(R.drawable.bg_chip_grey)
parts.chip.setTextColor(Color.parseColor("#2F2A49"))
}
}
}
} }
} }
private fun startQuestionnaireForButton(button: Button) { // öffentliche Wrapper
val fileName = questionnaireFiles[button] ?: return private fun updateButtonTexts() = applyUpdateButtonTexts(force = false)
val questionnaire = QuestionnaireGeneric(fileName) private fun setButtonsEnabled(enabledButtons: List<Button>, allowCompleted: Boolean = false) =
startQuestionnaire(questionnaire) applySetButtonsEnabled(enabledButtons, allowCompleted, force = false)
}
private fun startQuestionnaire(questionnaire: QuestionnaireBase<*>) { private fun startQuestionnaire(questionnaire: QuestionnaireBase<*>) {
activity.startQuestionnaire(questionnaire, languageID) activity.startQuestionnaire(questionnaire, languageID)
@ -285,48 +422,46 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
languageIDProvider = { languageID }, languageIDProvider = { languageID },
questionnaireFiles = questionnaireFiles, questionnaireFiles = questionnaireFiles,
buttonPoints = buttonPoints, buttonPoints = buttonPoints,
updateButtonTexts = { updateButtonTexts() }, updateButtonTexts = { applyUpdateButtonTexts(force = true) },
setButtonsEnabled = { setButtonsEnabled(it) }, setButtonsEnabled = { list, allowCompleted ->
applySetButtonsEnabled(list, allowCompleted, force = true)
},
setUiFreeze = { freeze -> uiFreeze = freeze },
triggerLoad = { buttonLoad.performClick() } triggerLoad = { buttonLoad.performClick() }
).setup() ).setup()
} }
private fun setupUploadButton() { private fun setupUploadButton() {
uploadButton.text = "Upload" uploadButton.text = t("upload")
uploadButton.setOnClickListener { uploadButton.setOnClickListener {
val clientCode = editText.text.toString().trim() val clientCode = editText.text.toString().trim()
GlobalValues.LAST_CLIENT_CODE = clientCode GlobalValues.LAST_CLIENT_CODE = clientCode
val input = EditText(activity).apply { hint = "Server-Passwort" } val input = EditText(activity).apply { hint = "Server-Passwort" }
android.app.AlertDialog.Builder(activity) android.app.AlertDialog.Builder(activity)
.setTitle("Login erforderlich") .setTitle(t("login_required"))
.setView(input) .setView(input)
.setPositiveButton("OK") { _, _ -> .setPositiveButton("OK") { _, _ ->
val password = input.text.toString() val password = input.text.toString()
if (password.isNotBlank()) { if (password.isNotBlank()) {
Toast.makeText(activity, "Login wird überprüft...", Toast.LENGTH_SHORT).show() Toast.makeText(activity, t("checking_login"), Toast.LENGTH_SHORT).show()
DatabaseUploader.uploadDatabaseWithLogin(activity, password) DatabaseUploader.uploadDatabaseWithLogin(activity, password)
} else { } else {
Toast.makeText(activity, "Bitte Passwort eingeben", Toast.LENGTH_SHORT).show() Toast.makeText(activity, t("enter_password"), Toast.LENGTH_SHORT).show()
} }
} }
.setNegativeButton("Abbrechen", null) .setNegativeButton(t("cancel"), null)
.show() .show()
} }
} }
private fun setupDownloadButton() { private fun setupDownloadButton() {
downloadButton.text = "Download" downloadButton.text = t("download")
downloadButton.setOnClickListener { downloadButton.setOnClickListener {
val clientCode = editText.text.toString().trim() val clientCode = editText.text.toString().trim()
GlobalValues.LAST_CLIENT_CODE = clientCode GlobalValues.LAST_CLIENT_CODE = clientCode
val input = EditText(activity).apply { hint = "Server-Passwort" } val input = EditText(activity).apply { hint = "Server-Passwort" }
android.app.AlertDialog.Builder(activity) android.app.AlertDialog.Builder(activity)
.setTitle("Login erforderlich") .setTitle(t("login_required"))
.setView(input) .setView(input)
.setPositiveButton("OK") { _, _ -> .setPositiveButton("OK") { _, _ ->
val password = input.text.toString() val password = input.text.toString()
@ -335,7 +470,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
context = activity, context = activity,
password = password, password = password,
onSuccess = { token -> onSuccess = { token ->
Toast.makeText(activity, "Login erfolgreich", Toast.LENGTH_SHORT).show() Toast.makeText(activity, t("login_ok"), Toast.LENGTH_SHORT).show()
DatabaseDownloader.downloadAndReplaceDatabase(activity, token) DatabaseDownloader.downloadAndReplaceDatabase(activity, token)
updateMainButtonsState(true) updateMainButtonsState(true)
}, },
@ -344,10 +479,10 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
} }
) )
} else { } else {
Toast.makeText(activity, "Bitte Passwort eingeben", Toast.LENGTH_SHORT).show() Toast.makeText(activity, t("enter_password"), Toast.LENGTH_SHORT).show()
} }
} }
.setNegativeButton("Abbrechen", null) .setNegativeButton(t("cancel"), null)
.show() .show()
} }
} }
@ -356,20 +491,92 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
DatabaseButtonHandler( DatabaseButtonHandler(
activity = activity, activity = activity,
databaseButton = databaseButton, databaseButton = databaseButton,
onClose = { onClose = { init() },
// zurück zum Opening-Screen languageIDProvider = { languageID }
init()
},
languageIDProvider = { languageID } // aktuelle Sprache an DatabaseButtonHandler weitergeben
).setup() ).setup()
} }
private fun updateMainButtonsState(isDatabaseAvailable: Boolean) { private fun updateMainButtonsState(isDatabaseAvailable: Boolean) {
val buttons = listOf(buttonLoad, saveButton, editButton, databaseButton) // <-- NEU dabei listOf(buttonLoad, saveButton, editButton, databaseButton).forEach { b ->
buttons.forEach { button -> b.isEnabled = isDatabaseAvailable
button.isEnabled = isDatabaseAvailable b.alpha = if (isDatabaseAvailable) 1.0f else 0.5f
button.alpha = if (isDatabaseAvailable) 1.0f else 0.5f
} }
} }
private fun dp(v: Int): Int =
(v * activity.resources.displayMetrics.density).toInt()
private fun isCompleted(button: Button): Boolean {
val fileName = questionnaireFiles[button] ?: return false
return buttonPoints.keys.any { k ->
fileName.contains(k, ignoreCase = true) || k.contains(fileName, ignoreCase = true)
}
}
private fun getPointsForButton(button: Button): Int? {
val fileName = questionnaireFiles[button] ?: return null
return buttonPoints.entries.firstOrNull { (k, _) ->
fileName.contains(k, ignoreCase = true) || k.contains(fileName, ignoreCase = true)
}?.value
}
/** Locked-Optik: schwarzer Hintergrund, fester grauer Rand */
private fun setLockedAppearance(button: Button, locked: Boolean) {
val mb = button as? MaterialButton ?: return
if (locked) {
mb.backgroundTintList = ColorStateList.valueOf(Color.BLACK)
mb.strokeColor = ColorStateList.valueOf(STROKE_DISABLED)
} else {
mb.backgroundTintList = ColorStateList.valueOf(Color.WHITE)
mb.strokeColor = ColorStateList.valueOf(if (button.isEnabled) STROKE_ENABLED else STROKE_DISABLED)
}
}
/** Standardfarben wiederherstellen (wenn showPoints == false) hier nicht mehr für locked zuständig */
private fun resetTint(button: Button) {
val mb = button as? MaterialButton ?: return
mb.backgroundTintList = ColorStateList.valueOf(Color.WHITE)
mb.strokeColor = ColorStateList.valueOf(STROKE_DISABLED)
}
/** Tönung nur anwenden, wenn showPoints == true (und Button nicht locked) */
private fun applyTintForButton(button: Button, points: Int?, emphasize: Boolean) {
val file = questionnaireFiles[button] ?: return
val entry = questionnaireEntries.firstOrNull { it.file == file }
if (entry?.showPoints != true) {
val mb = button as? MaterialButton ?: return
mb.backgroundTintList = ColorStateList.valueOf(if (emphasize) Color.parseColor("#F1EEFF") else Color.WHITE)
mb.strokeColor = ColorStateList.valueOf(if (emphasize) STROKE_ENABLED else STROKE_DISABLED)
return
}
setScoreTint(button, points, emphasize)
}
/** Dickere Kontur, wenn anklickbar; Farbe bleibt fix */
private fun setClickableStroke(button: Button, enabled: Boolean) {
val mb = button as? MaterialButton ?: return
mb.strokeWidth = if (enabled) dp(2) else dp(1)
mb.strokeColor = ColorStateList.valueOf(if (enabled) STROKE_ENABLED else STROKE_DISABLED)
}
/** Hintergrund nach Score; Rand bleibt fix */
private fun setScoreTint(button: Button, points: Int?, emphasize: Boolean) {
val mb = button as? MaterialButton ?: return
val bg = when {
points == null && emphasize -> Color.parseColor("#F1EEFF")
points == null -> Color.parseColor("#FFFFFF")
points in 0..12 && emphasize -> Color.parseColor("#C8E6C9")
points in 0..12 -> Color.parseColor("#E8F5E9")
points in 13..36 && emphasize -> Color.parseColor("#FFE0B2")
points in 13..36 -> Color.parseColor("#FFF8E1")
points >= 37 && emphasize -> Color.parseColor("#FFCDD2")
else -> Color.parseColor("#FFEBEE")
}
mb.backgroundTintList = ColorStateList.valueOf(bg)
mb.strokeColor = ColorStateList.valueOf(if (emphasize) STROKE_ENABLED else STROKE_DISABLED)
}
} }

View File

@ -352,10 +352,9 @@ object LanguageManager {
"export_success_downloads_headers" to "Export erfolgreich: Downloads/ClientHeaders.xlsx", "export_success_downloads_headers" to "Export erfolgreich: Downloads/ClientHeaders.xlsx",
"export_failed" to "Export fehlgeschlagen.", "export_failed" to "Export fehlgeschlagen.",
"error_generic" to "Fehler", "error_generic" to "Fehler",
"done" to "abgeschlossen", "not_done" to "Nicht Fertog",
"not_done" to "nicht abgeschlossen", "none" to "Keine Angabe",
"none" to "keine Angabe", "points" to "Punkte"
), ),
"ENGLISH" to mapOf( "ENGLISH" to mapOf(
@ -675,9 +674,12 @@ object LanguageManager {
"export_success_downloads_headers" to "Export successful: Downloads/ClientHeaders.xlsx", "export_success_downloads_headers" to "Export successful: Downloads/ClientHeaders.xlsx",
"export_failed" to "Export failed.", "export_failed" to "Export failed.",
"error_generic" to "Error", "error_generic" to "Error",
"done" to "Done",
"not_done" to "Not Done", "not_done" to "Not Done",
"none" to "None", "none" to "None",
"done" to "Done",
"locked" to "Locked",
"start" to "Start",
"points" to "Points"
), ),
"FRENCH" to mapOf( "FRENCH" to mapOf(
@ -1001,9 +1003,12 @@ object LanguageManager {
"export_success_downloads_headers" to "Export réussi : Downloads/ClientHeaders.xlsx", "export_success_downloads_headers" to "Export réussi : Downloads/ClientHeaders.xlsx",
"export_failed" to "Échec de lexportation.", "export_failed" to "Échec de lexportation.",
"error_generic" to "Erreur", "error_generic" to "Erreur",
"done" to "terminé",
"not_done" to "non terminé", "not_done" to "non terminé",
"none" to "aucune réponse", "none" to "aucune réponse",
"done" to "Terminé",
"locked" to "Verrouillé",
"start" to "Commencer",
"points" to "Points"
), ),
"RUSSIAN" to mapOf( "RUSSIAN" to mapOf(
@ -1323,9 +1328,12 @@ object LanguageManager {
"export_success_downloads_headers" to "Экспорт выполнен: Downloads/ClientHeaders.xlsx", "export_success_downloads_headers" to "Экспорт выполнен: Downloads/ClientHeaders.xlsx",
"export_failed" to "Экспорт не выполнен.", "export_failed" to "Экспорт не выполнен.",
"error_generic" to "Ошибка", "error_generic" to "Ошибка",
"done" to "выполнено",
"not_done" to "не выполнено", "not_done" to "не выполнено",
"none" to "нет ответа", "none" to "нет ответа",
"done" to "Готово",
"locked" to "Заблокировано",
"start" to "Начать",
"points" to "Баллы"
), ),
"UKRAINIAN" to mapOf( "UKRAINIAN" to mapOf(
@ -1649,9 +1657,12 @@ object LanguageManager {
"export_success_downloads_headers" to "Експорт успішний: Downloads/ClientHeaders.xlsx", "export_success_downloads_headers" to "Експорт успішний: Downloads/ClientHeaders.xlsx",
"export_failed" to "Помилка експорту.", "export_failed" to "Помилка експорту.",
"error_generic" to "Помилка", "error_generic" to "Помилка",
"done" to "завершено",
"not_done" to "не завершено", "not_done" to "не завершено",
"none" to "немає відповіді", "none" to "немає відповіді",
"done" to "Завершено",
"locked" to "Заблоковано",
"start" to "Почати",
"points" to "Бали"
), ),
"TURKISH" to mapOf( "TURKISH" to mapOf(
@ -1975,9 +1986,12 @@ object LanguageManager {
"export_success_downloads_headers" to "Dışa aktarma başarılı: Downloads/ClientHeaders.xlsx", "export_success_downloads_headers" to "Dışa aktarma başarılı: Downloads/ClientHeaders.xlsx",
"export_failed" to "Dışa aktarma başarısız.", "export_failed" to "Dışa aktarma başarısız.",
"error_generic" to "Hata", "error_generic" to "Hata",
"done" to "tamamlandı",
"not_done" to "tamamlanmadı", "not_done" to "tamamlanmadı",
"none" to "yanıt yok", "none" to "yanıt yok",
"done" to "Tamamlandı",
"locked" to "Kilitli",
"start" to "Başlat",
"points" to "Puan"
), ),
"POLISH" to mapOf( "POLISH" to mapOf(
@ -2301,9 +2315,12 @@ object LanguageManager {
"export_success_downloads_headers" to "Eksport zakończony: Downloads/ClientHeaders.xlsx", "export_success_downloads_headers" to "Eksport zakończony: Downloads/ClientHeaders.xlsx",
"export_failed" to "Eksport nieudany.", "export_failed" to "Eksport nieudany.",
"error_generic" to "Błąd", "error_generic" to "Błąd",
"done" to "zakończono",
"not_done" to "nie zakończono", "not_done" to "nie zakończono",
"none" to "brak odpowiedzi", "none" to "brak odpowiedzi",
"done" to "Gotowe",
"locked" to "Zablokowane",
"start" to "Rozpocznij",
"points" to "Punkty"
), ),
"ARABIC" to mapOf( "ARABIC" to mapOf(
@ -2627,9 +2644,12 @@ object LanguageManager {
"export_success_downloads_headers" to "تم التصدير بنجاح: Downloads/ClientHeaders.xlsx", "export_success_downloads_headers" to "تم التصدير بنجاح: Downloads/ClientHeaders.xlsx",
"export_failed" to "فشل التصدير.", "export_failed" to "فشل التصدير.",
"error_generic" to "خطأ", "error_generic" to "خطأ",
"done" to "مكتمل",
"not_done" to "غير مكتمل", "not_done" to "غير مكتمل",
"none" to "لا توجد إجابة", "none" to "لا توجد إجابة",
"done" to "تم",
"locked" to "مقفل",
"start" to "ابدأ",
"points" to "النقاط"
), ),
"ROMANIAN" to mapOf( "ROMANIAN" to mapOf(
@ -2953,9 +2973,12 @@ object LanguageManager {
"export_success_downloads_headers" to "Export reușit: Downloads/ClientHeaders.xlsx", "export_success_downloads_headers" to "Export reușit: Downloads/ClientHeaders.xlsx",
"export_failed" to "Export nereușit.", "export_failed" to "Export nereușit.",
"error_generic" to "Eroare", "error_generic" to "Eroare",
"done" to "finalizat",
"not_done" to "nefinalizat", "not_done" to "nefinalizat",
"none" to "niciun răspuns", "none" to "niciun răspuns",
"done" to "Finalizat",
"locked" to "Blocat",
"start" to "Începe",
"points" to "Puncte"
), ),
"SPANISH" to mapOf( "SPANISH" to mapOf(
@ -3279,9 +3302,12 @@ object LanguageManager {
"export_success_downloads_headers" to "Exportación correcta: Downloads/ClientHeaders.xlsx", "export_success_downloads_headers" to "Exportación correcta: Downloads/ClientHeaders.xlsx",
"export_failed" to "Fallo en la exportación.", "export_failed" to "Fallo en la exportación.",
"error_generic" to "Error", "error_generic" to "Error",
"done" to "completado",
"not_done" to "no completado", "not_done" to "no completado",
"none" to "sin respuesta", "none" to "sin respuesta",
"done" to "Completado",
"locked" to "Bloqueado",
"start" to "Iniciar",
"points" to "Puncte"
) )
) )
} }

View File

@ -25,10 +25,6 @@ class LoadButtonHandler(
} }
private fun handleLoadButton() { private fun handleLoadButton() {
buttonPoints.clear()
updateButtonTexts()
setButtonsEnabled(emptyList())
val inputText = editText.text.toString().trim() val inputText = editText.text.toString().trim()
if (inputText.isBlank()) { if (inputText.isBlank()) {
val message = LanguageManager.getText(languageIDProvider(), "please_client_code") val message = LanguageManager.getText(languageIDProvider(), "please_client_code")
@ -36,13 +32,16 @@ class LoadButtonHandler(
return return
} }
buttonPoints.clear()
setButtonsEnabled(emptyList()) // temporär sperren
updateButtonTexts() // Chips zeigen vorläufig „Gesperrt“
val clientCode = inputText val clientCode = inputText
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.database.clientDao().getClientByCode(clientCode)
if (client == null) { if (client == null) {
// Kein Profil → als NICHT geladen markieren
GlobalValues.LOADED_CLIENT_CODE = null GlobalValues.LOADED_CLIENT_CODE = null
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val message = LanguageManager.getText(languageIDProvider(), "no_profile") val message = LanguageManager.getText(languageIDProvider(), "no_profile")
@ -53,21 +52,18 @@ class LoadButtonHandler(
if (entry.condition is QuestionItem.Condition.AlwaysAvailable) btn else null if (entry.condition is QuestionItem.Condition.AlwaysAvailable) btn else null
} }
setButtonsEnabled(alwaysButtons) setButtonsEnabled(alwaysButtons)
updateButtonTexts() // <- nach dem Aktivieren Chips aktualisieren
} }
return@launch return@launch
} }
// Profil gefunden → als geladen markieren
GlobalValues.LOADED_CLIENT_CODE = clientCode GlobalValues.LOADED_CLIENT_CODE = clientCode
withContext(Dispatchers.Main) { updateMainButtonsState(true) }
withContext(Dispatchers.Main) {
updateMainButtonsState(true)
}
handleNormalLoad(clientCode) handleNormalLoad(clientCode)
} }
} }
private suspend fun evaluateCondition( private suspend fun evaluateCondition(
condition: QuestionItem.Condition?, condition: QuestionItem.Condition?,
clientCode: String, clientCode: String,
@ -134,10 +130,6 @@ class LoadButtonHandler(
} }
} }
withContext(Dispatchers.Main) {
updateButtonTexts()
}
val enabledButtons = mutableListOf<Button>() val enabledButtons = mutableListOf<Button>()
val questionnaireEntries = questionnaireEntriesProvider() val questionnaireEntries = questionnaireEntriesProvider()
val dynamicButtons = dynamicButtonsProvider() val dynamicButtons = dynamicButtonsProvider()
@ -158,13 +150,9 @@ class LoadButtonHandler(
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (enabledButtons.isEmpty()) { setButtonsEnabled(enabledButtons) // erst aktivieren …
setButtonsEnabled(emptyList()) updateButtonTexts() // … dann Chips/Labels korrekt setzen
val message = LanguageManager.getText(languageIDProvider(), "questionnaires_finished")
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
} else {
setButtonsEnabled(enabledButtons)
}
} }
} }
} }

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#F2B544"/>
<corners android:radius="18dp"/>
<padding android:left="8dp" android:top="6dp" android:right="8dp" android:bottom="6dp"/>
</shape>

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#43B581"/>
<corners android:radius="18dp"/>
<padding android:left="8dp" android:top="6dp" android:right="8dp" android:bottom="6dp"/>
</shape>

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#E3E5EF"/>
<corners android:radius="18dp"/>
<padding android:left="8dp" android:top="6dp" android:right="8dp" android:bottom="6dp"/>
</shape>

View File

@ -10,6 +10,7 @@
android:id="@+id/headerCard" android:id="@+id/headerCard"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:paddingStart="24dp" android:paddingStart="24dp"
android:paddingEnd="24dp" android:paddingEnd="24dp"
android:paddingTop="20dp" android:paddingTop="20dp"
@ -207,14 +208,24 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
<LinearLayout <androidx.core.widget.NestedScrollView
android:id="@+id/buttonContainer" android:id="@+id/questionnaireScroll"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="0dp"
android:orientation="vertical" android:fillViewport="true"
android:layout_marginTop="8dp" android:clipToPadding="false"
android:paddingBottom="16dp"
app:layout_constraintTop_toBottomOf="@id/textView" app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:id="@+id/buttonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="8dp"/>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>