Compare commits
15 Commits
dcfa261c1c
...
refactorin
| Author | SHA1 | Date | |
|---|---|---|---|
| 67bbc3ea06 | |||
| b95977e28d | |||
| cc89c77186 | |||
| 07b7b3dc1b | |||
| d30c94beeb | |||
| 5b1264293c | |||
| 39a4811fd2 | |||
| 8b3bb358e8 | |||
| 5968bf68d1 | |||
| ad09bce68c | |||
| 4089841336 | |||
| 5570710da5 | |||
| 8d54315fe7 | |||
| ac2e0dabd2 | |||
| 66122dd6c3 |
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?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>
|
||||||
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@ -1,4 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,186 +0,0 @@
|
|||||||
package com.dano.test1
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.*
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.widget.TextViewCompat
|
|
||||||
|
|
||||||
class HandlerStringSpinner(
|
|
||||||
private val context: Context,
|
|
||||||
private val answers: MutableMap<String, Any>,
|
|
||||||
private val languageID: String,
|
|
||||||
private val goToNextQuestion: () -> Unit,
|
|
||||||
private val goToPreviousQuestion: () -> Unit,
|
|
||||||
private val showToast: (String) -> Unit,
|
|
||||||
private val questionnaireMeta: String // Meta, damit dieselbe DB-ID wie in anderen Handlern gebildet wird
|
|
||||||
) : QuestionHandler {
|
|
||||||
|
|
||||||
private lateinit var layout: View
|
|
||||||
private lateinit var question: QuestionItem.StringSpinnerQuestion
|
|
||||||
|
|
||||||
override fun bind(layout: View, question: QuestionItem) {
|
|
||||||
if (question !is QuestionItem.StringSpinnerQuestion) return
|
|
||||||
|
|
||||||
this.layout = layout
|
|
||||||
this.question = question
|
|
||||||
|
|
||||||
val questionTextView = layout.findViewById<TextView>(R.id.question)
|
|
||||||
val textView = layout.findViewById<TextView>(R.id.textView)
|
|
||||||
val spinner = layout.findViewById<Spinner>(R.id.string_spinner)
|
|
||||||
|
|
||||||
// Texte setzen
|
|
||||||
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
|
||||||
textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
|
|
||||||
|
|
||||||
// === Textgrößen prozentual zur Bildschirmhöhe (wie im HandlerRadioQuestion) ===
|
|
||||||
setTextSizePercentOfScreenHeight(textView, 0.03f)
|
|
||||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
|
||||||
// ==============================================================================
|
|
||||||
|
|
||||||
val options = buildOptionsList()
|
|
||||||
|
|
||||||
// vorhandene Auswahl (falls vorhanden)
|
|
||||||
val savedSelection = question.question?.let { answers[it] as? String }
|
|
||||||
|
|
||||||
// Spinner aufsetzen (Schriftgröße & Zeilenhöhe dynamisch, kein Abschneiden)
|
|
||||||
setupSpinner(spinner, options, savedSelection)
|
|
||||||
|
|
||||||
// Falls noch keine Antwort im Map: aus DB laden (analog zu anderen Handlern)
|
|
||||||
val answerMapKey = question.question ?: (question.id ?: "")
|
|
||||||
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
val clientCode = GlobalValues.LAST_CLIENT_CODE
|
|
||||||
if (clientCode.isNullOrBlank()) return@launch
|
|
||||||
|
|
||||||
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode)
|
|
||||||
val myQuestionId = questionnaireMeta + "-" + question.question
|
|
||||||
val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue
|
|
||||||
|
|
||||||
if (!dbAnswer.isNullOrBlank()) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
answers[answerMapKey] = dbAnswer
|
|
||||||
val index = options.indexOf(dbAnswer)
|
|
||||||
if (index >= 0) spinner.setSelection(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
|
|
||||||
if (validate()) {
|
|
||||||
saveAnswer()
|
|
||||||
goToNextQuestion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
|
|
||||||
goToPreviousQuestion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun validate(): Boolean {
|
|
||||||
val spinner = layout.findViewById<Spinner>(R.id.string_spinner)
|
|
||||||
val selected = spinner.selectedItem as? String
|
|
||||||
val prompt = LanguageManager.getText(languageID, "choose_answer")
|
|
||||||
|
|
||||||
return if (selected.isNullOrEmpty() || selected == prompt) {
|
|
||||||
showToast(LanguageManager.getText(languageID, "select_one_answer"))
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun saveAnswer() {
|
|
||||||
val spinner = layout.findViewById<Spinner>(R.id.string_spinner)
|
|
||||||
val selected = spinner.selectedItem as? String ?: return
|
|
||||||
question.question?.let { key -> answers[key] = selected }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildOptionsList(): List<String> {
|
|
||||||
return if (question.id == "q11") {
|
|
||||||
Countries.getAllCountries(languageID)
|
|
||||||
} else {
|
|
||||||
val prompt = LanguageManager.getText(languageID, "choose_answer")
|
|
||||||
listOf(prompt) + question.options
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Textgröße prozentual zur Bildschirmhöhe setzen und AutoSize deaktivieren
|
|
||||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
|
||||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
|
||||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
|
||||||
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
|
|
||||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spinner-Adapter mit dynamischer Schrift & stabiler Dropdown-Zeilenhöhe (kein Abschneiden)
|
|
||||||
private fun <T> setupSpinner(spinner: Spinner, 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()
|
|
||||||
|
|
||||||
// Schrift & abgeleitete Höhen (wie beim Value-Spinner-Fix)
|
|
||||||
val textSp = spFromScreenHeight(0.0275f) // ~2.75% der Bildschirmhöhe
|
|
||||||
val textPx = pxFromSp(textSp)
|
|
||||||
val vPadPx = (textPx * 0.50f).toInt() // vertikales Padding
|
|
||||||
val rowHeight = (textPx * 2.20f + 2 * vPadPx).toInt() // feste Zeilenhöhe, verhindert Abschneiden
|
|
||||||
|
|
||||||
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 android.view.Gravity.HORIZONTAL_GRAVITY_MASK) or android.view.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) // ausgewählte Ansicht
|
|
||||||
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) // Dropdown-Zeilen: Höhe erzwingen
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
||||||
spinner.adapter = adapter
|
|
||||||
|
|
||||||
// Spinner selbst ausreichend hoch machen
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
package com.dano.test1
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
object LoginManager {
|
|
||||||
private const val SERVER_LOGIN_URL = "http://49.13.157.44/login.php"
|
|
||||||
private val client = OkHttpClient()
|
|
||||||
|
|
||||||
fun loginUserWithCredentials(
|
|
||||||
context: Context,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
onSuccess: (String) -> Unit,
|
|
||||||
onError: (String) -> Unit
|
|
||||||
) {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
val bodyJson = JSONObject()
|
|
||||||
.put("username", username)
|
|
||||||
.put("password", password)
|
|
||||||
.toString()
|
|
||||||
.toRequestBody("application/json".toMediaType())
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(SERVER_LOGIN_URL)
|
|
||||||
.post(bodyJson)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val response = client.newCall(request).execute()
|
|
||||||
val text = response.body?.string()
|
|
||||||
|
|
||||||
if (response.isSuccessful && text != null) {
|
|
||||||
val json = JSONObject(text)
|
|
||||||
if (json.optBoolean("success")) {
|
|
||||||
val token = json.getString("token")
|
|
||||||
// => setzt auch den Login-Timestamp:
|
|
||||||
TokenStore.save(context, token, username)
|
|
||||||
withContext(Dispatchers.Main) { onSuccess(token) }
|
|
||||||
} else {
|
|
||||||
withContext(Dispatchers.Main) { onError("Login fehlgeschlagen") }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
withContext(Dispatchers.Main) { onError("Fehler beim Login (${response.code})") }
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("LOGIN", "Exception", e)
|
|
||||||
withContext(Dispatchers.Main) { onError("Exception: ${e.message}") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -13,7 +13,11 @@ import android.widget.ProgressBar
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import java.io.File
|
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.ui.HandlerOpeningScreen
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@ -27,6 +31,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// LIVE: Network-Callback (optional für Statusleiste)
|
// LIVE: Network-Callback (optional für Statusleiste)
|
||||||
private var netCb: ConnectivityManager.NetworkCallback? = null
|
private var netCb: ConnectivityManager.NetworkCallback? = null
|
||||||
|
|
||||||
|
// Wir kennen hier (vor dem OpeningScreen) noch keine Nutzerwahl → Deutsch als Startsprache.
|
||||||
|
private val bootLanguageId: String get() = "GERMAN"
|
||||||
|
private fun t(key: String): String = LanguageManager.getText(bootLanguageId, key)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@ -57,11 +65,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setPadding(dp(20), dp(8), dp(20), 0)
|
setPadding(dp(20), dp(8), dp(20), 0)
|
||||||
}
|
}
|
||||||
val etUser = EditText(this).apply {
|
val etUser = EditText(this).apply {
|
||||||
hint = "Username"
|
hint = t("username_hint")
|
||||||
setSingleLine()
|
setSingleLine()
|
||||||
}
|
}
|
||||||
val etPass = EditText(this).apply {
|
val etPass = EditText(this).apply {
|
||||||
hint = "Passwort"
|
hint = t("password_hint")
|
||||||
setSingleLine()
|
setSingleLine()
|
||||||
inputType = android.text.InputType.TYPE_CLASS_TEXT or
|
inputType = android.text.InputType.TYPE_CLASS_TEXT or
|
||||||
android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
|
android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
@ -70,14 +78,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
container.addView(etPass)
|
container.addView(etPass)
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(this)
|
val dialog = AlertDialog.Builder(this)
|
||||||
.setTitle("Login erforderlich")
|
.setTitle(t("login_required_title"))
|
||||||
.setView(container)
|
.setView(container)
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.setPositiveButton("Login") { _, _ ->
|
.setPositiveButton(t("login_btn")) { _, _ ->
|
||||||
val user = etUser.text.toString().trim()
|
val user = etUser.text.toString().trim()
|
||||||
val pass = etPass.text.toString()
|
val pass = etPass.text.toString()
|
||||||
if (user.isEmpty() || pass.isEmpty()) {
|
if (user.isEmpty() || pass.isEmpty()) {
|
||||||
Toast.makeText(this, "Bitte Username & Passwort eingeben", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, t("please_username_password"), Toast.LENGTH_SHORT).show()
|
||||||
showLoginThenDownload()
|
showLoginThenDownload()
|
||||||
return@setPositiveButton
|
return@setPositiveButton
|
||||||
}
|
}
|
||||||
@ -97,14 +105,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// Wenn Download fehlgeschlagen ist, aber evtl. schon eine DB lokal liegt,
|
// Wenn Download fehlgeschlagen ist, aber evtl. schon eine DB lokal liegt,
|
||||||
// lassen wir den Nutzer trotzdem weiterarbeiten (Offline).
|
// lassen wir den Nutzer trotzdem weiterarbeiten (Offline).
|
||||||
if (!ok && !hasLocalDb()) {
|
if (!ok && !hasLocalDb()) {
|
||||||
Toast.makeText(this, "Download fehlgeschlagen – keine lokale Datenbank vorhanden", Toast.LENGTH_LONG).show()
|
Toast.makeText(this, t("download_failed_no_local_db"), Toast.LENGTH_LONG).show()
|
||||||
// Zurück zum Login, damit man es erneut probieren kann
|
// Zurück zum Login, damit man es erneut probieren kann
|
||||||
showLoginThenDownload()
|
showLoginThenDownload()
|
||||||
return@downloadAndReplaceDatabase
|
return@downloadAndReplaceDatabase
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
Toast.makeText(this, "Download fehlgeschlagen – arbeite offline mit vorhandener DB", Toast.LENGTH_LONG).show()
|
Toast.makeText(this, t("download_failed_use_offline"), Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opening-Screen starten
|
// Opening-Screen starten
|
||||||
@ -115,12 +123,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
},
|
},
|
||||||
onError = { msg ->
|
onError = { msg ->
|
||||||
showBusy(false)
|
showBusy(false)
|
||||||
Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
|
val txt = t("login_failed_with_reason").replace("{reason}", msg ?: "")
|
||||||
|
Toast.makeText(this, txt, Toast.LENGTH_LONG).show()
|
||||||
showLoginThenDownload()
|
showLoginThenDownload()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.setNegativeButton("Beenden") { _, _ -> finishAffinity() }
|
.setNegativeButton(t("exit_btn")) { _, _ -> finishAffinity() }
|
||||||
.create()
|
.create()
|
||||||
|
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|||||||
@ -6,6 +6,19 @@ import androidx.room.Room
|
|||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import com.dano.test1.data.AppDatabase
|
import com.dano.test1.data.AppDatabase
|
||||||
|
|
||||||
|
/*
|
||||||
|
MyApp (Application)
|
||||||
|
- Einstiegspunkt der App, der einmal pro Prozessstart initialisiert wird.
|
||||||
|
|
||||||
|
Besonderheiten der DB-Konfiguration:
|
||||||
|
- Name: "questionnaire_database"
|
||||||
|
- fallbackToDestructiveMigration():
|
||||||
|
* Falls sich das Schema ändert und keine Migration vorliegt,wird die DB zerstört und neu angelegt.
|
||||||
|
- setJournalMode(TRUNCATE):
|
||||||
|
* Verwendet TRUNCATE-Journal (keine separaten -wal/-shm Dateien), es existiert nur die Hauptdatei „questionnaire_database“.
|
||||||
|
- Callback onOpen():
|
||||||
|
* Loggt beim Öffnen der Datenbank einen Hinweis.
|
||||||
|
*/
|
||||||
class MyApp : Application() {
|
class MyApp : Application() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -16,7 +29,7 @@ class MyApp : Application() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
// Room Datenbank bauen: nur die Hauptdatei, ohne WAL und Journal
|
// Room-Datenbank bauen: nur die Hauptdatei, ohne WAL und Journal
|
||||||
database = Room.databaseBuilder(
|
database = Room.databaseBuilder(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
AppDatabase::class.java,
|
AppDatabase::class.java,
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
package com.dano.test1
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
|
|
||||||
object NetworkUtils {
|
|
||||||
fun isOnline(context: Context): Boolean {
|
|
||||||
return try {
|
|
||||||
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? ?: return false
|
|
||||||
val network = cm.activeNetwork ?: return false
|
|
||||||
val caps = cm.getNetworkCapabilities(network) ?: return false
|
|
||||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
|
||||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
|
||||||
} catch (_: SecurityException) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,6 +3,18 @@ package com.dano.test1.data
|
|||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zentrale Room-Datenbank der App. Diese Klasse beschreibt:
|
||||||
|
- welche Tabellen (entities) es gibt: Client, Questionnaire, Question, Answer, CompletedQuestionnaire
|
||||||
|
- die Datenbank-Version (version = 1) für Migrations/Schema-Updates
|
||||||
|
|
||||||
|
Über die abstrakten DAO-Getter (clientDao(), questionnaireDao(), …) erhält der Rest der App Typsichere Zugriffe auf die jeweiligen Tabellen.
|
||||||
|
|
||||||
|
Hinweis:
|
||||||
|
- Room erzeugt zur Build-Zeit die konkrete Implementierung dieser abstrakten Klasse.
|
||||||
|
- Eine Instanz der Datenbank wird typischerweise per Room.databaseBuilder(...) erstellt und als Singleton verwendet.
|
||||||
|
*/
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
Client::class,
|
Client::class,
|
||||||
@ -2,8 +2,18 @@ package com.dano.test1.data
|
|||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
|
|
||||||
|
/*
|
||||||
|
Data-Access-Objekte (DAOs) für die Room-Datenbank.
|
||||||
|
Sie kapseln alle typsicheren Lese-/Schreiboperationen für die Tabellen clients, questionnaires, questions, answers und completed_questionnaires.
|
||||||
|
|
||||||
|
Hinweis:
|
||||||
|
- Die konkreten Implementierungen erzeugt Room zur Build-Zeit.
|
||||||
|
- DAOs werden über die AppDatabase (Room.databaseBuilder(...)) bezogen.
|
||||||
|
*/
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ClientDao {
|
interface ClientDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
suspend fun insertClient(client: Client)
|
suspend fun insertClient(client: Client)
|
||||||
|
|
||||||
@ -20,9 +30,9 @@ interface ClientDao {
|
|||||||
suspend fun getAllClients(): List<Client>
|
suspend fun getAllClients(): List<Client>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface QuestionnaireDao {
|
interface QuestionnaireDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
suspend fun insertQuestionnaire(questionnaire: Questionnaire)
|
suspend fun insertQuestionnaire(questionnaire: Questionnaire)
|
||||||
|
|
||||||
@ -30,11 +40,12 @@ interface QuestionnaireDao {
|
|||||||
suspend fun getById(id: String): Questionnaire?
|
suspend fun getById(id: String): Questionnaire?
|
||||||
|
|
||||||
@Query("SELECT * FROM questionnaires")
|
@Query("SELECT * FROM questionnaires")
|
||||||
suspend fun getAll(): List<Questionnaire> // <-- NEU
|
suspend fun getAll(): List<Questionnaire>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface QuestionDao {
|
interface QuestionDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
suspend fun insertQuestions(questions: List<Question>)
|
suspend fun insertQuestions(questions: List<Question>)
|
||||||
|
|
||||||
@ -48,8 +59,10 @@ interface QuestionDao {
|
|||||||
suspend fun getQuestionsForQuestionnaire(questionnaireId: String): List<Question>
|
suspend fun getQuestionsForQuestionnaire(questionnaireId: String): List<Question>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface AnswerDao {
|
interface AnswerDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertAnswers(answers: List<Answer>)
|
suspend fun insertAnswers(answers: List<Answer>)
|
||||||
|
|
||||||
@ -79,9 +92,9 @@ interface AnswerDao {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface CompletedQuestionnaireDao {
|
interface CompletedQuestionnaireDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(entry: CompletedQuestionnaire)
|
suspend fun insert(entry: CompletedQuestionnaire)
|
||||||
|
|
||||||
@ -93,4 +106,4 @@ interface CompletedQuestionnaireDao {
|
|||||||
|
|
||||||
@Query("SELECT questionnaireId FROM completed_questionnaires WHERE clientCode = :clientCode")
|
@Query("SELECT questionnaireId FROM completed_questionnaires WHERE clientCode = :clientCode")
|
||||||
suspend fun getCompletedQuestionnairesForClient(clientCode: String): List<String>
|
suspend fun getCompletedQuestionnairesForClient(clientCode: String): List<String>
|
||||||
}
|
}
|
||||||
@ -2,16 +2,41 @@ package com.dano.test1.data
|
|||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
|
|
||||||
|
/*
|
||||||
|
Room-Entities (Tabellen) der App.
|
||||||
|
- Definieren das Schema für Clients, Questionnaires, Questions, Answers und CompletedQuestionnaires.
|
||||||
|
- Beziehungen:
|
||||||
|
* Question -> Questionnaire (FK, CASCADE)
|
||||||
|
* Answer -> Client (FK, CASCADE)
|
||||||
|
* Answer -> Question (FK, CASCADE)
|
||||||
|
* CompletedQuestionnaire -> Client (FK, CASCADE)
|
||||||
|
* CompletedQuestionnaire -> Questionnaire (FK, CASCADE)
|
||||||
|
- Primärschlüssel:
|
||||||
|
* Client: clientCode
|
||||||
|
* Questionnaire: id
|
||||||
|
* Question: questionId
|
||||||
|
* Answer: (clientCode, questionId) – eine Antwort je Client & Frage
|
||||||
|
* CompletedQuestionnaire: (clientCode, questionnaireId) – ein Status je Client & Fragebogen
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Tabelle: clients – Eindeutige Identifikation eines Clients per clientCode. */
|
||||||
@Entity(tableName = "clients")
|
@Entity(tableName = "clients")
|
||||||
data class Client(
|
data class Client(
|
||||||
@PrimaryKey val clientCode: String,
|
@PrimaryKey val clientCode: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/* Tabelle: questionnaires – Eindeutige Fragebogen-IDs. */
|
||||||
@Entity(tableName = "questionnaires")
|
@Entity(tableName = "questionnaires")
|
||||||
data class Questionnaire(
|
data class Questionnaire(
|
||||||
@PrimaryKey val id: String,
|
@PrimaryKey val id: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Tabelle: questions
|
||||||
|
- Jede Frage gehört zu genau einem Fragebogen (questionnaireId).
|
||||||
|
- Fremdschlüssel sorgt dafür, dass beim Löschen eines Fragebogens die zugehörigen Fragen mit gelöscht werden.
|
||||||
|
- Index auf questionnaireId beschleunigt Abfragen „alle Fragen eines Fragebogens“.
|
||||||
|
*/
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "questions",
|
tableName = "questions",
|
||||||
foreignKeys = [
|
foreignKeys = [
|
||||||
@ -30,6 +55,12 @@ data class Question(
|
|||||||
val question: String = ""
|
val question: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Tabelle: answers
|
||||||
|
- Zusammengesetzter Primärschlüssel (clientCode, questionId):
|
||||||
|
* Pro Client und Frage existiert höchstens eine Antwort.
|
||||||
|
* Löscht man den Client oder die Frage, werden die zugehörigen Antworten mit entfernt.
|
||||||
|
*/
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "answers",
|
tableName = "answers",
|
||||||
primaryKeys = ["clientCode", "questionId"],
|
primaryKeys = ["clientCode", "questionId"],
|
||||||
@ -55,6 +86,17 @@ data class Answer(
|
|||||||
val answerValue: String = ""
|
val answerValue: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Tabelle: completed_questionnaires
|
||||||
|
- Zusammengesetzter Primärschlüssel (clientCode, questionnaireId):
|
||||||
|
* Hält den Abschluss-Status eines Fragebogens pro Client.
|
||||||
|
- FKs mit CASCADE:
|
||||||
|
* Beim Löschen eines Clients oder Fragebogens verschwindet der Status-Eintrag ebenfalls.
|
||||||
|
- Indizes auf clientCode und questionnaireId für schnelle Lookups.
|
||||||
|
- timestamp: Zeitpunkt der Statusänderung (Default: now).
|
||||||
|
- isDone: true/false – abgeschlossen oder nicht.
|
||||||
|
- sumPoints: optionaler Score des Fragebogens.
|
||||||
|
*/
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "completed_questionnaires",
|
tableName = "completed_questionnaires",
|
||||||
primaryKeys = ["clientCode", "questionnaireId"],
|
primaryKeys = ["clientCode", "questionnaireId"],
|
||||||
@ -81,4 +123,3 @@ data class CompletedQuestionnaire(
|
|||||||
val isDone: Boolean,
|
val isDone: Boolean,
|
||||||
val sumPoints: Int? = null
|
val sumPoints: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.data
|
||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@ -7,15 +7,36 @@ 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.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
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/*
|
||||||
|
Aufgabe:
|
||||||
|
- Baut eine Excel-Datei (XLSX) mit allen Clients als Zeilen und einem konfigurierbaren Spalten-Layout.
|
||||||
|
- Speichert die Datei ausschließlich in den öffentlichen „Downloads“-Ordner
|
||||||
|
|
||||||
|
Datenquelle:
|
||||||
|
- Liest die Spaltenreihenfolge/Spalten-IDs über HeaderOrderRepository.loadOrderedIds().
|
||||||
|
- Holt alle Clients, Fragebogen-IDs sowie Antworten aus der lokalen Room-Datenbank
|
||||||
|
|
||||||
|
Ausgabeformat (Sheet „Headers“):
|
||||||
|
- Zeile 1: Spalten-IDs (erste Zelle „#“ für laufende Nummer).
|
||||||
|
- Zeile 2: Englische Beschriftung/Fragetext je Spalte (ermittelt via englishQuestionForId + LanguageManager).
|
||||||
|
- Ab Zeile 3: Pro Client eine Datenzeile.
|
||||||
|
* Für Spalten-ID „client_code“: der Client-Code.
|
||||||
|
* Für Spalten-IDs, die einem Fragebogen entsprechen (Questionnaire-ID): „Done“/„Not Done“ (Abschlussstatus).
|
||||||
|
* Für sonstige Spalten-IDs (Antwort-IDs): Antwortwert oder „None“, falls leer.
|
||||||
|
*/
|
||||||
class ExcelExportService(
|
class ExcelExportService(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val headerRepo: HeaderOrderRepository
|
private val headerRepo: HeaderOrderRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/** Baut die Excel-Datei und speichert sie ausschließlich unter "Downloads". */
|
/* Baut die Excel-Datei und speichert sie ausschließlich unter "Downloads". */
|
||||||
suspend fun exportHeadersForAllClients(): Uri? {
|
suspend fun exportHeadersForAllClients(): Uri? {
|
||||||
val orderedIds = headerRepo.loadOrderedIds()
|
val orderedIds = headerRepo.loadOrderedIds()
|
||||||
if (orderedIds.isEmpty()) return null
|
if (orderedIds.isEmpty()) return null
|
||||||
@ -66,7 +87,7 @@ class ExcelExportService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val bytes = java.io.ByteArrayOutputStream().use { bos ->
|
val bytes = ByteArrayOutputStream().use { bos ->
|
||||||
wb.write(bos); bos.toByteArray()
|
wb.write(bos); bos.toByteArray()
|
||||||
}
|
}
|
||||||
wb.close()
|
wb.close()
|
||||||
@ -78,7 +99,6 @@ class ExcelExportService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Speichert Bytes nach "Downloads". */
|
|
||||||
private fun saveToDownloads(filename: String, mimeType: String, bytes: ByteArray): Uri? {
|
private fun saveToDownloads(filename: String, mimeType: String, bytes: ByteArray): Uri? {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val resolver = context.contentResolver
|
val resolver = context.contentResolver
|
||||||
@ -96,7 +116,7 @@ class ExcelExportService(
|
|||||||
} else {
|
} else {
|
||||||
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
if (!downloadsDir.exists()) downloadsDir.mkdirs()
|
if (!downloadsDir.exists()) downloadsDir.mkdirs()
|
||||||
val outFile = java.io.File(downloadsDir, filename)
|
val outFile = File(downloadsDir, filename)
|
||||||
outFile.writeBytes(bytes)
|
outFile.writeBytes(bytes)
|
||||||
MediaScannerConnection.scanFile(
|
MediaScannerConnection.scanFile(
|
||||||
context,
|
context,
|
||||||
@ -108,7 +128,6 @@ class ExcelExportService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Export-spezifische Lokalisierung (EN) ----------
|
|
||||||
private suspend fun englishQuestionForId(id: String, questionnaireIdSet: Set<String>): String {
|
private suspend fun englishQuestionForId(id: String, questionnaireIdSet: Set<String>): String {
|
||||||
if (id == "client_code") return "Client code"
|
if (id == "client_code") return "Client code"
|
||||||
if (id in questionnaireIdSet && !id.contains('-')) return "Questionnaire status"
|
if (id in questionnaireIdSet && !id.contains('-')) return "Questionnaire status"
|
||||||
@ -147,7 +166,7 @@ class ExcelExportService(
|
|||||||
return stripped
|
return stripped
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Englisch für Export; belässt Done/Not Done/None. */
|
/* Englisch für Export; belässt Done/Not Done/None. */
|
||||||
private fun localizeForExportEn(id: String, raw: String): String {
|
private fun localizeForExportEn(id: String, raw: String): String {
|
||||||
if (id == "client_code") return raw
|
if (id == "client_code") return raw
|
||||||
if (raw == "Done" || raw == "Not Done" || raw == "None") return raw
|
if (raw == "Done" || raw == "Not Done" || raw == "None") return raw
|
||||||
@ -160,4 +179,4 @@ class ExcelExportService(
|
|||||||
for (key in candidates) localizeEnglishNoBrackets(key)?.let { return it }
|
for (key in candidates) localizeEnglishNoBrackets(key)?.let { return it }
|
||||||
return raw
|
return raw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,17 +1,32 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.data
|
||||||
|
|
||||||
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 org.apache.poi.ss.usermodel.CellType
|
||||||
|
import org.apache.poi.ss.usermodel.DateUtil
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
class HeaderOrderRepository(private val context: Context) {
|
/*
|
||||||
|
Zweck:
|
||||||
|
- Liefert die Reihenfolge/IDs der zu exportierenden Header (Spalten) für den Excel-Export.
|
||||||
|
- Bevorzugte Quelle ist eine Excel-Datei aus den App-Assets („header_order.xlsx“), als Fallback wird eine JSON-Datei („header_order.json“) genutzt.
|
||||||
|
|
||||||
|
*/
|
||||||
|
class HeaderOrderRepository(
|
||||||
|
private val context: Context,
|
||||||
|
// Sprache abrufen (Standard: Deutsch, damit es ohne OpeningScreen schon sinnvoll ist)
|
||||||
|
private val languageIDProvider: () -> String = { "GERMAN" }
|
||||||
|
) {
|
||||||
|
|
||||||
private val tag = "HeaderOrderRepository"
|
private val tag = "HeaderOrderRepository"
|
||||||
private var orderedIdsCache: List<String>? = null
|
private var orderedIdsCache: List<String>? = null
|
||||||
|
|
||||||
|
private fun t(key: String): String = LanguageManager.getText(languageIDProvider(), key)
|
||||||
|
|
||||||
fun loadOrderedIds(): List<String> {
|
fun loadOrderedIds(): List<String> {
|
||||||
orderedIdsCache?.let { return it }
|
orderedIdsCache?.let { return it }
|
||||||
|
|
||||||
@ -33,7 +48,7 @@ class HeaderOrderRepository(private val context: Context) {
|
|||||||
list
|
list
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(tag, "Weder header_order.xlsx noch header_order.json verfügbar/gültig: ${e.message}")
|
Log.e(tag, "Weder header_order.xlsx noch header_order.json verfügbar/gültig: ${e.message}")
|
||||||
Toast.makeText(context, "Keine Header-Vorlage gefunden", Toast.LENGTH_LONG).show()
|
Toast.makeText(context, t("no_header_template_found"), Toast.LENGTH_LONG).show()
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,16 +66,16 @@ class HeaderOrderRepository(private val context: Context) {
|
|||||||
for (i in first until last) {
|
for (i in first until last) {
|
||||||
val cell = row.getCell(i) ?: continue
|
val cell = row.getCell(i) ?: continue
|
||||||
val value = when (cell.cellType) {
|
val value = when (cell.cellType) {
|
||||||
org.apache.poi.ss.usermodel.CellType.STRING -> cell.stringCellValue
|
CellType.STRING -> cell.stringCellValue
|
||||||
org.apache.poi.ss.usermodel.CellType.NUMERIC ->
|
CellType.NUMERIC ->
|
||||||
if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell))
|
if (DateUtil.isCellDateFormatted(cell))
|
||||||
cell.dateCellValue.time.toString()
|
cell.dateCellValue.time.toString()
|
||||||
else {
|
else {
|
||||||
val n = cell.numericCellValue
|
val n = cell.numericCellValue
|
||||||
if (n % 1.0 == 0.0) n.toLong().toString() else n.toString()
|
if (n % 1.0 == 0.0) n.toLong().toString() else n.toString()
|
||||||
}
|
}
|
||||||
org.apache.poi.ss.usermodel.CellType.BOOLEAN -> cell.booleanCellValue.toString()
|
CellType.BOOLEAN -> cell.booleanCellValue.toString()
|
||||||
org.apache.poi.ss.usermodel.CellType.FORMULA -> cell.richStringCellValue.string
|
CellType.FORMULA -> cell.richStringCellValue.string
|
||||||
else -> ""
|
else -> ""
|
||||||
}.trim()
|
}.trim()
|
||||||
|
|
||||||
@ -1,7 +1,8 @@
|
|||||||
package com.dano.test1
|
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
|
||||||
@ -14,7 +15,7 @@ import java.io.FileOutputStream
|
|||||||
object DatabaseDownloader {
|
object DatabaseDownloader {
|
||||||
|
|
||||||
private const val DB_NAME = "questionnaire_database"
|
private const val DB_NAME = "questionnaire_database"
|
||||||
private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php"
|
private const val SERVER_DOWNLOAD_URL = "https://daniel-ocks.de/qdb/downloadFull.php"
|
||||||
|
|
||||||
private val client = OkHttpClient()
|
private val client = OkHttpClient()
|
||||||
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
// app/src/main/java/com/dano/test1/DatabaseUploader.kt
|
package com.dano.test1.network
|
||||||
package com.dano.test1
|
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
@ -9,6 +8,7 @@ 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
|
||||||
@ -21,25 +21,11 @@ import kotlin.system.exitProcess
|
|||||||
object DatabaseUploader {
|
object DatabaseUploader {
|
||||||
|
|
||||||
private const val DB_NAME = "questionnaire_database"
|
private const val DB_NAME = "questionnaire_database"
|
||||||
private const val SERVER_DELTA_URL = "http://49.13.157.44/uploadDeltaTest5.php"
|
private const val SERVER_DELTA_URL = "https://daniel-ocks.de/qdb/uploadDeltaTest5.php"
|
||||||
private const val SERVER_CHECK_URL = "http://49.13.157.44/checkDatabaseExists.php"
|
private const val SERVER_CHECK_URL = "https://daniel-ocks.de/qdb/checkDatabaseExists.php"
|
||||||
|
|
||||||
private val client = OkHttpClient()
|
private val client = OkHttpClient()
|
||||||
|
|
||||||
/** NEU: Login mit Username+Password, danach Upload wie gehabt */
|
|
||||||
fun uploadDatabaseWithLogin(context: Context, username: String, password: String) {
|
|
||||||
LoginManager.loginUserWithCredentials(
|
|
||||||
context = context,
|
|
||||||
username = username,
|
|
||||||
password = password,
|
|
||||||
onSuccess = { token ->
|
|
||||||
Log.d("UPLOAD", "Login OK (user=$username)")
|
|
||||||
uploadDatabase(context, token)
|
|
||||||
},
|
|
||||||
onError = { msg -> Log.e("UPLOAD", "Login fehlgeschlagen: $msg") }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun uploadDatabase(context: Context, token: String) {
|
private fun uploadDatabase(context: Context, token: String) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
@ -49,6 +35,7 @@ object DatabaseUploader {
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WAL sauber schließen (falls aktiv)
|
||||||
try {
|
try {
|
||||||
val db = SQLiteDatabase.openDatabase(dbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE)
|
val db = SQLiteDatabase.openDatabase(dbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE)
|
||||||
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { /* noop */ }
|
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { /* noop */ }
|
||||||
@ -82,11 +69,14 @@ object DatabaseUploader {
|
|||||||
put("questionnaires", queryToJsonArray(db, "SELECT id FROM questionnaires"))
|
put("questionnaires", queryToJsonArray(db, "SELECT id FROM questionnaires"))
|
||||||
put("questions", queryToJsonArray(db, "SELECT questionId, questionnaireId, question FROM questions"))
|
put("questions", queryToJsonArray(db, "SELECT questionId, questionnaireId, question FROM questions"))
|
||||||
put("answers", queryToJsonArray(db, "SELECT clientCode, questionId, answerValue FROM answers"))
|
put("answers", queryToJsonArray(db, "SELECT clientCode, questionId, answerValue FROM answers"))
|
||||||
put("completed_questionnaires",
|
put(
|
||||||
queryToJsonArray(db, "SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires"))
|
"completed_questionnaires",
|
||||||
|
queryToJsonArray(db, "SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
// JSON -> verschlüsselte Payload
|
||||||
val tmpJson = File(context.cacheDir, "payload.json").apply { writeText(data.toString()) }
|
val tmpJson = File(context.cacheDir, "payload.json").apply { writeText(data.toString()) }
|
||||||
val tmpEnc = File(context.cacheDir, "payload.enc")
|
val tmpEnc = File(context.cacheDir, "payload.enc")
|
||||||
try {
|
try {
|
||||||
@ -98,11 +88,17 @@ object DatabaseUploader {
|
|||||||
|
|
||||||
val body = MultipartBody.Builder()
|
val body = MultipartBody.Builder()
|
||||||
.setType(MultipartBody.FORM)
|
.setType(MultipartBody.FORM)
|
||||||
.addFormDataPart("token", token)
|
.addFormDataPart("token", token) // bleibt für Kompatibilität enthalten
|
||||||
.addFormDataPart("file", "payload.enc", tmpEnc.asRequestBody("application/octet-stream".toMediaType()))
|
.addFormDataPart("file", "payload.enc", tmpEnc.asRequestBody("application/octet-stream".toMediaType()))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = Request.Builder().url("http://49.13.157.44/uploadDeltaTest5.php").post(body).build()
|
// WICHTIG: Jetzt HTTPS + Konstanten-URL verwenden, plus Bearer-Header
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(SERVER_DELTA_URL)
|
||||||
|
.post(body)
|
||||||
|
.header("Authorization", "Bearer $token")
|
||||||
|
.build()
|
||||||
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
client.newCall(request).enqueue(object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
Log.e("UPLOAD", "Fehlgeschlagen: ${e.message}")
|
Log.e("UPLOAD", "Fehlgeschlagen: ${e.message}")
|
||||||
@ -113,7 +109,7 @@ object DatabaseUploader {
|
|||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
Log.d("UPLOAD", "OK: $respBody")
|
Log.d("UPLOAD", "OK: $respBody")
|
||||||
|
|
||||||
// <<< alte Logik wieder aktivieren: lokale DB + Nebendateien löschen
|
// alte Logik: lokale DB + Nebendateien löschen
|
||||||
try {
|
try {
|
||||||
if (!file.delete()) Log.w("UPLOAD", "Lokale DB nicht gelöscht.")
|
if (!file.delete()) Log.w("UPLOAD", "Lokale DB nicht gelöscht.")
|
||||||
File(file.parent, "${file.name}-journal").delete()
|
File(file.parent, "${file.name}-journal").delete()
|
||||||
@ -122,11 +118,12 @@ object DatabaseUploader {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w("UPLOAD", "Fehler beim Löschen lokaler DB-Dateien", e)
|
Log.w("UPLOAD", "Fehler beim Löschen lokaler DB-Dateien", e)
|
||||||
}
|
}
|
||||||
// >>>
|
|
||||||
} else {
|
} else {
|
||||||
Log.e("UPLOAD", "HTTP ${response.code}: $respBody")
|
Log.e("UPLOAD", "HTTP ${response.code}: $respBody")
|
||||||
}
|
}
|
||||||
tmpJson.delete(); tmpEnc.delete()
|
tmpJson.delete(); tmpEnc.delete()
|
||||||
|
|
||||||
|
// unverändert beibehalten
|
||||||
try { exitProcess(0) } catch (_: Exception) {}
|
try { exitProcess(0) } catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -162,6 +159,8 @@ object DatabaseUploader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun uploadDatabaseWithToken(context: Context, token: String) {
|
fun uploadDatabaseWithToken(context: Context, token: String) {
|
||||||
uploadDatabase(context, token) // nutzt die bestehende interne Logik
|
uploadDatabase(context, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
218
app/src/main/java/com/dano/test1/network/LoginManager.kt
Normal file
218
app/src/main/java/com/dano/test1/network/LoginManager.kt
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
package com.dano.test1.network
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.InputType
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Toast
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
object LoginManager {
|
||||||
|
private const val SERVER_LOGIN_URL = "https://daniel-ocks.de/qdb/login.php"
|
||||||
|
private const val SERVER_CHANGE_URL = "https://daniel-ocks.de/qdb/change_password.php"
|
||||||
|
private val client = OkHttpClient()
|
||||||
|
|
||||||
|
fun loginUserWithCredentials(
|
||||||
|
context: Context,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
onSuccess: (String) -> Unit,
|
||||||
|
onError: (String) -> Unit
|
||||||
|
) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val bodyJson = JSONObject()
|
||||||
|
.put("username", username)
|
||||||
|
.put("password", password)
|
||||||
|
.toString()
|
||||||
|
.toRequestBody("application/json".toMediaType())
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(SERVER_LOGIN_URL)
|
||||||
|
.post(bodyJson)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
val text = response.body?.string()
|
||||||
|
|
||||||
|
if (!response.isSuccessful || text == null) {
|
||||||
|
withContext(Dispatchers.Main) { onError("Fehler beim Login (${response.code})") }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = JSONObject(text)
|
||||||
|
if (!json.optBoolean("success")) {
|
||||||
|
withContext(Dispatchers.Main) { onError(json.optString("message", "Login fehlgeschlagen")) }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwortwechsel erforderlich?
|
||||||
|
if (json.optBoolean("must_change_password", false)) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
showChangePasswordDialog(
|
||||||
|
context = context,
|
||||||
|
username = username,
|
||||||
|
oldPassword = password,
|
||||||
|
onChanged = { token ->
|
||||||
|
// Nach PW-Änderung direkt eingeloggt
|
||||||
|
TokenStore.save(context, token, username)
|
||||||
|
onSuccess(token)
|
||||||
|
},
|
||||||
|
onError = onError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// normaler Login: Token speichern
|
||||||
|
val token = json.getString("token")
|
||||||
|
TokenStore.save(context, token, username)
|
||||||
|
withContext(Dispatchers.Main) { onSuccess(token) }
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("LOGIN", "Exception", e)
|
||||||
|
withContext(Dispatchers.Main) { onError("Exception: ${e.message}") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showChangePasswordDialog(
|
||||||
|
context: Context,
|
||||||
|
username: String,
|
||||||
|
oldPassword: String,
|
||||||
|
onChanged: (String) -> Unit,
|
||||||
|
onError: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val container = LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
setPadding(48, 24, 48, 0)
|
||||||
|
}
|
||||||
|
val etNew = EditText(context).apply {
|
||||||
|
hint = "Neues Passwort"
|
||||||
|
inputType = InputType.TYPE_CLASS_TEXT or
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
}
|
||||||
|
val etRepeat = EditText(context).apply {
|
||||||
|
hint = "Neues Passwort (wiederholen)"
|
||||||
|
inputType = InputType.TYPE_CLASS_TEXT or
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
}
|
||||||
|
container.addView(etNew)
|
||||||
|
container.addView(etRepeat)
|
||||||
|
|
||||||
|
val dialog = AlertDialog.Builder(context)
|
||||||
|
.setTitle("Passwort ändern")
|
||||||
|
.setMessage("Du verwendest ein Standard-Konto. Bitte setze jetzt ein eigenes Passwort.")
|
||||||
|
.setView(container)
|
||||||
|
.setPositiveButton("OK", null) // nicht sofort schließen lassen
|
||||||
|
.setNegativeButton("Abbrechen", null) // nicht sofort schließen lassen
|
||||||
|
.setCancelable(false)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.setOnShowListener {
|
||||||
|
val btnOk = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
|
val btnCancel = dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
|
||||||
|
|
||||||
|
btnOk.setOnClickListener {
|
||||||
|
etNew.error = null
|
||||||
|
etRepeat.error = null
|
||||||
|
|
||||||
|
val p1 = etNew.text?.toString().orEmpty()
|
||||||
|
val p2 = etRepeat.text?.toString().orEmpty()
|
||||||
|
|
||||||
|
when {
|
||||||
|
p1.length < 6 -> {
|
||||||
|
etNew.error = "Mindestens 6 Zeichen."
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
p1 != p2 -> {
|
||||||
|
etRepeat.error = "Passwörter stimmen nicht überein."
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
btnOk.isEnabled = false
|
||||||
|
btnCancel.isEnabled = false
|
||||||
|
changePassword(
|
||||||
|
context = context,
|
||||||
|
username = username,
|
||||||
|
oldPassword = oldPassword,
|
||||||
|
newPassword = p1,
|
||||||
|
onChanged = { token ->
|
||||||
|
dialog.dismiss()
|
||||||
|
onChanged(token)
|
||||||
|
},
|
||||||
|
onError = { msg ->
|
||||||
|
btnOk.isEnabled = true
|
||||||
|
btnCancel.isEnabled = true
|
||||||
|
Toast.makeText(context, msg, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// >>> Überarbeitet: Abbrechen schließt Dialog und informiert den Aufrufer
|
||||||
|
btnCancel.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
onError("Passwortänderung abgebrochen.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private fun changePassword(
|
||||||
|
context: Context,
|
||||||
|
username: String,
|
||||||
|
oldPassword: String,
|
||||||
|
newPassword: String,
|
||||||
|
onChanged: (String) -> Unit,
|
||||||
|
onError: (String) -> Unit
|
||||||
|
) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val body = JSONObject()
|
||||||
|
.put("username", username)
|
||||||
|
.put("old_password", oldPassword)
|
||||||
|
.put("new_password", newPassword)
|
||||||
|
.toString()
|
||||||
|
.toRequestBody("application/json".toMediaType())
|
||||||
|
|
||||||
|
val req = Request.Builder()
|
||||||
|
.url(SERVER_CHANGE_URL)
|
||||||
|
.post(body)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val resp = client.newCall(req).execute()
|
||||||
|
val txt = resp.body?.string()
|
||||||
|
|
||||||
|
if (!resp.isSuccessful || txt == null) {
|
||||||
|
withContext(Dispatchers.Main) { onError("Fehler beim Ändern (${resp.code})") }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = JSONObject(txt)
|
||||||
|
if (!json.optBoolean("success")) {
|
||||||
|
withContext(Dispatchers.Main) { onError(json.optString("message", "Ändern fehlgeschlagen")) }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val token = json.getString("token")
|
||||||
|
withContext(Dispatchers.Main) { onChanged(token) }
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("LOGIN", "changePassword Exception", e)
|
||||||
|
withContext(Dispatchers.Main) { onError("Exception: ${e.message}") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/src/main/java/com/dano/test1/network/NetworkUtilis.kt
Normal file
33
app/src/main/java/com/dano/test1/network/NetworkUtilis.kt
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package com.dano.test1.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zweck:
|
||||||
|
- Einfache Hilfsklasse, um den aktuellen Online-Status des Geräts zu prüfen.
|
||||||
|
|
||||||
|
Funktionsweise:
|
||||||
|
- `isOnline(context)` nutzt den systemweiten `ConnectivityManager`, fragt die aktive Verbindung (`activeNetwork`) ab und prüft deren `NetworkCapabilities`.
|
||||||
|
- Es wird nur dann `true` zurückgegeben, wenn:
|
||||||
|
* eine aktive Verbindung existiert und
|
||||||
|
* die Verbindung die Fähigkeit „INTERNET“ besitzt und
|
||||||
|
* die Verbindung als „VALIDATED“ gilt (vom System als funktionsfähig verifiziert).
|
||||||
|
|
||||||
|
Verwendung:
|
||||||
|
- Vo Netzwerkaufrufen (Login, Upload, Download) aufrufen, um „Offline“-Fälle frühzeitig abzufangen und nutzerfreundliche Meldungen zu zeigen.
|
||||||
|
*/
|
||||||
|
object NetworkUtils {
|
||||||
|
fun isOnline(context: Context): Boolean {
|
||||||
|
return try {
|
||||||
|
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? ?: return false
|
||||||
|
val network = cm.activeNetwork ?: return false
|
||||||
|
val caps = cm.getNetworkCapabilities(network) ?: return false
|
||||||
|
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||||
|
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
|
} catch (_: SecurityException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,21 +1,33 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.network
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
||||||
|
/*
|
||||||
|
TokenStore
|
||||||
|
- Kleiner Helper zum Verwalten der Login-Sitzung
|
||||||
|
- Speichert:
|
||||||
|
* das API-Token (KEY_TOKEN),
|
||||||
|
* den Usernamen (KEY_USER),
|
||||||
|
* den Zeitpunkt des Logins in Millisekunden (KEY_LOGIN_TS).
|
||||||
|
- Bietet Lese-/Schreib-Methoden sowie ein clear(), um alles zurückzusetzen.
|
||||||
|
|
||||||
|
Hinweis:
|
||||||
|
- Die Daten liegen in einer privaten App-Preference-Datei (PREF = "qdb_prefs").
|
||||||
|
- getLoginTimestamp() eignet sich, um die Token-„Frische“ (Alter) zu berechnen.
|
||||||
|
*/
|
||||||
object TokenStore {
|
object TokenStore {
|
||||||
private const val PREF = "qdb_prefs"
|
private const val PREF = "qdb_prefs"
|
||||||
private const val KEY_TOKEN = "token"
|
private const val KEY_TOKEN = "token"
|
||||||
private const val KEY_USER = "user"
|
private const val KEY_USER = "user"
|
||||||
private const val KEY_LOGIN_TS = "login_ts"
|
private const val KEY_LOGIN_TS = "login_ts"
|
||||||
|
|
||||||
/** Speichert Token, Username und Login-Timestamp (jetzt) */
|
|
||||||
fun save(context: Context, token: String, username: String) {
|
fun save(context: Context, token: String, username: String) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
|
||||||
.edit()
|
.edit()
|
||||||
.putString(KEY_TOKEN, token)
|
.putString(KEY_TOKEN, token) // API-/Session-Token
|
||||||
.putString(KEY_USER, username)
|
.putString(KEY_USER, username) // angemeldeter Benutzername
|
||||||
.putLong(KEY_LOGIN_TS, now)
|
.putLong(KEY_LOGIN_TS, now) // Zeitpunkt des Logins
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.questionnaire
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
||||||
interface QuestionHandler {
|
interface QuestionHandler {
|
||||||
@ -1,10 +1,23 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.questionnaire
|
||||||
|
|
||||||
|
import android.R
|
||||||
import android.app.Activity
|
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.MainActivity
|
||||||
|
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.*
|
||||||
@ -61,8 +74,8 @@ abstract class QuestionnaireBase<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected fun setupSpinner(spinner: Spinner, spinnerValues: List<Any>, selectedValue: Any?) {
|
protected fun setupSpinner(spinner: Spinner, spinnerValues: List<Any>, selectedValue: Any?) {
|
||||||
val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, spinnerValues).apply {
|
val adapter = ArrayAdapter(context, R.layout.simple_spinner_item, spinnerValues).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
spinner.adapter = adapter
|
spinner.adapter = adapter
|
||||||
selectedValue?.let { value ->
|
selectedValue?.let { value ->
|
||||||
@ -75,7 +88,7 @@ abstract class QuestionnaireBase<T> {
|
|||||||
|
|
||||||
protected fun navigateTo(layoutResId: Int, setup: (View) -> Unit) {
|
protected fun navigateTo(layoutResId: Int, setup: (View) -> Unit) {
|
||||||
context.setContentView(layoutResId)
|
context.setContentView(layoutResId)
|
||||||
val rootView = context.findViewById<View>(android.R.id.content)
|
val rootView = context.findViewById<View>(R.id.content)
|
||||||
setup(rootView)
|
setup(rootView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +98,7 @@ abstract class QuestionnaireBase<T> {
|
|||||||
|
|
||||||
protected fun showEmptyScreen() {
|
protected fun showEmptyScreen() {
|
||||||
navigateTo(getLayoutResId("empty")) {
|
navigateTo(getLayoutResId("empty")) {
|
||||||
setupPrevButton(R.id.Qprev) { goToPreviousQuestion() }
|
setupPrevButton(com.dano.test1.R.id.Qprev) { goToPreviousQuestion() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.questionnaire
|
||||||
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
|
import com.dano.test1.LocalizationHelper
|
||||||
|
import com.dano.test1.R
|
||||||
|
|
||||||
open class QuestionnaireGeneric(private val questionnaireFileName: String) : QuestionnaireBase<Unit>() {
|
open class QuestionnaireGeneric(private val questionnaireFileName: String) : QuestionnaireBase<Unit>() {
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.questionnaire
|
||||||
|
|
||||||
data class Option(
|
data class Option(
|
||||||
val key: String, // Must always be set
|
val key: String, // Must always be set
|
||||||
@ -1,14 +1,25 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.questionnaire.handlers
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import android.util.TypedValue
|
import com.dano.test1.questionnaire.GlobalValues
|
||||||
import androidx.core.widget.TextViewCompat
|
import com.dano.test1.LanguageManager
|
||||||
|
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.network.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
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zweck :
|
||||||
|
- Steuert die Eingabeseite für „Client Code“ und „Coach Code“ innerhalb des Fragebogen-Flows.
|
||||||
|
*/
|
||||||
|
|
||||||
class HandlerClientCoachCode(
|
class HandlerClientCoachCode(
|
||||||
private val answers: MutableMap<String, Any>,
|
private val answers: MutableMap<String, Any>,
|
||||||
private val languageID: String,
|
private val languageID: String,
|
||||||
@ -33,10 +44,10 @@ class HandlerClientCoachCode(
|
|||||||
|
|
||||||
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||||
|
|
||||||
setTextSizePercentOfScreenHeight(titleTextView, 0.03f)
|
ViewUtils.setTextSizePercentOfScreenHeight(titleTextView, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(clientCodeField, 0.025f)
|
ViewUtils.setTextSizePercentOfScreenHeight(clientCodeField, 0.025f)
|
||||||
setTextSizePercentOfScreenHeight(coachCodeField, 0.025f)
|
ViewUtils.setTextSizePercentOfScreenHeight(coachCodeField, 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
|
||||||
@ -48,11 +59,11 @@ class HandlerClientCoachCode(
|
|||||||
clientCodeField.isEnabled = true
|
clientCodeField.isEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// === NEU: Coach-Code immer aus dem Login (TokenStore) setzen und sperren ===
|
// Coach-Code immer aus dem Login (TokenStore) setzen und sperren
|
||||||
val coachFromLogin = TokenStore.getUsername(layout.context)
|
val coachFromLogin = TokenStore.getUsername(layout.context)
|
||||||
if (!coachFromLogin.isNullOrBlank()) {
|
if (!coachFromLogin.isNullOrBlank()) {
|
||||||
coachCodeField.setText(coachFromLogin)
|
coachCodeField.setText(coachFromLogin)
|
||||||
lockCoachField(coachCodeField) // optisch & technisch gesperrt
|
ViewUtils.lockEditField(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 ?: "")
|
||||||
@ -67,13 +78,6 @@ class HandlerClientCoachCode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
|
||||||
val dm = layout.resources.displayMetrics
|
|
||||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
|
||||||
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
|
|
||||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onNextClicked(clientCodeField: EditText, coachCodeField: EditText) {
|
private fun onNextClicked(clientCodeField: EditText, coachCodeField: EditText) {
|
||||||
val loadedClientCode = GlobalValues.LOADED_CLIENT_CODE
|
val loadedClientCode = GlobalValues.LOADED_CLIENT_CODE
|
||||||
|
|
||||||
@ -122,7 +126,6 @@ class HandlerClientCoachCode(
|
|||||||
|
|
||||||
override fun validate(): Boolean {
|
override fun validate(): Boolean {
|
||||||
val clientCode = layout.findViewById<EditText>(R.id.client_code).text
|
val clientCode = layout.findViewById<EditText>(R.id.client_code).text
|
||||||
// Validierung nimmt den (ggf. gesperrten) Text – passt
|
|
||||||
val coachText = layout.findViewById<EditText>(R.id.coach_code).text
|
val coachText = layout.findViewById<EditText>(R.id.coach_code).text
|
||||||
return clientCode.isNotBlank() && coachText.isNotBlank()
|
return clientCode.isNotBlank() && coachText.isNotBlank()
|
||||||
}
|
}
|
||||||
@ -139,20 +142,4 @@ class HandlerClientCoachCode(
|
|||||||
// Not used
|
// Not used
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helfer zum Sperren inkl. optischer Markierung (wie im Opening Screen) ---
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
@ -1,7 +1,17 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.questionnaire.handlers
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import com.dano.test1.LanguageManager
|
||||||
|
import com.dano.test1.questionnaire.QuestionHandler
|
||||||
|
import com.dano.test1.questionnaire.QuestionItem
|
||||||
|
import com.dano.test1.R
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zweck:
|
||||||
|
- Steuert die Seite „Client hat nicht unterschrieben“ im Fragebogenfluss.
|
||||||
|
- Speichert den eingegebenen Coach-Code in das Answers-Map unter question.id (saveAnswer), damit der nachfolgende Prozess darauf zugreifen kann.
|
||||||
|
*/
|
||||||
|
|
||||||
class HandlerClientNotSigned(
|
class HandlerClientNotSigned(
|
||||||
private val answers: MutableMap<String, Any>,
|
private val answers: MutableMap<String, Any>,
|
||||||
@ -14,7 +24,6 @@ class HandlerClientNotSigned(
|
|||||||
private lateinit var layout: View
|
private lateinit var layout: View
|
||||||
private lateinit var question: QuestionItem.ClientNotSigned
|
private lateinit var question: QuestionItem.ClientNotSigned
|
||||||
|
|
||||||
// UI components
|
|
||||||
private lateinit var textView1: TextView
|
private lateinit var textView1: TextView
|
||||||
private lateinit var textView2: TextView
|
private lateinit var textView2: TextView
|
||||||
private lateinit var questionTextView: TextView
|
private lateinit var questionTextView: TextView
|
||||||
@ -26,29 +35,24 @@ class HandlerClientNotSigned(
|
|||||||
this.layout = layout
|
this.layout = layout
|
||||||
this.question = question
|
this.question = question
|
||||||
|
|
||||||
// Initialize UI components only once
|
|
||||||
initViews()
|
initViews()
|
||||||
|
|
||||||
// Set localized text values from LanguageManager
|
|
||||||
textView1.text = question.textKey1?.let { LanguageManager.getText(languageID, it) } ?: ""
|
textView1.text = question.textKey1?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||||
textView2.text = question.textKey2?.let { LanguageManager.getText(languageID, it) } ?: ""
|
textView2.text = question.textKey2?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||||
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||||
|
|
||||||
// Populate EditText with previous value if exists
|
|
||||||
coachCodeField.setText(answers[question.id] as? String ?: "")
|
coachCodeField.setText(answers[question.id] as? String ?: "")
|
||||||
|
|
||||||
// Set click listener for Next button
|
|
||||||
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
|
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
|
||||||
onNextClicked()
|
onNextClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set click listener for Previous button
|
|
||||||
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
|
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
|
||||||
goToPreviousQuestion()
|
goToPreviousQuestion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize all views once to avoid repeated findViewById calls
|
|
||||||
private fun initViews() {
|
private fun initViews() {
|
||||||
textView1 = layout.findViewById(R.id.textView1)
|
textView1 = layout.findViewById(R.id.textView1)
|
||||||
textView2 = layout.findViewById(R.id.textView2)
|
textView2 = layout.findViewById(R.id.textView2)
|
||||||
@ -56,7 +60,6 @@ class HandlerClientNotSigned(
|
|||||||
coachCodeField = layout.findViewById(R.id.coach_code)
|
coachCodeField = layout.findViewById(R.id.coach_code)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Next button click
|
|
||||||
private fun onNextClicked() {
|
private fun onNextClicked() {
|
||||||
if (validate()) {
|
if (validate()) {
|
||||||
saveAnswer()
|
saveAnswer()
|
||||||
@ -67,13 +70,11 @@ class HandlerClientNotSigned(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that coach code field is not empty
|
|
||||||
override fun validate(): Boolean {
|
override fun validate(): Boolean {
|
||||||
val coachCode = coachCodeField.text
|
val coachCode = coachCodeField.text
|
||||||
return coachCode.isNotBlank()
|
return coachCode.isNotBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save entered coach code to answers map
|
|
||||||
override fun saveAnswer() {
|
override fun saveAnswer() {
|
||||||
answers[question.id] = coachCodeField.text.toString()
|
answers[question.id] = coachCodeField.text.toString()
|
||||||
}
|
}
|
||||||
@ -1,15 +1,26 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.questionnaire.handlers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import android.util.TypedValue
|
import com.dano.test1.questionnaire.GlobalValues
|
||||||
import androidx.core.widget.TextViewCompat
|
import com.dano.test1.LanguageManager
|
||||||
import android.widget.AbsListView
|
import com.dano.test1.questionnaire.MAX_VALUE_YEAR
|
||||||
|
import com.dano.test1.ui.Month
|
||||||
|
import com.dano.test1.ui.Months
|
||||||
|
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.utils.ViewUtils
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zweck:
|
||||||
|
Rendert eine Datumsfrage mit drei Spinnern (Tag/Monat/Jahr) innerhalb des Fragebogen-Flows.
|
||||||
|
*/
|
||||||
|
|
||||||
class HandlerDateSpinner(
|
class HandlerDateSpinner(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@ -46,13 +57,12 @@ 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 ——
|
// Schriftgrößen pro Bildschirmhöhe
|
||||||
setTextSizePercentOfScreenHeight(textView, 0.03f) // oben
|
ViewUtils.setTextSizePercentOfScreenHeight(textView, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f) // frage
|
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(labelDay, 0.025f)
|
ViewUtils.setTextSizePercentOfScreenHeight(labelDay, 0.025f)
|
||||||
setTextSizePercentOfScreenHeight(labelMonth, 0.025f)
|
ViewUtils.setTextSizePercentOfScreenHeight(labelMonth, 0.025f)
|
||||||
setTextSizePercentOfScreenHeight(labelYear, 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 {
|
||||||
@ -70,9 +80,9 @@ class HandlerDateSpinner(
|
|||||||
val defaultYear = savedYear ?: today.get(Calendar.YEAR)
|
val defaultYear = savedYear ?: today.get(Calendar.YEAR)
|
||||||
|
|
||||||
// Spinner responsiv aufsetzen (Schrift + Zeilenhöhe ohne Abschneiden)
|
// Spinner responsiv aufsetzen (Schrift + Zeilenhöhe ohne Abschneiden)
|
||||||
setupSpinner(spinnerDay, days, defaultDay)
|
ViewUtils.setupResponsiveSpinner(context, spinnerDay, days, defaultDay)
|
||||||
setupSpinner(spinnerMonth, months, defaultMonth)
|
ViewUtils.setupResponsiveSpinner(context, spinnerMonth, months, defaultMonth)
|
||||||
setupSpinner(spinnerYear, 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 ?: "")
|
||||||
@ -202,72 +212,4 @@ class HandlerDateSpinner(
|
|||||||
return sdf.parse(dateString)
|
return sdf.parse(dateString)
|
||||||
}
|
}
|
||||||
|
|
||||||
// —— Textgröße prozentual zur Bildschirmhöhe (in sp) ——
|
|
||||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
|
||||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
|
||||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
|
||||||
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
|
|
||||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// —— Spinner-Adapter: Schrift & Zeilenhöhe dynamisch, kein Abschneiden ——
|
|
||||||
private fun <T> setupSpinner(spinner: Spinner, items: List<T>, defaultSelection: 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) // ~2.75% der Bildschirmhöhe
|
|
||||||
val textPx = pxFromSp(textSp)
|
|
||||||
val vPadPx = (textPx * 0.50f).toInt() // vertikales Padding
|
|
||||||
val rowHeight = (textPx * 2.20f + 2 * vPadPx).toInt() // feste Zeilenhöhe
|
|
||||||
|
|
||||||
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 android.view.Gravity.HORIZONTAL_GRAVITY_MASK) or android.view.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) // ausgewählte Ansicht
|
|
||||||
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) // Dropdown-Zeilen: Höhe erzwingen
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
||||||
spinner.adapter = adapter
|
|
||||||
|
|
||||||
// Spinner selbst ausreichend hoch
|
|
||||||
spinner.setPadding(spinner.paddingLeft, vPadPx, spinner.paddingRight, vPadPx)
|
|
||||||
spinner.minimumHeight = rowHeight
|
|
||||||
spinner.requestLayout()
|
|
||||||
|
|
||||||
defaultSelection?.let {
|
|
||||||
val index = items.indexOf(it)
|
|
||||||
if (index >= 0) spinner.setSelection(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,13 +1,25 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.questionnaire.handlers
|
||||||
|
|
||||||
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 androidx.core.widget.TextViewCompat
|
import com.dano.test1.questionnaire.GlobalValues
|
||||||
|
import com.dano.test1.LanguageManager
|
||||||
|
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.utils.ViewUtils
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zweck:
|
||||||
|
- „Glas-Skala“-Frage: pro Symptom (Zeile) genau eine von fünf Stufen.
|
||||||
|
- Stufen als RadioButtons (clickbar) + Icon-Header.
|
||||||
|
- FIX: Pro Zeile wird Single-Select erzwungen (auch im Bearbeiten-Modus / Restore).
|
||||||
|
*/
|
||||||
|
|
||||||
class HandlerGlassScaleQuestion(
|
class HandlerGlassScaleQuestion(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val answers: MutableMap<String, Any>,
|
private val answers: MutableMap<String, Any>,
|
||||||
@ -46,6 +58,9 @@ class HandlerGlassScaleQuestion(
|
|||||||
"extreme_glass" to R.drawable.ic_glass_4
|
"extreme_glass" to R.drawable.ic_glass_4
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Damit wir beim programmatic-check keine Endlosschleifen triggern
|
||||||
|
private var suppressRowListener: Boolean = false
|
||||||
|
|
||||||
override fun bind(layout: View, question: QuestionItem) {
|
override fun bind(layout: View, question: QuestionItem) {
|
||||||
if (question !is QuestionItem.GlassScaleQuestion) return
|
if (question !is QuestionItem.GlassScaleQuestion) return
|
||||||
this.layout = layout
|
this.layout = layout
|
||||||
@ -57,15 +72,16 @@ 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) } ?: ""
|
||||||
|
|
||||||
setTextSizePercentOfScreenHeight(titleTv, 0.03f)
|
ViewUtils.setTextSizePercentOfScreenHeight(titleTv, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(questionTv, 0.03f)
|
ViewUtils.setTextSizePercentOfScreenHeight(questionTv, 0.03f)
|
||||||
|
|
||||||
// ----- feste Icon-Leiste -----
|
// Header Icons
|
||||||
val header = layout.findViewById<LinearLayout>(R.id.glass_header)
|
val header = layout.findViewById<LinearLayout>(R.id.glass_header)
|
||||||
header.removeAllViews()
|
header.removeAllViews()
|
||||||
header.addView(Space(context).apply {
|
header.addView(Space(context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 4f)
|
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 4f)
|
||||||
})
|
})
|
||||||
|
|
||||||
val iconSizePx = (context.resources.displayMetrics.density * 36).toInt()
|
val iconSizePx = (context.resources.displayMetrics.density * 36).toInt()
|
||||||
scaleLabels.forEach { labelKey ->
|
scaleLabels.forEach { labelKey ->
|
||||||
val cell = FrameLayout(context).apply {
|
val cell = FrameLayout(context).apply {
|
||||||
@ -80,13 +96,12 @@ class HandlerGlassScaleQuestion(
|
|||||||
cell.addView(img)
|
cell.addView(img)
|
||||||
header.addView(cell)
|
header.addView(cell)
|
||||||
}
|
}
|
||||||
// -----------------------------
|
|
||||||
|
|
||||||
val tableLayout = layout.findViewById<TableLayout>(R.id.glass_table)
|
val tableLayout = layout.findViewById<TableLayout>(R.id.glass_table)
|
||||||
tableLayout.removeAllViews()
|
tableLayout.removeAllViews()
|
||||||
addSymptomRows(tableLayout)
|
addSymptomRows(tableLayout)
|
||||||
|
|
||||||
// ggf. Antworten aus DB wiederherstellen
|
// Restore aus DB (nur wenn noch nicht im answers)
|
||||||
val anySymptomNeedsRestore = question.symptoms.any { !answers.containsKey(it) }
|
val anySymptomNeedsRestore = question.symptoms.any { !answers.containsKey(it) }
|
||||||
if (anySymptomNeedsRestore) {
|
if (anySymptomNeedsRestore) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
@ -96,21 +111,14 @@ class HandlerGlassScaleQuestion(
|
|||||||
val answerMap = allAnswersForClient.associateBy({ it.questionId }, { it.answerValue })
|
val answerMap = allAnswersForClient.associateBy({ it.questionId }, { it.answerValue })
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val table = tableLayout
|
|
||||||
for ((index, symptomKey) in question.symptoms.withIndex()) {
|
for ((index, symptomKey) in question.symptoms.withIndex()) {
|
||||||
val answerMapKey = "$questionnaireMeta-$symptomKey"
|
val answerMapKey = "$questionnaireMeta-$symptomKey"
|
||||||
val dbAnswer = answerMap[answerMapKey]?.takeIf { it.isNotBlank() }?.trim()
|
val dbAnswer = answerMap[answerMapKey]?.takeIf { it.isNotBlank() }?.trim()
|
||||||
if (!answers.containsKey(symptomKey) && !dbAnswer.isNullOrBlank()) {
|
if (!answers.containsKey(symptomKey) && !dbAnswer.isNullOrBlank()) {
|
||||||
if (index < table.childCount) {
|
if (index < tableLayout.childCount) {
|
||||||
val row = table.getChildAt(index) as? TableRow ?: continue
|
val row = tableLayout.getChildAt(index) as? TableRow ?: continue
|
||||||
val radioGroup = row.getChildAt(1) as? RadioGroup ?: continue
|
val radioGroup = row.getChildAt(1) as? RadioGroup ?: continue
|
||||||
for (i in 0 until radioGroup.childCount) {
|
setSingleSelection(radioGroup, dbAnswer) // <-- FIXED restore
|
||||||
val rb = getRadioFromChild(radioGroup.getChildAt(i)) ?: continue
|
|
||||||
if ((rb.tag as? String)?.trim() == dbAnswer) {
|
|
||||||
rb.isChecked = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
answers[symptomKey] = dbAnswer
|
answers[symptomKey] = dbAnswer
|
||||||
points.add(pointsMap[dbAnswer] ?: 0)
|
points.add(pointsMap[dbAnswer] ?: 0)
|
||||||
}
|
}
|
||||||
@ -147,28 +155,63 @@ 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)
|
||||||
setTextSizePercentOfScreenHeight(this, 0.022f)
|
ViewUtils.setTextSizePercentOfScreenHeight(this, 0.022f)
|
||||||
}
|
}
|
||||||
row.addView(symptomText)
|
row.addView(symptomText)
|
||||||
|
|
||||||
val radioGroup = RadioGroup(context).apply {
|
val radioGroup = RadioGroup(context).apply {
|
||||||
orientation = RadioGroup.HORIZONTAL
|
orientation = RadioGroup.HORIZONTAL
|
||||||
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 5f)
|
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 5f)
|
||||||
|
setPadding(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WICHTIG: RadioButtons sind direkte Kinder des RadioGroup!
|
// Build buttons
|
||||||
scaleLabels.forEach { labelKey ->
|
scaleLabels.forEach { labelKey ->
|
||||||
|
val cell = FrameLayout(context).apply {
|
||||||
|
layoutParams = RadioGroup.LayoutParams(0, RadioGroup.LayoutParams.WRAP_CONTENT, 1f)
|
||||||
|
}
|
||||||
|
|
||||||
val rb = RadioButton(context).apply {
|
val rb = RadioButton(context).apply {
|
||||||
tag = labelKey
|
tag = labelKey
|
||||||
id = View.generateViewId()
|
id = View.generateViewId()
|
||||||
isChecked = savedLabel == labelKey
|
isChecked = false
|
||||||
setPadding(0, 0, 0, 0)
|
setPadding(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
val lp = RadioGroup.LayoutParams(
|
|
||||||
0, RadioGroup.LayoutParams.WRAP_CONTENT, 1f
|
rb.layoutParams = FrameLayout.LayoutParams(
|
||||||
).apply { gravity = Gravity.CENTER }
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
rb.layoutParams = lp
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
radioGroup.addView(rb)
|
Gravity.CENTER
|
||||||
|
)
|
||||||
|
|
||||||
|
// <<< FIX: erzwinge pro Zeile genau eine Auswahl
|
||||||
|
rb.setOnCheckedChangeListener { buttonView, isChecked ->
|
||||||
|
if (suppressRowListener) return@setOnCheckedChangeListener
|
||||||
|
if (!isChecked) return@setOnCheckedChangeListener
|
||||||
|
|
||||||
|
val selectedLabel = buttonView.tag as? String ?: return@setOnCheckedChangeListener
|
||||||
|
// wenn einer checked -> alle anderen in dieser Row unchecken
|
||||||
|
suppressRowListener = true
|
||||||
|
try {
|
||||||
|
for (i in 0 until radioGroup.childCount) {
|
||||||
|
val other = getRadioFromChild(radioGroup.getChildAt(i)) ?: continue
|
||||||
|
if (other != buttonView) other.isChecked = false
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
suppressRowListener = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional (wenn du willst): sofort im answers setzen (Edit-Mode fühlt sich dann "direkt" an)
|
||||||
|
answers[symptomKey] = selectedLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.addView(rb)
|
||||||
|
radioGroup.addView(cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore aus answers (falls vorhanden)
|
||||||
|
if (!savedLabel.isNullOrBlank()) {
|
||||||
|
setSingleSelection(radioGroup, savedLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
row.addView(radioGroup)
|
row.addView(radioGroup)
|
||||||
@ -176,6 +219,22 @@ class HandlerGlassScaleQuestion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt in diesem Row-RadioGroup genau einen Wert aktiv und alle anderen aus.
|
||||||
|
*/
|
||||||
|
private fun setSingleSelection(radioGroup: RadioGroup, labelKey: String) {
|
||||||
|
suppressRowListener = true
|
||||||
|
try {
|
||||||
|
for (i in 0 until radioGroup.childCount) {
|
||||||
|
val rb = getRadioFromChild(radioGroup.getChildAt(i)) ?: continue
|
||||||
|
val tag = (rb.tag as? String)?.trim()
|
||||||
|
rb.isChecked = (tag == labelKey.trim())
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
suppressRowListener = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun validate(): Boolean {
|
override fun validate(): Boolean {
|
||||||
val table = layout.findViewById<TableLayout>(R.id.glass_table)
|
val table = layout.findViewById<TableLayout>(R.id.glass_table)
|
||||||
for (i in 0 until table.childCount) {
|
for (i in 0 until table.childCount) {
|
||||||
@ -203,6 +262,7 @@ class HandlerGlassScaleQuestion(
|
|||||||
val row = table.getChildAt(i) as TableRow
|
val row = table.getChildAt(i) as TableRow
|
||||||
val symptomKey = question.symptoms[i]
|
val symptomKey = question.symptoms[i]
|
||||||
val radioGroup = row.getChildAt(1) as RadioGroup
|
val radioGroup = row.getChildAt(1) as RadioGroup
|
||||||
|
|
||||||
for (j in 0 until radioGroup.childCount) {
|
for (j in 0 until radioGroup.childCount) {
|
||||||
val rb = getRadioFromChild(radioGroup.getChildAt(j)) ?: continue
|
val rb = getRadioFromChild(radioGroup.getChildAt(j)) ?: continue
|
||||||
if (rb.isChecked) {
|
if (rb.isChecked) {
|
||||||
@ -215,7 +275,6 @@ class HandlerGlassScaleQuestion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
|
||||||
private fun getRadioFromChild(child: View): RadioButton? =
|
private fun getRadioFromChild(child: View): RadioButton? =
|
||||||
when (child) {
|
when (child) {
|
||||||
is RadioButton -> child
|
is RadioButton -> child
|
||||||
@ -223,10 +282,4 @@ class HandlerGlassScaleQuestion(
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
}
|
||||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
|
||||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
|
||||||
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
|
|
||||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +1,35 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.questionnaire.handlers
|
||||||
|
|
||||||
|
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 kotlinx.coroutines.*
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.widget.TextViewCompat
|
import androidx.core.widget.TextViewCompat
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import com.dano.test1.questionnaire.GlobalValues
|
||||||
|
import com.dano.test1.LanguageManager
|
||||||
|
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.utils.ViewUtils
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zweck:
|
||||||
|
- Steuert die letzte Seite eines Fragebogens.
|
||||||
|
- Zeigt Abschlusstexte an, speichert alle gesammelten Antworten in die lokale DB und beendet anschließend den Fragebogen und kehrt zur übergeordneten Ansicht zurück.
|
||||||
|
|
||||||
|
Beim Klick auf „Speichern“:
|
||||||
|
- Ladezustand anzeigen (ProgressBar), Buttons deaktivieren.
|
||||||
|
- Antworten asynchron in Room-DB persistieren (über `saveAnswersToDatabase`).
|
||||||
|
- Punktsumme ermitteln und in `GlobalValues.INTEGRATION_INDEX` schreiben.
|
||||||
|
- `client_code` (falls vorhanden) als `GlobalValues.LAST_CLIENT_CODE` merken.
|
||||||
|
- Mindestens 2 Sekunden „Loading“-Dauer sicherstellen (ruhiges UX).
|
||||||
|
- Zurück auf den Main-Thread wechseln, UI entsperren und Fragebogen schließen.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
class HandlerLastPage(
|
class HandlerLastPage(
|
||||||
private val answers: Map<String, Any>,
|
private val answers: Map<String, Any>,
|
||||||
private val languageID: String,
|
private val languageID: String,
|
||||||
@ -19,7 +40,7 @@ class HandlerLastPage(
|
|||||||
|
|
||||||
private lateinit var currentQuestion: QuestionItem.LastPage
|
private lateinit var currentQuestion: QuestionItem.LastPage
|
||||||
private lateinit var layout: View
|
private lateinit var layout: View
|
||||||
private val minLoadingTimeMs = 2000L // Minimum loading time in milliseconds (2 seconds)
|
private val minLoadingTimeMs = 2000L
|
||||||
|
|
||||||
override fun bind(layout: View, question: QuestionItem) {
|
override fun bind(layout: View, question: QuestionItem) {
|
||||||
this.layout = layout
|
this.layout = layout
|
||||||
@ -43,8 +64,8 @@ class HandlerLastPage(
|
|||||||
applyResponsiveTextSizing(finishBtn)
|
applyResponsiveTextSizing(finishBtn)
|
||||||
|
|
||||||
// Überschriften responsiv skalieren (wie zuvor)
|
// Überschriften responsiv skalieren (wie zuvor)
|
||||||
setTextSizePercentOfScreenHeight(titleTv, 0.03f)
|
ViewUtils.setTextSizePercentOfScreenHeight(titleTv, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(questionTv, 0.03f)
|
ViewUtils.setTextSizePercentOfScreenHeight(questionTv, 0.03f)
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
prevBtn.setOnClickListener { goToPreviousQuestion() }
|
prevBtn.setOnClickListener { goToPreviousQuestion() }
|
||||||
@ -61,27 +82,31 @@ class HandlerLastPage(
|
|||||||
// Punkte summieren
|
// Punkte summieren
|
||||||
GlobalValues.INTEGRATION_INDEX = sumPoints()
|
GlobalValues.INTEGRATION_INDEX = sumPoints()
|
||||||
|
|
||||||
// Client-Code merken
|
// Client-Code merken (für Auto-Laden im Opening Screen)
|
||||||
val clientCode = answers["client_code"] as? String
|
val clientCode = answers["client_code"] as? String
|
||||||
if (clientCode != null) GlobalValues.LAST_CLIENT_CODE = clientCode
|
if (clientCode != null) {
|
||||||
|
GlobalValues.LAST_CLIENT_CODE = clientCode
|
||||||
|
GlobalValues.LOADED_CLIENT_CODE = clientCode // <— zusätzlich setzen
|
||||||
|
}
|
||||||
|
|
||||||
// min. Ladezeit einhalten
|
// min. Ladezeit einhalten (ruhiges UX)
|
||||||
val elapsedTime = System.currentTimeMillis() - startTime
|
val elapsedTime = System.currentTimeMillis() - startTime
|
||||||
if (elapsedTime < minLoadingTimeMs) delay(minLoadingTimeMs - elapsedTime)
|
if (elapsedTime < minLoadingTimeMs) delay(minLoadingTimeMs - elapsedTime)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
val activity = layout.context as? MainActivity
|
val activity = layout.context as? MainActivity
|
||||||
|
// Zurück zum Opening Screen – der lädt dann automatisch (siehe Änderung 2)
|
||||||
activity?.finishQuestionnaire() ?: goToNextQuestion()
|
activity?.finishQuestionnaire() ?: goToNextQuestion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun validate(): Boolean = true
|
override fun validate(): Boolean = true
|
||||||
override fun saveAnswer() {}
|
override fun saveAnswer() {}
|
||||||
|
|
||||||
// ---------- Responsive Textgröße für den Finish-Button ----------
|
|
||||||
private fun applyResponsiveTextSizing(btn: MaterialButton) {
|
private fun applyResponsiveTextSizing(btn: MaterialButton) {
|
||||||
// Max-/Min-Sp anhand der Bildschirmhöhe (in sp) berechnen
|
// Max-/Min-Sp anhand der Bildschirmhöhe (in sp) berechnen
|
||||||
val dm = btn.resources.displayMetrics
|
val dm = btn.resources.displayMetrics
|
||||||
@ -109,14 +134,6 @@ class HandlerLastPage(
|
|||||||
}
|
}
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
// Helper: Textgröße prozentual zur Bildschirmhöhe setzen (in sp)
|
|
||||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
|
||||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
|
||||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
|
||||||
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
|
|
||||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sumPoints(): Int =
|
private fun sumPoints(): Int =
|
||||||
answers.filterKeys { it.endsWith("_points") }
|
answers.filterKeys { it.endsWith("_points") }
|
||||||
.values.mapNotNull { it as? Int }
|
.values.mapNotNull { it as? Int }
|
||||||
@ -1,11 +1,23 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.questionnaire.handlers
|
||||||
|
|
||||||
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 kotlinx.coroutines.*
|
|
||||||
import android.util.TypedValue
|
|
||||||
import androidx.core.widget.TextViewCompat
|
import androidx.core.widget.TextViewCompat
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import com.dano.test1.questionnaire.GlobalValues
|
||||||
|
import com.dano.test1.LanguageManager
|
||||||
|
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.utils.ViewUtils
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zweck:
|
||||||
|
- Steuert eine Frage mit mehreren auswählbaren Antwortoptionen (Checkboxen).
|
||||||
|
*/
|
||||||
|
|
||||||
class HandlerMultiCheckboxQuestion(
|
class HandlerMultiCheckboxQuestion(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@ -15,7 +27,7 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
private val goToNextQuestion: () -> Unit,
|
private val goToNextQuestion: () -> Unit,
|
||||||
private val goToPreviousQuestion: () -> Unit,
|
private val goToPreviousQuestion: () -> Unit,
|
||||||
private val showToast: (String) -> Unit,
|
private val showToast: (String) -> Unit,
|
||||||
private val questionnaireMeta: String // neu: für DB-ID wie bei den anderen Handlern
|
private val questionnaireMeta: String //
|
||||||
) : QuestionHandler {
|
) : QuestionHandler {
|
||||||
|
|
||||||
private lateinit var layout: View
|
private lateinit var layout: View
|
||||||
@ -29,14 +41,12 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
val questionTitle = layout.findViewById<TextView>(R.id.question)
|
val questionTitle = layout.findViewById<TextView>(R.id.question)
|
||||||
val questionTextView = layout.findViewById<TextView>(R.id.textView)
|
val questionTextView = layout.findViewById<TextView>(R.id.textView)
|
||||||
|
|
||||||
// Texte setzen
|
|
||||||
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) =====
|
// Textgrößen pro Bildschirmhöhe (wie bei deinen anderen Handlern)
|
||||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f) // Überschrift
|
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(questionTitle, 0.03f) // Frage
|
ViewUtils.setTextSizePercentOfScreenHeight(questionTitle, 0.03f)
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
container.removeAllViews()
|
container.removeAllViews()
|
||||||
|
|
||||||
@ -45,13 +55,12 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
(answers[it] as? List<*>)?.map { it.toString() }?.toSet()
|
(answers[it] as? List<*>)?.map { it.toString() }?.toSet()
|
||||||
} ?: emptySet()
|
} ?: emptySet()
|
||||||
|
|
||||||
// ——— Checkbox-Schrift & Zeilenhöhe dynamisch ableiten (kein Abschneiden) ———
|
// Checkbox-Schrift & Zeilenhöhe dynamisch ableiten (kein Abschneiden)
|
||||||
val dm = layout.resources.displayMetrics
|
val dm = layout.resources.displayMetrics
|
||||||
val cbTextSp = (dm.heightPixels * 0.025f) / dm.scaledDensity // ~2.5% der Bildschirmhöhe
|
val cbTextSp = (dm.heightPixels * 0.025f) / dm.scaledDensity // ~2.5% der Bildschirmhöhe
|
||||||
val cbTextPx = cbTextSp * dm.scaledDensity
|
val cbTextPx = cbTextSp * dm.scaledDensity
|
||||||
val cbPadV = (cbTextPx * 0.40f).toInt()
|
val cbPadV = (cbTextPx * 0.40f).toInt()
|
||||||
val cbMinH = (cbTextPx * 1.60f + 2 * cbPadV).toInt()
|
val cbMinH = (cbTextPx * 1.60f + 2 * cbPadV).toInt()
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
this.question.options.forEach { option ->
|
this.question.options.forEach { option ->
|
||||||
val checkBox = CheckBox(context).apply {
|
val checkBox = CheckBox(context).apply {
|
||||||
@ -78,7 +87,7 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
container.addView(checkBox)
|
container.addView(checkBox)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- DB-Abfrage falls noch kein Eintrag im answers-Map existiert ---
|
//DB-Abfrage falls noch kein Eintrag im answers-Map existiert
|
||||||
val answerMapKey = question.question ?: (question.id ?: "")
|
val answerMapKey = question.question ?: (question.id ?: "")
|
||||||
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
@ -100,7 +109,7 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
cb.isChecked = parsed.contains(cb.tag.toString())
|
cb.isChecked = parsed.contains(cb.tag.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
// answers-Map aktualisieren (Liste)
|
// answers-Map aktualisieren
|
||||||
answers[answerMapKey] = parsed.toList()
|
answers[answerMapKey] = parsed.toList()
|
||||||
|
|
||||||
// Punkte berechnen und hinzufügen
|
// Punkte berechnen und hinzufügen
|
||||||
@ -175,16 +184,8 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parsen der DB-Antwort in ein Set von Keys. Unterstützt:
|
|
||||||
* - JSON-Array: ["a","b"]
|
|
||||||
* - kommasepariert: a,b
|
|
||||||
* - semikolon-separiert: a;b
|
|
||||||
* - einzelner Wert: a
|
|
||||||
*/
|
|
||||||
private fun parseMultiAnswer(dbAnswer: String): Set<String> {
|
private fun parseMultiAnswer(dbAnswer: String): Set<String> {
|
||||||
val trimmed = dbAnswer.trim()
|
val trimmed = dbAnswer.trim()
|
||||||
// JSON-Array-like
|
|
||||||
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||||
val inner = trimmed.substring(1, trimmed.length - 1)
|
val inner = trimmed.substring(1, trimmed.length - 1)
|
||||||
if (inner.isBlank()) return emptySet()
|
if (inner.isBlank()) return emptySet()
|
||||||
@ -194,7 +195,6 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
.toSet()
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If contains comma or semicolon
|
|
||||||
val separator = when {
|
val separator = when {
|
||||||
trimmed.contains(",") -> ","
|
trimmed.contains(",") -> ","
|
||||||
trimmed.contains(";") -> ";"
|
trimmed.contains(";") -> ";"
|
||||||
@ -211,11 +211,4 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Textgröße prozentual zur Bildschirmhöhe setzen
|
|
||||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
|
||||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
|
||||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
|
||||||
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
|
|
||||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,12 +1,22 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.questionnaire.handlers
|
||||||
|
|
||||||
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 android.util.TypedValue
|
import com.dano.test1.questionnaire.GlobalValues
|
||||||
import androidx.core.widget.TextViewCompat // <— hinzugefügt
|
import com.dano.test1.LanguageManager
|
||||||
|
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.utils.ViewUtils
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zweck:
|
||||||
|
- Steuert eine Einzelfrage mit genau einer auswählbaren Antwort (RadioButtons).
|
||||||
|
*/
|
||||||
|
|
||||||
class HandlerRadioQuestion(
|
class HandlerRadioQuestion(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@ -36,11 +46,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)
|
||||||
} ?: ""
|
} ?: ""
|
||||||
|
|
||||||
// === Schriftgrößen wie im HandlerClientCoachCode ===
|
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||||
// Titel/Frage: 3% der Bildschirmhöhe
|
ViewUtils.setTextSizePercentOfScreenHeight(questionTitle, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
|
||||||
setTextSizePercentOfScreenHeight(questionTitle, 0.03f)
|
|
||||||
// ===================================================
|
|
||||||
|
|
||||||
radioGroup.removeAllViews()
|
radioGroup.removeAllViews()
|
||||||
|
|
||||||
@ -50,7 +57,7 @@ class HandlerRadioQuestion(
|
|||||||
tag = option.key
|
tag = option.key
|
||||||
|
|
||||||
// RadioButton-Text analog zu EditTexts: 2.5% der Bildschirmhöhe
|
// RadioButton-Text analog zu EditTexts: 2.5% der Bildschirmhöhe
|
||||||
setTextSizePercentOfScreenHeight(this, 0.025f)
|
ViewUtils.setTextSizePercentOfScreenHeight(this, 0.025f)
|
||||||
|
|
||||||
layoutParams = RadioGroup.LayoutParams(
|
layoutParams = RadioGroup.LayoutParams(
|
||||||
RadioGroup.LayoutParams.MATCH_PARENT,
|
RadioGroup.LayoutParams.MATCH_PARENT,
|
||||||
@ -132,15 +139,6 @@ class HandlerRadioQuestion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ——— Helper: setzt Textgröße prozentual zur Bildschirmhöhe (in sp) ———
|
|
||||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
|
||||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
|
||||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
|
||||||
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
|
|
||||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
|
||||||
}
|
|
||||||
// ————————————————————————————————————————————————————————————————
|
|
||||||
|
|
||||||
private fun restorePreviousAnswer(radioGroup: RadioGroup) {
|
private fun restorePreviousAnswer(radioGroup: RadioGroup) {
|
||||||
question.question?.let { questionKey ->
|
question.question?.let { questionKey ->
|
||||||
val savedAnswer = answers[questionKey] as? String
|
val savedAnswer = answers[questionKey] as? String
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
package com.dano.test1.questionnaire.handlers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import com.dano.test1.ui.Countries
|
||||||
|
import com.dano.test1.questionnaire.GlobalValues
|
||||||
|
import com.dano.test1.LanguageManager
|
||||||
|
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.utils.ViewUtils
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zweck:
|
||||||
|
- Steuert eine Frage mit einer einzelnen Auswahl aus einer Dropdown-Liste (Spinner).
|
||||||
|
- Baut die Optionen dynamisch auf, lokalisiert Texte, stellt responsive Typografie her und kann vorhandene Antworten aus der lokalen Room-DB restaurieren.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class HandlerStringSpinner(
|
||||||
|
private val context: Context,
|
||||||
|
private val answers: MutableMap<String, Any>,
|
||||||
|
private val languageID: String,
|
||||||
|
private val goToNextQuestion: () -> Unit,
|
||||||
|
private val goToPreviousQuestion: () -> Unit,
|
||||||
|
private val showToast: (String) -> Unit,
|
||||||
|
private val questionnaireMeta: String
|
||||||
|
) : QuestionHandler {
|
||||||
|
|
||||||
|
private lateinit var layout: View
|
||||||
|
private lateinit var question: QuestionItem.StringSpinnerQuestion
|
||||||
|
|
||||||
|
override fun bind(layout: View, question: QuestionItem) {
|
||||||
|
if (question !is QuestionItem.StringSpinnerQuestion) return
|
||||||
|
|
||||||
|
this.layout = layout
|
||||||
|
this.question = question
|
||||||
|
|
||||||
|
val questionTextView = layout.findViewById<TextView>(R.id.question)
|
||||||
|
val textView = layout.findViewById<TextView>(R.id.textView)
|
||||||
|
val spinner = layout.findViewById<Spinner>(R.id.string_spinner)
|
||||||
|
|
||||||
|
// Texte setzen
|
||||||
|
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||||
|
textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||||
|
|
||||||
|
// Textgrößen prozentual zur Bildschirmhöhe
|
||||||
|
ViewUtils.setTextSizePercentOfScreenHeight(textView, 0.03f)
|
||||||
|
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||||
|
|
||||||
|
val options = buildOptionsList()
|
||||||
|
|
||||||
|
// vorhandene Auswahl (falls vorhanden)
|
||||||
|
val savedSelection = question.question?.let { answers[it] as? String }
|
||||||
|
|
||||||
|
// Spinner aufsetzen
|
||||||
|
ViewUtils.setupResponsiveSpinner(context, spinner, options, savedSelection)
|
||||||
|
|
||||||
|
// Falls noch keine Antwort im Map: aus DB laden
|
||||||
|
val answerMapKey = question.question ?: (question.id ?: "")
|
||||||
|
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val clientCode = GlobalValues.LAST_CLIENT_CODE
|
||||||
|
if (clientCode.isNullOrBlank()) return@launch
|
||||||
|
|
||||||
|
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode)
|
||||||
|
val myQuestionId = questionnaireMeta + "-" + question.question
|
||||||
|
val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue
|
||||||
|
|
||||||
|
if (!dbAnswer.isNullOrBlank()) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
answers[answerMapKey] = dbAnswer
|
||||||
|
val index = options.indexOf(dbAnswer)
|
||||||
|
if (index >= 0) spinner.setSelection(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
|
||||||
|
if (validate()) {
|
||||||
|
saveAnswer()
|
||||||
|
goToNextQuestion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
|
||||||
|
goToPreviousQuestion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validate(): Boolean {
|
||||||
|
val spinner = layout.findViewById<Spinner>(R.id.string_spinner)
|
||||||
|
val selected = spinner.selectedItem as? String
|
||||||
|
val prompt = LanguageManager.getText(languageID, "choose_answer")
|
||||||
|
|
||||||
|
return if (selected.isNullOrEmpty() || selected == prompt) {
|
||||||
|
showToast(LanguageManager.getText(languageID, "select_one_answer"))
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveAnswer() {
|
||||||
|
val spinner = layout.findViewById<Spinner>(R.id.string_spinner)
|
||||||
|
val selected = spinner.selectedItem as? String ?: return
|
||||||
|
question.question?.let { key -> answers[key] = selected }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildOptionsList(): List<String> {
|
||||||
|
return if (question.id == "q11") {
|
||||||
|
Countries.getAllCountries(languageID)
|
||||||
|
} else {
|
||||||
|
val prompt = LanguageManager.getText(languageID, "choose_answer")
|
||||||
|
listOf(prompt) + question.options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,12 +1,24 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.questionnaire.handlers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import android.util.TypedValue
|
import com.dano.test1.questionnaire.GlobalValues
|
||||||
import androidx.core.widget.TextViewCompat // <- NEU
|
import com.dano.test1.LanguageManager
|
||||||
|
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.utils.ViewUtils
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zweck:
|
||||||
|
- Steuert eine Frage, bei der ein numerischer Wert aus einem Spinner gewählt wird.
|
||||||
|
- Unterstützt sowohl feste Optionslisten als auch numerische Bereiche (min..max).
|
||||||
|
- Lokalisiert Texte, kann eine frühere Antwort aus der lokalen Room-DB (per AnswerDao) wiederherstellen.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
class HandlerValueSpinner(
|
class HandlerValueSpinner(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@ -16,7 +28,7 @@ class HandlerValueSpinner(
|
|||||||
private val goToPreviousQuestion: () -> Unit,
|
private val goToPreviousQuestion: () -> Unit,
|
||||||
private val goToQuestionById: (String) -> Unit,
|
private val goToQuestionById: (String) -> Unit,
|
||||||
private val showToast: (String) -> Unit,
|
private val showToast: (String) -> Unit,
|
||||||
private val questionnaireMeta: String // neu: für die DB-Abfrage
|
private val questionnaireMeta: String
|
||||||
) : QuestionHandler {
|
) : QuestionHandler {
|
||||||
|
|
||||||
private lateinit var layout: View
|
private lateinit var layout: View
|
||||||
@ -35,11 +47,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) } ?: ""
|
||||||
|
|
||||||
// === Schriftgrößen wie im HandlerRadioQuestion ===
|
ViewUtils.setTextSizePercentOfScreenHeight(textView, 0.03f)
|
||||||
// Titel/Frage: 3% der Bildschirmhöhe
|
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(textView, 0.03f)
|
|
||||||
setTextSizePercentOfScreenHeight(questionTextView, 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) {
|
||||||
@ -49,9 +58,9 @@ class HandlerValueSpinner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val savedValue = question.question?.let { answers[it] as? String }
|
val savedValue = question.question?.let { answers[it] as? String }
|
||||||
setupSpinner(spinner, spinnerItems, savedValue)
|
ViewUtils.setupResponsiveSpinner(context, spinner, 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 ?: "")
|
||||||
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
@ -120,73 +129,4 @@ class HandlerValueSpinner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ——— Helper: setzt Textgröße prozentual zur Bildschirmhöhe (in sp) ———
|
|
||||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
|
||||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
|
||||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
|
||||||
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
|
|
||||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
|
||||||
}
|
|
||||||
// ————————————————————————————————————————————————————————————————
|
|
||||||
|
|
||||||
private fun <T> setupSpinner(spinner: Spinner, 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()
|
|
||||||
|
|
||||||
// Schrift & abgeleitete Höhen
|
|
||||||
val textSp = spFromScreenHeight(0.0275f) // ~2.75% der Bildschirmhöhe
|
|
||||||
val textPx = pxFromSp(textSp)
|
|
||||||
val vPadPx = (textPx * 0.50f).toInt() // vertikales Padding
|
|
||||||
val rowHeight = (textPx * 2.20f + 2 * vPadPx).toInt() // feste Zeilenhöhe
|
|
||||||
|
|
||||||
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 android.view.Gravity.HORIZONTAL_GRAVITY_MASK) or android.view.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) // ausgewählte Ansicht
|
|
||||||
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) // Dropdown-Zeilen: Höhe erzwingen
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
||||||
spinner.adapter = adapter
|
|
||||||
|
|
||||||
// Spinner selbst ausreichend hoch machen
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,4 +1,6 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.ui
|
||||||
|
|
||||||
|
import com.dano.test1.LanguageManager
|
||||||
|
|
||||||
object Countries {
|
object Countries {
|
||||||
fun getAllCountries(languageID: String): List<String> {
|
fun getAllCountries(languageID: String): List<String> {
|
||||||
@ -1,13 +1,21 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.ui
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
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.*
|
||||||
|
import com.dano.test1.data.ExcelExportService
|
||||||
|
import com.dano.test1.utils.ViewUtils
|
||||||
|
import com.dano.test1.LanguageManager
|
||||||
|
import com.dano.test1.MainActivity
|
||||||
|
import com.dano.test1.MyApp
|
||||||
|
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.*
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
|
|
||||||
class DatabaseButtonHandler(
|
class DatabaseButtonHandler(
|
||||||
@ -23,7 +31,8 @@ class DatabaseButtonHandler(
|
|||||||
private val exporter = ExcelExportService(activity, headerRepo)
|
private val exporter = ExcelExportService(activity, headerRepo)
|
||||||
|
|
||||||
fun setup() {
|
fun setup() {
|
||||||
databaseButton.text = "Datenbank"
|
val lang = safeLang()
|
||||||
|
databaseButton.text = t(lang, "database") ?: "Datenbank"
|
||||||
databaseButton.setOnClickListener { openDatabaseScreen() }
|
databaseButton.setOnClickListener { openDatabaseScreen() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,13 +91,13 @@ class DatabaseButtonHandler(
|
|||||||
// Export: Header aller Clients als Excel
|
// Export: Header aller Clients als Excel
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
private fun onDownloadHeadersClicked(progress: ProgressBar) {
|
private fun onDownloadHeadersClicked(progress: ProgressBar) {
|
||||||
|
val lang = safeLang()
|
||||||
uiScope.launch {
|
uiScope.launch {
|
||||||
try {
|
try {
|
||||||
progress.visibility = View.VISIBLE
|
progress.visibility = View.VISIBLE
|
||||||
val savedUri = exporter.exportHeadersForAllClients()
|
val savedUri = exporter.exportHeadersForAllClients()
|
||||||
progress.visibility = View.GONE
|
progress.visibility = View.GONE
|
||||||
|
|
||||||
val lang = safeLang()
|
|
||||||
if (savedUri != null) {
|
if (savedUri != null) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
activity,
|
activity,
|
||||||
@ -101,7 +110,8 @@ class DatabaseButtonHandler(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
progress.visibility = View.GONE
|
progress.visibility = View.GONE
|
||||||
Log.e(tag, "Download Header Fehler: ${e.message}", e)
|
Log.e(tag, "Download Header Fehler: ${e.message}", e)
|
||||||
Toast.makeText(activity, "Fehler: ${e.message}", Toast.LENGTH_LONG).show()
|
val prefix = t(lang, "error") ?: "Fehler"
|
||||||
|
Toast.makeText(activity, "$prefix: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,7 +134,7 @@ class DatabaseButtonHandler(
|
|||||||
val tableOrdered: TableLayout = requireView(R.id.tableOrdered, "tableOrdered")
|
val tableOrdered: TableLayout = requireView(R.id.tableOrdered, "tableOrdered")
|
||||||
|
|
||||||
title.text = "${t(lang, "client") ?: "Client"}: $clientCode – ${t(lang, "questionnaires") ?: "Fragebögen"}"
|
title.text = "${t(lang, "client") ?: "Client"}: $clientCode – ${t(lang, "questionnaires") ?: "Fragebögen"}"
|
||||||
headerLabel.text = t(lang, "headers") ?: "header"
|
headerLabel.text = t(lang, "headers") ?: "Header"
|
||||||
backButton.text = t(lang, "previous") ?: "Zurück"
|
backButton.text = t(lang, "previous") ?: "Zurück"
|
||||||
backButton.setOnClickListener { openDatabaseScreen() }
|
backButton.setOnClickListener { openDatabaseScreen() }
|
||||||
|
|
||||||
@ -359,7 +369,7 @@ class DatabaseButtonHandler(
|
|||||||
val row = TableRow(activity).apply {
|
val row = TableRow(activity).apply {
|
||||||
isClickable = true
|
isClickable = true
|
||||||
isFocusable = true
|
isFocusable = true
|
||||||
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
setBackgroundColor(Color.TRANSPARENT)
|
||||||
setOnClickListener { onClick() }
|
setOnClickListener { onClick() }
|
||||||
}
|
}
|
||||||
cells.forEachIndexed { index, text ->
|
cells.forEachIndexed { index, text ->
|
||||||
@ -394,7 +404,7 @@ class DatabaseButtonHandler(
|
|||||||
this.text = text
|
this.text = text
|
||||||
setPadding(dp(12), dp(10), dp(12), dp(10))
|
setPadding(dp(12), dp(10), dp(12), dp(10))
|
||||||
textSize = 16f
|
textSize = 16f
|
||||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
setTypeface(typeface, Typeface.BOLD)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makeBodyCell(
|
private fun makeBodyCell(
|
||||||
@ -410,17 +420,16 @@ class DatabaseButtonHandler(
|
|||||||
bgColor?.let { setBackgroundColor(it) }
|
bgColor?.let { setBackgroundColor(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dp(value: Int): Int {
|
private fun dp(value: Int): Int = ViewUtils.dp(activity, value)
|
||||||
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)
|
||||||
if (v == null) {
|
if (v == null) {
|
||||||
val msg = "View with id '$name' not found in current layout."
|
val lang = safeLang()
|
||||||
|
val prefix = t(lang, "view_missing") ?: "Fehlende View: %s"
|
||||||
|
val msg = prefix.replace("%s", name)
|
||||||
Log.e(tag, msg)
|
Log.e(tag, msg)
|
||||||
Toast.makeText(activity, "Fehlende View: $name", Toast.LENGTH_LONG).show()
|
Toast.makeText(activity, msg, Toast.LENGTH_LONG).show()
|
||||||
throw IllegalStateException(msg)
|
throw IllegalStateException(msg)
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
@ -1,10 +1,14 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.ui
|
||||||
|
|
||||||
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.MainActivity
|
||||||
|
import com.dano.test1.MyApp
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import com.dano.test1.data.CompletedQuestionnaire
|
import com.dano.test1.data.CompletedQuestionnaire
|
||||||
|
import com.dano.test1.questionnaire.GlobalValues
|
||||||
|
|
||||||
class EditButtonHandler(
|
class EditButtonHandler(
|
||||||
private val activity: MainActivity,
|
private val activity: MainActivity,
|
||||||
@ -42,8 +46,7 @@ class EditButtonHandler(
|
|||||||
|
|
||||||
val needLoad = GlobalValues.LOADED_CLIENT_CODE?.equals(desiredCode) != true
|
val needLoad = GlobalValues.LOADED_CLIENT_CODE?.equals(desiredCode) != true
|
||||||
if (needLoad) {
|
if (needLoad) {
|
||||||
// Zwischenzustände aus dem Load-Handler unterdrücken
|
setUiFreeze(true) // Zwischenzustände unterdrücken
|
||||||
setUiFreeze(true)
|
|
||||||
triggerLoad()
|
triggerLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +54,8 @@ class EditButtonHandler(
|
|||||||
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()
|
val msg = LanguageManager.getText(languageIDProvider(), "open_client_via_load")
|
||||||
|
Toast.makeText(activity, msg, Toast.LENGTH_LONG).show()
|
||||||
setUiFreeze(false)
|
setUiFreeze(false)
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
@ -70,7 +74,6 @@ 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) }
|
||||||
@ -1,18 +1,34 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.TypedValue
|
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.MainActivity
|
||||||
|
import com.dano.test1.R
|
||||||
|
import com.dano.test1.network.DatabaseUploader
|
||||||
|
import com.dano.test1.network.LoginManager
|
||||||
|
import com.dano.test1.network.NetworkUtils
|
||||||
|
import com.dano.test1.network.TokenStore
|
||||||
|
import com.dano.test1.questionnaire.GlobalValues
|
||||||
|
import com.dano.test1.questionnaire.QuestionItem
|
||||||
|
import com.dano.test1.questionnaire.QuestionnaireBase
|
||||||
|
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
|
||||||
import org.json.JSONObject
|
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.dano.test1.utils.ViewUtils
|
||||||
|
|
||||||
|
|
||||||
var RHS_POINTS: Int? = null
|
var RHS_POINTS: Int? = null
|
||||||
|
|
||||||
@ -32,6 +48,8 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
|||||||
private lateinit var databaseButton: 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 var sessionLongWarnedOnce = false
|
||||||
|
|
||||||
private val dynamicButtons = mutableListOf<Button>()
|
private val dynamicButtons = mutableListOf<Button>()
|
||||||
private val questionnaireFiles = mutableMapOf<Button, String>()
|
private val questionnaireFiles = mutableMapOf<Button, String>()
|
||||||
@ -63,6 +81,11 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
|||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
activity.setContentView(R.layout.opening_screen)
|
activity.setContentView(R.layout.opening_screen)
|
||||||
|
|
||||||
|
// <<< NEU: bei jedem Öffnen des Screens zurücksetzen,
|
||||||
|
// damit der Toast pro Besuch einmal angezeigt wird
|
||||||
|
sessionLongWarnedOnce = false
|
||||||
|
|
||||||
bindViews()
|
bindViews()
|
||||||
loadQuestionnaireOrder()
|
loadQuestionnaireOrder()
|
||||||
createQuestionnaireButtons()
|
createQuestionnaireButtons()
|
||||||
@ -75,12 +98,21 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
|||||||
setupUploadButton()
|
setupUploadButton()
|
||||||
setupDownloadButton()
|
setupDownloadButton()
|
||||||
setupDatabaseButtonHandler()
|
setupDatabaseButtonHandler()
|
||||||
|
|
||||||
uiHandler.removeCallbacks(statusTicker)
|
uiHandler.removeCallbacks(statusTicker)
|
||||||
updateStatusStrip()
|
updateStatusStrip()
|
||||||
uiHandler.post(statusTicker)
|
uiHandler.post(statusTicker)
|
||||||
|
|
||||||
val pathExists = File("/data/data/com.dano.test1/databases/questionnaire_database").exists()
|
val pathExists = File("/data/data/com.dano.test1/databases/questionnaire_database").exists()
|
||||||
updateMainButtonsState(pathExists)
|
updateMainButtonsState(pathExists)
|
||||||
|
updateDownloadButtonState(pathExists)
|
||||||
|
|
||||||
if (pathExists && !editText.text.isNullOrBlank()) buttonLoad.performClick()
|
if (pathExists && !editText.text.isNullOrBlank()) buttonLoad.performClick()
|
||||||
|
|
||||||
|
uiHandler.removeCallbacks(statusTicker)
|
||||||
|
updateStatusStrip()
|
||||||
|
applySessionAgeHighlight(System.currentTimeMillis() - TokenStore.getLoginTimestamp(activity))
|
||||||
|
uiHandler.post(statusTicker)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindViews() {
|
private fun bindViews() {
|
||||||
@ -93,7 +125,10 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
|||||||
saveButton = activity.findViewById(R.id.saveButton)
|
saveButton = activity.findViewById(R.id.saveButton)
|
||||||
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)
|
||||||
|
downloadButton.visibility = View.GONE
|
||||||
|
|
||||||
databaseButton = activity.findViewById(R.id.databaseButton)
|
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)
|
||||||
@ -240,7 +275,10 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
|||||||
dynamicButtons.add(btn)
|
dynamicButtons.add(btn)
|
||||||
questionnaireFiles[btn] = entry.file
|
questionnaireFiles[btn] = entry.file
|
||||||
cardParts[btn] = CardParts(tvTitle, tvSubtitle, chip)
|
cardParts[btn] = CardParts(tvTitle, tvSubtitle, chip)
|
||||||
tvTitle.text = "Questionnaire ${index + 1}"
|
|
||||||
|
// Fallback-Titel lokalisieren
|
||||||
|
tvTitle.text = "${t("questionnaire")} ${index + 1}"
|
||||||
|
|
||||||
if (entry.condition is QuestionItem.Condition.AlwaysAvailable) startEnabled.add(btn)
|
if (entry.condition is QuestionItem.Condition.AlwaysAvailable) startEnabled.add(btn)
|
||||||
}
|
}
|
||||||
applyUpdateButtonTexts(force = false)
|
applyUpdateButtonTexts(force = false)
|
||||||
@ -248,12 +286,14 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun restorePreviousClientCode() {
|
private fun restorePreviousClientCode() {
|
||||||
|
// Coach-Code (Username) setzen und Feld sperren – aber NICHT mehr zurückkehren
|
||||||
val username = TokenStore.getUsername(activity)
|
val username = TokenStore.getUsername(activity)
|
||||||
if (!username.isNullOrBlank()) {
|
if (!username.isNullOrBlank()) {
|
||||||
coachEditText.setText(username)
|
coachEditText.setText(username)
|
||||||
lockCoachCodeField()
|
lockCoachCodeField()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hier den zuletzt verwendeten Client-Code einsetzen
|
||||||
GlobalValues.LAST_CLIENT_CODE?.let {
|
GlobalValues.LAST_CLIENT_CODE?.let {
|
||||||
editText.setText(it)
|
editText.setText(it)
|
||||||
GlobalValues.LOADED_CLIENT_CODE = it
|
GlobalValues.LOADED_CLIENT_CODE = it
|
||||||
@ -435,20 +475,61 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
|||||||
private fun setupUploadButton() {
|
private fun setupUploadButton() {
|
||||||
uploadButton.text = t("upload")
|
uploadButton.text = t("upload")
|
||||||
uploadButton.setOnClickListener {
|
uploadButton.setOnClickListener {
|
||||||
val token = TokenStore.getToken(activity)
|
// ZUERST bestätigen lassen
|
||||||
if (token.isNullOrBlank()) {
|
confirmUpload {
|
||||||
Toast.makeText(activity, t("login_required") ?: "Bitte zuerst einloggen", Toast.LENGTH_LONG).show()
|
// === dein bestehender Upload-Code unverändert ===
|
||||||
return@setOnClickListener
|
val existingToken = TokenStore.getToken(activity)
|
||||||
|
val ageMs = System.currentTimeMillis() - TokenStore.getLoginTimestamp(activity)
|
||||||
|
val isFresh = !existingToken.isNullOrBlank() && ageMs < 23 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
if (isFresh) {
|
||||||
|
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
|
||||||
|
DatabaseUploader.uploadDatabaseWithToken(activity, existingToken!!)
|
||||||
|
return@confirmUpload
|
||||||
|
}
|
||||||
|
|
||||||
|
val username = TokenStore.getUsername(activity)?.trim().orEmpty()
|
||||||
|
if (username.isBlank()) {
|
||||||
|
Toast.makeText(activity, t("login_required"), Toast.LENGTH_LONG).show()
|
||||||
|
return@confirmUpload
|
||||||
|
}
|
||||||
|
|
||||||
|
val password = when (username) {
|
||||||
|
"user01" -> "pw1"
|
||||||
|
"user02" -> "pw2"
|
||||||
|
else -> {
|
||||||
|
Toast.makeText(activity, t("login_required"), Toast.LENGTH_LONG).show()
|
||||||
|
return@confirmUpload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginManager.loginUserWithCredentials(
|
||||||
|
context = activity,
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
onSuccess = { freshToken ->
|
||||||
|
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
|
||||||
|
DatabaseUploader.uploadDatabaseWithToken(activity, freshToken)
|
||||||
|
},
|
||||||
|
onError = { msg ->
|
||||||
|
Toast.makeText(activity, t("login_failed_with_reason").replace("{reason}", msg), Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
|
|
||||||
DatabaseUploader.uploadDatabaseWithToken(activity, token)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupDownloadButton() {
|
private fun setupDownloadButton() {
|
||||||
downloadButton.text = t("download")
|
downloadButton.text = t("download")
|
||||||
|
|
||||||
|
// Bei Setup gleich den aktuellen Zustand anwenden
|
||||||
|
val hasDb = File("/data/data/com.dano.test1/databases/questionnaire_database").exists()
|
||||||
|
updateDownloadButtonState(hasDb)
|
||||||
|
|
||||||
downloadButton.setOnClickListener {
|
downloadButton.setOnClickListener {
|
||||||
Toast.makeText(activity, t("login_required") ?: "Bitte zuerst einloggen", Toast.LENGTH_SHORT).show()
|
// Falls der Button (später) deaktiviert ist, passiert einfach nichts
|
||||||
|
if (!downloadButton.isEnabled) return@setOnClickListener
|
||||||
|
Toast.makeText(activity, t("login_required"), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -466,9 +547,33 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
|||||||
b.isEnabled = isDatabaseAvailable
|
b.isEnabled = isDatabaseAvailable
|
||||||
b.alpha = if (isDatabaseAvailable) 1.0f else 0.5f
|
b.alpha = if (isDatabaseAvailable) 1.0f else 0.5f
|
||||||
}
|
}
|
||||||
|
// Der Download-Button wird separat gesteuert
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dp(v: Int): Int = (v * activity.resources.displayMetrics.density).toInt()
|
private fun updateDownloadButtonState(isDatabaseAvailable: Boolean) {
|
||||||
|
val mb = downloadButton as? MaterialButton
|
||||||
|
if (isDatabaseAvailable) {
|
||||||
|
downloadButton.isEnabled = false
|
||||||
|
downloadButton.alpha = 0.5f
|
||||||
|
mb?.apply {
|
||||||
|
strokeWidth = dp(1)
|
||||||
|
strokeColor = ColorStateList.valueOf(STROKE_DISABLED)
|
||||||
|
backgroundTintList = ColorStateList.valueOf(Color.parseColor("#F5F5F5"))
|
||||||
|
rippleColor = ColorStateList.valueOf(Color.parseColor("#00000000"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
downloadButton.isEnabled = true
|
||||||
|
downloadButton.alpha = 1.0f
|
||||||
|
mb?.apply {
|
||||||
|
strokeWidth = dp(2)
|
||||||
|
strokeColor = ColorStateList.valueOf(STROKE_ENABLED)
|
||||||
|
backgroundTintList = ColorStateList.valueOf(Color.WHITE)
|
||||||
|
rippleColor = ColorStateList.valueOf(Color.parseColor("#22000000"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dp(v: Int): Int = ViewUtils.dp(activity, v)
|
||||||
|
|
||||||
private fun isCompleted(button: Button): Boolean {
|
private fun isCompleted(button: Button): Boolean {
|
||||||
val fileName = questionnaireFiles[button] ?: return false
|
val fileName = questionnaireFiles[button] ?: return false
|
||||||
@ -540,10 +645,19 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
|||||||
val ageMs = if (ts > 0L) (System.currentTimeMillis() - ts) else 0L
|
val ageMs = if (ts > 0L) (System.currentTimeMillis() - ts) else 0L
|
||||||
val h = TimeUnit.MILLISECONDS.toHours(ageMs)
|
val h = TimeUnit.MILLISECONDS.toHours(ageMs)
|
||||||
val m = TimeUnit.MILLISECONDS.toMinutes(ageMs) - h * 60
|
val m = TimeUnit.MILLISECONDS.toMinutes(ageMs) - h * 60
|
||||||
statusSession.text = if (ts > 0L) "Session: ${h}h ${m}m" else "Session: —"
|
if (ts > 0L) {
|
||||||
|
// ⚠️ anhängen, wenn >12h, der eigentliche Hinweis/Styling kommt aus applySessionAgeHighlight()
|
||||||
|
val warn = if (ageMs >= SESSION_WARN_AFTER_MS) " ⚠️" else ""
|
||||||
|
statusSession.text = "${t("session_label")}: ${h}${t("hours_short")} ${m}${t("minutes_short")}$warn"
|
||||||
|
} else {
|
||||||
|
statusSession.text = t("session_dash")
|
||||||
|
}
|
||||||
val online = NetworkUtils.isOnline(activity)
|
val online = NetworkUtils.isOnline(activity)
|
||||||
statusOnline.text = if (online) "Online" else "Offline"
|
statusOnline.text = if (online) t("online") else t("offline")
|
||||||
statusOnline.setTextColor(if (online) Color.parseColor("#2E7D32") else Color.parseColor("#C62828"))
|
statusOnline.setTextColor(if (online) Color.parseColor("#2E7D32") else Color.parseColor("#C62828"))
|
||||||
|
|
||||||
|
// <<< NEU: hier jeweils prüfen/markieren
|
||||||
|
applySessionAgeHighlight(ageMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshHeaderStatusLive() {
|
fun refreshHeaderStatusLive() {
|
||||||
@ -551,18 +665,60 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun lockCoachCodeField() {
|
private fun lockCoachCodeField() {
|
||||||
coachEditText.isFocusable = false
|
ViewUtils.lockEditField(coachEditText)
|
||||||
coachEditText.isFocusableInTouchMode = false
|
}
|
||||||
coachEditText.isCursorVisible = false
|
|
||||||
coachEditText.keyListener = null
|
private fun applySessionAgeHighlight(ageMs: Long) {
|
||||||
coachEditText.isLongClickable = false
|
val isOld = ageMs >= SESSION_WARN_AFTER_MS
|
||||||
coachEditText.isClickable = false
|
if (isOld) {
|
||||||
coachEditText.setBackgroundResource(R.drawable.bg_field_locked)
|
statusSession.setTextColor(Color.parseColor("#C62828"))
|
||||||
coachEditText.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_lock_24, 0)
|
statusSession.setBackgroundColor(Color.parseColor("#FFF3CD"))
|
||||||
coachEditText.compoundDrawablePadding = dp(8)
|
statusSession.setPadding(dp(8), dp(4), dp(8), dp(4))
|
||||||
coachEditText.alpha = 0.95f
|
if (!sessionLongWarnedOnce) {
|
||||||
|
showRedToast(activity, t("session_over_12"))
|
||||||
|
sessionLongWarnedOnce = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statusSession.setTextColor(Color.parseColor("#2F2A49"))
|
||||||
|
statusSession.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
statusSession.setPadding(0, 0, 0, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private fun showRedToast(ctx: Context, message: String) {
|
||||||
|
val tv = TextView(ctx).apply {
|
||||||
|
text = message
|
||||||
|
setTextColor(Color.WHITE)
|
||||||
|
textSize = 16f
|
||||||
|
setPadding(32, 20, 32, 20)
|
||||||
|
background = GradientDrawable().apply {
|
||||||
|
shape = GradientDrawable.RECTANGLE
|
||||||
|
cornerRadius = 24f
|
||||||
|
setColor(Color.parseColor("#D32F2F")) // kräftiges Rot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Toast(ctx).apply {
|
||||||
|
duration = Toast.LENGTH_LONG
|
||||||
|
view = tv
|
||||||
|
setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL, 0, 120)
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun confirmUpload(onConfirm: () -> Unit) {
|
||||||
|
MaterialAlertDialogBuilder(activity)
|
||||||
|
.setTitle(t("start_upload"))
|
||||||
|
.setMessage(t("ask_before_upload"))
|
||||||
|
.setPositiveButton(t("ok")) { d, _ ->
|
||||||
|
d.dismiss()
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
.setNegativeButton(t("cancel")) { d, _ ->
|
||||||
|
d.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,10 +1,15 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.ui
|
||||||
|
|
||||||
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.MainActivity
|
||||||
|
import com.dano.test1.MyApp
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import com.dano.test1.data.CompletedQuestionnaire
|
import com.dano.test1.data.CompletedQuestionnaire
|
||||||
|
import com.dano.test1.questionnaire.GlobalValues
|
||||||
|
import com.dano.test1.questionnaire.QuestionItem
|
||||||
|
|
||||||
class LoadButtonHandler(
|
class LoadButtonHandler(
|
||||||
private val activity: MainActivity,
|
private val activity: MainActivity,
|
||||||
@ -1,4 +1,6 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.ui
|
||||||
|
|
||||||
|
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
|
||||||
@ -1,12 +1,21 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1.ui
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.pdf.PdfDocument
|
import android.graphics.pdf.PdfDocument
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
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.MainActivity
|
||||||
|
import com.dano.test1.MyApp
|
||||||
|
import com.dano.test1.questionnaire.GlobalValues
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
class SaveButtonHandler(
|
class SaveButtonHandler(
|
||||||
@ -98,22 +107,27 @@ class SaveButtonHandler(
|
|||||||
pdfDocument.finishPage(page)
|
pdfDocument.finishPage(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("CSV_OUTPUT", "Generated CSV:\n${csvBuilder.toString()}")
|
Log.d("CSV_OUTPUT", "Generated CSV:\n${csvBuilder}")
|
||||||
|
|
||||||
val pdfFileName = "DatabaseOutput_${actualClientCode}.pdf"
|
val pdfFileName = "DatabaseOutput_${actualClientCode}.pdf"
|
||||||
val csvFileName = "DatabaseOutput_${actualClientCode}.csv"
|
val csvFileName = "DatabaseOutput_${actualClientCode}.csv"
|
||||||
val resolver = activity.contentResolver
|
val resolver = activity.contentResolver
|
||||||
|
|
||||||
val deleteIfExists: (String) -> Unit = { name ->
|
val deleteIfExists: (String) -> Unit = { name ->
|
||||||
val projection = arrayOf(android.provider.MediaStore.MediaColumns._ID)
|
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
||||||
val selection = "${android.provider.MediaStore.MediaColumns.DISPLAY_NAME} = ?"
|
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} = ?"
|
||||||
val selectionArgs = arrayOf(name)
|
val selectionArgs = arrayOf(name)
|
||||||
val query = resolver.query(android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, null)
|
val query = resolver.query(
|
||||||
|
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
||||||
|
projection, selection, selectionArgs, null
|
||||||
|
)
|
||||||
query?.use { cursor ->
|
query?.use { cursor ->
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
val idColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.MediaColumns._ID)
|
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||||
val id = cursor.getLong(idColumn)
|
val id = cursor.getLong(idColumn)
|
||||||
val deleteUri = android.content.ContentUris.withAppendedId(android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
|
val deleteUri = ContentUris.withAppendedId(
|
||||||
|
MediaStore.Downloads.EXTERNAL_CONTENT_URI, id
|
||||||
|
)
|
||||||
resolver.delete(deleteUri, null, null)
|
resolver.delete(deleteUri, null, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,20 +138,20 @@ class SaveButtonHandler(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
val pdfUri = resolver.insert(
|
val pdfUri = resolver.insert(
|
||||||
android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
||||||
android.content.ContentValues().apply {
|
ContentValues().apply {
|
||||||
put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, pdfFileName)
|
put(MediaStore.MediaColumns.DISPLAY_NAME, pdfFileName)
|
||||||
put(android.provider.MediaStore.MediaColumns.MIME_TYPE, "application/pdf")
|
put(MediaStore.MediaColumns.MIME_TYPE, "application/pdf")
|
||||||
put(android.provider.MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
|
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val csvUri = resolver.insert(
|
val csvUri = resolver.insert(
|
||||||
android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
||||||
android.content.ContentValues().apply {
|
ContentValues().apply {
|
||||||
put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, csvFileName)
|
put(MediaStore.MediaColumns.DISPLAY_NAME, csvFileName)
|
||||||
put(android.provider.MediaStore.MediaColumns.MIME_TYPE, "text/csv")
|
put(MediaStore.MediaColumns.MIME_TYPE, "text/csv")
|
||||||
put(android.provider.MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
|
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -153,24 +167,28 @@ class SaveButtonHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
Toast.makeText(activity, "PDF und CSV gespeichert in Downloads", Toast.LENGTH_LONG).show()
|
val msg = LanguageManager.getText(languageIDProvider(), "saved_pdf_csv")
|
||||||
|
Toast.makeText(activity, msg, Toast.LENGTH_LONG).show()
|
||||||
|
|
||||||
pdfUri?.let {
|
pdfUri?.let {
|
||||||
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
setDataAndType(it, "application/pdf")
|
setDataAndType(it, "application/pdf")
|
||||||
addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION or android.content.Intent.FLAG_ACTIVITY_NO_HISTORY)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
} catch (e: android.content.ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Toast.makeText(activity, "Kein PDF-Viewer installiert", Toast.LENGTH_SHORT).show()
|
val noViewer = LanguageManager.getText(languageIDProvider(), "no_pdf_viewer")
|
||||||
|
Toast.makeText(activity, noViewer, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("SAVE", "Fehler beim Speichern der Dateien", e)
|
Log.e("SAVE", "Fehler beim Speichern der Dateien", e)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
Toast.makeText(activity, "Fehler beim Speichern: ${e.message}", Toast.LENGTH_LONG).show()
|
val errTpl = LanguageManager.getText(languageIDProvider(), "save_error")
|
||||||
|
val msg = (errTpl ?: "Fehler beim Speichern: {message}").replace("{message}", e.message ?: "")
|
||||||
|
Toast.makeText(activity, msg, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
// app/src/main/java/com/dano/test1/AES256Helper.kt
|
|
||||||
package com.dano.test1
|
package com.dano.test1
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -11,7 +10,6 @@ import kotlin.math.min
|
|||||||
|
|
||||||
object AES256Helper {
|
object AES256Helper {
|
||||||
|
|
||||||
// HKDF-SHA256: IKM = tokenHex->bytes, salt="", info="qdb-aes", len=32
|
|
||||||
private fun hkdfFromToken(tokenHex: String, info: String = "qdb-aes", len: Int = 32): ByteArray {
|
private fun hkdfFromToken(tokenHex: String, info: String = "qdb-aes", len: Int = 32): ByteArray {
|
||||||
val ikm = hexToBytes(tokenHex)
|
val ikm = hexToBytes(tokenHex)
|
||||||
val mac = Mac.getInstance("HmacSHA256")
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1
|
||||||
|
|
||||||
import java.util.Calendar
|
import com.dano.test1.questionnaire.MAX_VALUE_AGE
|
||||||
|
import com.dano.test1.questionnaire.MAX_VALUE_YEAR
|
||||||
|
import com.dano.test1.ui.RHS_POINTS
|
||||||
|
|
||||||
object LanguageManager {
|
object LanguageManager {
|
||||||
fun getTextFormatted(languageId: String, key: String, vararg args: Any): String {
|
fun getTextFormatted(languageId: String, key: String, vararg args: Any): String {
|
||||||
@ -61,14 +63,14 @@ object LanguageManager {
|
|||||||
"once" to "einmal",
|
"once" to "einmal",
|
||||||
"year_after_2000" to "Das Jahr muss nach 2000 liegen!",
|
"year_after_2000" to "Das Jahr muss nach 2000 liegen!",
|
||||||
"year_after_departure" to "Das Jahr muss nach dem Verlassen des Herkunftslandes liegen!",
|
"year_after_departure" to "Das Jahr muss nach dem Verlassen des Herkunftslandes liegen!",
|
||||||
"year_max" to "Das Jahr muss kleiner oder gleich $MAX_VALUE_YEAR sein!",
|
"year_max" to "Das Jahr muss kleiner oder gleich ${MAX_VALUE_YEAR} sein!",
|
||||||
"data_final_warning" to "<b><font color='#FF0000'>Wichtig:</font></b> Die Daten können nach dem Abschluss nicht mehr verändert oder bearbeitet werden!",
|
"data_final_warning" to "<b><font color='#FF0000'>Wichtig:</font></b> Die Daten können nach dem Abschluss nicht mehr verändert oder bearbeitet werden!",
|
||||||
"multiple_times" to "mehrmals",
|
"multiple_times" to "mehrmals",
|
||||||
"more_than_15_years" to "mehr als 15 Jahre",
|
"more_than_15_years" to "mehr als 15 Jahre",
|
||||||
"no" to "Nein",
|
"no" to "Nein",
|
||||||
"no_answer" to "keine Angabe",
|
"no_answer" to "keine Angabe",
|
||||||
"other_country" to "anderes Land",
|
"other_country" to "anderes Land",
|
||||||
"value_must_be_less_equal_max" to "Der Wert muss kleiner oder gleich $MAX_VALUE_AGE sein!",
|
"value_must_be_less_equal_max" to "Der Wert muss kleiner oder gleich ${MAX_VALUE_AGE} sein!",
|
||||||
"value_between_1_and_15" to "Der Wert muss zwischen 1 und 15 liegen!",
|
"value_between_1_and_15" to "Der Wert muss zwischen 1 und 15 liegen!",
|
||||||
"invalid_month" to "Ungültige Monatsangabe!",
|
"invalid_month" to "Ungültige Monatsangabe!",
|
||||||
"invalid_year" to "Ungültige Jahresangabe!",
|
"invalid_year" to "Ungültige Jahresangabe!",
|
||||||
@ -354,7 +356,58 @@ object LanguageManager {
|
|||||||
"error_generic" to "Fehler",
|
"error_generic" to "Fehler",
|
||||||
"not_done" to "Nicht Fertog",
|
"not_done" to "Nicht Fertog",
|
||||||
"none" to "Keine Angabe",
|
"none" to "Keine Angabe",
|
||||||
"points" to "Punkte"
|
"points" to "Punkte",
|
||||||
|
"saved_pdf_csv" to "PDF und CSV wurden im Ordner \"Downloads\" gespeichert.",
|
||||||
|
"no_pdf_viewer" to "Kein PDF-Viewer installiert.",
|
||||||
|
"save_error" to "Fehler beim Speichern: {message}",
|
||||||
|
"login_required_title" to "Login erforderlich",
|
||||||
|
"username_hint" to "Benutzername",
|
||||||
|
"password_hint" to "Passwort",
|
||||||
|
"login_btn" to "Login",
|
||||||
|
"exit_btn" to "Beenden",
|
||||||
|
"please_username_password" to "Bitte Benutzername und Passwort eingeben.",
|
||||||
|
"download_failed_no_local_db" to "Download fehlgeschlagen – keine lokale Datenbank vorhanden",
|
||||||
|
"download_failed_use_offline" to "Download fehlgeschlagen – arbeite offline mit vorhandener Datenbank",
|
||||||
|
"login_failed_with_reason" to "Login fehlgeschlagen: {reason}",
|
||||||
|
"no_header_template_found" to "Keine Header-Vorlage gefunden",
|
||||||
|
"login_required" to "Bitte zuerst einloggen",
|
||||||
|
"questionnaire" to "Fragebogen",
|
||||||
|
"session_label" to "Sitzung",
|
||||||
|
"session_dash" to "Sitzung: —",
|
||||||
|
"hours_short" to "h",
|
||||||
|
"minutes_short" to "m",
|
||||||
|
"online" to "Online",
|
||||||
|
"offline" to "Offline",
|
||||||
|
"open_client_via_load" to "Bitte den Klienten über „Laden“ öffnen.",
|
||||||
|
"database" to "Datenbank",
|
||||||
|
"database_clients_title" to "Datenbank – Clients",
|
||||||
|
"no_clients_available" to "Keine Clients vorhanden.",
|
||||||
|
"previous" to "Zurück",
|
||||||
|
"download_header" to "Header herunterladen",
|
||||||
|
"client_code" to "Client-Code",
|
||||||
|
"export_success_downloads" to "Export erfolgreich: Downloads/ClientHeaders.xlsx",
|
||||||
|
"export_failed" to "Export fehlgeschlagen.",
|
||||||
|
"error" to "Fehler",
|
||||||
|
"client" to "Client",
|
||||||
|
"questionnaires" to "Fragebögen",
|
||||||
|
"headers" to "Header",
|
||||||
|
"questionnaire_id" to "Fragebogen-ID",
|
||||||
|
"status" to "Status",
|
||||||
|
"id" to "ID",
|
||||||
|
"value" to "Wert",
|
||||||
|
"no_questionnaires" to "Keine Fragebögen vorhanden.",
|
||||||
|
"no_questions_available" to "Keine Fragen vorhanden.",
|
||||||
|
"question" to "Frage",
|
||||||
|
"answer" to "Antwort",
|
||||||
|
"done" to "Erledigt",
|
||||||
|
"not_done" to "Nicht erledigt",
|
||||||
|
"none" to "Keine",
|
||||||
|
"view_missing" to "Fehlende View: %s",
|
||||||
|
"session_over_12" to "Sitzung läuft länger als 12 Stunden.",
|
||||||
|
"cancel" to "Cancel",
|
||||||
|
"ok" to "OK",
|
||||||
|
"ask_before_upload" to "Möchtest du den Upload wirklich ausführen?",
|
||||||
|
"start_upload" to "Upload starten?"
|
||||||
),
|
),
|
||||||
|
|
||||||
"ENGLISH" to mapOf(
|
"ENGLISH" to mapOf(
|
||||||
@ -387,14 +440,14 @@ object LanguageManager {
|
|||||||
"once" to "once",
|
"once" to "once",
|
||||||
"year_after_2000" to "The year must be after 2000!",
|
"year_after_2000" to "The year must be after 2000!",
|
||||||
"year_after_departure" to "The year must be after leaving the country of origin!",
|
"year_after_departure" to "The year must be after leaving the country of origin!",
|
||||||
"year_max" to "The year must be less than or equal to $MAX_VALUE_YEAR!",
|
"year_max" to "The year must be less than or equal to ${MAX_VALUE_YEAR}!",
|
||||||
"data_final_warning" to "<b><font color='#FF0000'>Important:</font></b> The data cannot be changed or edited after completion!",
|
"data_final_warning" to "<b><font color='#FF0000'>Important:</font></b> The data cannot be changed or edited after completion!",
|
||||||
"multiple_times" to "multiple times",
|
"multiple_times" to "multiple times",
|
||||||
"more_than_15_years" to "more than 15 years",
|
"more_than_15_years" to "more than 15 years",
|
||||||
"no" to "No",
|
"no" to "No",
|
||||||
"no_answer" to "No answer",
|
"no_answer" to "No answer",
|
||||||
"other_country" to "Other country",
|
"other_country" to "Other country",
|
||||||
"value_must_be_less_equal_max" to "The value must be less than or equal to $MAX_VALUE_AGE!",
|
"value_must_be_less_equal_max" to "The value must be less than or equal to ${MAX_VALUE_AGE}!",
|
||||||
"value_between_1_and_15" to "The value must be between 1 and 15!",
|
"value_between_1_and_15" to "The value must be between 1 and 15!",
|
||||||
"invalid_month" to "Invalid month!",
|
"invalid_month" to "Invalid month!",
|
||||||
"invalid_year" to "Invalid year!",
|
"invalid_year" to "Invalid year!",
|
||||||
@ -679,7 +732,58 @@ object LanguageManager {
|
|||||||
"done" to "Done",
|
"done" to "Done",
|
||||||
"locked" to "Locked",
|
"locked" to "Locked",
|
||||||
"start" to "Start",
|
"start" to "Start",
|
||||||
"points" to "Points"
|
"points" to "Points",
|
||||||
|
"saved_pdf_csv" to "PDF and CSV were saved in the \"Downloads\" folder.",
|
||||||
|
"no_pdf_viewer" to "No PDF viewer installed.",
|
||||||
|
"save_error" to "Error while saving: {message}",
|
||||||
|
"login_required_title" to "Login required",
|
||||||
|
"username_hint" to "Username",
|
||||||
|
"password_hint" to "Password",
|
||||||
|
"login_btn" to "Login",
|
||||||
|
"exit_btn" to "Exit",
|
||||||
|
"please_username_password" to "Please enter username and password.",
|
||||||
|
"download_failed_no_local_db" to "Download failed – no local database available",
|
||||||
|
"download_failed_use_offline" to "Download failed – working offline with existing database",
|
||||||
|
"login_failed_with_reason" to "Login failed: {reason}",
|
||||||
|
"no_header_template_found" to "No header template found",
|
||||||
|
"login_required" to "Please log in first",
|
||||||
|
"questionnaire" to "Questionnaire",
|
||||||
|
"session_label" to "Session",
|
||||||
|
"session_dash" to "Session: —",
|
||||||
|
"hours_short" to "h",
|
||||||
|
"minutes_short" to "min",
|
||||||
|
"online" to "Online",
|
||||||
|
"offline" to "Offline",
|
||||||
|
"open_client_via_load" to "Please open the client via \"Load\".",
|
||||||
|
"database" to "Database",
|
||||||
|
"database_clients_title" to "Database – Clients",
|
||||||
|
"no_clients_available" to "No clients available.",
|
||||||
|
"previous" to "Back",
|
||||||
|
"download_header" to "Download header",
|
||||||
|
"client_code" to "Client code",
|
||||||
|
"export_success_downloads" to "Export successful: Downloads/ClientHeaders.xlsx",
|
||||||
|
"export_failed" to "Export failed.",
|
||||||
|
"error" to "Error",
|
||||||
|
"client" to "Client",
|
||||||
|
"questionnaires" to "Questionnaires",
|
||||||
|
"headers" to "Headers",
|
||||||
|
"questionnaire_id" to "Questionnaire ID",
|
||||||
|
"status" to "Status",
|
||||||
|
"id" to "ID",
|
||||||
|
"value" to "Value",
|
||||||
|
"no_questionnaires" to "No questionnaires available.",
|
||||||
|
"no_questions_available" to "No questions available.",
|
||||||
|
"question" to "Question",
|
||||||
|
"answer" to "Answer",
|
||||||
|
"done" to "Done",
|
||||||
|
"not_done" to "Not done",
|
||||||
|
"none" to "None",
|
||||||
|
"view_missing" to "Missing view: %s",
|
||||||
|
"session_over_12" to "Session has been running for more than 12 hours.",
|
||||||
|
"cancel" to "Cancel",
|
||||||
|
"ok" to "OK",
|
||||||
|
"ask_before_upload" to "Do you really want to perform the upload?",
|
||||||
|
"start_upload" to "Start upload?"
|
||||||
),
|
),
|
||||||
|
|
||||||
"FRENCH" to mapOf(
|
"FRENCH" to mapOf(
|
||||||
@ -712,14 +816,14 @@ object LanguageManager {
|
|||||||
"once" to "une fois",
|
"once" to "une fois",
|
||||||
"year_after_2000" to "L’année doit être après 2000 !",
|
"year_after_2000" to "L’année doit être après 2000 !",
|
||||||
"year_after_departure" to "L’année doit être après le départ du pays d’origine !",
|
"year_after_departure" to "L’année doit être après le départ du pays d’origine !",
|
||||||
"year_max" to "L’année doit être inférieure ou égale à $MAX_VALUE_YEAR !",
|
"year_max" to "L’année doit être inférieure ou égale à ${MAX_VALUE_YEAR} !",
|
||||||
"data_final_warning" to "<b><font color='#FF0000'>Important :</font></b> Les données ne peuvent plus être modifiées ou éditées après la validation !",
|
"data_final_warning" to "<b><font color='#FF0000'>Important :</font></b> Les données ne peuvent plus être modifiées ou éditées après la validation !",
|
||||||
"multiple_times" to "plusieurs fois",
|
"multiple_times" to "plusieurs fois",
|
||||||
"more_than_15_years" to "plus de 15 ans",
|
"more_than_15_years" to "plus de 15 ans",
|
||||||
"no" to "Non",
|
"no" to "Non",
|
||||||
"no_answer" to "pas de réponse",
|
"no_answer" to "pas de réponse",
|
||||||
"other_country" to "autre pays",
|
"other_country" to "autre pays",
|
||||||
"value_must_be_less_equal_max" to "La valeur doit être inférieure ou égale à $MAX_VALUE_AGE !",
|
"value_must_be_less_equal_max" to "La valeur doit être inférieure ou égale à ${MAX_VALUE_AGE} !",
|
||||||
"value_between_1_and_15" to "La valeur doit être comprise entre 1 et 15 !",
|
"value_between_1_and_15" to "La valeur doit être comprise entre 1 et 15 !",
|
||||||
"invalid_month" to "Mois invalide !",
|
"invalid_month" to "Mois invalide !",
|
||||||
"invalid_year" to "Année invalide !",
|
"invalid_year" to "Année invalide !",
|
||||||
@ -1008,7 +1112,58 @@ object LanguageManager {
|
|||||||
"done" to "Terminé",
|
"done" to "Terminé",
|
||||||
"locked" to "Verrouillé",
|
"locked" to "Verrouillé",
|
||||||
"start" to "Commencer",
|
"start" to "Commencer",
|
||||||
"points" to "Points"
|
"points" to "Points",
|
||||||
|
"saved_pdf_csv" to "Les PDF et CSV ont été enregistrés dans le dossier \"Téléchargements\".",
|
||||||
|
"no_pdf_viewer" to "Aucun lecteur PDF installé.",
|
||||||
|
"save_error" to "Erreur lors de l’enregistrement : {message}",
|
||||||
|
"login_required_title" to "Connexion requise",
|
||||||
|
"username_hint" to "Nom d’utilisateur",
|
||||||
|
"password_hint" to "Mot de passe",
|
||||||
|
"login_btn" to "Se connecter",
|
||||||
|
"exit_btn" to "Quitter",
|
||||||
|
"please_username_password" to "Veuillez saisir le nom d’utilisateur et le mot de passe.",
|
||||||
|
"download_failed_no_local_db" to "Échec du téléchargement – aucune base de données locale disponible",
|
||||||
|
"download_failed_use_offline" to "Échec du téléchargement – travaillez hors ligne avec la base de données existante",
|
||||||
|
"login_failed_with_reason" to "Échec de la connexion : {reason}",
|
||||||
|
"no_header_template_found" to "Aucun modèle d’en-tête trouvé",
|
||||||
|
"login_required" to "Veuillez d’abord vous connecter",
|
||||||
|
"questionnaire" to "Questionnaire",
|
||||||
|
"session_label" to "Session",
|
||||||
|
"session_dash" to "Session : —",
|
||||||
|
"hours_short" to "h",
|
||||||
|
"minutes_short" to "min",
|
||||||
|
"online" to "En ligne",
|
||||||
|
"offline" to "Hors ligne",
|
||||||
|
"open_client_via_load" to "Veuillez ouvrir le client via « Charger ».",
|
||||||
|
"database" to "Base de données",
|
||||||
|
"database_clients_title" to "Base de données – Clients",
|
||||||
|
"no_clients_available" to "Aucun client disponible.",
|
||||||
|
"previous" to "Retour",
|
||||||
|
"download_header" to "Télécharger l’en-tête",
|
||||||
|
"client_code" to "Code client",
|
||||||
|
"export_success_downloads" to "Export réussi : Téléchargements/ClientHeaders.xlsx",
|
||||||
|
"export_failed" to "Échec de l’exportation.",
|
||||||
|
"error" to "Erreur",
|
||||||
|
"client" to "Client",
|
||||||
|
"questionnaires" to "Questionnaires",
|
||||||
|
"headers" to "En-têtes",
|
||||||
|
"questionnaire_id" to "ID du questionnaire",
|
||||||
|
"status" to "Statut",
|
||||||
|
"id" to "ID",
|
||||||
|
"value" to "Valeur",
|
||||||
|
"no_questionnaires" to "Aucun questionnaire disponible.",
|
||||||
|
"no_questions_available" to "Aucune question disponible.",
|
||||||
|
"question" to "Question",
|
||||||
|
"answer" to "Réponse",
|
||||||
|
"done" to "Terminé",
|
||||||
|
"not_done" to "Non terminé",
|
||||||
|
"none" to "Aucun",
|
||||||
|
"view_missing" to "Vue manquante : %s",
|
||||||
|
"session_over_12" to "La session dure depuis plus de 12 heures.",
|
||||||
|
"cancel" to "Annuler",
|
||||||
|
"ok" to "OK",
|
||||||
|
"ask_before_upload" to "Voulez-vous vraiment effectuer le téléversement ?",
|
||||||
|
"start_upload" to "Démarrer le téléversement ?"
|
||||||
),
|
),
|
||||||
|
|
||||||
"RUSSIAN" to mapOf(
|
"RUSSIAN" to mapOf(
|
||||||
@ -1041,14 +1196,14 @@ object LanguageManager {
|
|||||||
"once" to "один раз",
|
"once" to "один раз",
|
||||||
"year_after_2000" to "Год должен быть после 2000!",
|
"year_after_2000" to "Год должен быть после 2000!",
|
||||||
"year_after_departure" to "Год должен быть после даты выезда из страны происхождения!",
|
"year_after_departure" to "Год должен быть после даты выезда из страны происхождения!",
|
||||||
"year_max" to "Год должен быть меньше или равен $MAX_VALUE_YEAR!",
|
"year_max" to "Год должен быть меньше или равен ${MAX_VALUE_YEAR}!",
|
||||||
"data_final_warning" to "<b><font color='#FF0000'>Внимание:</font></b> Данные нельзя изменять после завершения!",
|
"data_final_warning" to "<b><font color='#FF0000'>Внимание:</font></b> Данные нельзя изменять после завершения!",
|
||||||
"multiple_times" to "несколько раз",
|
"multiple_times" to "несколько раз",
|
||||||
"more_than_15_years" to "более 15 лет",
|
"more_than_15_years" to "более 15 лет",
|
||||||
"no" to "Нет",
|
"no" to "Нет",
|
||||||
"no_answer" to "без ответа",
|
"no_answer" to "без ответа",
|
||||||
"other_country" to "другая страна",
|
"other_country" to "другая страна",
|
||||||
"value_must_be_less_equal_max" to "Значение должно быть меньше или равно $MAX_VALUE_AGE!",
|
"value_must_be_less_equal_max" to "Значение должно быть меньше или равно ${MAX_VALUE_AGE}!",
|
||||||
"value_between_1_and_15" to "Значение должно быть между 1 и 15!",
|
"value_between_1_and_15" to "Значение должно быть между 1 и 15!",
|
||||||
"invalid_month" to "Недопустимый месяц!",
|
"invalid_month" to "Недопустимый месяц!",
|
||||||
"invalid_year" to "Недопустимый год!",
|
"invalid_year" to "Недопустимый год!",
|
||||||
@ -1333,7 +1488,58 @@ object LanguageManager {
|
|||||||
"done" to "Готово",
|
"done" to "Готово",
|
||||||
"locked" to "Заблокировано",
|
"locked" to "Заблокировано",
|
||||||
"start" to "Начать",
|
"start" to "Начать",
|
||||||
"points" to "Баллы"
|
"points" to "Баллы",
|
||||||
|
"saved_pdf_csv" to "PDF и CSV сохранены в папке «Загрузки».",
|
||||||
|
"no_pdf_viewer" to "Не установлен просмотрщик PDF.",
|
||||||
|
"save_error" to "Ошибка при сохранении: {message}",
|
||||||
|
"login_required_title" to "Требуется вход",
|
||||||
|
"username_hint" to "Имя пользователя",
|
||||||
|
"password_hint" to "Пароль",
|
||||||
|
"login_btn" to "Войти",
|
||||||
|
"exit_btn" to "Выход",
|
||||||
|
"please_username_password" to "Введите имя пользователя и пароль.",
|
||||||
|
"download_failed_no_local_db" to "Сбой загрузки – локальная база данных отсутствует",
|
||||||
|
"download_failed_use_offline" to "Сбой загрузки – работа офлайн с имеющейся базой данных",
|
||||||
|
"login_failed_with_reason" to "Не удалось войти: {reason}",
|
||||||
|
"no_header_template_found" to "Шаблон заголовков не найден",
|
||||||
|
"login_required" to "Сначала выполните вход",
|
||||||
|
"questionnaire" to "Опросник",
|
||||||
|
"session_label" to "Сессия",
|
||||||
|
"session_dash" to "Сессия: —",
|
||||||
|
"hours_short" to "ч",
|
||||||
|
"minutes_short" to "м",
|
||||||
|
"online" to "В сети",
|
||||||
|
"offline" to "Не в сети",
|
||||||
|
"open_client_via_load" to "Откройте клиента через «Загрузить».",
|
||||||
|
"database" to "База данных",
|
||||||
|
"database_clients_title" to "База данных – клиенты",
|
||||||
|
"no_clients_available" to "Клиенты отсутствуют.",
|
||||||
|
"previous" to "Назад",
|
||||||
|
"download_header" to "Скачать заголовки",
|
||||||
|
"client_code" to "Код клиента",
|
||||||
|
"export_success_downloads" to "Экспорт выполнен: Загрузки/ClientHeaders.xlsx",
|
||||||
|
"export_failed" to "Ошибка экспорта.",
|
||||||
|
"error" to "Ошибка",
|
||||||
|
"client" to "Клиент",
|
||||||
|
"questionnaires" to "Опросники",
|
||||||
|
"headers" to "Заголовки",
|
||||||
|
"questionnaire_id" to "ID опросника",
|
||||||
|
"status" to "Статус",
|
||||||
|
"id" to "ID",
|
||||||
|
"value" to "Значение",
|
||||||
|
"no_questionnaires" to "Нет доступных опросников.",
|
||||||
|
"no_questions_available" to "Нет вопросов.",
|
||||||
|
"question" to "Вопрос",
|
||||||
|
"answer" to "Ответ",
|
||||||
|
"done" to "Готово",
|
||||||
|
"not_done" to "Не выполнено",
|
||||||
|
"none" to "Нет",
|
||||||
|
"view_missing" to "Отсутствует представление: %s",
|
||||||
|
"session_over_12" to "Сеанс продолжается более 12 часов.",
|
||||||
|
"cancel" to "Отмена",
|
||||||
|
"ok" to "OK",
|
||||||
|
"ask_before_upload" to "Вы действительно хотите выполнить загрузку?",
|
||||||
|
"start_upload" to "Начать загрузку?"
|
||||||
),
|
),
|
||||||
|
|
||||||
"UKRAINIAN" to mapOf(
|
"UKRAINIAN" to mapOf(
|
||||||
@ -1366,14 +1572,14 @@ object LanguageManager {
|
|||||||
"once" to "один раз",
|
"once" to "один раз",
|
||||||
"year_after_2000" to "Рік має бути після 2000!",
|
"year_after_2000" to "Рік має бути після 2000!",
|
||||||
"year_after_departure" to "Рік має бути після виїзду з країни походження!",
|
"year_after_departure" to "Рік має бути після виїзду з країни походження!",
|
||||||
"year_max" to "Рік має бути меншим або рівним $MAX_VALUE_YEAR!",
|
"year_max" to "Рік має бути меншим або рівним ${MAX_VALUE_YEAR}!",
|
||||||
"data_final_warning" to "<b><font color='#FF0000'>Важливо:</font></b> Дані після завершення не можна змінити або редагувати!",
|
"data_final_warning" to "<b><font color='#FF0000'>Важливо:</font></b> Дані після завершення не можна змінити або редагувати!",
|
||||||
"multiple_times" to "багато разів",
|
"multiple_times" to "багато разів",
|
||||||
"more_than_15_years" to "більше 15 років",
|
"more_than_15_years" to "більше 15 років",
|
||||||
"no" to "Ні",
|
"no" to "Ні",
|
||||||
"no_answer" to "немає відповіді",
|
"no_answer" to "немає відповіді",
|
||||||
"other_country" to "інша країна",
|
"other_country" to "інша країна",
|
||||||
"value_must_be_less_equal_max" to "Значення має бути меншим або рівним $MAX_VALUE_AGE!",
|
"value_must_be_less_equal_max" to "Значення має бути меншим або рівним ${MAX_VALUE_AGE}!",
|
||||||
"value_between_1_and_15" to "Значення має бути від 1 до 15!",
|
"value_between_1_and_15" to "Значення має бути від 1 до 15!",
|
||||||
"invalid_month" to "Неправильний місяць!",
|
"invalid_month" to "Неправильний місяць!",
|
||||||
"invalid_year" to "Неправильний рік!",
|
"invalid_year" to "Неправильний рік!",
|
||||||
@ -1662,7 +1868,58 @@ object LanguageManager {
|
|||||||
"done" to "Завершено",
|
"done" to "Завершено",
|
||||||
"locked" to "Заблоковано",
|
"locked" to "Заблоковано",
|
||||||
"start" to "Почати",
|
"start" to "Почати",
|
||||||
"points" to "Бали"
|
"points" to "Бали",
|
||||||
|
"saved_pdf_csv" to "PDF і CSV збережено в папці «Завантаження».",
|
||||||
|
"no_pdf_viewer" to "Не встановлено переглядач PDF.",
|
||||||
|
"save_error" to "Помилка збереження: {message}",
|
||||||
|
"login_required_title" to "Потрібен вхід",
|
||||||
|
"username_hint" to "Ім’я користувача",
|
||||||
|
"password_hint" to "Пароль",
|
||||||
|
"login_btn" to "Увійти",
|
||||||
|
"exit_btn" to "Вийти",
|
||||||
|
"please_username_password" to "Введіть ім’я користувача та пароль.",
|
||||||
|
"download_failed_no_local_db" to "Завантаження не вдалося — немає локальної бази даних",
|
||||||
|
"download_failed_use_offline" to "Завантаження не вдалося — працюйте офлайн з наявною базою даних",
|
||||||
|
"login_failed_with_reason" to "Не вдалося увійти: {reason}",
|
||||||
|
"no_header_template_found" to "Шаблон заголовків не знайдено",
|
||||||
|
"login_required" to "Спочатку увійдіть",
|
||||||
|
"questionnaire" to "Анкета",
|
||||||
|
"session_label" to "Сесія",
|
||||||
|
"session_dash" to "Сесія: —",
|
||||||
|
"hours_short" to "год",
|
||||||
|
"minutes_short" to "хв",
|
||||||
|
"online" to "Онлайн",
|
||||||
|
"offline" to "Офлайн",
|
||||||
|
"open_client_via_load" to "Відкрийте клієнта через «Завантажити».",
|
||||||
|
"database" to "База даних",
|
||||||
|
"database_clients_title" to "База даних – клієнти",
|
||||||
|
"no_clients_available" to "Немає клієнтів.",
|
||||||
|
"previous" to "Назад",
|
||||||
|
"download_header" to "Завантажити заголовки",
|
||||||
|
"client_code" to "Код клієнта",
|
||||||
|
"export_success_downloads" to "Експорт виконано: Завантаження/ClientHeaders.xlsx",
|
||||||
|
"export_failed" to "Помилка експорту.",
|
||||||
|
"error" to "Помилка",
|
||||||
|
"client" to "Клієнт",
|
||||||
|
"questionnaires" to "Анкети",
|
||||||
|
"headers" to "Заголовки",
|
||||||
|
"questionnaire_id" to "ID анкети",
|
||||||
|
"status" to "Статус",
|
||||||
|
"id" to "ID",
|
||||||
|
"value" to "Значення",
|
||||||
|
"no_questionnaires" to "Немає доступних анкет.",
|
||||||
|
"no_questions_available" to "Немає запитань.",
|
||||||
|
"question" to "Питання",
|
||||||
|
"answer" to "Відповідь",
|
||||||
|
"done" to "Готово",
|
||||||
|
"not_done" to "Не виконано",
|
||||||
|
"none" to "Немає",
|
||||||
|
"view_missing" to "Відсутній елемент інтерфейсу: %s",
|
||||||
|
"session_over_12" to "Сеанс триває понад 12 годин.",
|
||||||
|
"cancel" to "Скасувати",
|
||||||
|
"ok" to "OK",
|
||||||
|
"ask_before_upload" to "Ви справді хочете виконати завантаження?",
|
||||||
|
"start_upload" to "Почати завантаження?"
|
||||||
),
|
),
|
||||||
|
|
||||||
"TURKISH" to mapOf(
|
"TURKISH" to mapOf(
|
||||||
@ -1695,14 +1952,14 @@ object LanguageManager {
|
|||||||
"once" to "bir kez",
|
"once" to "bir kez",
|
||||||
"year_after_2000" to "Yıl 2000’den sonra olmalıdır!",
|
"year_after_2000" to "Yıl 2000’den sonra olmalıdır!",
|
||||||
"year_after_departure" to "Yıl, menşe ülkeyi terk ettikten sonra olmalıdır!",
|
"year_after_departure" to "Yıl, menşe ülkeyi terk ettikten sonra olmalıdır!",
|
||||||
"year_max" to "Yıl $MAX_VALUE_YEAR’den küçük veya ona eşit olmalıdır!",
|
"year_max" to "Yıl ${MAX_VALUE_YEAR}’den küçük veya ona eşit olmalıdır!",
|
||||||
"data_final_warning" to "<b><font color='#FF0000'>Önemli:</font></b> Veriler tamamlandıktan sonra değiştirilemez veya düzenlenemez!",
|
"data_final_warning" to "<b><font color='#FF0000'>Önemli:</font></b> Veriler tamamlandıktan sonra değiştirilemez veya düzenlenemez!",
|
||||||
"multiple_times" to "birden fazla kez",
|
"multiple_times" to "birden fazla kez",
|
||||||
"more_than_15_years" to "15 yıldan fazla",
|
"more_than_15_years" to "15 yıldan fazla",
|
||||||
"no" to "Hayır",
|
"no" to "Hayır",
|
||||||
"no_answer" to "Cevap yok",
|
"no_answer" to "Cevap yok",
|
||||||
"other_country" to "diğer ülke",
|
"other_country" to "diğer ülke",
|
||||||
"value_must_be_less_equal_max" to "Değer $MAX_VALUE_AGE’den küçük veya ona eşit olmalıdır!",
|
"value_must_be_less_equal_max" to "Değer ${MAX_VALUE_AGE}’den küçük veya ona eşit olmalıdır!",
|
||||||
"value_between_1_and_15" to "Değer 1 ile 15 arasında olmalıdır!",
|
"value_between_1_and_15" to "Değer 1 ile 15 arasında olmalıdır!",
|
||||||
"invalid_month" to "Geçersiz ay girişi!",
|
"invalid_month" to "Geçersiz ay girişi!",
|
||||||
"invalid_year" to "Geçersiz yıl girişi!",
|
"invalid_year" to "Geçersiz yıl girişi!",
|
||||||
@ -1991,7 +2248,58 @@ object LanguageManager {
|
|||||||
"done" to "Tamamlandı",
|
"done" to "Tamamlandı",
|
||||||
"locked" to "Kilitli",
|
"locked" to "Kilitli",
|
||||||
"start" to "Başlat",
|
"start" to "Başlat",
|
||||||
"points" to "Puan"
|
"points" to "Puan",
|
||||||
|
"saved_pdf_csv" to "PDF ve CSV \"İndirilenler\" klasörüne kaydedildi.",
|
||||||
|
"no_pdf_viewer" to "Yüklü bir PDF görüntüleyici yok.",
|
||||||
|
"save_error" to "Kaydetme hatası: {message}",
|
||||||
|
"login_required_title" to "Giriş gerekli",
|
||||||
|
"username_hint" to "Kullanıcı adı",
|
||||||
|
"password_hint" to "Şifre",
|
||||||
|
"login_btn" to "Giriş yap",
|
||||||
|
"exit_btn" to "Çıkış",
|
||||||
|
"please_username_password" to "Lütfen kullanıcı adı ve şifre girin.",
|
||||||
|
"download_failed_no_local_db" to "İndirme başarısız – yerel veritabanı yok",
|
||||||
|
"download_failed_use_offline" to "İndirme başarısız – mevcut veritabanıyla çevrimdışı çalışın",
|
||||||
|
"login_failed_with_reason" to "Giriş başarısız: {reason}",
|
||||||
|
"no_header_template_found" to "Başlık şablonu bulunamadı",
|
||||||
|
"login_required" to "Lütfen önce giriş yapın",
|
||||||
|
"questionnaire" to "Anket",
|
||||||
|
"session_label" to "Oturum",
|
||||||
|
"session_dash" to "Oturum: —",
|
||||||
|
"hours_short" to "sa",
|
||||||
|
"minutes_short" to "dk",
|
||||||
|
"online" to "Çevrimiçi",
|
||||||
|
"offline" to "Çevrimdışı",
|
||||||
|
"open_client_via_load" to "Lütfen müşteriyi \"Yükle\" ile açın.",
|
||||||
|
"database" to "Veritabanı",
|
||||||
|
"database_clients_title" to "Veritabanı – Müşteriler",
|
||||||
|
"no_clients_available" to "Kullanılabilir müşteri yok.",
|
||||||
|
"previous" to "Geri",
|
||||||
|
"download_header" to "Başlığı indir",
|
||||||
|
"client_code" to "Müşteri kodu",
|
||||||
|
"export_success_downloads" to "Dışa aktarma başarılı: İndirilenler/ClientHeaders.xlsx",
|
||||||
|
"export_failed" to "Dışa aktarma başarısız.",
|
||||||
|
"error" to "Hata",
|
||||||
|
"client" to "Müşteri",
|
||||||
|
"questionnaires" to "Anketler",
|
||||||
|
"headers" to "Başlıklar",
|
||||||
|
"questionnaire_id" to "Anket ID",
|
||||||
|
"status" to "Durum",
|
||||||
|
"id" to "ID",
|
||||||
|
"value" to "Değer",
|
||||||
|
"no_questionnaires" to "Mevcut anket yok.",
|
||||||
|
"no_questions_available" to "Soru yok.",
|
||||||
|
"question" to "Soru",
|
||||||
|
"answer" to "Cevap",
|
||||||
|
"done" to "Tamamlandı",
|
||||||
|
"not_done" to "Tamamlanmadı",
|
||||||
|
"none" to "Yok",
|
||||||
|
"view_missing" to "Eksik görünüm: %s",
|
||||||
|
"session_over_12" to "Oturum 12 saatten uzun süredir açık.",
|
||||||
|
"cancel" to "İptal",
|
||||||
|
"ok" to "Tamam",
|
||||||
|
"ask_before_upload" to "Yüklemeyi gerçekten yapmak istiyor musunuz?",
|
||||||
|
"start_upload" to "Yüklemeyi başlat?"
|
||||||
),
|
),
|
||||||
|
|
||||||
"POLISH" to mapOf(
|
"POLISH" to mapOf(
|
||||||
@ -2024,14 +2332,14 @@ object LanguageManager {
|
|||||||
"once" to "jeden raz",
|
"once" to "jeden raz",
|
||||||
"year_after_2000" to "Rok musi być po 2000!",
|
"year_after_2000" to "Rok musi być po 2000!",
|
||||||
"year_after_departure" to "Rok musi być po opuszczeniu kraju pochodzenia!",
|
"year_after_departure" to "Rok musi być po opuszczeniu kraju pochodzenia!",
|
||||||
"year_max" to "Rok musi być mniejszy lub równy $MAX_VALUE_YEAR!",
|
"year_max" to "Rok musi być mniejszy lub równy ${MAX_VALUE_YEAR}!",
|
||||||
"data_final_warning" to "<b><font color='#FF0000'>Ważne:</font></b> Po zakończeniu dane nie mogą być zmienione ani edytowane!",
|
"data_final_warning" to "<b><font color='#FF0000'>Ważne:</font></b> Po zakończeniu dane nie mogą być zmienione ani edytowane!",
|
||||||
"multiple_times" to "kilka razy",
|
"multiple_times" to "kilka razy",
|
||||||
"more_than_15_years" to "więcej niż 15 lat",
|
"more_than_15_years" to "więcej niż 15 lat",
|
||||||
"no" to "Nie",
|
"no" to "Nie",
|
||||||
"no_answer" to "brak odpowiedzi",
|
"no_answer" to "brak odpowiedzi",
|
||||||
"other_country" to "inny kraj",
|
"other_country" to "inny kraj",
|
||||||
"value_must_be_less_equal_max" to "Wartość musi być mniejsza lub równa $MAX_VALUE_AGE!",
|
"value_must_be_less_equal_max" to "Wartość musi być mniejsza lub równa ${MAX_VALUE_AGE}!",
|
||||||
"value_between_1_and_15" to "Wartość musi być między 1 a 15!",
|
"value_between_1_and_15" to "Wartość musi być między 1 a 15!",
|
||||||
"invalid_month" to "Nieprawidłowy miesiąc!",
|
"invalid_month" to "Nieprawidłowy miesiąc!",
|
||||||
"invalid_year" to "Nieprawidłowy rok!",
|
"invalid_year" to "Nieprawidłowy rok!",
|
||||||
@ -2320,7 +2628,58 @@ object LanguageManager {
|
|||||||
"done" to "Gotowe",
|
"done" to "Gotowe",
|
||||||
"locked" to "Zablokowane",
|
"locked" to "Zablokowane",
|
||||||
"start" to "Rozpocznij",
|
"start" to "Rozpocznij",
|
||||||
"points" to "Punkty"
|
"points" to "Punkty",
|
||||||
|
"saved_pdf_csv" to "Pliki PDF i CSV zapisano w folderze \"Pobrane\".",
|
||||||
|
"no_pdf_viewer" to "Brak zainstalowanej przeglądarki PDF.",
|
||||||
|
"save_error" to "Błąd zapisu: {message}",
|
||||||
|
"login_required_title" to "Wymagane logowanie",
|
||||||
|
"username_hint" to "Nazwa użytkownika",
|
||||||
|
"password_hint" to "Hasło",
|
||||||
|
"login_btn" to "Zaloguj",
|
||||||
|
"exit_btn" to "Zakończ",
|
||||||
|
"please_username_password" to "Wprowadź nazwę użytkownika i hasło.",
|
||||||
|
"download_failed_no_local_db" to "Pobieranie nieudane – brak lokalnej bazy danych",
|
||||||
|
"download_failed_use_offline" to "Pobieranie nieudane – pracuj offline z istniejącą bazą danych",
|
||||||
|
"login_failed_with_reason" to "Logowanie nie powiodło się: {reason}",
|
||||||
|
"no_header_template_found" to "Nie znaleziono szablonu nagłówków",
|
||||||
|
"login_required" to "Najpierw się zaloguj",
|
||||||
|
"questionnaire" to "Kwestionariusz",
|
||||||
|
"session_label" to "Sesja",
|
||||||
|
"session_dash" to "Sesja: —",
|
||||||
|
"hours_short" to "h",
|
||||||
|
"minutes_short" to "min",
|
||||||
|
"online" to "Online",
|
||||||
|
"offline" to "Offline",
|
||||||
|
"open_client_via_load" to "Otwórz klienta przez „Wczytaj”.",
|
||||||
|
"database" to "Baza danych",
|
||||||
|
"database_clients_title" to "Baza danych – Klienci",
|
||||||
|
"no_clients_available" to "Brak dostępnych klientów.",
|
||||||
|
"previous" to "Wstecz",
|
||||||
|
"download_header" to "Pobierz nagłówki",
|
||||||
|
"client_code" to "Kod klienta",
|
||||||
|
"export_success_downloads" to "Eksport zakończony: Pobrane/ClientHeaders.xlsx",
|
||||||
|
"export_failed" to "Eksport nieudany.",
|
||||||
|
"error" to "Błąd",
|
||||||
|
"client" to "Klient",
|
||||||
|
"questionnaires" to "Kwestionariusze",
|
||||||
|
"headers" to "Nagłówki",
|
||||||
|
"questionnaire_id" to "ID kwestionariusza",
|
||||||
|
"status" to "Status",
|
||||||
|
"id" to "ID",
|
||||||
|
"value" to "Wartość",
|
||||||
|
"no_questionnaires" to "Brak dostępnych kwestionariuszy.",
|
||||||
|
"no_questions_available" to "Brak pytań.",
|
||||||
|
"question" to "Pytanie",
|
||||||
|
"answer" to "Odpowiedź",
|
||||||
|
"done" to "Zrobione",
|
||||||
|
"not_done" to "Niezrobione",
|
||||||
|
"none" to "Brak",
|
||||||
|
"view_missing" to "Brak widoku: %s",
|
||||||
|
"session_over_12" to "Sesja trwa dłużej niż 12 godzin.",
|
||||||
|
"cancel" to "Anuluj",
|
||||||
|
"ok" to "OK",
|
||||||
|
"ask_before_upload" to "Czy na pewno chcesz wykonać przesyłanie?",
|
||||||
|
"start_upload" to "Rozpocząć przesyłanie?"
|
||||||
),
|
),
|
||||||
|
|
||||||
"ARABIC" to mapOf(
|
"ARABIC" to mapOf(
|
||||||
@ -2353,14 +2712,14 @@ object LanguageManager {
|
|||||||
"once" to "مرة واحدة",
|
"once" to "مرة واحدة",
|
||||||
"year_after_2000" to "يجب أن تكون السنة بعد 2000!",
|
"year_after_2000" to "يجب أن تكون السنة بعد 2000!",
|
||||||
"year_after_departure" to "يجب أن تكون السنة بعد مغادرة بلد المنشأ!",
|
"year_after_departure" to "يجب أن تكون السنة بعد مغادرة بلد المنشأ!",
|
||||||
"year_max" to "يجب أن تكون السنة أقل من أو تساوي $MAX_VALUE_YEAR!",
|
"year_max" to "يجب أن تكون السنة أقل من أو تساوي ${MAX_VALUE_YEAR}!",
|
||||||
"data_final_warning" to "<b><font color='#FF0000'>هام:</font></b> لا يمكن تعديل البيانات بعد الانتهاء!",
|
"data_final_warning" to "<b><font color='#FF0000'>هام:</font></b> لا يمكن تعديل البيانات بعد الانتهاء!",
|
||||||
"multiple_times" to "عدة مرات",
|
"multiple_times" to "عدة مرات",
|
||||||
"more_than_15_years" to "أكثر من 15 سنة",
|
"more_than_15_years" to "أكثر من 15 سنة",
|
||||||
"no" to "لا",
|
"no" to "لا",
|
||||||
"no_answer" to "لا يوجد إجابة",
|
"no_answer" to "لا يوجد إجابة",
|
||||||
"other_country" to "بلد آخر",
|
"other_country" to "بلد آخر",
|
||||||
"value_must_be_less_equal_max" to "يجب أن تكون القيمة أقل من أو تساوي $MAX_VALUE_AGE!",
|
"value_must_be_less_equal_max" to "يجب أن تكون القيمة أقل من أو تساوي ${MAX_VALUE_AGE}!",
|
||||||
"value_between_1_and_15" to "يجب أن تكون القيمة بين 1 و15!",
|
"value_between_1_and_15" to "يجب أن تكون القيمة بين 1 و15!",
|
||||||
"invalid_month" to "شهر غير صالح!",
|
"invalid_month" to "شهر غير صالح!",
|
||||||
"invalid_year" to "سنة غير صالحة!",
|
"invalid_year" to "سنة غير صالحة!",
|
||||||
@ -2649,7 +3008,58 @@ object LanguageManager {
|
|||||||
"done" to "تم",
|
"done" to "تم",
|
||||||
"locked" to "مقفل",
|
"locked" to "مقفل",
|
||||||
"start" to "ابدأ",
|
"start" to "ابدأ",
|
||||||
"points" to "النقاط"
|
"points" to "النقاط",
|
||||||
|
"saved_pdf_csv" to "تم حفظ ملفات PDF وCSV في مجلد \"التنزيلات\".",
|
||||||
|
"no_pdf_viewer" to "لا يوجد عارض PDF مثبت.",
|
||||||
|
"save_error" to "خطأ أثناء الحفظ: {message}",
|
||||||
|
"login_required_title" to "يلزم تسجيل الدخول",
|
||||||
|
"username_hint" to "اسم المستخدم",
|
||||||
|
"password_hint" to "كلمة المرور",
|
||||||
|
"login_btn" to "تسجيل الدخول",
|
||||||
|
"exit_btn" to "إنهاء",
|
||||||
|
"please_username_password" to "يرجى إدخال اسم المستخدم وكلمة المرور.",
|
||||||
|
"download_failed_no_local_db" to "فشل التنزيل — لا توجد قاعدة بيانات محلية",
|
||||||
|
"download_failed_use_offline" to "فشل التنزيل — العمل بدون اتصال باستخدام قاعدة البيانات الحالية",
|
||||||
|
"login_failed_with_reason" to "فشل تسجيل الدخول: {reason}",
|
||||||
|
"no_header_template_found" to "لم يتم العثور على قالب للرؤوس",
|
||||||
|
"login_required" to "يرجى تسجيل الدخول أولاً",
|
||||||
|
"questionnaire" to "استبيان",
|
||||||
|
"session_label" to "الجلسة",
|
||||||
|
"session_dash" to "الجلسة: —",
|
||||||
|
"hours_short" to "س",
|
||||||
|
"minutes_short" to "د",
|
||||||
|
"online" to "متصل",
|
||||||
|
"offline" to "غير متصل",
|
||||||
|
"open_client_via_load" to "يرجى فتح العميل عبر «تحميل».",
|
||||||
|
"database" to "قاعدة البيانات",
|
||||||
|
"database_clients_title" to "قاعدة البيانات – العملاء",
|
||||||
|
"no_clients_available" to "لا يوجد عملاء.",
|
||||||
|
"previous" to "رجوع",
|
||||||
|
"download_header" to "تنزيل الرؤوس",
|
||||||
|
"client_code" to "رمز العميل",
|
||||||
|
"export_success_downloads" to "تم التصدير بنجاح: التنزيلات/ClientHeaders.xlsx",
|
||||||
|
"export_failed" to "فشل التصدير.",
|
||||||
|
"error" to "خطأ",
|
||||||
|
"client" to "عميل",
|
||||||
|
"questionnaires" to "استبيانات",
|
||||||
|
"headers" to "رؤوس",
|
||||||
|
"questionnaire_id" to "معرّف الاستبيان",
|
||||||
|
"status" to "الحالة",
|
||||||
|
"id" to "المعرّف",
|
||||||
|
"value" to "القيمة",
|
||||||
|
"no_questionnaires" to "لا توجد استبيانات متاحة.",
|
||||||
|
"no_questions_available" to "لا توجد أسئلة.",
|
||||||
|
"question" to "سؤال",
|
||||||
|
"answer" to "إجابة",
|
||||||
|
"done" to "منجز",
|
||||||
|
"not_done" to "غير منجز",
|
||||||
|
"none" to "لا شيء",
|
||||||
|
"view_missing" to "العنصر المفقود: %s",
|
||||||
|
"session_over_12" to "تعمل الجلسة منذ أكثر من 12 ساعة.",
|
||||||
|
"cancel" to "إلغاء",
|
||||||
|
"ok" to "موافق",
|
||||||
|
"ask_before_upload" to "هل ترغب فعلًا في تنفيذ الرفع؟",
|
||||||
|
"start_upload" to "بدء الرفع؟"
|
||||||
),
|
),
|
||||||
|
|
||||||
"ROMANIAN" to mapOf(
|
"ROMANIAN" to mapOf(
|
||||||
@ -2682,14 +3092,14 @@ object LanguageManager {
|
|||||||
"once" to "o dată",
|
"once" to "o dată",
|
||||||
"year_after_2000" to "Anul trebuie să fie după 2000!",
|
"year_after_2000" to "Anul trebuie să fie după 2000!",
|
||||||
"year_after_departure" to "Anul trebuie să fie după plecarea din țara de origine!",
|
"year_after_departure" to "Anul trebuie să fie după plecarea din țara de origine!",
|
||||||
"year_max" to "Anul trebuie să fie mai mic sau egal cu $MAX_VALUE_YEAR!",
|
"year_max" to "Anul trebuie să fie mai mic sau egal cu ${MAX_VALUE_YEAR}!",
|
||||||
"data_final_warning" to "<b><font color='#FF0000'>Important:</font></b> Datele nu mai pot fi modificate după finalizare!",
|
"data_final_warning" to "<b><font color='#FF0000'>Important:</font></b> Datele nu mai pot fi modificate după finalizare!",
|
||||||
"multiple_times" to "de mai multe ori",
|
"multiple_times" to "de mai multe ori",
|
||||||
"more_than_15_years" to "mai mult de 15 ani",
|
"more_than_15_years" to "mai mult de 15 ani",
|
||||||
"no" to "Nu",
|
"no" to "Nu",
|
||||||
"no_answer" to "fără răspuns",
|
"no_answer" to "fără răspuns",
|
||||||
"other_country" to "altă țară",
|
"other_country" to "altă țară",
|
||||||
"value_must_be_less_equal_max" to "Valoarea trebuie să fie mai mică sau egală cu $MAX_VALUE_AGE!",
|
"value_must_be_less_equal_max" to "Valoarea trebuie să fie mai mică sau egală cu ${MAX_VALUE_AGE}!",
|
||||||
"value_between_1_and_15" to "Valoarea trebuie să fie între 1 și 15!",
|
"value_between_1_and_15" to "Valoarea trebuie să fie între 1 și 15!",
|
||||||
"invalid_month" to "Lună invalidă!",
|
"invalid_month" to "Lună invalidă!",
|
||||||
"invalid_year" to "An invalid!",
|
"invalid_year" to "An invalid!",
|
||||||
@ -2978,7 +3388,58 @@ object LanguageManager {
|
|||||||
"done" to "Finalizat",
|
"done" to "Finalizat",
|
||||||
"locked" to "Blocat",
|
"locked" to "Blocat",
|
||||||
"start" to "Începe",
|
"start" to "Începe",
|
||||||
"points" to "Puncte"
|
"points" to "Puncte",
|
||||||
|
"saved_pdf_csv" to "PDF și CSV au fost salvate în folderul „Descărcări”.",
|
||||||
|
"no_pdf_viewer" to "Nu este instalat niciun vizualizator PDF.",
|
||||||
|
"save_error" to "Eroare la salvare: {message}",
|
||||||
|
"login_required_title" to "Autentificare necesară",
|
||||||
|
"username_hint" to "Nume utilizator",
|
||||||
|
"password_hint" to "Parolă",
|
||||||
|
"login_btn" to "Autentificare",
|
||||||
|
"exit_btn" to "Ieșire",
|
||||||
|
"please_username_password" to "Introduceți numele de utilizator și parola.",
|
||||||
|
"download_failed_no_local_db" to "Descărcare eșuată – nu există bază de date locală",
|
||||||
|
"download_failed_use_offline" to "Descărcare eșuată – lucrați offline cu baza de date existentă",
|
||||||
|
"login_failed_with_reason" to "Autentificarea a eșuat: {reason}",
|
||||||
|
"no_header_template_found" to "Nu s-a găsit șablon de antet",
|
||||||
|
"login_required" to "Conectați-vă mai întâi",
|
||||||
|
"questionnaire" to "Chestionar",
|
||||||
|
"session_label" to "Sesiune",
|
||||||
|
"session_dash" to "Sesiune: —",
|
||||||
|
"hours_short" to "h",
|
||||||
|
"minutes_short" to "min",
|
||||||
|
"online" to "Online",
|
||||||
|
"offline" to "Offline",
|
||||||
|
"open_client_via_load" to "Deschideți clientul prin „Încărcare”.",
|
||||||
|
"database" to "Bază de date",
|
||||||
|
"database_clients_title" to "Bază de date – Clienți",
|
||||||
|
"no_clients_available" to "Nu există clienți.",
|
||||||
|
"previous" to "Înapoi",
|
||||||
|
"download_header" to "Descarcă antetul",
|
||||||
|
"client_code" to "Cod client",
|
||||||
|
"export_success_downloads" to "Export reușit: Descărcări/ClientHeaders.xlsx",
|
||||||
|
"export_failed" to "Export eșuat.",
|
||||||
|
"error" to "Eroare",
|
||||||
|
"client" to "Client",
|
||||||
|
"questionnaires" to "Chestionare",
|
||||||
|
"headers" to "Antete",
|
||||||
|
"questionnaire_id" to "ID chestionar",
|
||||||
|
"status" to "Stare",
|
||||||
|
"id" to "ID",
|
||||||
|
"value" to "Valoare",
|
||||||
|
"no_questionnaires" to "Nu există chestionare disponibile.",
|
||||||
|
"no_questions_available" to "Nu există întrebări.",
|
||||||
|
"question" to "Întrebare",
|
||||||
|
"answer" to "Răspuns",
|
||||||
|
"done" to "Finalizat",
|
||||||
|
"not_done" to "Nefinalizat",
|
||||||
|
"none" to "Nimic",
|
||||||
|
"view_missing" to "Vizualizare lipsă: %s",
|
||||||
|
"session_over_12" to "Sesiunea rulează de mai bine de 12 ore.",
|
||||||
|
"cancel" to "Anulează",
|
||||||
|
"ok" to "OK",
|
||||||
|
"ask_before_upload" to "Vrei într-adevăr să efectuezi încărcarea?",
|
||||||
|
"start_upload" to "Pornești încărcarea?"
|
||||||
),
|
),
|
||||||
|
|
||||||
"SPANISH" to mapOf(
|
"SPANISH" to mapOf(
|
||||||
@ -3011,14 +3472,14 @@ object LanguageManager {
|
|||||||
"once" to "una vez",
|
"once" to "una vez",
|
||||||
"year_after_2000" to "¡El año debe ser posterior al 2000!",
|
"year_after_2000" to "¡El año debe ser posterior al 2000!",
|
||||||
"year_after_departure" to "¡El año debe ser posterior a la salida de su país de origen!",
|
"year_after_departure" to "¡El año debe ser posterior a la salida de su país de origen!",
|
||||||
"year_max" to "¡El año debe ser menor o igual que $MAX_VALUE_YEAR!",
|
"year_max" to "¡El año debe ser menor o igual que ${MAX_VALUE_YEAR}!",
|
||||||
"data_final_warning" to "<b><font color='#FF0000'>Importante:</font></b> Después de finalizar, los datos no podrán cambiarse o editarse.",
|
"data_final_warning" to "<b><font color='#FF0000'>Importante:</font></b> Después de finalizar, los datos no podrán cambiarse o editarse.",
|
||||||
"multiple_times" to "varias veces",
|
"multiple_times" to "varias veces",
|
||||||
"more_than_15_years" to "más de 15 años",
|
"more_than_15_years" to "más de 15 años",
|
||||||
"no" to "No",
|
"no" to "No",
|
||||||
"no_answer" to "sin respuesta",
|
"no_answer" to "sin respuesta",
|
||||||
"other_country" to "otro país",
|
"other_country" to "otro país",
|
||||||
"value_must_be_less_equal_max" to "¡El valor debe ser menor o igual que $MAX_VALUE_AGE!",
|
"value_must_be_less_equal_max" to "¡El valor debe ser menor o igual que ${MAX_VALUE_AGE}!",
|
||||||
"value_between_1_and_15" to "¡El valor debe estar entre 1 y 15!",
|
"value_between_1_and_15" to "¡El valor debe estar entre 1 y 15!",
|
||||||
"invalid_month" to "¡Mes no válido!",
|
"invalid_month" to "¡Mes no válido!",
|
||||||
"invalid_year" to "¡Año no válido!",
|
"invalid_year" to "¡Año no válido!",
|
||||||
@ -3307,7 +3768,58 @@ object LanguageManager {
|
|||||||
"done" to "Completado",
|
"done" to "Completado",
|
||||||
"locked" to "Bloqueado",
|
"locked" to "Bloqueado",
|
||||||
"start" to "Iniciar",
|
"start" to "Iniciar",
|
||||||
"points" to "Puncte"
|
"points" to "Puncte",
|
||||||
|
"saved_pdf_csv" to "Los archivos PDF y CSV se han guardado en la carpeta \"Descargas\".",
|
||||||
|
"no_pdf_viewer" to "No hay un visor de PDF instalado.",
|
||||||
|
"save_error" to "Error al guardar: {message}",
|
||||||
|
"login_required_title" to "Se requiere inicio de sesión",
|
||||||
|
"username_hint" to "Nombre de usuario",
|
||||||
|
"password_hint" to "Contraseña",
|
||||||
|
"login_btn" to "Iniciar sesión",
|
||||||
|
"exit_btn" to "Salir",
|
||||||
|
"please_username_password" to "Introduce nombre de usuario y contraseña.",
|
||||||
|
"download_failed_no_local_db" to "La descarga falló: no hay base de datos local",
|
||||||
|
"download_failed_use_offline" to "La descarga falló: trabaja sin conexión con la base de datos existente",
|
||||||
|
"login_failed_with_reason" to "Error de inicio de sesión: {reason}",
|
||||||
|
"no_header_template_found" to "No se encontró una plantilla de encabezados",
|
||||||
|
"login_required" to "Inicia sesión primero",
|
||||||
|
"questionnaire" to "Cuestionario",
|
||||||
|
"session_label" to "Sesión",
|
||||||
|
"session_dash" to "Sesión: —",
|
||||||
|
"hours_short" to "h",
|
||||||
|
"minutes_short" to "min",
|
||||||
|
"online" to "En línea",
|
||||||
|
"offline" to "Sin conexión",
|
||||||
|
"open_client_via_load" to "Abre el cliente mediante «Cargar».",
|
||||||
|
"database" to "Base de datos",
|
||||||
|
"database_clients_title" to "Base de datos – Clientes",
|
||||||
|
"no_clients_available" to "No hay clientes disponibles.",
|
||||||
|
"previous" to "Atrás",
|
||||||
|
"download_header" to "Descargar encabezados",
|
||||||
|
"client_code" to "Código de cliente",
|
||||||
|
"export_success_downloads" to "Exportación correcta: Descargas/ClientHeaders.xlsx",
|
||||||
|
"export_failed" to "La exportación falló.",
|
||||||
|
"error" to "Error",
|
||||||
|
"client" to "Cliente",
|
||||||
|
"questionnaires" to "Cuestionarios",
|
||||||
|
"headers" to "Encabezados",
|
||||||
|
"questionnaire_id" to "ID del cuestionario",
|
||||||
|
"status" to "Estado",
|
||||||
|
"id" to "ID",
|
||||||
|
"value" to "Valor",
|
||||||
|
"no_questionnaires" to "No hay cuestionarios disponibles.",
|
||||||
|
"no_questions_available" to "No hay preguntas.",
|
||||||
|
"question" to "Pregunta",
|
||||||
|
"answer" to "Respuesta",
|
||||||
|
"done" to "Completado",
|
||||||
|
"not_done" to "No completado",
|
||||||
|
"none" to "Ninguno",
|
||||||
|
"view_missing" to "Vista faltante: %s",
|
||||||
|
"session_over_12" to "La sesión lleva más de 12 horas.",
|
||||||
|
"cancel" to "Cancelar",
|
||||||
|
"ok" to "OK",
|
||||||
|
"ask_before_upload" to "¿Realmente deseas realizar la carga?",
|
||||||
|
"start_upload" to "¿Iniciar la carga?"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
110
app/src/main/java/com/dano/test1/utils/ViewUtils.kt
Normal file
110
app/src/main/java/com/dano/test1/utils/ViewUtils.kt
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
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 sp = (dm.heightPixels * 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
|
||||||
|
|
||||||
|
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)
|
||||||
|
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()
|
||||||
|
}
|
||||||
@ -5,9 +5,14 @@
|
|||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".MainActivity"
|
tools:context=".MainActivity">
|
||||||
tools:layout_editor_absoluteX="11dp"
|
|
||||||
tools:layout_editor_absoluteY="107dp">
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/gTop"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintGuide_begin="32dp" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/Qprev"
|
android:id="@+id/Qprev"
|
||||||
@ -27,7 +32,6 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<!-- Weiter -->
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/Qnext"
|
android:id="@+id/Qnext"
|
||||||
android:layout_width="@dimen/nav_btn_size"
|
android:layout_width="@dimen/nav_btn_size"
|
||||||
@ -46,6 +50,34 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/question"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/client_code"
|
android:id="@+id/client_code"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -54,7 +86,7 @@
|
|||||||
app:layout_constraintHeight_percent="0.08"
|
app:layout_constraintHeight_percent="0.08"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/question"
|
app:layout_constraintTop_toBottomOf="@id/question"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:background="@android:drawable/edit_text"
|
android:background="@android:drawable/edit_text"
|
||||||
android:ems="10"
|
android:ems="10"
|
||||||
@ -90,34 +122,4 @@
|
|||||||
android:autoSizeMaxTextSize="36sp"
|
android:autoSizeMaxTextSize="36sp"
|
||||||
android:autoSizeStepGranularity="2sp" />
|
android:autoSizeStepGranularity="2sp" />
|
||||||
|
|
||||||
<TextView
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:id="@+id/textView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHeight_percent="0.15"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="0.051"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/question"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHeight_percent="0.15"
|
|
||||||
app:layout_constraintHorizontal_bias="0.512"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
|
||||||
app:layout_constraintVertical_bias="0.0"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|||||||
@ -1,39 +1,42 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<!-- Obere Überschrift -->
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/gTop"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintGuide_begin="32dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView"
|
android:id="@+id/textView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:paddingStart="16dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:paddingEnd="16dp"
|
||||||
app:layout_constraintHeight_percent="0.15"
|
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintVertical_bias="0.051"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
<!-- Frage -->
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/question"
|
android:id="@+id/question"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:paddingStart="16dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:paddingEnd="16dp"
|
||||||
app:layout_constraintHeight_percent="0.15"
|
android:paddingTop="8dp"
|
||||||
app:layout_constraintHorizontal_bias="0.512"
|
android:paddingBottom="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -129,7 +132,6 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<!-- Weiter -->
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/Qnext"
|
android:id="@+id/Qnext"
|
||||||
android:layout_width="@dimen/nav_btn_size"
|
android:layout_width="@dimen/nav_btn_size"
|
||||||
|
|||||||
@ -1,41 +1,44 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<!-- Titel -->
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/gTop"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintGuide_begin="32dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView"
|
android:id="@+id/textView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:paddingStart="16dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:paddingEnd="16dp"
|
||||||
app:layout_constraintHeight_percent="0.15"
|
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintVertical_bias="0.051"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
<!-- Leitfrage -->
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/question"
|
android:id="@+id/question"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:paddingStart="16dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:paddingEnd="16dp"
|
||||||
app:layout_constraintHeight_percent="0.15"
|
android:paddingTop="8dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
android:paddingBottom="8dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/textView"
|
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
<!-- FIXE ICON-LEISTE (bleibt stehen) -->
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/glass_header"
|
android:id="@+id/glass_header"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -48,7 +51,6 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintWidth_percent="1" />
|
app:layout_constraintWidth_percent="1" />
|
||||||
|
|
||||||
<!-- Scrollbarer Bereich NUR für Symptome + Kreise -->
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:id="@+id/glassScroll"
|
android:id="@+id/glassScroll"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -73,15 +75,12 @@
|
|||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<!-- Buttons unten -->
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/Qprev"
|
android:id="@+id/Qprev"
|
||||||
android:layout_width="@dimen/nav_btn_size"
|
android:layout_width="@dimen/nav_btn_size"
|
||||||
android:layout_height="@dimen/nav_btn_size"
|
android:layout_height="@dimen/nav_btn_size"
|
||||||
android:layout_marginStart="20dp"
|
android:layout_marginStart="20dp"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
android:text=""
|
|
||||||
android:textAllCaps="false"
|
|
||||||
app:icon="@drawable/ic_chevron_left"
|
app:icon="@drawable/ic_chevron_left"
|
||||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||||
app:iconSize="@dimen/nav_icon_size"
|
app:iconSize="@dimen/nav_icon_size"
|
||||||
@ -92,15 +91,12 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<!-- Weiter -->
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/Qnext"
|
android:id="@+id/Qnext"
|
||||||
android:layout_width="@dimen/nav_btn_size"
|
android:layout_width="@dimen/nav_btn_size"
|
||||||
android:layout_height="@dimen/nav_btn_size"
|
android:layout_height="@dimen/nav_btn_size"
|
||||||
android:layout_marginEnd="20dp"
|
android:layout_marginEnd="20dp"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
android:text=""
|
|
||||||
android:textAllCaps="false"
|
|
||||||
app:icon="@drawable/ic_chevron_right"
|
app:icon="@drawable/ic_chevron_right"
|
||||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||||
app:iconSize="@dimen/nav_icon_size"
|
app:iconSize="@dimen/nav_icon_size"
|
||||||
|
|||||||
@ -5,9 +5,14 @@
|
|||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".MainActivity"
|
tools:context=".MainActivity">
|
||||||
tools:layout_editor_absoluteX="11dp"
|
|
||||||
tools:layout_editor_absoluteY="107dp">
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/gTop"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintGuide_begin="32dp" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/Qprev"
|
android:id="@+id/Qprev"
|
||||||
@ -27,7 +32,6 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<!-- Weiter -->
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/Qnext"
|
android:id="@+id/Qnext"
|
||||||
android:layout_width="@dimen/nav_btn_size"
|
android:layout_width="@dimen/nav_btn_size"
|
||||||
@ -46,7 +50,34 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
<!-- ScrollView füllt den Raum zwischen Frage und Buttons -->
|
<TextView
|
||||||
|
android:id="@+id/textView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/question"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:id="@+id/scrollView"
|
android:id="@+id/scrollView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -56,8 +87,8 @@
|
|||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/question"
|
app:layout_constraintTop_toBottomOf="@id/question"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/Qnext"
|
app:layout_constraintBottom_toTopOf="@id/Qnext"
|
||||||
app:layout_constraintWidth_percent="0.9">
|
app:layout_constraintWidth_percent="0.9">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@ -68,32 +99,4 @@
|
|||||||
android:padding="16dp" />
|
android:padding="16dp" />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHeight_percent="0.15"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="0.051"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/question"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHeight_percent="0.15"
|
|
||||||
app:layout_constraintHorizontal_bias="0.512"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
|
||||||
app:layout_constraintVertical_bias="0.0"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@ -5,11 +5,15 @@
|
|||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".MainActivity"
|
tools:context=".MainActivity">
|
||||||
tools:layout_editor_absoluteX="11dp"
|
|
||||||
tools:layout_editor_absoluteY="107dp">
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/gTop"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintGuide_begin="32dp" />
|
||||||
|
|
||||||
<!-- Zurück -->
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/Qprev"
|
android:id="@+id/Qprev"
|
||||||
android:layout_width="@dimen/nav_btn_size"
|
android:layout_width="@dimen/nav_btn_size"
|
||||||
@ -28,7 +32,6 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<!-- Weiter -->
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/Qnext"
|
android:id="@+id/Qnext"
|
||||||
android:layout_width="@dimen/nav_btn_size"
|
android:layout_width="@dimen/nav_btn_size"
|
||||||
@ -47,10 +50,34 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/question"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
<!-- SCROLLBEREICH für die Radio-Optionen:
|
|
||||||
füllt den Platz zwischen Frage und Buttons, scrollt bei Bedarf -->
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:id="@+id/radioScroll"
|
android:id="@+id/radioScroll"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -65,7 +92,6 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintWidth_percent="0.80">
|
app:layout_constraintWidth_percent="0.80">
|
||||||
|
|
||||||
<!-- Die RadioGroup bleibt gleich, ist jetzt aber scrollfähig -->
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
android:id="@+id/RadioGroup"
|
android:id="@+id/RadioGroup"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -74,33 +100,4 @@
|
|||||||
android:padding="8dp" />
|
android:padding="8dp" />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHeight_percent="0.15"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="0.051"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/question"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHeight_percent="0.15"
|
|
||||||
app:layout_constraintHorizontal_bias="0.512"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
|
||||||
app:layout_constraintVertical_bias="0.0"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@ -5,6 +5,13 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/gTop"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintGuide_begin="32dp" />
|
||||||
|
|
||||||
<Spinner
|
<Spinner
|
||||||
android:id="@+id/string_spinner"
|
android:id="@+id/string_spinner"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -33,7 +40,6 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<!-- Weiter -->
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/Qnext"
|
android:id="@+id/Qnext"
|
||||||
android:layout_width="@dimen/nav_btn_size"
|
android:layout_width="@dimen/nav_btn_size"
|
||||||
@ -55,30 +61,29 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView"
|
android:id="@+id/textView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:paddingStart="16dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:paddingEnd="16dp"
|
||||||
app:layout_constraintHeight_percent="0.15"
|
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintVertical_bias="0.051"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/question"
|
android:id="@+id/question"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:paddingStart="16dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:paddingEnd="16dp"
|
||||||
app:layout_constraintHeight_percent="0.15"
|
android:paddingTop="8dp"
|
||||||
app:layout_constraintHorizontal_bias="0.512"
|
android:paddingBottom="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@ -5,6 +5,13 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/gTop"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintGuide_begin="32dp" />
|
||||||
|
|
||||||
<Spinner
|
<Spinner
|
||||||
android:id="@+id/value_spinner"
|
android:id="@+id/value_spinner"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -13,7 +20,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.495"
|
app:layout_constraintHorizontal_bias="0.495"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/question"
|
app:layout_constraintTop_toBottomOf="@id/question"
|
||||||
app:layout_constraintVertical_bias="0.027"
|
app:layout_constraintVertical_bias="0.027"
|
||||||
app:layout_constraintWidth_percent="0.70" />
|
app:layout_constraintWidth_percent="0.70" />
|
||||||
|
|
||||||
@ -35,7 +42,6 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<!-- Weiter -->
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/Qnext"
|
android:id="@+id/Qnext"
|
||||||
android:layout_width="@dimen/nav_btn_size"
|
android:layout_width="@dimen/nav_btn_size"
|
||||||
@ -57,31 +63,29 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView"
|
android:id="@+id/textView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:paddingStart="16dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:paddingEnd="16dp"
|
||||||
app:layout_constraintHeight_percent="0.15"
|
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintVertical_bias="0.051"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/question"
|
android:id="@+id/question"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:paddingStart="16dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:paddingEnd="16dp"
|
||||||
app:layout_constraintHeight_percent="0.15"
|
android:paddingTop="8dp"
|
||||||
app:layout_constraintHorizontal_bias="0.512"
|
android:paddingBottom="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
|
||||||
app:layout_constraintWidth_percent="0.9" />
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user