implementing first functionality
This commit is contained in:
1
.idea/.name
generated
1
.idea/.name
generated
@ -1 +0,0 @@
|
||||
NAT-AS
|
||||
17
.idea/deploymentTargetSelector.xml
generated
17
.idea/deploymentTargetSelector.xml
generated
@ -4,14 +4,27 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-05-27T17:45:31.648786459Z">
|
||||
<DropdownSelection timestamp="2026-06-29T18:39:40.854601Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=R52T605XE0L" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
<DialogSelection>
|
||||
<targets>
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=R52T605XE0L" />
|
||||
</handle>
|
||||
</Target>
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=b6bfcb67" />
|
||||
</handle>
|
||||
</Target>
|
||||
</targets>
|
||||
</DialogSelection>
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
||||
71
.kotlin/errors/errors-1782740918600.log
Normal file
71
.kotlin/errors/errors-1782740918600.log
Normal file
@ -0,0 +1,71 @@
|
||||
kotlin version: 2.0.21
|
||||
error message: Daemon compilation failed: null
|
||||
java.lang.Exception
|
||||
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
|
||||
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
|
||||
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240)
|
||||
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
|
||||
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
|
||||
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:76)
|
||||
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
|
||||
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
|
||||
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
|
||||
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
|
||||
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
|
||||
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
|
||||
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
|
||||
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
|
||||
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
|
||||
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
|
||||
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
|
||||
at org.gradle.internal.Factories$1.create(Factories.java:31)
|
||||
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
|
||||
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
|
||||
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
|
||||
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
|
||||
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
|
||||
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
|
||||
at java.base/java.lang.Thread.run(Unknown Source)
|
||||
Caused by: java.nio.file.NoSuchFileException: C:\Users\tomhe\AppData\Local\Temp\kotlin-backups853413239455496257\8.backup -> C:\Promotion-Software\Barometer\app\build\tmp\kapt3\incrementalData\debug\com\tomhempel\rbarometer\ui\ReviewScoresHandler$Companion.class
|
||||
at java.base/sun.nio.fs.WindowsException.translateToIOException(Unknown Source)
|
||||
at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source)
|
||||
at java.base/sun.nio.fs.WindowsFileCopy.move(Unknown Source)
|
||||
at java.base/sun.nio.fs.WindowsFileSystemProvider.move(Unknown Source)
|
||||
at java.base/java.nio.file.Files.move(Unknown Source)
|
||||
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.revertChanges(CompilationTransaction.kt:231)
|
||||
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.close(CompilationTransaction.kt:256)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally(IncrementalCompilerRunner.kt:747)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:120)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
|
||||
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
|
||||
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
|
||||
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
|
||||
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
|
||||
... 3 more
|
||||
|
||||
|
||||
316
app/src/main/assets/bundled_german_defaults.json
Normal file
316
app/src/main/assets/bundled_german_defaults.json
Normal file
@ -0,0 +1,316 @@
|
||||
{
|
||||
"all_clients": "Alle Klienten",
|
||||
"answer": "Antwort",
|
||||
"april": "April",
|
||||
"august": "August",
|
||||
"auth_footer_accounts": "Konten werden von Ihrer Organisation vergeben.",
|
||||
"auth_subtitle_change_password": "Bitte legen Sie jetzt ein eigenes Passwort fest.",
|
||||
"auth_subtitle_login": "Melden Sie sich mit Ihrem Berater-Konto an.",
|
||||
"auth_title": "NAT-AS Ressourcenbarometer",
|
||||
"cancel": "Abbrechen",
|
||||
"category_green": "Grün",
|
||||
"category_red": "Rot",
|
||||
"category_yellow": "Gelb",
|
||||
"choose_answer": "Antwort wählen",
|
||||
"choose_more_elements": "Es müssen mehr Elemenete ausgewählt werden.",
|
||||
"client": "Klient",
|
||||
"client_code": "Klientencode",
|
||||
"client_code_exists": "Dieser Client-Code existiert bereits.",
|
||||
"coach_code": "Berater-Code",
|
||||
"confirm_password_hint": "Passwort bestätigen",
|
||||
"date_after": "Das Datum muss nach",
|
||||
"date_before": "Das Datum muss vor",
|
||||
"day": "Tag",
|
||||
"december": "Dezember",
|
||||
"done": "Erledigt",
|
||||
"download": "Herunterladen",
|
||||
"download_failed_use_offline": "Download fehlgeschlagen – arbeite offline mit vorhandener Datenbank",
|
||||
"enter_coach_code": "Bitte geben Sie Ihren Berater-Code ein, um fortzufahren!",
|
||||
"error": "Fehler",
|
||||
"error_invalid_data": "Ungültige Daten",
|
||||
"error_not_found": "Nicht gefunden",
|
||||
"questionnaire_title": "Fragebögen",
|
||||
"exit_btn": "Beenden",
|
||||
"extreme_glass": "extrem",
|
||||
"february": "Februar",
|
||||
"fill_all_fields": "Bitte alle Felder ausfüllen!",
|
||||
"fill_both_fields": "Bitte Klienten-Code und Berater-Code prüfen.",
|
||||
"hours_short": "h",
|
||||
"january": "Januar",
|
||||
"july": "Juli",
|
||||
"june": "Juni",
|
||||
"lay": "liegen.",
|
||||
"little_glass": "ein wenig",
|
||||
"consultation_unlock_requires_rhs": "Freischaltung nach Abschluss von Demografie und RHS",
|
||||
"enter_ages_ranges_hint": "Bitte Alter als Bereich (z. B. 8–12) oder einzelne Jahre (z. B. 6, 9, 13) eingeben",
|
||||
"questionnaire_unlock_2_rhs": "Verfügbar nach Abschluss von Demografie",
|
||||
"questionnaire_unlock_3_integration_index": "Verfügbar nach Abschluss von Demografie und RHS",
|
||||
"questionnaire_unlock_4_consultation_results": "Verfügbar nach Abschluss von Demografie (Einwilligung unterschrieben), RHS und IPL",
|
||||
"locked": "Gesperrt",
|
||||
"questionnaire_chip_edit": "Bearbeiten",
|
||||
"questionnaire_chip_edit_pending": "Bearbeiten · Upload ausstehend",
|
||||
"login_btn": "Login",
|
||||
"login_failed_with_reason": "Login fehlgeschlagen: {reason}",
|
||||
"login_required": "Bitte zuerst einloggen",
|
||||
"logout": "Abmelden",
|
||||
"logout_confirm_action": "Abmelden",
|
||||
"logout_confirm_message": "Alle Daten auf diesem Gerät werden gelöscht. Sie müssen sich danach erneut anmelden.",
|
||||
"logout_confirm_title": "Wirklich abmelden?",
|
||||
"logout_offline_message": "Sie sind offline. Stellen Sie eine Internetverbindung her, laden Sie ausstehende Daten hoch und versuchen Sie es dann erneut.",
|
||||
"logout_offline_title": "Abmelden nicht möglich",
|
||||
"logout_pending_detail_in_progress": "{count} begonnene(r) Fragebogen mit lokalen Antworten",
|
||||
"logout_pending_detail_questionnaires": "{count} abgeschlossene(r) Fragebogen warten auf Upload",
|
||||
"logout_pending_detail_scoring": "{count} Punkteprüfung(en) warten auf Upload",
|
||||
"logout_pending_message": "Auf diesem Gerät liegen noch nicht hochgeladene Daten vor. Bitte tippen Sie auf „Hochladen“ und warten Sie, bis der Upload abgeschlossen ist. Schließen Sie begonnene Fragebögen zuerst ab.",
|
||||
"logout_pending_title": "Abmelden nicht möglich",
|
||||
"march": "März",
|
||||
"may": "Mai",
|
||||
"never_glass": "überhaupt nicht",
|
||||
"minutes_short": "m",
|
||||
"moderate_glass": "moderat",
|
||||
"month": "Monat",
|
||||
"much_glass": "ziemlich",
|
||||
"new_password_hint": "Neues Passwort",
|
||||
"next_step_completed_body": "Für diesen Klienten sind alle verfügbaren Schritte abgeschlossen.",
|
||||
"next_step_completed_title": "Alles ist abgeschlossen",
|
||||
"next_step_loading_body": "Der nächste sinnvolle Schritt wird vorbereitet.",
|
||||
"next_step_loading_title": "Nächster Schritt",
|
||||
"next_step_label": "Nächster Schritt",
|
||||
"next_step_questionnaire_body": "Wählen Sie unten den nächsten verfügbaren Fragebogen.",
|
||||
"next_step_questionnaire_title": "Nächsten Fragebogen ausfüllen",
|
||||
"next_step_review_body": "Prüfen Sie die berechneten Kategorien vor dem Upload.",
|
||||
"next_step_review_title": "Punkteprüfung abschließen",
|
||||
"next_step_show_questionnaire": "Fragebogen anzeigen",
|
||||
"next_step_upload_body": "Bitte prüfen und hochladen, sobald Sie bereit sind.",
|
||||
"next_step_upload_title": "Daten warten auf Upload",
|
||||
"no_clients_assigned": "Ihr Supervisor hat Ihnen noch keine Klienten zugewiesen.",
|
||||
"no_profile": "Dieser Klient ist noch nicht Teil der Datenbank",
|
||||
"no_questionnaires": "Keine Fragebögen vorhanden.",
|
||||
"no_questions_available": "Keine Fragen vorhanden.",
|
||||
"questionnaire_missing_options": "Dieser Fragebogen ist unvollständig (fehlende Antwortoptionen). Bitte wenden Sie sich an Ihren Administrator.",
|
||||
"none": "Keine",
|
||||
"not_done": "Nicht erledigt",
|
||||
"november": "November",
|
||||
"october": "Oktober",
|
||||
"offline": "Offline",
|
||||
"other_country": "anderes Land",
|
||||
"other_option": "Sonstiges",
|
||||
"ok": "OK",
|
||||
"online": "Online",
|
||||
"password_hint": "Passwort",
|
||||
"password_too_short": "Mindestens 6 Zeichen",
|
||||
"passwords_dont_match": "Passwörter stimmen nicht überein",
|
||||
"please_client_code": "Bitte Klienten Code eingeben",
|
||||
"please_username_password": "Bitte Benutzername und Passwort eingeben.",
|
||||
"points": "Punkte",
|
||||
"previous": "Zurück",
|
||||
"question": "Frage",
|
||||
"questions_filled": "Antworten",
|
||||
"refresh": "Aktualisieren",
|
||||
"review_scores": "Punkte prüfen",
|
||||
"review_scores_agree": "Berechnete Kategorie bestätigen",
|
||||
"review_scores_calculated_category": "Berechnete Kategorie",
|
||||
"review_scores_coach_category": "Berater-Kategorie",
|
||||
"review_scores_empty": "Noch keine vollständigen Profilwerte zum Prüfen.",
|
||||
"review_scores_pending_upload": "Ausstehend — wird mit dem Upload gesendet",
|
||||
"review_scores_complete_body": "Alle Kategorien sind ausgewählt. Die Prüfung wird beim nächsten Upload gesendet.",
|
||||
"review_scores_complete_title": "Punkteprüfung abgeschlossen",
|
||||
"review_scores_save_failed": "Kategorie konnte nicht gespeichert werden.",
|
||||
"review_scores_saved": "Kategorie gespeichert.",
|
||||
"review_scores_saved_pending": "Kategorie gespeichert — wird beim Upload gesendet.",
|
||||
"review_scores_set_category": "Kategorie setzen",
|
||||
"review_scores_subtitle": "Prüfen Sie die berechneten Werte und bestätigen oder setzen Sie die Kategorie.",
|
||||
"review_scores_total": "Gewichtete Summe",
|
||||
"save": "Speichern",
|
||||
"save_password_btn": "Passwort speichern",
|
||||
"select_one_answer": "Bitte wählen Sie eine Antwort aus!",
|
||||
"select_one_answer_per_row": "Bitte wählen Sie eine Antwort pro Reihe aus!",
|
||||
"september": "September",
|
||||
"session_dash": "Sitzung: —",
|
||||
"session_label": "Sitzung",
|
||||
"start": "Start",
|
||||
"start_upload": "Upload starten?",
|
||||
"upload": "Hochladen",
|
||||
"upload_failed_title": "Upload fehlgeschlagen",
|
||||
"upload_item_scoring_review": "Punkteprüfung: {profile}",
|
||||
"upload_nothing_pending": "Es wurde nichts hochgeladen. Es liegen keine abgeschlossenen Fragebögen zum Upload vor. Bitte zuerst einen Fragebogen abschließen.",
|
||||
"upload_prepare_failed": "Es wurde nichts hochgeladen. Die Daten konnten nicht für den Upload vorbereitet werden. Bitte erneut synchronisieren oder den Support kontaktieren.",
|
||||
"upload_scoring_review_summary": "Berater: {band} · Summe {total}",
|
||||
"upload_summary": "{qn} Fragebögen · {scores} Punkteprüfungen · {clients} Klienten",
|
||||
"upload_success_message": "Hochladen: {count} erledigt.",
|
||||
"upload_success_questionnaires": "{count} Fragebogen/Fragebögen",
|
||||
"upload_success_scoring_reviews": "{count} Punkteprüfung(en)",
|
||||
"upload_success_sync_next": "Nach diesem Upload startet die Synchronisierung automatisch.",
|
||||
"upload_success_title": "Upload abgeschlossen",
|
||||
"username_hint": "Benutzername",
|
||||
"year": "Jahr",
|
||||
"questionnaire_group_barometer_measures": "Barometer-Maßnahmen",
|
||||
"brs_instruction": "Geben Sie bitte an, wie sehr Sie den folgenden Aussagen jeweils zustimmen.",
|
||||
"brs_item_1_note_before": "Geben Sie bitte an, wie sehr Sie den folgenden Aussagen jeweils zustimmen.",
|
||||
"finish_data_entry": "Speichern",
|
||||
"brs_item_1": "Ich neige dazu, mich nach schwierigen Zeiten schnell zu erholen.",
|
||||
"strongly_disagree": "Stimme überhaupt nicht zu",
|
||||
"disagree": "Stimme nicht zu",
|
||||
"neutral": "Neutral",
|
||||
"agree": "Stimme zu",
|
||||
"strongly_agree": "Stimme voll und ganz zu",
|
||||
"brs_item_2": "Es fällt mir, schwer stressige Situationen durchzustehen.",
|
||||
"brs_item_3": "Ich brauche nicht viel Zeit, um mich von einem stressigen Ereignis zu erholen.",
|
||||
"brs_item_4": "Es fällt mir schwer zur Normalität zurückzukehren, wenn etwas Schlimmes passiert ist.",
|
||||
"brs_item_5": "Normalerweise überstehe ich schwierige Zeiten ohne größere Probleme.",
|
||||
"brs_item_6": "Ich brauche tendenziell lange, um über Rückschläge in meinem Leben hinwegzukommen.",
|
||||
"data_final_warning": "Bitte prüfen Sie Ihre Angaben. Nach dem Speichern kehren Sie zur Sitzungsübersicht zurück.",
|
||||
"fies_store_access_question": "Ist es schwierig, Geschäfte zu finden, die die von Ihnen bevorzugten Lebensmittel anbieten?",
|
||||
"very_difficult": "sehr schwer",
|
||||
"somewhat_difficult": "schwierig",
|
||||
"neither": "Weder noch",
|
||||
"somewhat_easy": "wenig",
|
||||
"very_easy": "sehr leicht",
|
||||
"fies_item_2": "Gab es in den letzten 4 Wochen Zeiten, in denen Sie besorgt waren, nicht genug zu essen zu haben, weil Sie nicht genug Geld oder sonstige Ressourcen hatten.",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"fies_item_3": "Gab es in den letzten 4 Wochen Zeiten, in denen Sie sich nicht gesund und ausgewogen ernähren konnten, weil Sie nicht genug Geld oder sonstige Ressourcen hatten.",
|
||||
"fies_item_4": "Gab es in den letzten 4 Wochen Zeiten, in denen Sie nur eine limitierte Auswahl an Lebensmitteln gegessen haben, weil Sie nicht genug Geld oder sonstige Ressourcen hatten.",
|
||||
"fies_item_5": "Gab es in den letzten 4 Wochen Zeiten, in denen Sie eine Mahlzeit auslassen mussten, weil Sie nicht genug Geld oder sonstige Ressourcen hatten, um Essen zu kaufen.",
|
||||
"fies_item_6": "Gab es in den letzten 4 Wochen Zeiten, in denen Sie weniger aßen als Sie Ihrer Ansicht nach essen sollten, weil Sie nicht genug Geld oder sonstige Ressourcen hatten.",
|
||||
"fies_item_7": "Gab es in den letzten 4 Wochen Zeiten, in denen Ihrem Haushalt die Lebensmittel ausgingen, weil Sie nicht genug Geld oder sonstige Ressourcen hatten.",
|
||||
"fies_item_8": "Gab es in den letzten 4 Wochen Zeiten, in denen Sie hungrig waren, aber nicht aßen, weil Sie nicht genug Geld oder sonstige Ressourcen für Essen hatten.",
|
||||
"fies_item_9": "Gab es in den letzten 4 Wochen Zeiten, in denen Sie einen ganzen Tag lang nichts gegessen haben, weil Sie nicht genug Geld oder sonstige Ressourcen hatten.",
|
||||
"ges3_instruction": "Die folgenden Aussagen können mehr oder weniger auf Sie zutreffen. Bitte geben Sie bei jeder Aussage an, inwieweit diese auf Sie persönlich zutrifft.",
|
||||
"ges3_item_1_note_before": "Die folgenden Aussagen können mehr oder weniger auf Sie zutreffen. Bitte geben Sie bei jeder Aussage an, inwieweit diese auf Sie persönlich zutrifft.",
|
||||
"ges3_item_1": "In schwierigen Situationen kann ich mich auf meine Fähigkeiten verlassen.",
|
||||
"not_at_all": "gar nicht",
|
||||
"a_little": "wenig",
|
||||
"somewhat": "mittel",
|
||||
"mostly": "trifft ziemlich zu",
|
||||
"completely": "trifft voll und ganz zu",
|
||||
"ges3_item_2": "Die meisten Probleme kann ich aus eigener Kraft gut meistern.",
|
||||
"ges3_item_3": "Auch anstrengende und komplizierte Aufgaben kann ich in der Regel gut lösen.",
|
||||
"intro_life_in_germany": "Im Folgenden geht es um Ihr Leben in Deutschland.",
|
||||
"feeling_connected_to_germany_question": "Wie stark fühlen Sie sich Deutschland verbunden?",
|
||||
"very_connected": "sehr eng",
|
||||
"quite_connected": "ziemlich eng",
|
||||
"moderately_connected": "mäßig eng",
|
||||
"somewhat_loose": "eher locker",
|
||||
"not_connected_at_all": "überhaupt nicht eng",
|
||||
"understanding_political_issues": "Wie gut verstehen Sie die wichtigsten politischen Fragen in Deutschland?",
|
||||
"very_good": "sehr gut",
|
||||
"good": "gut",
|
||||
"fairly_good": "ziemlich gut",
|
||||
"unexpected_expense_question": "Stellen Sie sich vor, dass Sie eine unerwartete, aber notwendige Zahlung tätigen müssen. Könnten Sie sich die folgenden Euro-Zahlungen leisten? (Sie können mehrere Antworten ankreuzen)",
|
||||
"expense_50": "50 €",
|
||||
"expense_100": "100 €",
|
||||
"expense_300": "300 €",
|
||||
"expense_500": "500 €",
|
||||
"expense_800": "800 €",
|
||||
"dining_with_germans_question": "Wie oft haben Sie in den letzten 12 Monaten mit Deutschen eine Mahlzeit gegessen oder haben gemeinsam etwas unternommen?",
|
||||
"never": "nie",
|
||||
"once_a_year": "2 mal im Jahr",
|
||||
"once_a_month": "1 mal im Monat",
|
||||
"once_a_week": "1 mal pro Woche",
|
||||
"almost_every_day": "fast jeden Tag",
|
||||
"reading_german_articles_question": "Wie gut können Sie auf Deutsch einfache Zeitungsartikel über Ihnen vertraute Themen lesen und die wichtigsten Punkte verstehen?",
|
||||
"moderately_good": "mäßig gut",
|
||||
"not_good": "nicht gut",
|
||||
"visiting_doctor_question": "Wie leicht oder schwer würde es Ihnen hier fallen einen Arzt aufzusuchen?",
|
||||
"rather_difficult": "eher schwer",
|
||||
"neither_nor": "weder schwer noch leicht",
|
||||
"rather_easy": "eher leicht",
|
||||
"feeling_as_outsider_question": "Wie oft fühlen Sie sich in Deutschland als Außenseiter?",
|
||||
"rarely": "selten",
|
||||
"sometimes": "manchmal",
|
||||
"often": "oft",
|
||||
"always": "immer",
|
||||
"recent_occupation_question": "Waren Sie in den letzten vier Wochen beschäftigt? (Bitte nur eine Antwort)",
|
||||
"employed": "in bezahlter Beschäftigung, auch wenn Sie nur kurzfristig nicht gearbeitet haben (angestellt, selbständig, kleinere Jobs)",
|
||||
"education": "in Ausbildung (Schule, Berufsausbildung, Hochschule), auch wenn Ferien waren",
|
||||
"internship": "Praktikum",
|
||||
"unemployed_searching": "arbeitslos und aktiv auf Arbeitssuche /arbeitslos und nicht aktiv auf Arbeitssuche",
|
||||
"unemployed_not_searching": "krank oder arbeitsunfähig",
|
||||
"sick_or_disabled": "in unbezahlter Hausarbeit, Betreuung von Kindern / anderer Personen",
|
||||
"unpaid_housework": "Sonstiges (z.B. Sprachkurs)",
|
||||
"discussing_politics_question": "Wie oft haben Sie, seit Sie in Deutschland sind, mit anderen Leuten über wichtige politische Themen in Deutschland diskutiert?",
|
||||
"contact_with_germans_question": "Mit wie vielen Deutschen haben Sie sich in den letzten 4 Wochen entweder telefonisch, per Messenger-Chat (Whatsapp, Facebook...) oder per SMS ausgetauscht?",
|
||||
"zero": "mit keinem",
|
||||
"one_to_three": "mit 1 bis 3",
|
||||
"three_to_six": "mit 3 bis 6",
|
||||
"seven_to_fourteen": "mit 7 bis14",
|
||||
"fifteen_or_more": "mit 15 oder mehr",
|
||||
"speaking_german_opinion_question": "Wie gut können Sie in einer Unterhaltung auf Deutsch über vertraute Themen sprechen und Ihre Meinung ausdrücken?",
|
||||
"job_search_question": "Wie leicht oder schwer würde es Ihnen in diesem Land fallen, einen Job zu suchen (richtige Angebote finden)?",
|
||||
"lsns_item_1": "Wie viele Familienangehörige sehen Sie mindestens einmal im Monat oder hören von ihnen (z.B. telefonisch)?",
|
||||
"one": "1",
|
||||
"two": "2",
|
||||
"three_or_four": "3 oder 4",
|
||||
"five_to_eight": "5 bis 8",
|
||||
"nine_or_more": "9 oder mehr",
|
||||
"lsns_item_2": "Mit wie vielen Verwandten fühlen Sie sich so wohl, dass Sie über private Dinge sprechen können?",
|
||||
"lsns_item_3": "Wie vielen Familienangehörigen fühlen Sie sich so nahe, dass Sie sie um Hilfe bitten könnten?",
|
||||
"lsns_item_4": "Wie viele Ihrer Freunde sehen Sie mindestens einmal im Monat oder hören von ihnen (z.B. telefonisch)?",
|
||||
"lsns_item_5": "Mit wie vielen Freunden fühlen Sie sich so wohl, dass Sie über private Dinge sprechen können?",
|
||||
"lsns_item_6": "Wie vielen Nachbarn fühlen Sie sich so nahe, dass Sie sie um Hilfe bitten könnten?",
|
||||
"mhls_instruction": "Bitte geben Sie an, inwieweit Sie den folgenden Aussagen zustimmen.",
|
||||
"mhls_item_1_note_before": "Bitte geben Sie an, inwieweit Sie den folgenden Aussagen zustimmen.",
|
||||
"mhls_item_1": "Ich bin zuversichtlich, dass ich wüsste, wo ich Informationen über psychische Erkrankungen finden kann.",
|
||||
"mhls_item_2": "Ich bin sicher im Umgang mit dem Computer oder dem Telefon, um Informationen über psychische Erkrankungen zu finden.",
|
||||
"mhls_item_3": "Ich traue mir zu, persönliche Termine wahrzunehmen, um Informationen über psychische Erkrankungen zu erhalten (z. B. beim Hausarzt/Allgemeinarzt).",
|
||||
"mhls_item_4": "Ich bin mir sicher, dass ich Zugang zu Ressourcen habe (z. B. Hausarzt, Internet, Freunde), die ich nutzen kann, um mich über psychische Erkrankungen zu informieren.",
|
||||
"mhls_item_5": "Menschen mit einer psychischen Erkrankung können diese überwinden, wenn sie es wollen.",
|
||||
"mhls_item_6": "Eine psychische Krankheit ist ein Zeichen von persönlicher Schwäche.",
|
||||
"mhls_item_7": "Eine psychische Krankheit ist keine echte medizinische Krankheit.",
|
||||
"mhls_item_8": "Menschen mit einer psychischen Erkrankung sind gefährlich.",
|
||||
"mhls_item_9": "Es ist am besten, Menschen mit psychischen Erkrankungen zu meiden, damit man dieses Problem nicht selbst bekommt.",
|
||||
"mhls_item_10": "Wenn ich eine psychische Krankheit hätte, würde ich es niemandem erzählen.",
|
||||
"mhls_item_11": "Wenn man sich an eine psychologische Beratungsstelle / Psychotherapeut wendet, heißt das, dass man nicht stark genug ist, seine eigenen Schwierigkeiten zu bewältigen.",
|
||||
"mhls_item_12": "Wenn ich eine psychische Erkrankung hätte, würde ich keine Hilfe bei einer psychologische Beratungsstelle/Psychotherapeut suchen.",
|
||||
"mhls_item_13": "Ich glaube, dass die Behandlung einer psychischen Erkrankung, die von einer psychologische Beratungsstelle/Psychotherapeut durchgeführt wird, nicht wirksam wäre.",
|
||||
"rhs_symptom_1": "Muskel-, Knochen-, Gelenk- schmerzen",
|
||||
"rhs_symptom_2": "Sich die meiste Zeit unglücklich, traurig, oder deprimiert fühlen",
|
||||
"rhs_symptom_3": "Zu viel Nachdenken oder sich zu viele Gedanken machen",
|
||||
"rhs_symptom_4": "Sich hilflos fühlen",
|
||||
"rhs_symptom_5": "Sich plötzlich ohne Grund erschrecken",
|
||||
"rhs_symptom_6": "Mattigkeit, Schwindel, oder Schwächegefühl",
|
||||
"rhs_symptom_7": "Nervosität oder innere Unsicherheit",
|
||||
"rhs_symptom_8": "Sich ruhelos fühlen, nicht still sitzen können",
|
||||
"rhs_symptom_9": "Leicht weinen müssen",
|
||||
"q10_reexperience_trauma": "Wie stark haben Sie im vergangenen Monat die Erfahrung gemacht, das Trauma wieder zu erleben; sich zu verhalten oder zu fühlen, als geschähe es wieder?",
|
||||
"q11_physical_reaction": "Wie stark haben Sie im vergangenen Monat körperliche Reaktionen gehabt (z.B. Schweißausbruchausbruch, schneller Herzschlag), wenn Sie an das Trauma erinnert wurden?",
|
||||
"q12_emotional_numbness": "Wie stark haben Sie im vergangenen Monat sich emotional taub gefühlt (z.B. sich traurig fühlen, aber nicht weinen können, keine liebevollen Gefühle empfinden können)?",
|
||||
"q13_easily_startled": "Wie stark haben Sie im vergangenen Monat sich schreckhafter, leichter erschreckbar gefühlt (z.B. wenn jemand von hinten näher kommt)?",
|
||||
"resilience_reflection_prompt": "Welche der folgenden Aussagen trifft am besten für Sie zu: Wenn Sie an Ihr Leben in den letzten vier Wochen denken, wie stark haben Sie das Gefühl, dass Sie:",
|
||||
"glass_explanation": "Bitte geben Sie an, in welchem Ausmaß das Problem während des letzten Monats/ der letzten 4 Wochen störend war. Die Antwortmöglichkeiten sind in Worten und am Bild eines Glasbehälters dargestellt: wenn etwas überhaupt nicht für Sie zutrifft, ist das Glas leer, wenn etwas ein wenig oder ziemlich zutrifft, wird das Glas zunehmend voller, wenn Sie etwas extrem erlebt haben, ist das Glas voll.",
|
||||
"how_strong_past_month": "Trauma-bezogene Beschwerden im vergangenen Monat",
|
||||
"q14_intro": "Welche der folgenden Aussagen trifft am besten für Sie zu: Wenn Sie an Ihr Leben in den letzten vier Wochen denken, wie stark haben Sie das Gefühl, dass Sie:",
|
||||
"resilience_fully_capable": "Dazu in der Lage sind, alles zu schaffen (zu bewältigen), was Ihnen begegnet",
|
||||
"resilience_mostly_capable": "Dazu in der Lage sind, die meisten Dinge zu schaffen (zu bewältigen), die Ihnen begegnen",
|
||||
"resilience_some_capable_some_not": "Dazu in der Lage sind, manche Dinge zu schaffen (zu bewältigen), aber andere Dinge nicht zu schaffen/zu bewältigen",
|
||||
"resilience_mostly_not_capable": "Nicht dazu in der Lage sind, die meisten Dinge zu schaffen (zu bewältigen)",
|
||||
"resilience_not_capable": "Nicht dazu in der Lage sind, irgendetwas zu schaffen",
|
||||
"pain_rating_instruction": "Wählen Sie die Nummer (0-10), die am besten beschreibt, wie viel Leid Sie in der vergangenen Woche einschließlich heute fühlen müssen.",
|
||||
"swls_instruction": "Es folgen fünf Aussagen, denen Sie zustimmen bzw. die Sie ablehnen können. Bitte benutzen Sie die folgende Skala, um Ihre Zustimmung bzw. Ablehnung zu jeder Aussage zum Ausdruck zu bringen.",
|
||||
"swls_item_1_note_before": "Es folgen fünf Aussagen, denen Sie zustimmen bzw. die Sie ablehnen können. Bitte benutzen Sie die folgende Skala, um Ihre Zustimmung bzw. Ablehnung zu jeder Aussage zum Ausdruck zu bringen.",
|
||||
"swls_item_1": "In den meisten Bereichen entspricht mein Leben meinen Idealvorstellungen..",
|
||||
"swls_1": "trifft überhaupt nicht zu",
|
||||
"swls_2": "trifft nicht zu",
|
||||
"swls_3": "trifft eher nicht zu",
|
||||
"swls_4": "teils/teils",
|
||||
"swls_5": "trifft eher zu",
|
||||
"swls_6": "trifft zu",
|
||||
"swls_7": "trifft vollständig zu",
|
||||
"swls_item_2": "Meine Lebensbedingungen sind ausgezeichnet.",
|
||||
"swls_item_3": "Ich bin mit meinem Leben zufrieden.",
|
||||
"swls_item_4": "Bisher habe ich die wesentlichen Dinge erreicht, die ich mir für mein Leben wünsche.",
|
||||
"swls_item_5": "Wenn ich mein Leben noch einmal leben könnte, würde ich kaum etwas ändern.",
|
||||
"sss8_instruction": "Wie stark fühlten Sie sich im Verlauf der letzten 7 Tage durch die folgenden Beschwerden beeinträchtigt?",
|
||||
"sss8_item_1_note_before": "Wie stark fühlten Sie sich im Verlauf der letzten 7 Tage durch die folgenden Beschwerden beeinträchtigt?",
|
||||
"sss8_item_1": "Bauchschmerzen oder Verdauungsbeschwerden",
|
||||
"quite_a_bit": "stark",
|
||||
"very_much": "sehr stark",
|
||||
"sss8_item_2": "Rückenschmerzen",
|
||||
"sss8_item_3": "Schmerzen in Armen, Beinen oder Gelenken",
|
||||
"sss8_item_4": "Kopfschmerzen",
|
||||
"sss8_item_5": "Schmerzen im Brustbereich oder Kurzatmigkeit",
|
||||
"sss8_item_6": "Schwindel",
|
||||
"sss8_item_7": "Müdigkeit oder Gefühl, keine Energie zu haben",
|
||||
"sss8_item_8": "Schlafstörungen"
|
||||
}
|
||||
@ -37,6 +37,8 @@ import com.tomhempel.rbarometer.network.SyncProgress
|
||||
import com.tomhempel.rbarometer.network.SyncResult
|
||||
import com.tomhempel.rbarometer.network.TokenStore
|
||||
import com.tomhempel.rbarometer.questionnaire.QuestionnaireBase
|
||||
import com.tomhempel.rbarometer.questionnaire.QuestionnaireLanguagePicker
|
||||
import com.tomhempel.rbarometer.session.SessionFlowController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -54,6 +56,10 @@ class MainActivity : AppCompatActivity() {
|
||||
private lateinit var openingScreenHandler: HandlerOpeningScreen
|
||||
|
||||
var isInQuestionnaire: Boolean = false
|
||||
private var sessionFlowController: SessionFlowController? = null
|
||||
private var activeQuestionnaire: QuestionnaireBase<*>? = null
|
||||
|
||||
val isInSessionFlow: Boolean get() = sessionFlowController != null
|
||||
var isFirstQuestionnairePage: Boolean = false
|
||||
/** Set before opening DevSettings so onResume runs a full opening-screen rebuild. */
|
||||
private var needsOpeningReinit: Boolean = false
|
||||
@ -104,6 +110,7 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun handleOnBackPressed() {
|
||||
when {
|
||||
isInQuestionnaire -> confirmExitQuestionnaire()
|
||||
isInSessionFlow -> sessionFlowController?.pauseFromBackPress()
|
||||
backPressInterceptor?.invoke() == true -> Unit
|
||||
::openingScreenHandler.isInitialized && openingScreenHandler.onBackPressed() -> Unit
|
||||
else -> {
|
||||
@ -168,7 +175,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private suspend fun routeCachedStartup(token: String) {
|
||||
val hasPendingUploads = withContext(Dispatchers.IO) {
|
||||
PendingUploadRepository.hasAny()
|
||||
PendingUploadRepository.hasUploadReady(this@MainActivity)
|
||||
}
|
||||
if (hasPendingUploads || !NetworkUtils.isOnline(this)) {
|
||||
showCachedOpeningScreen(refreshAfterOpen = false, showUploadPrompt = hasPendingUploads)
|
||||
@ -487,20 +494,46 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
fun startSessionFlow(clientCode: String, languageID: String) {
|
||||
sessionFlowController = SessionFlowController(this, languageID, clientCode) { completion ->
|
||||
sessionFlowController = null
|
||||
isInQuestionnaire = false
|
||||
if (::openingScreenHandler.isInitialized) {
|
||||
openingScreenHandler.refreshOpeningUi()
|
||||
if (completion != null) {
|
||||
openingScreenHandler.showSessionCompletionDialog(completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
sessionFlowController?.start()
|
||||
}
|
||||
|
||||
fun startQuestionnaire(questionnaire: QuestionnaireBase<*>, languageID: String) {
|
||||
activeQuestionnaire = questionnaire
|
||||
isInQuestionnaire = true
|
||||
isFirstQuestionnairePage = true
|
||||
questionnaire.attach(this, languageID)
|
||||
questionnaire.startQuestionnaire()
|
||||
}
|
||||
|
||||
fun finishQuestionnaire() {
|
||||
fun finishQuestionnaire(wasCompleted: Boolean = false) {
|
||||
val draftSnapshot = activeQuestionnaire?.draftSnapshot()
|
||||
activeQuestionnaire = null
|
||||
QuestionnaireLanguagePicker.detach(this)
|
||||
if (sessionFlowController?.handleQuestionnaireFinished(wasCompleted, draftSnapshot) == true) {
|
||||
isInQuestionnaire = false
|
||||
isFirstQuestionnairePage = false
|
||||
ViewUtils.setTextSizeScale(1f)
|
||||
LanguageManager.clearQuestionnaireTranslations()
|
||||
return
|
||||
}
|
||||
isInQuestionnaire = false
|
||||
isFirstQuestionnairePage = false
|
||||
ViewUtils.setTextSizeScale(1f)
|
||||
LanguageManager.clearQuestionnaireTranslations()
|
||||
sessionFlowController = null
|
||||
if (::openingScreenHandler.isInitialized) {
|
||||
openingScreenHandler.init()
|
||||
openingScreenHandler.refreshOpeningUi()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ class MyApp : Application() {
|
||||
super.onCreate()
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
SensitiveDataCrypto.init(this)
|
||||
LanguageManager.warmBundledDefaults(this)
|
||||
|
||||
// Room-Datenbank bauen: nur die Hauptdatei, ohne WAL und Journal
|
||||
database = Room.databaseBuilder(
|
||||
|
||||
@ -1,7 +1,29 @@
|
||||
package com.tomhempel.rbarometer.data
|
||||
|
||||
import android.content.Context
|
||||
import com.tomhempel.rbarometer.session.SessionProgramHelper
|
||||
|
||||
object PendingUploadRepository {
|
||||
|
||||
suspend fun countUploadReady(context: Context): Int =
|
||||
getUploadReadyQuestionnaires(context).size +
|
||||
getUploadReadyScoringReviews(context).size
|
||||
|
||||
suspend fun hasUploadReady(context: Context): Boolean = countUploadReady(context) > 0
|
||||
|
||||
suspend fun getUploadReadyQuestionnaires(context: Context): List<CompletedQuestionnaire> =
|
||||
SessionProgramHelper.filterUploadReadyQuestionnaires(
|
||||
context,
|
||||
CompletedQuestionnaireRepository.getAllPendingUploads(),
|
||||
)
|
||||
|
||||
suspend fun getUploadReadyScoringReviews(context: Context): List<CoachScoringReview> =
|
||||
SessionProgramHelper.filterUploadReadyScoringReviews(
|
||||
context,
|
||||
CoachScoringReviewRepository.getAllPendingUploads(),
|
||||
)
|
||||
|
||||
/** Raw pending count (ignores session completion). */
|
||||
suspend fun countAll(): Int =
|
||||
CompletedQuestionnaireRepository.countPendingUploads() +
|
||||
CoachScoringReviewRepository.countPendingUploads()
|
||||
|
||||
@ -67,6 +67,44 @@ data class SubmitQuestionnaireResponse(
|
||||
val sumPoints: Int = 0,
|
||||
val structureRevision: Int? = null,
|
||||
val legacySubmit: Boolean = false,
|
||||
val cycleAdvanced: Boolean = false,
|
||||
val cycleNumber: Int? = null,
|
||||
)
|
||||
|
||||
data class ProgramSessionQuestionnaire(
|
||||
val questionnaireID: String,
|
||||
val orderIndex: Int = 0,
|
||||
)
|
||||
|
||||
data class ProgramSessionDefinition(
|
||||
val sessionID: String,
|
||||
val name: String,
|
||||
val description: String = "",
|
||||
val sessionNumber: Int = 0,
|
||||
val orderIndex: Int = 0,
|
||||
val questionnaires: List<ProgramSessionQuestionnaire> = emptyList(),
|
||||
)
|
||||
|
||||
data class ProgramSessionSettings(
|
||||
val autoRepeatSessions: Boolean = false,
|
||||
)
|
||||
|
||||
data class ProgramSessionsPayload(
|
||||
val sessions: List<ProgramSessionDefinition> = emptyList(),
|
||||
val settings: ProgramSessionSettings = ProgramSessionSettings(),
|
||||
)
|
||||
|
||||
data class ProgramProgress(
|
||||
val cycleNumber: Int = 1,
|
||||
val cycleStartedAt: Long = 0L,
|
||||
val totalSessions: Int = 0,
|
||||
val sessionsCompletedInCycle: Int = 0,
|
||||
val currentSessionID: String? = null,
|
||||
val currentSessionNumber: Int? = null,
|
||||
val currentSessionName: String? = null,
|
||||
val cycleComplete: Boolean = false,
|
||||
val programComplete: Boolean = false,
|
||||
val autoRepeatSessions: Boolean = false,
|
||||
)
|
||||
|
||||
/** One questionnaire already completed on the server side. */
|
||||
@ -80,7 +118,8 @@ data class ServerCompletedQuestionnaire(
|
||||
/** A client with its server-side completion records. */
|
||||
data class AssignedClientInfo(
|
||||
val clientCode: String,
|
||||
val completedQuestionnaires: List<ServerCompletedQuestionnaire> = emptyList()
|
||||
val completedQuestionnaires: List<ServerCompletedQuestionnaire> = emptyList(),
|
||||
val programProgress: ProgramProgress? = null,
|
||||
)
|
||||
|
||||
data class ScoringProfileMember(
|
||||
|
||||
@ -3,6 +3,7 @@ package com.tomhempel.rbarometer.network
|
||||
import android.content.Context
|
||||
import com.tomhempel.rbarometer.LanguageManager
|
||||
import com.tomhempel.rbarometer.notification.UploadReminderScheduler
|
||||
import com.tomhempel.rbarometer.session.SessionProgramHelper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -48,10 +49,14 @@ object DatabaseUploader {
|
||||
onResult: (UploadResult) -> Unit,
|
||||
) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val allPending = com.tomhempel.rbarometer.data.CompletedQuestionnaireRepository
|
||||
.getAllPendingUploads()
|
||||
val pendingScoring = com.tomhempel.rbarometer.data.CoachScoringReviewRepository
|
||||
.getAllPendingUploads()
|
||||
val allPending = SessionProgramHelper.filterUploadReadyQuestionnaires(
|
||||
context,
|
||||
com.tomhempel.rbarometer.data.CompletedQuestionnaireRepository.getAllPendingUploads(),
|
||||
)
|
||||
val pendingScoring = SessionProgramHelper.filterUploadReadyScoringReviews(
|
||||
context,
|
||||
com.tomhempel.rbarometer.data.CoachScoringReviewRepository.getAllPendingUploads(),
|
||||
)
|
||||
if (allPending.isEmpty() && pendingScoring.isEmpty()) {
|
||||
val assigned = QuestionnaireCache.getAssignedClients(context)
|
||||
if (assigned.isEmpty()) {
|
||||
@ -189,22 +194,12 @@ object DatabaseUploader {
|
||||
}
|
||||
|
||||
if (!errors.any() && (submittedQuestionnaires + submittedScoringReviews) > 0) {
|
||||
val stillPending = UploadReminderScheduler.hasPendingUploads()
|
||||
val stillPending = UploadReminderScheduler.hasPendingUploads(context)
|
||||
if (!stillPending) {
|
||||
UploadReminderScheduler.cancel(context)
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh server cache after upload so local interview rows are not wiped first.
|
||||
runCatching {
|
||||
SyncCoordinator.refreshManual(context, token)
|
||||
}.onFailure { e ->
|
||||
SyncLog.w("UPLOAD", "Post-upload sync failed: ${e.message}")
|
||||
if (TokenStore.isAuthError(e)) {
|
||||
errors += UploadError(clientCode = "", message = "Session expired", isAuthError = true)
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onResult(
|
||||
UploadResult(
|
||||
@ -216,6 +211,13 @@ object DatabaseUploader {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Refresh cache in the background; opening screen runs a visible sync after the user closes upload.
|
||||
runCatching {
|
||||
SyncCoordinator.refreshManual(context, token)
|
||||
}.onFailure { e ->
|
||||
SyncLog.w("UPLOAD", "Post-upload sync failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import okhttp3.OkHttpClient
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object HttpModule {
|
||||
const val BASE_URL = "http://49.13.157.44/nat-as-server/api"
|
||||
const val BASE_URL = "http://49.13.157.44/barometer-server/api"
|
||||
|
||||
val client: OkHttpClient by lazy {
|
||||
val dispatcher = Dispatcher().apply {
|
||||
|
||||
@ -202,7 +202,11 @@ object QuestionnaireApiClient {
|
||||
completedAt = cObj.get("completedAt")?.asLong ?: 0L
|
||||
)
|
||||
} ?: emptyList()
|
||||
AssignedClientInfo(clientCode = code, completedQuestionnaires = completions)
|
||||
AssignedClientInfo(
|
||||
clientCode = code,
|
||||
completedQuestionnaires = completions,
|
||||
programProgress = parseProgramProgress(obj.get("programProgress")),
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
@ -300,6 +304,67 @@ object QuestionnaireApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getProgramSessions(token: String): ProgramSessionsPayload = withContext(Dispatchers.IO) {
|
||||
val request = authorizedRequest(token)
|
||||
.url("${HttpModule.BASE_URL}/app_questionnaires?sessions=1")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
val bodyText = response.body?.string()
|
||||
SyncLog.apiResponse("API_PROGRAM_SESSIONS", "getProgramSessions", response.code, bodyText)
|
||||
val root = parseEnvelope(response.code, bodyText)
|
||||
val data = root.getAsJsonObject("data") ?: JsonObject()
|
||||
val sessionsArr = data.getAsJsonArray("sessions") ?: JsonArray()
|
||||
val sessions = sessionsArr.mapNotNull { item ->
|
||||
if (!item.isJsonObject) return@mapNotNull null
|
||||
val obj = item.asJsonObject
|
||||
val sessionID = obj.get("sessionID")?.asString?.takeIf { it.isNotBlank() }
|
||||
?: return@mapNotNull null
|
||||
val members = obj.getAsJsonArray("questionnaires")?.mapNotNull { mEl ->
|
||||
if (!mEl.isJsonObject) return@mapNotNull null
|
||||
val m = mEl.asJsonObject
|
||||
val qId = m.get("questionnaireID")?.asString?.takeIf { it.isNotBlank() }
|
||||
?: return@mapNotNull null
|
||||
ProgramSessionQuestionnaire(
|
||||
questionnaireID = qId,
|
||||
orderIndex = m.get("orderIndex")?.asInt ?: 0,
|
||||
)
|
||||
}?.sortedBy { it.orderIndex } ?: emptyList()
|
||||
ProgramSessionDefinition(
|
||||
sessionID = sessionID,
|
||||
name = obj.get("name")?.asString ?: sessionID,
|
||||
description = obj.get("description")?.asString.orEmpty(),
|
||||
sessionNumber = obj.get("sessionNumber")?.asInt ?: 0,
|
||||
orderIndex = obj.get("orderIndex")?.asInt ?: 0,
|
||||
questionnaires = members,
|
||||
)
|
||||
}.sortedBy { it.orderIndex }
|
||||
val settingsObj = data.getAsJsonObject("settings")
|
||||
val settings = ProgramSessionSettings(
|
||||
autoRepeatSessions = settingsObj?.get("autoRepeatSessions")?.asBoolean ?: false,
|
||||
)
|
||||
ProgramSessionsPayload(sessions = sessions, settings = settings)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseProgramProgress(element: JsonElement?): ProgramProgress? {
|
||||
if (element == null || element.isJsonNull || !element.isJsonObject) return null
|
||||
val obj = element.asJsonObject
|
||||
return ProgramProgress(
|
||||
cycleNumber = obj.get("cycleNumber")?.asInt ?: 1,
|
||||
cycleStartedAt = obj.get("cycleStartedAt")?.asLong ?: 0L,
|
||||
totalSessions = obj.get("totalSessions")?.asInt ?: 0,
|
||||
sessionsCompletedInCycle = obj.get("sessionsCompletedInCycle")?.asInt ?: 0,
|
||||
currentSessionID = obj.get("currentSessionID")?.takeIf { !it.isJsonNull }?.asString,
|
||||
currentSessionNumber = obj.get("currentSessionNumber")?.takeIf { !it.isJsonNull }?.asInt,
|
||||
currentSessionName = obj.get("currentSessionName")?.takeIf { !it.isJsonNull }?.asString,
|
||||
cycleComplete = obj.get("cycleComplete")?.asBoolean ?: false,
|
||||
programComplete = obj.get("programComplete")?.asBoolean ?: false,
|
||||
autoRepeatSessions = obj.get("autoRepeatSessions")?.asBoolean ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getQuestionnaire(token: String, questionnaireId: String): JsonObject = withContext(Dispatchers.IO) {
|
||||
val request = authorizedRequest(token)
|
||||
.url("${HttpModule.BASE_URL}/app_questionnaires?id=$questionnaireId")
|
||||
|
||||
@ -17,6 +17,8 @@ object QuestionnaireCache {
|
||||
private const val TRANSLATIONS_FILE = "translations.json"
|
||||
private const val QUESTIONNAIRES_FILE = "questionnaires.json"
|
||||
private const val CLIENTS_FILE = "assigned_clients.json"
|
||||
private const val CLIENT_INFOS_FILE = "assigned_client_infos.json"
|
||||
private const val PROGRAM_SESSIONS_FILE = "program_sessions.json"
|
||||
private const val SCORING_PROFILES_FILE = "scoring_profiles.json"
|
||||
|
||||
private val gson = Gson()
|
||||
@ -140,6 +142,44 @@ object QuestionnaireCache {
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
fun saveAssignedClientInfos(context: Context, infos: List<AssignedClientInfo>) {
|
||||
writeCacheText(clientInfosFile(context), gson.toJson(infos))
|
||||
}
|
||||
|
||||
fun getAssignedClientInfos(context: Context): List<AssignedClientInfo> {
|
||||
val file = clientInfosFile(context)
|
||||
if (!file.exists()) return emptyList()
|
||||
return runCatching {
|
||||
gson.fromJson(readCacheText(file), Array<AssignedClientInfo>::class.java).toList()
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
fun getProgramProgress(context: Context, clientCode: String): ProgramProgress? =
|
||||
getAssignedClientInfos(context)
|
||||
.firstOrNull { it.clientCode.equals(clientCode, ignoreCase = true) }
|
||||
?.programProgress
|
||||
|
||||
fun saveProgramSessions(context: Context, payload: ProgramSessionsPayload) {
|
||||
writeCacheText(programSessionsFile(context), gson.toJson(payload))
|
||||
}
|
||||
|
||||
fun getProgramSessionsPayload(context: Context): ProgramSessionsPayload {
|
||||
val file = programSessionsFile(context)
|
||||
if (!file.exists()) return ProgramSessionsPayload()
|
||||
return runCatching {
|
||||
gson.fromJson(readCacheText(file), ProgramSessionsPayload::class.java)
|
||||
}.getOrDefault(ProgramSessionsPayload())
|
||||
}
|
||||
|
||||
fun getProgramSessions(context: Context): List<ProgramSessionDefinition> =
|
||||
getProgramSessionsPayload(context).sessions
|
||||
|
||||
fun getProgramSessionSettings(context: Context): ProgramSessionSettings =
|
||||
getProgramSessionsPayload(context).settings
|
||||
|
||||
fun findProgramSession(context: Context, sessionID: String): ProgramSessionDefinition? =
|
||||
getProgramSessions(context).firstOrNull { it.sessionID == sessionID }
|
||||
|
||||
private fun parseTranslations(root: JsonObject): Map<String, Map<String, String>> {
|
||||
return root.entrySet().mapNotNull { (language, values) ->
|
||||
if (!values.isJsonObject) return@mapNotNull null
|
||||
@ -165,6 +205,12 @@ object QuestionnaireCache {
|
||||
private fun clientsFile(context: Context): File =
|
||||
File(cacheDir(context), CLIENTS_FILE)
|
||||
|
||||
private fun clientInfosFile(context: Context): File =
|
||||
File(cacheDir(context), CLIENT_INFOS_FILE)
|
||||
|
||||
private fun programSessionsFile(context: Context): File =
|
||||
File(cacheDir(context), PROGRAM_SESSIONS_FILE)
|
||||
|
||||
private fun writeCacheText(file: File, plain: String) {
|
||||
file.writeText(SensitiveDataCrypto.encrypt(plain))
|
||||
}
|
||||
|
||||
@ -47,6 +47,9 @@ object QuestionnaireSyncService {
|
||||
val scoringProfilesDeferred = async(Dispatchers.IO) {
|
||||
runCatching { QuestionnaireApiClient.getScoringProfiles(token) }
|
||||
}
|
||||
val programSessionsDeferred = async(Dispatchers.IO) {
|
||||
runCatching { QuestionnaireApiClient.getProgramSessions(token) }
|
||||
}
|
||||
val detailsDeferred = async {
|
||||
fetchAndCacheQuestionnaireDetails(context, token, questionnaires)
|
||||
}
|
||||
@ -58,6 +61,12 @@ object QuestionnaireSyncService {
|
||||
}.onFailure { e ->
|
||||
SyncLog.w(TAG, "getScoringProfiles failed: ${e.message}")
|
||||
}
|
||||
programSessionsDeferred.await().onSuccess { payload ->
|
||||
QuestionnaireCache.saveProgramSessions(context, payload)
|
||||
SyncLog.d(TAG, "program sessions cached: ${payload.sessions.size}")
|
||||
}.onFailure { e ->
|
||||
SyncLog.w(TAG, "getProgramSessions failed: ${e.message}")
|
||||
}
|
||||
pacer.emit(SyncPhase.DOWNLOADING_CLIENT_INFO)
|
||||
val clientInfosFromApi = clientResult.getOrElse { e ->
|
||||
SyncLog.w(TAG, "getAssignedClients failed: ${e.message}")
|
||||
@ -71,7 +80,7 @@ object QuestionnaireSyncService {
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
persistAssignedClients(context, apiSucceeded, assignedClientCodes, clientCodesToSave)
|
||||
persistAssignedClients(context, apiSucceeded, assignedClientCodes, clientCodesToSave, clientInfosFromApi)
|
||||
|
||||
if (!forceAnswerPull || !pullAnswers) {
|
||||
seedServerCompletions(clientInfosFromApi)
|
||||
@ -155,7 +164,7 @@ object QuestionnaireSyncService {
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
persistAssignedClients(context, apiSucceeded, fallbackCodes, clientCodesToSave)
|
||||
persistAssignedClients(context, apiSucceeded, fallbackCodes, clientCodesToSave, clientInfosFromApi)
|
||||
|
||||
if (!forceAnswerPull || !pullAnswers) {
|
||||
seedServerCompletions(clientInfosFromApi)
|
||||
@ -186,10 +195,12 @@ object QuestionnaireSyncService {
|
||||
apiSucceeded: Boolean,
|
||||
fallbackCodes: List<String>,
|
||||
clientCodesToSave: List<String>,
|
||||
clientInfosFromApi: List<AssignedClientInfo> = emptyList(),
|
||||
) {
|
||||
when {
|
||||
apiSucceeded -> {
|
||||
QuestionnaireCache.replaceAssignedClients(context, clientCodesToSave)
|
||||
QuestionnaireCache.saveAssignedClientInfos(context, clientInfosFromApi)
|
||||
SyncLog.d(TAG, "assigned clients from API: ${clientCodesToSave.size}")
|
||||
}
|
||||
fallbackCodes.isNotEmpty() -> {
|
||||
|
||||
@ -24,7 +24,7 @@ class UploadReminderReceiver : BroadcastReceiver() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
// Only fire the notification if there are actually pending uploads
|
||||
val hasPending = UploadReminderScheduler.hasPendingUploads()
|
||||
val hasPending = UploadReminderScheduler.hasPendingUploads(context)
|
||||
if (hasPending) {
|
||||
withContext(Dispatchers.Main) {
|
||||
showNotification(context)
|
||||
|
||||
@ -46,12 +46,12 @@ object UploadReminderScheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any assigned client has a pending (completed but not uploaded) questionnaire.
|
||||
* Runs on the calling thread — call from a background coroutine.
|
||||
* Returns true if any assigned client has upload-ready pending work
|
||||
* (current session fully complete when applicable).
|
||||
*/
|
||||
suspend fun hasPendingUploads(): Boolean {
|
||||
suspend fun hasPendingUploads(context: Context): Boolean {
|
||||
return try {
|
||||
com.tomhempel.rbarometer.data.PendingUploadRepository.hasAny()
|
||||
com.tomhempel.rbarometer.data.PendingUploadRepository.hasUploadReady(context)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "hasPendingUploads check failed: ${e.message}")
|
||||
false
|
||||
|
||||
@ -105,6 +105,8 @@ class QuestionnaireAllInOne(private val questionnaireFileName: String) : Questio
|
||||
ViewUtils.setTextSizeScale(ViewUtils.EMBEDDED_TEXT_SCALE)
|
||||
|
||||
context.setContentView(R.layout.questionnaire_all_in_one)
|
||||
val contentRoot = context.findViewById<android.view.ViewGroup>(android.R.id.content)?.getChildAt(0)
|
||||
com.tomhempel.rbarometer.ui.SessionUi.applyQuestionScreen(context, contentRoot)
|
||||
ViewUtils.suppressAutofillForSubtree(context.findViewById(android.R.id.content))
|
||||
|
||||
container = context.findViewById(R.id.questionContainer)
|
||||
@ -274,22 +276,12 @@ class QuestionnaireAllInOne(private val questionnaireFileName: String) : Questio
|
||||
sections.filter { it.card.visibility == View.VISIBLE }.map { it.index }.toSet()
|
||||
|
||||
private fun setupLanguageSpinner(spinner: Spinner) {
|
||||
val labels = AppLanguages.spinnerLabels(languageID)
|
||||
val selected = labels.getOrNull(AppLanguages.indexOf(languageID))
|
||||
ViewUtils.setupLanguagePickerSpinner(context, spinner, labels, selected)
|
||||
spinner.setSelection(AppLanguages.indexOf(languageID))
|
||||
|
||||
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, v: View?, position: Int, id: Long) {
|
||||
val newLang = AppLanguages.ids[position]
|
||||
if (newLang == languageID) return
|
||||
spinner.post { onLanguageChanged(newLang) }
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
QuestionnaireLanguagePicker.wireSpinner(context, spinner, languageID) { newLang ->
|
||||
onLanguageChanged(newLang)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLanguageChanged(newLang: String) {
|
||||
override fun onLanguageChanged(newLang: String) {
|
||||
for (section in sections) {
|
||||
if (section.card.visibility == View.VISIBLE) {
|
||||
try { section.handler?.saveAnswer() } catch (_: Exception) {}
|
||||
@ -1121,7 +1113,7 @@ class QuestionnaireAllInOne(private val questionnaireFileName: String) : Questio
|
||||
val validToken = token?.takeIf { it.isNotBlank() }
|
||||
if (activity == null || clientCode.isBlank() || validToken == null || !TokenStore.isSessionValid(activity)) {
|
||||
showLoading(progressBar, saveOverlay, scrollContent, false)
|
||||
activity?.finishQuestionnaire()
|
||||
activity?.finishQuestionnaire(wasCompleted = true)
|
||||
return
|
||||
}
|
||||
|
||||
@ -1131,7 +1123,7 @@ class QuestionnaireAllInOne(private val questionnaireFileName: String) : Questio
|
||||
}
|
||||
showLoading(progressBar, saveOverlay, scrollContent, false)
|
||||
if (!hasScores) {
|
||||
activity.finishQuestionnaire()
|
||||
activity.finishQuestionnaire(wasCompleted = true)
|
||||
return@launch
|
||||
}
|
||||
|
||||
@ -1144,7 +1136,7 @@ class QuestionnaireAllInOne(private val questionnaireFileName: String) : Questio
|
||||
languageID = newLang
|
||||
AppLanguageStore.set(activity, newLang)
|
||||
},
|
||||
onClose = { activity.finishQuestionnaire() },
|
||||
onClose = { activity.finishQuestionnaire(wasCompleted = true) },
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import android.view.View
|
||||
import android.widget.*
|
||||
import com.tomhempel.rbarometer.LanguageManager
|
||||
import com.tomhempel.rbarometer.MainActivity
|
||||
import com.tomhempel.rbarometer.session.QuestionnaireDraftSnapshot
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
@ -66,9 +67,21 @@ abstract class QuestionnaireBase<T> {
|
||||
|
||||
protected fun navigateTo(layoutResId: Int, setup: (View) -> Unit) {
|
||||
context.setContentView(layoutResId)
|
||||
val contentHost = context.findViewById<android.view.ViewGroup>(android.R.id.content)
|
||||
val rootView = contentHost?.getChildAt(0) ?: contentHost ?: context.window.decorView
|
||||
setup(rootView)
|
||||
val questionRoot = QuestionnaireLanguagePicker.ensureToolbarLayout(
|
||||
activity = context,
|
||||
languageId = languageID,
|
||||
onLanguageChanged = { newLang ->
|
||||
languageID = newLang
|
||||
com.tomhempel.rbarometer.ui.AppLanguageStore.set(context, newLang)
|
||||
onLanguageChanged(newLang)
|
||||
},
|
||||
)
|
||||
com.tomhempel.rbarometer.ui.SessionUi.applyQuestionScreen(context, questionRoot)
|
||||
setup(questionRoot)
|
||||
}
|
||||
|
||||
protected open fun onLanguageChanged(newLanguageId: String) {
|
||||
showCurrentQuestion()
|
||||
}
|
||||
|
||||
protected fun setupPrevButton(buttonId: Int, action: () -> Unit) {
|
||||
@ -137,7 +150,17 @@ abstract class QuestionnaireBase<T> {
|
||||
QuestionnairePersistence.saveAnswers(context, answers, questionnaireId, languageID, questions)
|
||||
}
|
||||
|
||||
fun draftSnapshot(): QuestionnaireDraftSnapshot? {
|
||||
if (!::questionnaireMeta.isInitialized) return null
|
||||
return QuestionnaireDraftSnapshot(
|
||||
questionnaireId = questionnaireMeta.id,
|
||||
answers = answers.toMap(),
|
||||
questions = if (::questions.isInitialized) questions else emptyList(),
|
||||
languageId = languageID,
|
||||
)
|
||||
}
|
||||
|
||||
fun endQuestionnaire() {
|
||||
(context as? MainActivity)?.finishQuestionnaire()
|
||||
(context as? MainActivity)?.finishQuestionnaire(wasCompleted = false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ object QuestionnaireCompletionService {
|
||||
if (elapsed < minLoadingMs) delay(minLoadingMs - elapsed)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onCompleted?.invoke() ?: (context as? MainActivity)?.finishQuestionnaire()
|
||||
onCompleted?.invoke() ?: (context as? MainActivity)?.finishQuestionnaire(wasCompleted = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
package com.tomhempel.rbarometer.questionnaire
|
||||
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Spinner
|
||||
import com.tomhempel.rbarometer.R
|
||||
import com.tomhempel.rbarometer.utils.ViewUtils
|
||||
import com.tomhempel.rbarometer.network.TokenStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
open class QuestionnaireGeneric(private val questionnaireFileName: String) : QuestionnaireBase<Unit>() {
|
||||
|
||||
@ -16,64 +14,47 @@ open class QuestionnaireGeneric(private val questionnaireFileName: String) : Que
|
||||
questionnaireMeta = meta
|
||||
questions = questionsList
|
||||
currentIndex = 0
|
||||
showCurrentQuestion()
|
||||
|
||||
val clientCode = GlobalValues.LOADED_CLIENT_CODE?.trim().orEmpty()
|
||||
.ifEmpty { GlobalValues.LAST_CLIENT_CODE?.trim().orEmpty() }
|
||||
if (clientCode.isEmpty()) {
|
||||
showCurrentQuestion()
|
||||
return
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
SavedAnswerLoader.loadInto(
|
||||
clientCode = clientCode,
|
||||
questionnaireId = meta.id,
|
||||
questions = questionsList,
|
||||
answers = answers,
|
||||
)
|
||||
}
|
||||
answers["client_code"] = clientCode
|
||||
TokenStore.getUsername(context)?.let { answers["coach_code"] = it }
|
||||
showCurrentQuestion()
|
||||
}
|
||||
}
|
||||
|
||||
override fun showCurrentQuestion() {
|
||||
val question = questions[currentIndex]
|
||||
|
||||
val layoutResId = getLayoutResId(question.layout ?: "default_layout")
|
||||
|
||||
if (layoutResId == 0) {
|
||||
showEmptyScreen()
|
||||
return
|
||||
}
|
||||
|
||||
navigateTo(layoutResId) { layout ->
|
||||
layout.findViewById<Button>(R.id.Qprev)?.setOnClickListener {
|
||||
layout.findViewById<android.widget.Button>(R.id.Qprev)?.setOnClickListener {
|
||||
goToPreviousQuestion()
|
||||
}
|
||||
|
||||
val handler = createHandlerForQuestion(question)
|
||||
|
||||
if (handler != null) {
|
||||
handler.bind(layout, question)
|
||||
} else {
|
||||
showEmptyScreen()
|
||||
}
|
||||
|
||||
injectLanguageSpinner(layout)
|
||||
}
|
||||
}
|
||||
|
||||
private fun injectLanguageSpinner(layout: View) {
|
||||
val container = layout as? FrameLayout ?: return
|
||||
|
||||
val spinner = Spinner(context)
|
||||
val labels = AppLanguages.spinnerLabels(languageID)
|
||||
val selected = labels.getOrNull(AppLanguages.indexOf(languageID))
|
||||
ViewUtils.setupLanguagePickerSpinner(context, spinner, labels, selected)
|
||||
spinner.setSelection(AppLanguages.indexOf(languageID))
|
||||
|
||||
val margin = ViewUtils.dp(context, 10)
|
||||
container.addView(
|
||||
spinner,
|
||||
FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
Gravity.TOP or Gravity.END
|
||||
).apply { setMargins(0, margin, margin, 0) }
|
||||
)
|
||||
|
||||
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, v: View?, position: Int, id: Long) {
|
||||
val newLang = AppLanguages.ids[position]
|
||||
if (newLang == languageID) return
|
||||
languageID = newLang
|
||||
com.tomhempel.rbarometer.ui.AppLanguageStore.set(context, newLang)
|
||||
spinner.post { showCurrentQuestion() }
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,166 @@
|
||||
package com.tomhempel.rbarometer.questionnaire
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import com.tomhempel.rbarometer.R
|
||||
import com.tomhempel.rbarometer.ui.SystemBarInsets
|
||||
import com.tomhempel.rbarometer.utils.ViewUtils
|
||||
|
||||
/** Reusable language dropdown — toolbar row above content so question text sits below. */
|
||||
object QuestionnaireLanguagePicker {
|
||||
|
||||
/**
|
||||
* Ensures a language toolbar sits above the screen content.
|
||||
* @return the inner content root (question layout) for handlers to bind against.
|
||||
*/
|
||||
fun ensureToolbarLayout(
|
||||
activity: Activity,
|
||||
languageId: String,
|
||||
onLanguageChanged: (String) -> Unit,
|
||||
): View {
|
||||
val content = activity.findViewById<ViewGroup>(android.R.id.content)
|
||||
?: return activity.window.decorView
|
||||
|
||||
val currentRoot = content.getChildAt(0) ?: return activity.window.decorView
|
||||
|
||||
val questionRoot = when {
|
||||
currentRoot.findViewById<Spinner>(R.id.langSpinner) != null -> {
|
||||
wireSpinner(
|
||||
activity,
|
||||
currentRoot.findViewById(R.id.langSpinner)!!,
|
||||
languageId,
|
||||
onLanguageChanged,
|
||||
)
|
||||
contentRoot(currentRoot)
|
||||
}
|
||||
|
||||
currentRoot.id == R.id.questionnaire_screen_wrapper -> {
|
||||
currentRoot.findViewById<Spinner>(R.id.langSpinner)?.let {
|
||||
wireSpinner(activity, it, languageId, onLanguageChanged)
|
||||
}
|
||||
contentRoot(currentRoot)
|
||||
}
|
||||
|
||||
else -> wrapWithLanguageToolbar(activity, content, currentRoot, languageId, onLanguageChanged)
|
||||
}
|
||||
|
||||
// setContentView runs SystemBarInsets before this wrapper exists; re-apply on the live root.
|
||||
SystemBarInsets.applyToContentRoot(activity, ensureMinContentPadding = true)
|
||||
return questionRoot
|
||||
}
|
||||
|
||||
private fun wrapWithLanguageToolbar(
|
||||
activity: Activity,
|
||||
content: ViewGroup,
|
||||
currentRoot: View,
|
||||
languageId: String,
|
||||
onLanguageChanged: (String) -> Unit,
|
||||
): View {
|
||||
content.removeView(currentRoot)
|
||||
currentRoot.id = R.id.questionnaire_content_root
|
||||
|
||||
val margin = ViewUtils.dp(activity, 12)
|
||||
val pickerHeight = activity.resources.getDimensionPixelSize(R.dimen.language_picker_height)
|
||||
|
||||
val toolbar = LinearLayout(activity).apply {
|
||||
id = R.id.questionnaire_lang_toolbar
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
||||
setPadding(margin, margin, margin, margin)
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
}
|
||||
|
||||
val spinner = Spinner(activity).apply { id = R.id.langSpinner }
|
||||
toolbar.addView(
|
||||
spinner,
|
||||
LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
pickerHeight,
|
||||
),
|
||||
)
|
||||
wireSpinner(activity, spinner, languageId, onLanguageChanged)
|
||||
|
||||
val wrapper = LinearLayout(activity).apply {
|
||||
id = R.id.questionnaire_screen_wrapper
|
||||
orientation = LinearLayout.VERTICAL
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
}
|
||||
wrapper.addView(toolbar)
|
||||
wrapper.addView(
|
||||
currentRoot,
|
||||
LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
0,
|
||||
1f,
|
||||
),
|
||||
)
|
||||
content.addView(wrapper)
|
||||
return currentRoot
|
||||
}
|
||||
|
||||
/** @deprecated Use [ensureToolbarLayout]; kept for session interstitial refresh. */
|
||||
fun attach(
|
||||
root: View,
|
||||
context: Context,
|
||||
languageId: String,
|
||||
onLanguageChanged: (String) -> Unit,
|
||||
) {
|
||||
val activity = context as? Activity
|
||||
if (activity != null) {
|
||||
ensureToolbarLayout(activity, languageId, onLanguageChanged)
|
||||
return
|
||||
}
|
||||
root.findViewById<Spinner>(R.id.langSpinner)?.let {
|
||||
wireSpinner(context, it, languageId, onLanguageChanged)
|
||||
}
|
||||
}
|
||||
|
||||
fun detach(activity: Activity) {
|
||||
// setContentView replaces the tree; nothing to tear down.
|
||||
}
|
||||
|
||||
fun wireSpinner(
|
||||
context: Context,
|
||||
spinner: Spinner,
|
||||
languageId: String,
|
||||
onLanguageChanged: (String) -> Unit,
|
||||
) {
|
||||
val labels = AppLanguages.spinnerLabels(languageId)
|
||||
val selected = labels.getOrNull(AppLanguages.indexOf(languageId))
|
||||
ViewUtils.setupLanguagePickerSpinner(context, spinner, labels, selected)
|
||||
spinner.onItemSelectedListener = null
|
||||
spinner.setSelection(AppLanguages.indexOf(languageId))
|
||||
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, v: View?, position: Int, id: Long) {
|
||||
val newLang = AppLanguages.ids[position]
|
||||
if (newLang == languageId) return
|
||||
spinner.post { onLanguageChanged(newLang) }
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun contentRoot(root: View): View {
|
||||
if (root.id == R.id.questionnaire_screen_wrapper) {
|
||||
val wrapper = root as ViewGroup
|
||||
return wrapper.findViewById(R.id.questionnaire_content_root)
|
||||
?: wrapper.getChildAt(1)
|
||||
?: root
|
||||
}
|
||||
return root
|
||||
}
|
||||
}
|
||||
@ -73,6 +73,61 @@ object QuestionnairePersistence {
|
||||
UploadReminderScheduler.schedule(context)
|
||||
}
|
||||
|
||||
/** Persists partial answers without marking the questionnaire complete (for session pause/resume). */
|
||||
suspend fun saveDraftAnswers(
|
||||
context: Context,
|
||||
answers: Map<String, Any>,
|
||||
questionnaireId: String,
|
||||
languageId: String,
|
||||
questions: List<QuestionItem> = emptyList(),
|
||||
) {
|
||||
if (answers.isEmpty()) return
|
||||
val persistedAnswers = canonicalPersistedAnswers(answers, questions)
|
||||
if (persistedAnswers.isEmpty()) return
|
||||
SyncLog.d("QUESTIONNAIRE", "Persisting draft ${persistedAnswers.size} answer field(s)")
|
||||
if (!ClientAssignmentGuard.canPersistAnswers(context, answers)) {
|
||||
val clientCode = answers["client_code"] as? String
|
||||
SyncLog.w("QUESTIONNAIRE", "Skipping draft save for unassigned ${SyncLog.clientRef(clientCode)}")
|
||||
return
|
||||
}
|
||||
|
||||
val clientCode = answers["client_code"] as? String ?: return
|
||||
val existing = CompletedQuestionnaireRepository.getStatus(clientCode, questionnaireId)
|
||||
if (existing?.isDone == true) {
|
||||
SyncLog.d("QUESTIONNAIRE", "Skipping draft save — questionnaire already completed")
|
||||
return
|
||||
}
|
||||
|
||||
val db = MyApp.database
|
||||
ClientRepository.ensureClient(clientCode)
|
||||
db.questionnaireDao().insertQuestionnaire(Questionnaire(id = questionnaireId))
|
||||
|
||||
AnswerRepository.deleteAnswersForClientAndQuestionnaire(clientCode, questionnaireId)
|
||||
|
||||
val questionEntities = persistedAnswers.keys
|
||||
.filterNot { it.startsWith("client") }
|
||||
.map { key ->
|
||||
Question(
|
||||
questionId = AnswerKeyUtils.roomQuestionId(questionnaireId, key),
|
||||
questionnaireId = questionnaireId,
|
||||
question = LanguageManager.getText(languageId, key),
|
||||
)
|
||||
}
|
||||
db.questionDao().insertQuestions(questionEntities)
|
||||
|
||||
val answerEntities = persistedAnswers.entries
|
||||
.filterNot { it.key.startsWith("client") }
|
||||
.map { (key, value) ->
|
||||
Answer(
|
||||
clientCode = clientCode,
|
||||
questionId = AnswerKeyUtils.roomQuestionId(questionnaireId, key),
|
||||
answerValue = value.toString(),
|
||||
)
|
||||
}
|
||||
AnswerRepository.insertAnswers(answerEntities)
|
||||
ClientAnswersCache.invalidate(clientCode)
|
||||
}
|
||||
|
||||
private fun canonicalPersistedAnswers(
|
||||
answers: Map<String, Any>,
|
||||
questions: List<QuestionItem>,
|
||||
|
||||
@ -93,6 +93,7 @@ class HandlerLastPage(
|
||||
val validToken = token?.takeIf { it.isNotBlank() }
|
||||
val reviewRequest = if (
|
||||
activity != null &&
|
||||
!activity.isInSessionFlow &&
|
||||
clientCode.isNotBlank() &&
|
||||
validToken != null &&
|
||||
TokenStore.isSessionValid(activity) &&
|
||||
@ -120,11 +121,11 @@ class HandlerLastPage(
|
||||
onLanguageChanged = { newLang ->
|
||||
AppLanguageStore.set(reviewRequest.first, newLang)
|
||||
},
|
||||
onClose = { reviewRequest.first.finishQuestionnaire() },
|
||||
onClose = { reviewRequest.first.finishQuestionnaire(wasCompleted = true) },
|
||||
).show()
|
||||
} else {
|
||||
// Zurück zum Opening Screen – der lädt dann automatisch (siehe Änderung 2)
|
||||
activity?.finishQuestionnaire() ?: goToNextQuestion()
|
||||
activity?.finishQuestionnaire(wasCompleted = true) ?: goToNextQuestion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import com.tomhempel.rbarometer.questionnaire.QuestionFlowResolver
|
||||
import com.tomhempel.rbarometer.questionnaire.QuestionHandler
|
||||
import com.tomhempel.rbarometer.questionnaire.QuestionItem
|
||||
import com.tomhempel.rbarometer.R
|
||||
import com.tomhempel.rbarometer.ui.SessionUi
|
||||
import com.tomhempel.rbarometer.utils.ViewUtils
|
||||
|
||||
/*
|
||||
@ -41,7 +42,7 @@ class HandlerRadioQuestion(
|
||||
val questionTextView = layout.findViewById<TextView>(R.id.textView)
|
||||
val questionTitle = layout.findViewById<TextView>(R.id.question)
|
||||
|
||||
questionTextView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||
questionTextView.text = LanguageManager.resolvedText(languageID, question.noteBeforeKey, question.textKey)
|
||||
questionTitle.text = question.question?.let {
|
||||
Html.fromHtml(LanguageManager.getText(languageID, it), Html.FROM_HTML_MODE_LEGACY)
|
||||
} ?: ""
|
||||
@ -70,6 +71,7 @@ class HandlerRadioQuestion(
|
||||
|
||||
val padding = (12 * resources.displayMetrics.density).toInt()
|
||||
setPadding(padding, padding, padding, padding)
|
||||
SessionUi.styleAnswerChoice(context, this)
|
||||
}
|
||||
radioGroup.addView(radioButton)
|
||||
}
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
package com.tomhempel.rbarometer.session
|
||||
|
||||
import com.tomhempel.rbarometer.questionnaire.QuestionItem
|
||||
|
||||
data class QuestionnaireDraftSnapshot(
|
||||
val questionnaireId: String?,
|
||||
val answers: Map<String, Any>,
|
||||
val questions: List<QuestionItem>,
|
||||
val languageId: String,
|
||||
)
|
||||
@ -0,0 +1,7 @@
|
||||
package com.tomhempel.rbarometer.session
|
||||
|
||||
data class SessionCompletionResult(
|
||||
val clientCode: String,
|
||||
val sessionName: String,
|
||||
val canUpload: Boolean,
|
||||
)
|
||||
@ -0,0 +1,353 @@
|
||||
package com.tomhempel.rbarometer.session
|
||||
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.tomhempel.rbarometer.MainActivity
|
||||
import com.tomhempel.rbarometer.R
|
||||
import com.tomhempel.rbarometer.network.AssignedClientInfo
|
||||
import com.tomhempel.rbarometer.network.ProgramProgress
|
||||
import com.tomhempel.rbarometer.network.ProgramSessionDefinition
|
||||
import com.tomhempel.rbarometer.network.QuestionnaireCache
|
||||
import com.tomhempel.rbarometer.questionnaire.GlobalValues
|
||||
import com.tomhempel.rbarometer.questionnaire.QuestionnaireGeneric
|
||||
import com.tomhempel.rbarometer.questionnaire.QuestionnaireLanguagePicker
|
||||
import com.tomhempel.rbarometer.questionnaire.QuestionnairePersistence
|
||||
import com.tomhempel.rbarometer.ui.AppLanguageStore
|
||||
import com.tomhempel.rbarometer.ui.SessionMeasureUi
|
||||
import com.tomhempel.rbarometer.utils.ViewUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SessionFlowController(
|
||||
private val activity: MainActivity,
|
||||
private var languageID: String,
|
||||
private val clientCode: String,
|
||||
private val onExit: (SessionCompletionResult?) -> Unit,
|
||||
) {
|
||||
private var session: ProgramSessionDefinition? = null
|
||||
private var progress: ProgramProgress? = null
|
||||
private var clientInfo: AssignedClientInfo? = null
|
||||
private var measures: List<SessionProgramHelper.SessionMeasure> = emptyList()
|
||||
private var measureIndex = 0
|
||||
private var pendingUpload = false
|
||||
|
||||
fun start() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (!withContext(Dispatchers.IO) { loadSessionState() }) {
|
||||
showErrorAndExit(hubFallback("Keine Sitzung konfiguriert", "No session configured"))
|
||||
return@launch
|
||||
}
|
||||
measureIndex = resolveStartingMeasureIndex()
|
||||
pendingUpload = withContext(Dispatchers.IO) {
|
||||
SessionProgramHelper.clientHasPendingUpload(activity, clientCode)
|
||||
}
|
||||
when {
|
||||
measures.all { it.completedInCycle } && pendingUpload -> {
|
||||
measureIndex = measureIndex.coerceIn(0, (measures.size - 1).coerceAtLeast(0))
|
||||
showMeasureInterstitial(editBeforeUpload = true)
|
||||
}
|
||||
measures.all { it.completedInCycle } -> returnToMainWithCompletionDialog()
|
||||
else -> showMeasureInterstitial()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleQuestionnaireFinished(
|
||||
wasCompleted: Boolean,
|
||||
draftSnapshot: QuestionnaireDraftSnapshot? = null,
|
||||
): Boolean {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!wasCompleted) {
|
||||
saveDraftSnapshot(draftSnapshot)
|
||||
}
|
||||
reloadMeasures()
|
||||
refreshPendingUploadFlag()
|
||||
}
|
||||
if (wasCompleted) {
|
||||
if (measures.all { it.completedInCycle }) {
|
||||
returnToMainWithCompletionDialog()
|
||||
} else {
|
||||
measureIndex++
|
||||
advancePastCompletedMeasures()
|
||||
}
|
||||
} else {
|
||||
persistPauseState()
|
||||
val resumeEdit = pendingUpload && measures.all { it.completedInCycle }
|
||||
showMeasureInterstitial(editBeforeUpload = resumeEdit)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun resolveStartingMeasureIndex(): Int {
|
||||
val prog = progress ?: return 0
|
||||
val sess = session ?: return 0
|
||||
val paused = SessionPauseStore.load(activity, clientCode)
|
||||
if (
|
||||
paused != null &&
|
||||
paused.sessionId == sess.sessionID &&
|
||||
paused.cycleNumber == prog.cycleNumber &&
|
||||
paused.cycleStartedAt == prog.cycleStartedAt
|
||||
) {
|
||||
return paused.measureIndex.coerceIn(0, (measures.size - 1).coerceAtLeast(0))
|
||||
}
|
||||
return measures.indexOfFirst { !it.completedInCycle }.let { idx ->
|
||||
if (idx < 0) 0 else idx
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveDraftSnapshot(snapshot: QuestionnaireDraftSnapshot?) {
|
||||
val snap = snapshot ?: return
|
||||
val questionnaireId = snap.questionnaireId ?: return
|
||||
if (snap.answers.isEmpty()) return
|
||||
QuestionnairePersistence.saveDraftAnswers(
|
||||
activity,
|
||||
snap.answers,
|
||||
questionnaireId,
|
||||
snap.languageId,
|
||||
snap.questions,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun persistPauseState() {
|
||||
val sess = session ?: return
|
||||
val prog = progress ?: return
|
||||
SessionPauseStore.save(
|
||||
activity,
|
||||
SessionPauseStore.PauseState(
|
||||
clientCode = clientCode,
|
||||
sessionId = sess.sessionID,
|
||||
cycleNumber = prog.cycleNumber,
|
||||
cycleStartedAt = prog.cycleStartedAt,
|
||||
measureIndex = measureIndex.coerceIn(0, (measures.size - 1).coerceAtLeast(0)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun advancePastCompletedMeasures() {
|
||||
while (measureIndex < measures.size && measures[measureIndex].completedInCycle) {
|
||||
measureIndex++
|
||||
}
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
withContext(Dispatchers.IO) { persistPauseState() }
|
||||
}
|
||||
if (measures.isEmpty() || measures.all { it.completedInCycle } || measureIndex >= measures.size) {
|
||||
returnToMainWithCompletionDialog()
|
||||
} else {
|
||||
showMeasureInterstitial()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadSessionState(): Boolean {
|
||||
clientInfo = QuestionnaireCache.getAssignedClientInfos(activity)
|
||||
.firstOrNull { it.clientCode.equals(clientCode, ignoreCase = true) }
|
||||
?: AssignedClientInfo(
|
||||
clientCode = clientCode,
|
||||
programProgress = QuestionnaireCache.getProgramProgress(activity, clientCode),
|
||||
)
|
||||
progress = clientInfo?.programProgress ?: return false
|
||||
val currentSessionId = progress?.currentSessionID ?: return false
|
||||
session = QuestionnaireCache.findProgramSession(activity, currentSessionId) ?: return false
|
||||
reloadMeasures()
|
||||
return session != null && measures.isNotEmpty()
|
||||
}
|
||||
|
||||
private suspend fun refreshPendingUploadFlag() {
|
||||
pendingUpload = SessionProgramHelper.clientHasPendingUpload(activity, clientCode)
|
||||
}
|
||||
|
||||
private suspend fun reloadMeasures() {
|
||||
val info = clientInfo ?: return
|
||||
val sess = session ?: return
|
||||
val prog = progress ?: return
|
||||
measures = SessionProgramHelper.measuresForSession(activity, info, sess, prog.cycleStartedAt)
|
||||
}
|
||||
|
||||
private fun showMeasureInterstitial(editBeforeUpload: Boolean = false) {
|
||||
if (measures.isEmpty()) return
|
||||
if (!editBeforeUpload) {
|
||||
val firstIncomplete = measures.indexOfFirst { !it.completedInCycle }
|
||||
if (firstIncomplete >= 0) {
|
||||
measureIndex = firstIncomplete
|
||||
}
|
||||
}
|
||||
if (measureIndex >= measures.size) {
|
||||
returnToMainWithCompletionDialog()
|
||||
return
|
||||
}
|
||||
val sess = session ?: return
|
||||
val prog = progress ?: return
|
||||
val measure = measures[measureIndex]
|
||||
val completedCount = measures.count { it.completedInCycle }
|
||||
val sessionName = prog.currentSessionName ?: sess.name
|
||||
val allowMeasureSelection = editBeforeUpload
|
||||
|
||||
activity.setContentView(R.layout.session_measure_interstitial)
|
||||
if (editBeforeUpload) {
|
||||
activity.findViewById<TextView>(R.id.interstitialEyebrow).text =
|
||||
hubFallback("Sitzung bearbeiten", "Edit session")
|
||||
activity.findViewById<TextView>(R.id.interstitialTitle).text = sessionName
|
||||
activity.findViewById<TextView>(R.id.interstitialSessionLabel).text =
|
||||
hubFallback("Messpunkt zum Bearbeiten wählen", "Select a measure to edit")
|
||||
} else {
|
||||
activity.findViewById<TextView>(R.id.interstitialEyebrow).text =
|
||||
hubFallback("Sitzungsmesspunkt", "Session measure")
|
||||
activity.findViewById<TextView>(R.id.interstitialTitle).text = sessionName
|
||||
activity.findViewById<TextView>(R.id.interstitialSessionLabel).text = measure.displayName
|
||||
}
|
||||
activity.findViewById<TextView>(R.id.interstitialProgress).text =
|
||||
hubFallback(
|
||||
"Messpunkt ${measureIndex + 1} von ${measures.size} · Zyklus ${prog.cycleNumber} · Sitzung ${prog.currentSessionNumber ?: sess.sessionNumber} von ${prog.totalSessions}",
|
||||
"Measure ${measureIndex + 1} of ${measures.size} · Cycle ${prog.cycleNumber} · Session ${prog.currentSessionNumber ?: sess.sessionNumber} of ${prog.totalSessions}",
|
||||
)
|
||||
activity.findViewById<TextView>(R.id.interstitialClient).text = clientCode
|
||||
|
||||
val progressBar = activity.findViewById<LinearProgressIndicator>(R.id.interstitialProgressBar)
|
||||
progressBar.visibility = View.VISIBLE
|
||||
progressBar.max = measures.size
|
||||
progressBar.progress = completedCount
|
||||
|
||||
activity.findViewById<TextView>(R.id.interstitialMeasureHeading).text =
|
||||
if (editBeforeUpload) {
|
||||
hubFallback("Messpunkte — zum Auswählen tippen", "Measures — tap to select")
|
||||
} else {
|
||||
hubFallback("Ihre Messpunkte in dieser Sitzung", "Your measures in this session")
|
||||
}
|
||||
|
||||
val measureList = activity.findViewById<LinearLayout>(R.id.interstitialMeasureList)
|
||||
measureList.removeAllViews()
|
||||
measures.forEachIndexed { index, item ->
|
||||
measureList.addView(
|
||||
SessionMeasureUi.measureRow(
|
||||
context = activity,
|
||||
label = item.displayName,
|
||||
checked = item.completedInCycle,
|
||||
isCurrent = index == measureIndex,
|
||||
verticalPaddingDp = 0,
|
||||
highlightCurrentRow = true,
|
||||
selectable = allowMeasureSelection,
|
||||
onSelect = if (allowMeasureSelection) {
|
||||
{
|
||||
measureIndex = index
|
||||
showMeasureInterstitial(editBeforeUpload = editBeforeUpload)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
statusText = measureStatusLabel(
|
||||
completed = item.completedInCycle,
|
||||
selected = index == measureIndex,
|
||||
editBeforeUpload = editBeforeUpload,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
activity.findViewById<MaterialButton>(R.id.interstitialContinue).apply {
|
||||
text = when {
|
||||
editBeforeUpload -> hubFallback("Ausgewählten bearbeiten", "Edit selected")
|
||||
measure.completedInCycle -> hubFallback("Weiter", "Continue")
|
||||
else -> hubFallback("Messung starten", "Start measure")
|
||||
}
|
||||
setOnClickListener {
|
||||
when {
|
||||
editBeforeUpload -> startMeasure(measure.questionnaireId)
|
||||
measure.completedInCycle -> advancePastCompletedMeasures()
|
||||
else -> startMeasure(measure.questionnaireId)
|
||||
}
|
||||
}
|
||||
}
|
||||
activity.findViewById<MaterialButton>(R.id.interstitialPause).apply {
|
||||
visibility = View.VISIBLE
|
||||
text = hubFallback("Sitzung pausieren", "Pause session")
|
||||
setOnClickListener { pauseSession() }
|
||||
}
|
||||
|
||||
activity.findViewById<TextView>(R.id.interstitialSummary).text =
|
||||
hubFallback(
|
||||
"$completedCount von ${measures.size} Messpunkten in dieser Sitzung erledigt",
|
||||
"$completedCount of ${measures.size} measures completed in this session",
|
||||
)
|
||||
attachLanguagePicker()
|
||||
}
|
||||
|
||||
private fun returnToMainWithCompletionDialog() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
withContext(Dispatchers.IO) { refreshPendingUploadFlag() }
|
||||
SessionPauseStore.clear(activity, clientCode)
|
||||
val sessionName = progress?.currentSessionName ?: session?.name.orEmpty()
|
||||
QuestionnaireLanguagePicker.detach(activity)
|
||||
activity.isInQuestionnaire = false
|
||||
onExit(
|
||||
SessionCompletionResult(
|
||||
clientCode = clientCode,
|
||||
sessionName = sessionName,
|
||||
canUpload = pendingUpload,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startMeasure(questionnaireId: String) {
|
||||
GlobalValues.LAST_CLIENT_CODE = clientCode
|
||||
GlobalValues.LOADED_CLIENT_CODE = clientCode
|
||||
activity.startQuestionnaire(QuestionnaireGeneric(questionnaireId), languageID)
|
||||
}
|
||||
|
||||
private fun attachLanguagePicker() {
|
||||
QuestionnaireLanguagePicker.ensureToolbarLayout(
|
||||
activity = activity,
|
||||
languageId = languageID,
|
||||
onLanguageChanged = { newLang ->
|
||||
languageID = newLang
|
||||
AppLanguageStore.set(activity, newLang)
|
||||
if (measures.all { it.completedInCycle } && pendingUpload) {
|
||||
showMeasureInterstitial(editBeforeUpload = true)
|
||||
} else {
|
||||
showMeasureInterstitial()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun pauseSession() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
withContext(Dispatchers.IO) { persistPauseState() }
|
||||
QuestionnaireLanguagePicker.detach(activity)
|
||||
activity.isInQuestionnaire = false
|
||||
onExit(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun pauseFromBackPress() {
|
||||
pauseSession()
|
||||
}
|
||||
|
||||
private fun showErrorAndExit(message: String) {
|
||||
android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_LONG).show()
|
||||
pauseSession()
|
||||
}
|
||||
|
||||
private fun measureStatusLabel(
|
||||
completed: Boolean,
|
||||
selected: Boolean,
|
||||
editBeforeUpload: Boolean,
|
||||
): String = when {
|
||||
selected && editBeforeUpload -> hubFallback("Ausgewählt zum Bearbeiten", "Selected to edit")
|
||||
selected && !completed -> hubFallback("Als Nächstes", "Up next")
|
||||
selected -> hubFallback("Ausgewählt", "Selected")
|
||||
completed -> hubFallback("Abgeschlossen", "Complete")
|
||||
else -> hubFallback("Noch offen", "Not started")
|
||||
}
|
||||
|
||||
private fun hubFallback(de: String, en: String): String =
|
||||
if (languageID == "GERMAN") de else en
|
||||
|
||||
private fun dp(v: Int): Int = ViewUtils.dp(activity, v)
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package com.tomhempel.rbarometer.session
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.tomhempel.rbarometer.security.SensitiveDataCrypto
|
||||
import java.io.File
|
||||
|
||||
/** Encrypted on-device session pause state so coaches can resume a program session. */
|
||||
object SessionPauseStore {
|
||||
private const val FILE_NAME = "session_pause_states.json"
|
||||
private val gson = Gson()
|
||||
|
||||
data class PauseState(
|
||||
@SerializedName("clientCode") val clientCode: String,
|
||||
@SerializedName("sessionId") val sessionId: String,
|
||||
@SerializedName("cycleNumber") val cycleNumber: Int,
|
||||
@SerializedName("cycleStartedAt") val cycleStartedAt: Long,
|
||||
@SerializedName("measureIndex") val measureIndex: Int,
|
||||
@SerializedName("updatedAt") val updatedAt: Long = System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
private data class Store(
|
||||
@SerializedName("byClient") val byClient: Map<String, PauseState> = emptyMap(),
|
||||
)
|
||||
|
||||
fun save(context: Context, state: PauseState) {
|
||||
val current = loadStore(context)
|
||||
writeStore(context, current.copy(byClient = current.byClient + (state.clientCode.lowercase() to state)))
|
||||
}
|
||||
|
||||
fun load(context: Context, clientCode: String): PauseState? =
|
||||
loadStore(context).byClient[clientCode.lowercase()]
|
||||
|
||||
fun matchesCurrentProgram(
|
||||
context: Context,
|
||||
clientCode: String,
|
||||
sessionId: String?,
|
||||
cycleNumber: Int,
|
||||
cycleStartedAt: Long,
|
||||
): Boolean {
|
||||
val paused = load(context, clientCode) ?: return false
|
||||
if (sessionId.isNullOrBlank()) return false
|
||||
return paused.sessionId == sessionId &&
|
||||
paused.cycleNumber == cycleNumber &&
|
||||
paused.cycleStartedAt == cycleStartedAt
|
||||
}
|
||||
|
||||
fun clear(context: Context, clientCode: String) {
|
||||
val key = clientCode.lowercase()
|
||||
val current = loadStore(context).byClient
|
||||
if (!current.containsKey(key)) return
|
||||
writeStore(context, Store(byClient = current - key))
|
||||
}
|
||||
|
||||
private fun storeFile(context: Context): File = File(context.filesDir, FILE_NAME)
|
||||
|
||||
private fun loadStore(context: Context): Store {
|
||||
val file = storeFile(context)
|
||||
if (!file.exists()) return Store()
|
||||
val raw = file.readText()
|
||||
val plain = if (raw.startsWith("v1:")) SensitiveDataCrypto.decrypt(raw) else raw
|
||||
return runCatching { gson.fromJson(plain, Store::class.java) }.getOrDefault(Store())
|
||||
}
|
||||
|
||||
private fun writeStore(context: Context, store: Store) {
|
||||
storeFile(context).writeText(SensitiveDataCrypto.encrypt(gson.toJson(store)))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,290 @@
|
||||
package com.tomhempel.rbarometer.session
|
||||
|
||||
import android.content.Context
|
||||
import com.tomhempel.rbarometer.data.CompletedQuestionnaire
|
||||
import com.tomhempel.rbarometer.data.CompletedQuestionnaireRepository
|
||||
import com.tomhempel.rbarometer.data.CoachScoringReviewRepository
|
||||
import com.tomhempel.rbarometer.network.AssignedClientInfo
|
||||
import com.tomhempel.rbarometer.network.ProgramProgress
|
||||
import com.tomhempel.rbarometer.network.ProgramSessionDefinition
|
||||
import com.tomhempel.rbarometer.network.QuestionnaireCache
|
||||
|
||||
object SessionProgramHelper {
|
||||
|
||||
data class SessionMeasure(
|
||||
val questionnaireId: String,
|
||||
val displayName: String,
|
||||
val completedInCycle: Boolean,
|
||||
)
|
||||
|
||||
data class ClientSessionView(
|
||||
val clientCode: String,
|
||||
val progress: ProgramProgress,
|
||||
val session: ProgramSessionDefinition?,
|
||||
val measures: List<SessionMeasure>,
|
||||
val canResumeSession: Boolean = false,
|
||||
val isSessionMeasuresComplete: Boolean = false,
|
||||
val hasPendingUpload: Boolean = false,
|
||||
) {
|
||||
val pendingMeasureCount: Int get() = measures.count { !it.completedInCycle }
|
||||
val canStart: Boolean get() = session != null && !progress.programComplete && pendingMeasureCount > 0
|
||||
/** Session can be opened for start, resume, or edit-before-upload. */
|
||||
val canEnterSession: Boolean get() = when {
|
||||
progress.programComplete -> false
|
||||
session == null -> false
|
||||
isSessionMeasuresComplete && !hasPendingUpload -> false
|
||||
else -> canStart || canResumeSession || measures.any { it.completedInCycle } ||
|
||||
(isSessionMeasuresComplete && hasPendingUpload)
|
||||
}
|
||||
}
|
||||
|
||||
enum class SessionDisplayBucket {
|
||||
READY,
|
||||
IN_PROGRESS,
|
||||
WAITING_UPLOAD,
|
||||
COMPLETE,
|
||||
}
|
||||
|
||||
fun sessionDisplayBucket(view: ClientSessionView): SessionDisplayBucket = when {
|
||||
view.progress.programComplete -> SessionDisplayBucket.COMPLETE
|
||||
view.isSessionMeasuresComplete && view.hasPendingUpload -> SessionDisplayBucket.WAITING_UPLOAD
|
||||
view.isSessionMeasuresComplete -> SessionDisplayBucket.COMPLETE
|
||||
view.canResumeSession || view.measures.any { it.completedInCycle } -> SessionDisplayBucket.IN_PROGRESS
|
||||
view.canStart -> SessionDisplayBucket.READY
|
||||
view.session != null -> SessionDisplayBucket.IN_PROGRESS
|
||||
else -> SessionDisplayBucket.COMPLETE
|
||||
}
|
||||
|
||||
suspend fun clientHasPendingUpload(context: Context, clientCode: String): Boolean {
|
||||
val info = clientInfo(context, clientCode)
|
||||
val progress = info.programProgress ?: return false
|
||||
val session = progress.currentSessionID?.let { QuestionnaireCache.findProgramSession(context, it) }
|
||||
?: return false
|
||||
val measures = measuresForSession(context, info, session, progress.cycleStartedAt)
|
||||
return sessionHasPendingUpload(clientCode, measures)
|
||||
}
|
||||
|
||||
/** Raw pending rows for a completed session — must not call upload-ready filters (avoids recursion). */
|
||||
private suspend fun sessionHasPendingUpload(
|
||||
clientCode: String,
|
||||
measures: List<SessionMeasure>,
|
||||
): Boolean {
|
||||
if (measures.isEmpty() || !measures.all { it.completedInCycle }) return false
|
||||
val pendingQ = CompletedQuestionnaireRepository.getPendingUploadForClient(clientCode)
|
||||
if (pendingQ.any { entry ->
|
||||
measures.any { m -> questionnaireIdsMatch(m.questionnaireId, entry.questionnaireId) }
|
||||
}
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return CoachScoringReviewRepository.getAllForClient(clientCode).any { it.isPendingUpload }
|
||||
}
|
||||
|
||||
private fun questionnaireIdsMatch(a: String, b: String): Boolean {
|
||||
val na = normalizeQuestionnaireId(a)
|
||||
val nb = normalizeQuestionnaireId(b)
|
||||
if (na.isBlank() || nb.isBlank()) return false
|
||||
return na == nb || na.contains(nb) || nb.contains(na)
|
||||
}
|
||||
|
||||
suspend fun buildClientSessionViews(context: Context): List<ClientSessionView> {
|
||||
val questionnaireNames = QuestionnaireCache.getQuestionnaireList(context)
|
||||
val clientInfos = QuestionnaireCache.getAssignedClientInfos(context)
|
||||
if (clientInfos.isNotEmpty()) {
|
||||
return clientInfos.mapNotNull { info ->
|
||||
buildClientSessionView(context, info, questionnaireNames)
|
||||
}.sortedBy { it.clientCode }
|
||||
}
|
||||
return QuestionnaireCache.getAssignedClients(context).mapNotNull { code ->
|
||||
buildClientSessionView(
|
||||
context,
|
||||
AssignedClientInfo(clientCode = code, programProgress = QuestionnaireCache.getProgramProgress(context, code)),
|
||||
questionnaireNames,
|
||||
)
|
||||
}.sortedBy { it.clientCode }
|
||||
}
|
||||
|
||||
suspend fun buildClientSessionView(
|
||||
context: Context,
|
||||
clientInfo: AssignedClientInfo,
|
||||
questionnaireNames: List<com.tomhempel.rbarometer.network.QuestionnaireListItem> =
|
||||
QuestionnaireCache.getQuestionnaireList(context),
|
||||
): ClientSessionView? {
|
||||
val progress = clientInfo.programProgress ?: return null
|
||||
val session = progress.currentSessionID?.let { QuestionnaireCache.findProgramSession(context, it) }
|
||||
val localCompleted = CompletedQuestionnaireRepository.getAllForClient(clientInfo.clientCode)
|
||||
.associateBy { it.questionnaireId.lowercase() }
|
||||
val measures = if (session != null) {
|
||||
measuresForSession(context, clientInfo, session, progress.cycleStartedAt, localCompleted, questionnaireNames)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val isSessionMeasuresComplete = measures.isNotEmpty() && measures.all { it.completedInCycle }
|
||||
val hasPendingUpload = if (isSessionMeasuresComplete) {
|
||||
sessionHasPendingUpload(clientInfo.clientCode, measures)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
return ClientSessionView(
|
||||
clientCode = clientInfo.clientCode,
|
||||
progress = progress,
|
||||
session = session,
|
||||
measures = measures,
|
||||
canResumeSession = SessionPauseStore.matchesCurrentProgram(
|
||||
context = context,
|
||||
clientCode = clientInfo.clientCode,
|
||||
sessionId = session?.sessionID,
|
||||
cycleNumber = progress.cycleNumber,
|
||||
cycleStartedAt = progress.cycleStartedAt,
|
||||
),
|
||||
isSessionMeasuresComplete = isSessionMeasuresComplete,
|
||||
hasPendingUpload = hasPendingUpload,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun measuresForSession(
|
||||
context: Context,
|
||||
clientInfo: AssignedClientInfo,
|
||||
session: ProgramSessionDefinition,
|
||||
cycleStartedAtSec: Long,
|
||||
): List<SessionMeasure> {
|
||||
val localCompleted = CompletedQuestionnaireRepository.getAllForClient(clientInfo.clientCode)
|
||||
.associateBy { it.questionnaireId.lowercase() }
|
||||
return measuresForSession(
|
||||
context,
|
||||
clientInfo,
|
||||
session,
|
||||
cycleStartedAtSec,
|
||||
localCompleted,
|
||||
QuestionnaireCache.getQuestionnaireList(context),
|
||||
)
|
||||
}
|
||||
|
||||
private fun measuresForSession(
|
||||
context: Context,
|
||||
clientInfo: AssignedClientInfo,
|
||||
session: ProgramSessionDefinition,
|
||||
cycleStartedAtSec: Long,
|
||||
localCompleted: Map<String, CompletedQuestionnaire>,
|
||||
questionnaireNames: List<com.tomhempel.rbarometer.network.QuestionnaireListItem>,
|
||||
): List<SessionMeasure> =
|
||||
session.questionnaires.sortedBy { it.orderIndex }.map { member ->
|
||||
SessionMeasure(
|
||||
questionnaireId = member.questionnaireID,
|
||||
displayName = questionnaireDisplayName(context, member.questionnaireID, questionnaireNames),
|
||||
completedInCycle = isMeasureCompleteInCycle(
|
||||
clientInfo,
|
||||
member.questionnaireID,
|
||||
cycleStartedAtSec,
|
||||
findLocalCompletion(localCompleted, member.questionnaireID),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun isMeasureCompleteInCycle(
|
||||
clientInfo: AssignedClientInfo,
|
||||
questionnaireId: String,
|
||||
cycleStartedAtSec: Long,
|
||||
): Boolean {
|
||||
val localCompleted = CompletedQuestionnaireRepository.getAllForClient(clientInfo.clientCode)
|
||||
.associateBy { it.questionnaireId.lowercase() }
|
||||
val local = findLocalCompletion(localCompleted, questionnaireId)
|
||||
return isMeasureCompleteInCycle(clientInfo, questionnaireId, cycleStartedAtSec, local)
|
||||
}
|
||||
|
||||
private fun normalizeQuestionnaireId(name: String): String =
|
||||
name.lowercase().removeSuffix(".json")
|
||||
|
||||
private fun findLocalCompletion(
|
||||
localCompleted: Map<String, CompletedQuestionnaire>,
|
||||
questionnaireId: String,
|
||||
): CompletedQuestionnaire? {
|
||||
localCompleted[questionnaireId.lowercase()]?.let { return it }
|
||||
val target = normalizeQuestionnaireId(questionnaireId)
|
||||
if (target.isBlank()) return null
|
||||
return localCompleted.entries.firstOrNull { (key, _) ->
|
||||
val candidate = normalizeQuestionnaireId(key)
|
||||
candidate == target || candidate.contains(target) || target.contains(candidate)
|
||||
}?.value
|
||||
}
|
||||
|
||||
private fun isMeasureCompleteInCycle(
|
||||
clientInfo: AssignedClientInfo,
|
||||
questionnaireId: String,
|
||||
cycleStartedAtSec: Long,
|
||||
local: CompletedQuestionnaire?,
|
||||
): Boolean {
|
||||
if (local?.isDone == true) {
|
||||
val tsSec = local.timestamp / 1000L
|
||||
if (cycleStartedAtSec <= 0L || tsSec > cycleStartedAtSec) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
clientInfo.completedQuestionnaires
|
||||
.firstOrNull { it.questionnaireID.equals(questionnaireId, ignoreCase = true) }
|
||||
?.let { server ->
|
||||
if (cycleStartedAtSec <= 0L || server.completedAt > cycleStartedAtSec) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun questionnaireDisplayName(
|
||||
context: Context,
|
||||
questionnaireId: String,
|
||||
questionnaireNames: List<com.tomhempel.rbarometer.network.QuestionnaireListItem> =
|
||||
QuestionnaireCache.getQuestionnaireList(context),
|
||||
): String {
|
||||
val fromList = questionnaireNames
|
||||
.firstOrNull { it.id.equals(questionnaireId, ignoreCase = true) }
|
||||
?.name
|
||||
?.takeIf { it.isNotBlank() }
|
||||
return fromList ?: questionnaireId
|
||||
}
|
||||
|
||||
private fun clientInfo(context: Context, clientCode: String): AssignedClientInfo =
|
||||
QuestionnaireCache.getAssignedClientInfos(context)
|
||||
.firstOrNull { it.clientCode.equals(clientCode, ignoreCase = true) }
|
||||
?: AssignedClientInfo(
|
||||
clientCode = clientCode,
|
||||
programProgress = QuestionnaireCache.getProgramProgress(context, clientCode),
|
||||
)
|
||||
|
||||
/** True when the client's current program session has all measures completed (or no active session). */
|
||||
suspend fun isClientSessionReadyForUpload(context: Context, clientCode: String): Boolean {
|
||||
val info = clientInfo(context, clientCode)
|
||||
val progress = info.programProgress ?: return true
|
||||
if (progress.programComplete) return true
|
||||
val session = progress.currentSessionID?.let { QuestionnaireCache.findProgramSession(context, it) }
|
||||
?: return true
|
||||
val measures = measuresForSession(context, info, session, progress.cycleStartedAt)
|
||||
return measures.isEmpty() || measures.all { it.completedInCycle }
|
||||
}
|
||||
|
||||
suspend fun filterUploadReadyQuestionnaires(
|
||||
context: Context,
|
||||
pending: List<CompletedQuestionnaire>,
|
||||
): List<CompletedQuestionnaire> {
|
||||
if (pending.isEmpty()) return pending
|
||||
val readyByClient = mutableMapOf<String, Boolean>()
|
||||
return pending.filter { entry ->
|
||||
readyByClient.getOrPut(entry.clientCode.lowercase()) {
|
||||
isClientSessionReadyForUpload(context, entry.clientCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun filterUploadReadyScoringReviews(
|
||||
context: Context,
|
||||
pending: List<com.tomhempel.rbarometer.data.CoachScoringReview>,
|
||||
): List<com.tomhempel.rbarometer.data.CoachScoringReview> {
|
||||
if (pending.isEmpty()) return pending
|
||||
val readyByClient = mutableMapOf<String, Boolean>()
|
||||
return pending.filter { review ->
|
||||
readyByClient.getOrPut(review.clientCode.lowercase()) {
|
||||
isClientSessionReadyForUpload(context, review.clientCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,7 @@ import com.tomhempel.rbarometer.network.TokenStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.tomhempel.rbarometer.questionnaire.ConditionJsonParser
|
||||
@ -36,6 +37,8 @@ import com.tomhempel.rbarometer.questionnaire.QuestionnaireBase
|
||||
import com.tomhempel.rbarometer.questionnaire.QuestionnaireAllInOne
|
||||
import com.tomhempel.rbarometer.questionnaire.QuestionnaireGeneric
|
||||
import com.tomhempel.rbarometer.questionnaire.QuestionnaireValidation
|
||||
import com.tomhempel.rbarometer.session.SessionProgramHelper
|
||||
import com.tomhempel.rbarometer.session.SessionCompletionResult
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
@ -89,6 +92,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
private var loadHandler: LoadButtonHandler? = null
|
||||
private var refreshJob: Job? = null
|
||||
private var nextStepJob: Job? = null
|
||||
private var sessionViewsJob: Job? = null
|
||||
private var spinnerReady = false
|
||||
private var suppressLanguageCallback = false
|
||||
private var firstClientLoadDone = false
|
||||
@ -113,63 +117,8 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
UPLOAD,
|
||||
}
|
||||
|
||||
private data class TodaySession(
|
||||
val clientCode: String,
|
||||
val status: String,
|
||||
val cycle: Int,
|
||||
val session: Int,
|
||||
val totalSessions: Int,
|
||||
val plannedDate: String,
|
||||
val lastSession: String,
|
||||
val nextSuggestedDate: String,
|
||||
val measures: List<String>,
|
||||
)
|
||||
|
||||
private val dummyTodaySessions = listOf(
|
||||
TodaySession(
|
||||
clientCode = "A102",
|
||||
status = "Today",
|
||||
cycle = 3,
|
||||
session = 2,
|
||||
totalSessions = 6,
|
||||
plannedDate = "Today 14:00",
|
||||
lastSession = "Jun 17",
|
||||
nextSuggestedDate = "Jul 01",
|
||||
measures = listOf("WHO-5", "Stress thermometer", "Relationship check"),
|
||||
),
|
||||
TodaySession(
|
||||
clientCode = "B447",
|
||||
status = "Overdue",
|
||||
cycle = 1,
|
||||
session = 5,
|
||||
totalSessions = 6,
|
||||
plannedDate = "Jun 18",
|
||||
lastSession = "Jun 10",
|
||||
nextSuggestedDate = "Jun 26",
|
||||
measures = listOf("Mood check", "Goal tracking", "Counselor score review"),
|
||||
),
|
||||
TodaySession(
|
||||
clientCode = "C083",
|
||||
status = "Upcoming",
|
||||
cycle = 2,
|
||||
session = 1,
|
||||
totalSessions = 6,
|
||||
plannedDate = "Tomorrow",
|
||||
lastSession = "Jun 21",
|
||||
nextSuggestedDate = "Jul 08",
|
||||
measures = listOf("Cycle baseline", "WHO-5", "Stress thermometer"),
|
||||
),
|
||||
TodaySession(
|
||||
clientCode = "D219",
|
||||
status = "Inactive",
|
||||
cycle = 4,
|
||||
session = 6,
|
||||
totalSessions = 6,
|
||||
plannedDate = "Not planned",
|
||||
lastSession = "Jun 03",
|
||||
nextSuggestedDate = "On hold",
|
||||
measures = listOf("End-of-cycle review", "Participation status"),
|
||||
),
|
||||
private data class ClientSessionCard(
|
||||
val view: SessionProgramHelper.ClientSessionView,
|
||||
)
|
||||
|
||||
private fun color(@ColorRes res: Int) = ContextCompat.getColor(activity, res)
|
||||
@ -233,6 +182,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
/** Updates client picker after a background assigned-clients sync (single UI reload). */
|
||||
fun onAssignedClientsSynced() {
|
||||
refreshClientSpinner()
|
||||
renderTodaySessions()
|
||||
if (selectedClientCode.isNotBlank() && isClientAssigned(selectedClientCode)) {
|
||||
refreshLoadedClientUi()
|
||||
}
|
||||
@ -241,6 +191,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
fun init() {
|
||||
refreshJob?.cancel()
|
||||
nextStepJob?.cancel()
|
||||
sessionViewsJob?.cancel()
|
||||
activity.bumpAssignedClientsRefreshGeneration()
|
||||
firstClientLoadDone = false
|
||||
afterFirstClientLoadCallbacks.clear()
|
||||
@ -251,7 +202,14 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
bindViews()
|
||||
loadQuestionnaireOrder()
|
||||
renderTodaySessions()
|
||||
createQuestionnaireButtons()
|
||||
if (assignedClientsForUi().isEmpty()) {
|
||||
createQuestionnaireButtons()
|
||||
} else {
|
||||
buttonContainer.removeAllViews()
|
||||
dynamicButtons.clear()
|
||||
questionnaireFiles.clear()
|
||||
cardParts.clear()
|
||||
}
|
||||
setupCoachDisplay()
|
||||
setupLanguageSpinner()
|
||||
setupLoadHandler()
|
||||
@ -271,6 +229,25 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Lightweight refresh when returning from a questionnaire or upload — avoids full layout rebuild. */
|
||||
fun refreshOpeningUi() {
|
||||
if (!isOpeningScreenVisible()) {
|
||||
init()
|
||||
return
|
||||
}
|
||||
updateStatusStrip()
|
||||
updateQuestionnaireSectionVisibility()
|
||||
renderTodaySessions()
|
||||
refreshPendingUploadBadge()
|
||||
updateSessionMetrics()
|
||||
if (assignedClientsForUi().isEmpty()) {
|
||||
applyUpdateButtonTexts(force = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isOpeningScreenVisible(): Boolean =
|
||||
activity.findViewById<View?>(R.id.todaySessionsContainer) != null
|
||||
|
||||
private fun bindViews() {
|
||||
headerWelcome = activity.findViewById(R.id.headerWelcome)
|
||||
coachNameDisplay = activity.findViewById(R.id.coachNameDisplay)
|
||||
@ -338,7 +315,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
sessionMetricMeasuresValue.text = questionnaireEntries.size.toString()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val count = withContext(Dispatchers.IO) {
|
||||
PendingUploadRepository.countAll()
|
||||
PendingUploadRepository.countUploadReady(activity)
|
||||
}
|
||||
if (::sessionMetricSyncValue.isInitialized) {
|
||||
sessionMetricSyncValue.text = if (count > 0) count.toString() else "OK"
|
||||
@ -349,24 +326,71 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
private fun renderTodaySessions() {
|
||||
if (!::todaySessionsContainer.isInitialized) return
|
||||
todaySessionsTitle.text = hubFallback("Sitzungen", "Sessions")
|
||||
val dueCount = dummyTodaySessions.count { it.status == "Today" || it.status == "Overdue" }
|
||||
todaySessionsSummary.text = "$dueCount today"
|
||||
todaySessionsContainer.removeAllViews()
|
||||
addSessionGroup(
|
||||
title = hubFallback("Heute", "Today"),
|
||||
sessions = dummyTodaySessions.filter { it.status == "Today" || it.status == "Overdue" },
|
||||
)
|
||||
addSessionGroup(
|
||||
title = hubFallback("Demnächst", "Upcoming"),
|
||||
sessions = dummyTodaySessions.filter { it.status == "Upcoming" },
|
||||
)
|
||||
addSessionGroup(
|
||||
title = hubFallback("Inaktiv", "Inactive"),
|
||||
sessions = dummyTodaySessions.filter { it.status == "Inactive" },
|
||||
)
|
||||
sessionViewsJob?.cancel()
|
||||
sessionViewsJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
val views = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
SessionProgramHelper.buildClientSessionViews(activity)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "renderTodaySessions failed", e)
|
||||
emptyList()
|
||||
}
|
||||
if (!isActive) return@launch
|
||||
bindTodaySessions(views)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSessionGroup(title: String, sessions: List<TodaySession>) {
|
||||
private fun bindTodaySessions(views: List<SessionProgramHelper.ClientSessionView>) {
|
||||
val ready = views.filter {
|
||||
SessionProgramHelper.sessionDisplayBucket(it) == SessionProgramHelper.SessionDisplayBucket.READY
|
||||
}
|
||||
val inProgress = views.filter {
|
||||
SessionProgramHelper.sessionDisplayBucket(it) == SessionProgramHelper.SessionDisplayBucket.IN_PROGRESS
|
||||
}
|
||||
val waitingUpload = views.filter {
|
||||
SessionProgramHelper.sessionDisplayBucket(it) == SessionProgramHelper.SessionDisplayBucket.WAITING_UPLOAD
|
||||
}
|
||||
val complete = views.filter {
|
||||
SessionProgramHelper.sessionDisplayBucket(it) == SessionProgramHelper.SessionDisplayBucket.COMPLETE
|
||||
}
|
||||
val dueCount = ready.size
|
||||
todaySessionsSummary.text = if (dueCount == 1) {
|
||||
hubFallback("1 Sitzung bereit", "1 session ready")
|
||||
} else {
|
||||
hubFallback("$dueCount Sitzungen bereit", "$dueCount sessions ready")
|
||||
}
|
||||
todaySessionsContainer.removeAllViews()
|
||||
addSessionGroup(
|
||||
title = hubFallback("Bereit", "Ready"),
|
||||
sessions = ready.map { ClientSessionCard(it) },
|
||||
)
|
||||
addSessionGroup(
|
||||
title = hubFallback("In Bearbeitung", "In progress"),
|
||||
sessions = inProgress.map { ClientSessionCard(it) },
|
||||
)
|
||||
addSessionGroup(
|
||||
title = hubFallback("Warte auf Upload", "Waiting for upload"),
|
||||
sessions = waitingUpload.map { ClientSessionCard(it) },
|
||||
)
|
||||
addSessionGroup(
|
||||
title = hubFallback("Abgeschlossen", "Complete"),
|
||||
sessions = complete.map { ClientSessionCard(it) },
|
||||
)
|
||||
if (views.isEmpty()) {
|
||||
todaySessionsContainer.addView(TextView(activity).apply {
|
||||
text = hubFallback(
|
||||
"Keine Sitzungsdaten — bitte synchronisieren.",
|
||||
"No session data — please sync.",
|
||||
)
|
||||
setPadding(dp(2), dp(8), 0, dp(4))
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
|
||||
setTextColor(color(R.color.brand_text_muted))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSessionGroup(title: String, sessions: List<ClientSessionCard>) {
|
||||
if (sessions.isEmpty()) return
|
||||
todaySessionsContainer.addView(TextView(activity).apply {
|
||||
text = title
|
||||
@ -380,7 +404,9 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTodaySessionCard(session: TodaySession): View {
|
||||
private fun createTodaySessionCard(session: ClientSessionCard): View {
|
||||
val view = session.view
|
||||
val progress = view.progress
|
||||
val card = MaterialCardView(activity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
@ -389,8 +415,8 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
it.setMargins(0, 0, 0, dp(10))
|
||||
}
|
||||
radius = dp(14).toFloat()
|
||||
strokeWidth = dp(if (session.status == "Today") 2 else 1)
|
||||
setStrokeColor(color(if (session.status == "Overdue") R.color.brand_chip_start_fill else R.color.brand_purple))
|
||||
strokeWidth = dp(if (view.canEnterSession) 2 else 1)
|
||||
setStrokeColor(color(if (view.canEnterSession) R.color.brand_purple else R.color.brand_header_stroke))
|
||||
setCardBackgroundColor(color(R.color.brand_header_surface))
|
||||
cardElevation = dp(2).toFloat()
|
||||
}
|
||||
@ -407,116 +433,173 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
}
|
||||
titleBlock.addView(TextView(activity).apply {
|
||||
text = session.clientCode
|
||||
text = view.clientCode
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
setTextColor(color(R.color.brand_text_dark))
|
||||
})
|
||||
titleBlock.addView(TextView(activity).apply {
|
||||
text = "Cycle ${session.cycle} - Session ${session.session} of ${session.totalSessions}"
|
||||
text = hubFallback(
|
||||
"Zyklus ${progress.cycleNumber} · Sitzung ${progress.currentSessionNumber ?: "—"} von ${progress.totalSessions}",
|
||||
"Cycle ${progress.cycleNumber} · Session ${progress.currentSessionNumber ?: "—"} of ${progress.totalSessions}",
|
||||
)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
|
||||
setTextColor(color(R.color.brand_text_muted))
|
||||
})
|
||||
header.addView(titleBlock)
|
||||
header.addView(sessionStatusChip(session.status))
|
||||
header.addView(sessionStatusChip(view))
|
||||
content.addView(header)
|
||||
|
||||
progress.currentSessionName?.takeIf { it.isNotBlank() }?.let { sessionName ->
|
||||
content.addView(TextView(activity).apply {
|
||||
text = sessionName
|
||||
setPadding(0, dp(10), 0, dp(8))
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f)
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
setTextColor(color(R.color.brand_text_dark))
|
||||
})
|
||||
}
|
||||
content.addView(TextView(activity).apply {
|
||||
text = "Planned: ${session.plannedDate} Last: ${session.lastSession}"
|
||||
setPadding(0, dp(10), 0, dp(8))
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
|
||||
setTextColor(color(R.color.brand_text_dark))
|
||||
})
|
||||
content.addView(TextView(activity).apply {
|
||||
text = hubFallback("Startet automatisch", "Starts automatically")
|
||||
text = hubFallback("Messpunkte in dieser Sitzung", "Measures in this session")
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
setTextColor(color(R.color.brand_text_muted))
|
||||
})
|
||||
session.measures.forEachIndexed { index, measure ->
|
||||
content.addView(measureRow(measure, checked = index == 0))
|
||||
view.measures.forEach { measure ->
|
||||
content.addView(
|
||||
SessionMeasureUi.measureRow(
|
||||
context = activity,
|
||||
label = measure.displayName,
|
||||
checked = measure.completedInCycle,
|
||||
),
|
||||
)
|
||||
}
|
||||
content.addView(scheduleNextDateBlock(session.nextSuggestedDate))
|
||||
|
||||
val actions = LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
setPadding(0, dp(12), 0, 0)
|
||||
if (view.canEnterSession) {
|
||||
val canResume = view.canResumeSession
|
||||
val editBeforeUpload = view.isSessionMeasuresComplete && view.hasPendingUpload
|
||||
val actions = LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
setPadding(0, dp(12), 0, 0)
|
||||
}
|
||||
actions.addView(
|
||||
todayAction(
|
||||
hubFallback(
|
||||
when {
|
||||
editBeforeUpload -> "Sitzung bearbeiten"
|
||||
canResume -> "Sitzung fortsetzen"
|
||||
else -> "Sitzung starten"
|
||||
},
|
||||
when {
|
||||
editBeforeUpload -> "Edit session"
|
||||
canResume -> "Continue session"
|
||||
else -> "Start session"
|
||||
},
|
||||
),
|
||||
filled = true,
|
||||
weight = 1f,
|
||||
) {
|
||||
startSessionForClient(view.clientCode)
|
||||
},
|
||||
)
|
||||
content.addView(actions)
|
||||
}
|
||||
actions.addView(todayAction("Manage", filled = false, weight = 1f) {
|
||||
showManageSessionSheet(session)
|
||||
})
|
||||
actions.addView(todayAction("Start session", filled = true, weight = 1.25f))
|
||||
content.addView(actions)
|
||||
card.addView(content)
|
||||
return card
|
||||
}
|
||||
|
||||
private fun sessionStatusChip(status: String): TextView =
|
||||
TextView(activity).apply {
|
||||
text = status
|
||||
private fun sessionStatusChip(view: SessionProgramHelper.ClientSessionView): TextView {
|
||||
val canResume = view.canResumeSession
|
||||
val (label, style) = when (SessionProgramHelper.sessionDisplayBucket(view)) {
|
||||
SessionProgramHelper.SessionDisplayBucket.COMPLETE ->
|
||||
hubFallback("Abgeschlossen", "Complete") to "complete"
|
||||
SessionProgramHelper.SessionDisplayBucket.WAITING_UPLOAD ->
|
||||
hubFallback("Warte auf Upload", "Waiting for upload") to "upload"
|
||||
SessionProgramHelper.SessionDisplayBucket.IN_PROGRESS ->
|
||||
hubFallback(
|
||||
if (canResume) "Pausiert" else "In Bearbeitung",
|
||||
if (canResume) "Paused" else "In progress",
|
||||
) to "waiting"
|
||||
SessionProgramHelper.SessionDisplayBucket.READY ->
|
||||
hubFallback("Bereit", "Ready") to "ready"
|
||||
}
|
||||
return TextView(activity).apply {
|
||||
text = label
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
setTextColor(color(if (status == "Upcoming" || status == "Inactive") R.color.brand_chip_locked_text else R.color.brand_text_on_dark))
|
||||
setTextColor(
|
||||
color(
|
||||
when (style) {
|
||||
"waiting" -> R.color.brand_chip_locked_text
|
||||
"upload" -> R.color.brand_text_on_dark
|
||||
else -> R.color.brand_text_on_dark
|
||||
},
|
||||
),
|
||||
)
|
||||
setBackgroundResource(
|
||||
when (status) {
|
||||
"Overdue" -> R.drawable.bg_chip_amber
|
||||
"Upcoming", "Inactive" -> R.drawable.bg_chip_locked
|
||||
when (style) {
|
||||
"complete" -> R.drawable.bg_chip_locked
|
||||
"waiting" -> R.drawable.bg_chip_locked
|
||||
"upload" -> R.drawable.bg_chip_amber
|
||||
else -> R.drawable.bg_chip_green
|
||||
},
|
||||
)
|
||||
setPadding(dp(10), dp(6), dp(10), dp(6))
|
||||
}
|
||||
}
|
||||
|
||||
private fun measureRow(label: String, checked: Boolean): View =
|
||||
LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
setPadding(0, dp(7), 0, 0)
|
||||
addView(TextView(activity).apply {
|
||||
text = if (checked) "[x]" else "[ ]"
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
setTextColor(color(if (checked) R.color.brand_purple else R.color.brand_text_muted))
|
||||
})
|
||||
addView(TextView(activity).apply {
|
||||
text = label
|
||||
setPadding(dp(8), 0, 0, 0)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
|
||||
setTextColor(color(R.color.brand_text_dark))
|
||||
})
|
||||
private fun startSessionForClient(clientCode: String) {
|
||||
if (!isClientAssigned(clientCode)) {
|
||||
Toast.makeText(activity, t("no_profile"), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
selectClient(clientCode, userInitiated = false)
|
||||
GlobalValues.LOADED_CLIENT_CODE = clientCode
|
||||
activity.startSessionFlow(clientCode, languageID)
|
||||
}
|
||||
|
||||
fun showSessionCompletionDialog(result: SessionCompletionResult) {
|
||||
selectClient(result.clientCode, userInitiated = false)
|
||||
GlobalValues.LAST_CLIENT_CODE = result.clientCode
|
||||
GlobalValues.LOADED_CLIENT_CODE = result.clientCode
|
||||
|
||||
val title = hubFallback("Sitzung abgeschlossen", "Session complete")
|
||||
val message = when {
|
||||
result.sessionName.isNotBlank() && result.canUpload ->
|
||||
hubFallback(
|
||||
"${result.sessionName} ist abgeschlossen. Sie können die Daten jetzt hochladen oder vor dem Upload noch bearbeiten.",
|
||||
"${result.sessionName} is complete. You can upload now or edit before uploading.",
|
||||
)
|
||||
result.sessionName.isNotBlank() ->
|
||||
hubFallback(
|
||||
"${result.sessionName} ist abgeschlossen.",
|
||||
"${result.sessionName} is complete.",
|
||||
)
|
||||
result.canUpload ->
|
||||
hubFallback(
|
||||
"Alle Messpunkte dieser Sitzung sind erledigt. Sie können die Daten jetzt hochladen oder vor dem Upload noch bearbeiten.",
|
||||
"All measures in this session are complete. You can upload now or edit before uploading.",
|
||||
)
|
||||
else ->
|
||||
hubFallback(
|
||||
"Alle Messpunkte dieser Sitzung sind erledigt.",
|
||||
"All measures in this session are complete.",
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleNextDateBlock(suggestedDate: String): View =
|
||||
LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
background = ContextCompat.getDrawable(activity, R.drawable.bg_session_metric)
|
||||
setPadding(dp(12), dp(10), dp(12), dp(10))
|
||||
val lp = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
lp.setMargins(0, dp(12), 0, 0)
|
||||
layoutParams = lp
|
||||
addView(TextView(activity).apply {
|
||||
text = hubFallback("Nächsten Termin planen", "Schedule next date")
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f)
|
||||
setTextColor(color(R.color.brand_text_muted))
|
||||
})
|
||||
val row = LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
setPadding(0, dp(4), 0, 0)
|
||||
}
|
||||
row.addView(TextView(activity).apply {
|
||||
text = suggestedDate
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
setTextColor(color(R.color.brand_text_dark))
|
||||
})
|
||||
row.addView(todayAction("Edit date", filled = false, weight = 0f, fixedWidth = dp(96)))
|
||||
addView(row)
|
||||
}
|
||||
LogoutDialogs.showThreeAction(
|
||||
activity = activity,
|
||||
title = title,
|
||||
message = message,
|
||||
tone = LogoutDialogs.Tone.Info,
|
||||
primaryLabel = hubFallback("Hochladen", "Upload"),
|
||||
secondaryLabel = hubFallback("Bearbeiten", "Edit"),
|
||||
tertiaryLabel = hubFallback("Schließen", "Close"),
|
||||
showPrimary = result.canUpload,
|
||||
onPrimary = { openUploadOverview(syncAfterClose = true) },
|
||||
onSecondary = { startSessionForClient(result.clientCode) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun todayAction(
|
||||
label: String,
|
||||
@ -541,158 +624,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
setTextColor(color(if (filled) R.color.brand_text_on_dark else R.color.brand_purple))
|
||||
backgroundTintList = ColorStateList.valueOf(color(if (filled) R.color.brand_purple else R.color.brand_header_surface))
|
||||
strokeColor = ColorStateList.valueOf(color(R.color.brand_purple))
|
||||
setOnClickListener {
|
||||
if (onClick != null) onClick()
|
||||
else Toast.makeText(activity, "$label is prototype-only", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showManageSessionSheet(session: TodaySession) {
|
||||
val container = LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setBackgroundColor(color(R.color.brand_surface))
|
||||
setPadding(dp(20), dp(18), dp(20), dp(18))
|
||||
}
|
||||
|
||||
val header = LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
}
|
||||
val titleBlock = LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
}
|
||||
titleBlock.addView(TextView(activity).apply {
|
||||
text = "Manage ${session.clientCode}"
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
setTextColor(color(R.color.brand_text_dark))
|
||||
})
|
||||
titleBlock.addView(TextView(activity).apply {
|
||||
text = "Cycle ${session.cycle} - Session ${session.session} of ${session.totalSessions}"
|
||||
setPadding(0, dp(3), 0, 0)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
|
||||
setTextColor(color(R.color.brand_text_muted))
|
||||
})
|
||||
val closeButton = MaterialButton(activity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(dp(78), dp(40))
|
||||
text = "Close"
|
||||
isAllCaps = false
|
||||
cornerRadius = dp(18)
|
||||
strokeWidth = dp(1)
|
||||
insetTop = 0
|
||||
insetBottom = 0
|
||||
setTextColor(color(R.color.brand_purple))
|
||||
backgroundTintList = ColorStateList.valueOf(color(R.color.brand_header_surface))
|
||||
strokeColor = ColorStateList.valueOf(color(R.color.brand_header_stroke))
|
||||
}
|
||||
header.addView(titleBlock)
|
||||
header.addView(closeButton)
|
||||
|
||||
val body = LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(0, dp(14), 0, 0)
|
||||
}
|
||||
body.addView(manageSectionTitle("Client status"))
|
||||
body.addView(LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
addView(manageStatusButton("Active", selected = session.status != "Inactive"))
|
||||
addView(manageStatusButton("Inactive", selected = session.status == "Inactive"))
|
||||
addView(manageStatusButton("Archived", selected = false))
|
||||
})
|
||||
body.addView(manageSectionTitle("Scheduling"))
|
||||
body.addView(manageAction("Edit next planned date", "Current suggestion: ${session.nextSuggestedDate}"))
|
||||
body.addView(manageAction("Reschedule current session", "Move this session to another date"))
|
||||
body.addView(manageAction("Skip this occurrence", "Keep the cycle but omit this planned visit"))
|
||||
body.addView(manageSectionTitle("Session"))
|
||||
body.addView(manageAction("View session protocol", "${session.measures.size} measures assigned"))
|
||||
body.addView(manageAction("Add counselor note", "Attach a local note to this client"))
|
||||
body.addView(manageAction("Sync client data", "Review pending local data for this client"))
|
||||
body.addView(manageSectionTitle("Lifecycle"))
|
||||
body.addView(manageAction("Mark no longer participating", "Move client to inactive"))
|
||||
body.addView(manageAction("Archive client", "Hide from active scheduling"))
|
||||
body.addView(manageAction("Delete client", "Prototype-only destructive option", destructive = true))
|
||||
|
||||
val scroll = ScrollView(activity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
dp(430),
|
||||
)
|
||||
isFillViewport = false
|
||||
addView(body)
|
||||
}
|
||||
|
||||
container.addView(header)
|
||||
container.addView(scroll)
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(activity)
|
||||
.setView(container)
|
||||
.create()
|
||||
closeButton.setOnClickListener { dialog.dismiss() }
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun manageSectionTitle(title: String): View =
|
||||
TextView(activity).apply {
|
||||
text = title
|
||||
setPadding(dp(2), dp(14), 0, dp(7))
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
setTextColor(color(R.color.brand_text_muted))
|
||||
}
|
||||
|
||||
private fun manageStatusButton(label: String, selected: Boolean): MaterialButton =
|
||||
MaterialButton(activity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(0, dp(40), 1f).also {
|
||||
it.setMargins(0, 0, dp(8), 0)
|
||||
}
|
||||
text = label
|
||||
isAllCaps = false
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
|
||||
cornerRadius = dp(18)
|
||||
strokeWidth = dp(1)
|
||||
insetTop = 0
|
||||
insetBottom = 0
|
||||
setTextColor(color(if (selected) R.color.brand_text_on_dark else R.color.brand_text_dark))
|
||||
backgroundTintList = ColorStateList.valueOf(
|
||||
color(if (selected) R.color.brand_purple else R.color.brand_header_surface),
|
||||
)
|
||||
strokeColor = ColorStateList.valueOf(color(R.color.brand_purple))
|
||||
setOnClickListener {
|
||||
Toast.makeText(activity, "Set status to $label is prototype-only", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun manageAction(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
destructive: Boolean = false,
|
||||
): View =
|
||||
MaterialButton(activity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
).also { it.setMargins(0, 0, 0, dp(8)) }
|
||||
text = "$title\n$subtitle"
|
||||
isAllCaps = false
|
||||
gravity = Gravity.START or Gravity.CENTER_VERTICAL
|
||||
textAlignment = View.TEXT_ALIGNMENT_VIEW_START
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
|
||||
cornerRadius = dp(16)
|
||||
strokeWidth = dp(1)
|
||||
insetTop = 0
|
||||
insetBottom = 0
|
||||
setPadding(dp(14), dp(11), dp(14), dp(11))
|
||||
setTextColor(color(if (destructive) R.color.brand_destructive_text else R.color.brand_text_dark))
|
||||
backgroundTintList = ColorStateList.valueOf(
|
||||
color(if (destructive) R.color.brand_destructive_fill else R.color.brand_header_surface),
|
||||
)
|
||||
strokeColor = ColorStateList.valueOf(
|
||||
color(if (destructive) R.color.brand_destructive_fill else R.color.brand_header_stroke),
|
||||
)
|
||||
setOnClickListener {
|
||||
Toast.makeText(activity, "$title is prototype-only", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
setOnClickListener { onClick?.invoke() }
|
||||
}
|
||||
|
||||
private fun loadQuestionnaireOrder() {
|
||||
@ -1126,6 +1058,13 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
}
|
||||
pendingUploadDialogShowing = true
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val uploadReady = withContext(Dispatchers.IO) {
|
||||
PendingUploadRepository.hasUploadReady(activity)
|
||||
}
|
||||
if (!uploadReady) {
|
||||
pendingUploadDialogShowing = false
|
||||
return@launch
|
||||
}
|
||||
val summary = withContext(Dispatchers.IO) { UnuploadedWorkRepository.summarize() }
|
||||
val details = SessionLogout.formatPendingDetails(languageID, summary)
|
||||
LogoutDialogs.showAction(
|
||||
@ -1171,6 +1110,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
applyUpdateButtonTexts(force = true)
|
||||
updateStatusStrip()
|
||||
updateSessionMetrics()
|
||||
renderTodaySessions()
|
||||
updateQuestionnaireSectionVisibility()
|
||||
refreshLoadedClientUi()
|
||||
}
|
||||
@ -1398,11 +1338,13 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
if (!::uploadButton.isInitialized) return
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val count = withContext(Dispatchers.IO) {
|
||||
PendingUploadRepository.countAll()
|
||||
PendingUploadRepository.countUploadReady(activity)
|
||||
}
|
||||
val base = t("upload")
|
||||
uploadButton.text = if (count > 0) "$base ($count)" else base
|
||||
uploadButton.contentDescription = uploadButton.text
|
||||
uploadButton.isEnabled = true
|
||||
uploadButton.alpha = if (count > 0) 1f else 0.55f
|
||||
if (::sessionMetricSyncValue.isInitialized) {
|
||||
sessionMetricSyncValue.text = if (count > 0) count.toString() else "OK"
|
||||
}
|
||||
@ -1646,7 +1588,23 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
openUploadOverview(token = existingToken!!, syncAfterClose = true)
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val readyCount = withContext(Dispatchers.IO) {
|
||||
PendingUploadRepository.countUploadReady(activity)
|
||||
}
|
||||
if (readyCount == 0) {
|
||||
android.widget.Toast.makeText(
|
||||
activity,
|
||||
hubFallback(
|
||||
"Upload ist erst möglich, wenn alle Messpunkte der Sitzung abgeschlossen sind.",
|
||||
"Upload is only available after all measures in the session are complete.",
|
||||
),
|
||||
android.widget.Toast.LENGTH_LONG,
|
||||
).show()
|
||||
return@launch
|
||||
}
|
||||
openUploadOverview(token = existingToken!!, syncAfterClose = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1671,7 +1629,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
AppLanguageStore.set(activity, newLang)
|
||||
},
|
||||
onClose = { uploaded ->
|
||||
init()
|
||||
refreshOpeningUi()
|
||||
refreshPendingUploadBadge()
|
||||
if (uploaded && syncAfterClose && NetworkUtils.isOnline(activity) && TokenStore.isSessionValid(activity)) {
|
||||
TokenStore.getToken(activity)?.takeIf { it.isNotBlank() }?.let {
|
||||
|
||||
@ -39,7 +39,9 @@ object LogoutDialogs {
|
||||
details = details,
|
||||
primaryLabel = msg(languageId, "ok", "OK"),
|
||||
secondaryLabel = null,
|
||||
tertiaryLabel = null,
|
||||
onPrimary = null,
|
||||
onSecondary = null,
|
||||
)
|
||||
}
|
||||
|
||||
@ -60,7 +62,9 @@ object LogoutDialogs {
|
||||
details = null,
|
||||
primaryLabel = msg(languageId, "logout_confirm_action", msg(languageId, "logout", "Log out")),
|
||||
secondaryLabel = msg(languageId, "cancel", "Cancel"),
|
||||
tertiaryLabel = null,
|
||||
onPrimary = onConfirm,
|
||||
onSecondary = null,
|
||||
)
|
||||
}
|
||||
|
||||
@ -83,10 +87,53 @@ object LogoutDialogs {
|
||||
details = details,
|
||||
primaryLabel = primaryLabel,
|
||||
secondaryLabel = secondaryLabel,
|
||||
tertiaryLabel = null,
|
||||
onPrimary = onPrimary,
|
||||
onSecondary = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun showThreeAction(
|
||||
activity: AppCompatActivity,
|
||||
title: String,
|
||||
message: String,
|
||||
tone: Tone = Tone.Info,
|
||||
primaryLabel: String,
|
||||
secondaryLabel: String,
|
||||
tertiaryLabel: String,
|
||||
showPrimary: Boolean = true,
|
||||
onPrimary: () -> Unit,
|
||||
onSecondary: () -> Unit,
|
||||
) {
|
||||
if (showPrimary) {
|
||||
show(
|
||||
activity = activity,
|
||||
tone = tone,
|
||||
title = title,
|
||||
message = message,
|
||||
details = null,
|
||||
primaryLabel = primaryLabel,
|
||||
secondaryLabel = secondaryLabel,
|
||||
tertiaryLabel = tertiaryLabel,
|
||||
onPrimary = onPrimary,
|
||||
onSecondary = onSecondary,
|
||||
)
|
||||
} else {
|
||||
show(
|
||||
activity = activity,
|
||||
tone = tone,
|
||||
title = title,
|
||||
message = message,
|
||||
details = null,
|
||||
primaryLabel = secondaryLabel,
|
||||
secondaryLabel = tertiaryLabel,
|
||||
tertiaryLabel = null,
|
||||
onPrimary = onSecondary,
|
||||
onSecondary = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun show(
|
||||
activity: AppCompatActivity,
|
||||
tone: Tone,
|
||||
@ -95,7 +142,9 @@ object LogoutDialogs {
|
||||
details: String?,
|
||||
primaryLabel: String,
|
||||
secondaryLabel: String?,
|
||||
tertiaryLabel: String?,
|
||||
onPrimary: (() -> Unit)?,
|
||||
onSecondary: (() -> Unit)?,
|
||||
) {
|
||||
val dialog = Dialog(activity)
|
||||
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
@ -109,7 +158,10 @@ object LogoutDialogs {
|
||||
val detailsScroll = root.findViewById<NestedScrollView>(R.id.dialogDetailsScroll)
|
||||
val detailsView = root.findViewById<TextView>(R.id.dialogDetails)
|
||||
val btnPrimary = root.findViewById<MaterialButton>(R.id.dialogBtnPrimary)
|
||||
val btnPrimaryInline = root.findViewById<MaterialButton>(R.id.dialogBtnPrimaryInline)
|
||||
val btnSecondary = root.findViewById<MaterialButton>(R.id.dialogBtnSecondary)
|
||||
val btnTertiary = root.findViewById<MaterialButton>(R.id.dialogBtnTertiary)
|
||||
val buttonRow = root.findViewById<ViewGroup>(R.id.dialogButtonRow)
|
||||
|
||||
when (tone) {
|
||||
Tone.Info -> {
|
||||
@ -152,37 +204,71 @@ object LogoutDialogs {
|
||||
detailsScroll.visibility = View.GONE
|
||||
}
|
||||
|
||||
btnPrimary.text = primaryLabel
|
||||
if (tone == Tone.Logout) {
|
||||
btnPrimary.backgroundTintList = ColorStateList.valueOf(
|
||||
ContextCompat.getColor(activity, R.color.brand_destructive_text),
|
||||
)
|
||||
btnPrimary.setTextColor(ContextCompat.getColor(activity, R.color.white))
|
||||
} else {
|
||||
btnPrimary.backgroundTintList = ColorStateList.valueOf(
|
||||
ContextCompat.getColor(activity, R.color.brand_purple),
|
||||
)
|
||||
btnPrimary.setTextColor(ContextCompat.getColor(activity, R.color.white))
|
||||
val layoutMode = when {
|
||||
tertiaryLabel != null && secondaryLabel != null -> DialogButtonLayout.TRIPLE
|
||||
secondaryLabel != null -> DialogButtonLayout.PAIR
|
||||
else -> DialogButtonLayout.SINGLE
|
||||
}
|
||||
|
||||
btnPrimary.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
onPrimary?.invoke()
|
||||
}
|
||||
|
||||
if (secondaryLabel != null) {
|
||||
btnSecondary.visibility = View.VISIBLE
|
||||
btnSecondary.text = secondaryLabel
|
||||
btnSecondary.setOnClickListener { dialog.dismiss() }
|
||||
// Safer on small screens: cancel above the destructive / primary action.
|
||||
(btnPrimary.parent as? ViewGroup)?.let { buttonRow ->
|
||||
buttonRow.removeView(btnPrimary)
|
||||
buttonRow.removeView(btnSecondary)
|
||||
buttonRow.addView(btnSecondary)
|
||||
buttonRow.addView(btnPrimary)
|
||||
fun stylePrimaryButton(button: MaterialButton) {
|
||||
if (tone == Tone.Logout) {
|
||||
button.backgroundTintList = ColorStateList.valueOf(
|
||||
ContextCompat.getColor(activity, R.color.brand_destructive_text),
|
||||
)
|
||||
button.setTextColor(ContextCompat.getColor(activity, R.color.white))
|
||||
} else {
|
||||
button.backgroundTintList = ColorStateList.valueOf(
|
||||
ContextCompat.getColor(activity, R.color.brand_purple),
|
||||
)
|
||||
button.setTextColor(ContextCompat.getColor(activity, R.color.white))
|
||||
}
|
||||
}
|
||||
|
||||
fun bindPrimaryButton(button: MaterialButton) {
|
||||
button.text = primaryLabel
|
||||
stylePrimaryButton(button)
|
||||
button.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
onPrimary?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
when (layoutMode) {
|
||||
DialogButtonLayout.SINGLE -> {
|
||||
btnPrimary.visibility = View.VISIBLE
|
||||
buttonRow.visibility = View.GONE
|
||||
bindPrimaryButton(btnPrimary)
|
||||
}
|
||||
DialogButtonLayout.PAIR -> {
|
||||
btnPrimary.visibility = View.GONE
|
||||
buttonRow.visibility = View.VISIBLE
|
||||
btnSecondary.visibility = View.VISIBLE
|
||||
btnTertiary.visibility = View.GONE
|
||||
btnPrimaryInline.visibility = View.VISIBLE
|
||||
|
||||
btnSecondary.text = secondaryLabel
|
||||
btnSecondary.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
onSecondary?.invoke()
|
||||
}
|
||||
bindPrimaryButton(btnPrimaryInline)
|
||||
}
|
||||
DialogButtonLayout.TRIPLE -> {
|
||||
btnPrimary.visibility = View.VISIBLE
|
||||
buttonRow.visibility = View.VISIBLE
|
||||
btnSecondary.visibility = View.VISIBLE
|
||||
btnTertiary.visibility = View.VISIBLE
|
||||
btnPrimaryInline.visibility = View.GONE
|
||||
|
||||
bindPrimaryButton(btnPrimary)
|
||||
btnSecondary.text = secondaryLabel
|
||||
btnSecondary.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
onSecondary?.invoke()
|
||||
}
|
||||
btnTertiary.text = tertiaryLabel
|
||||
btnTertiary.setOnClickListener { dialog.dismiss() }
|
||||
}
|
||||
} else {
|
||||
btnSecondary.visibility = View.GONE
|
||||
}
|
||||
|
||||
dialog.setContentView(root)
|
||||
@ -205,6 +291,12 @@ object LogoutDialogs {
|
||||
})
|
||||
}
|
||||
|
||||
private enum class DialogButtonLayout {
|
||||
SINGLE,
|
||||
PAIR,
|
||||
TRIPLE,
|
||||
}
|
||||
|
||||
private fun msg(languageId: String, key: String, fallback: String): String =
|
||||
SessionLogout.message(languageId, key, fallback)
|
||||
}
|
||||
|
||||
@ -0,0 +1,179 @@
|
||||
package com.tomhempel.rbarometer.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.tomhempel.rbarometer.R
|
||||
import com.tomhempel.rbarometer.utils.ViewUtils
|
||||
|
||||
object SessionMeasureUi {
|
||||
|
||||
fun indicatorSymbol(checked: Boolean, isCurrent: Boolean = false): String = when {
|
||||
checked -> "\u2713"
|
||||
isCurrent -> "\u2022"
|
||||
else -> "\u25CB"
|
||||
}
|
||||
|
||||
fun measureRow(
|
||||
context: Context,
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
isCurrent: Boolean = false,
|
||||
verticalPaddingDp: Int = 7,
|
||||
labelTextSizeSp: Float = 14f,
|
||||
highlightCurrentRow: Boolean = false,
|
||||
selectable: Boolean = false,
|
||||
onSelect: (() -> Unit)? = null,
|
||||
statusText: String? = null,
|
||||
): View {
|
||||
if (highlightCurrentRow) {
|
||||
return cardMeasureRow(
|
||||
context = context,
|
||||
label = label,
|
||||
checked = checked,
|
||||
isCurrent = isCurrent,
|
||||
selectable = selectable,
|
||||
onSelect = onSelect,
|
||||
statusText = statusText,
|
||||
)
|
||||
}
|
||||
return compactMeasureRow(
|
||||
context = context,
|
||||
label = label,
|
||||
checked = checked,
|
||||
isCurrent = isCurrent,
|
||||
verticalPaddingDp = verticalPaddingDp,
|
||||
labelTextSizeSp = labelTextSizeSp,
|
||||
)
|
||||
}
|
||||
|
||||
private fun cardMeasureRow(
|
||||
context: Context,
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
isCurrent: Boolean,
|
||||
selectable: Boolean,
|
||||
onSelect: (() -> Unit)?,
|
||||
statusText: String?,
|
||||
): View {
|
||||
val root = LayoutInflater.from(context).inflate(R.layout.item_session_measure_row, null, false)
|
||||
val card = root.findViewById<MaterialCardView>(R.id.sessionMeasureCard)
|
||||
val indicatorWrap = root.findViewById<FrameLayout>(R.id.sessionMeasureIndicatorWrap)
|
||||
val indicator = root.findViewById<TextView>(R.id.sessionMeasureIndicator)
|
||||
val title = root.findViewById<TextView>(R.id.sessionMeasureTitle)
|
||||
val status = root.findViewById<TextView>(R.id.sessionMeasureStatus)
|
||||
|
||||
title.text = label
|
||||
indicator.text = indicatorSymbol(checked, isCurrent)
|
||||
|
||||
when {
|
||||
isCurrent && checked -> {
|
||||
card.setCardBackgroundColor(color(context, R.color.brand_questionnaire_highlight_bg))
|
||||
card.strokeWidth = ViewUtils.dp(context, 2)
|
||||
card.setStrokeColor(ColorStateList.valueOf(color(context, R.color.brand_purple)))
|
||||
indicatorWrap.setBackgroundResource(R.drawable.bg_measure_row_done)
|
||||
indicator.setTextColor(color(context, R.color.brand_text_on_dark))
|
||||
}
|
||||
isCurrent -> {
|
||||
card.setCardBackgroundColor(color(context, R.color.brand_questionnaire_highlight_bg))
|
||||
card.strokeWidth = ViewUtils.dp(context, 2)
|
||||
card.setStrokeColor(ColorStateList.valueOf(color(context, R.color.brand_purple)))
|
||||
indicatorWrap.setBackgroundResource(R.drawable.bg_measure_row_pending)
|
||||
indicator.setTextColor(color(context, R.color.brand_purple))
|
||||
}
|
||||
checked -> {
|
||||
card.setCardBackgroundColor(color(context, R.color.brand_header_surface))
|
||||
card.strokeWidth = ViewUtils.dp(context, 1)
|
||||
card.setStrokeColor(ColorStateList.valueOf(color(context, R.color.brand_purple)))
|
||||
indicatorWrap.setBackgroundResource(R.drawable.bg_measure_row_done)
|
||||
indicator.setTextColor(color(context, R.color.brand_text_on_dark))
|
||||
}
|
||||
else -> {
|
||||
card.setCardBackgroundColor(color(context, R.color.brand_header_surface))
|
||||
card.strokeWidth = ViewUtils.dp(context, 1)
|
||||
card.setStrokeColor(ColorStateList.valueOf(color(context, R.color.brand_card_stroke)))
|
||||
indicatorWrap.setBackgroundResource(R.drawable.bg_measure_row_pending)
|
||||
indicator.setTextColor(color(context, R.color.brand_text_muted))
|
||||
}
|
||||
}
|
||||
|
||||
if (statusText.isNullOrBlank()) {
|
||||
status.visibility = View.GONE
|
||||
} else {
|
||||
status.visibility = View.VISIBLE
|
||||
status.text = statusText
|
||||
status.setTextColor(
|
||||
color(
|
||||
context,
|
||||
when {
|
||||
isCurrent -> R.color.brand_purple
|
||||
checked -> R.color.brand_text_muted
|
||||
else -> R.color.brand_text_muted
|
||||
},
|
||||
),
|
||||
)
|
||||
if (isCurrent) status.setTypeface(status.typeface, android.graphics.Typeface.BOLD)
|
||||
}
|
||||
|
||||
if (selectable) {
|
||||
card.isClickable = true
|
||||
card.isFocusable = true
|
||||
val ripple = TypedValue()
|
||||
if (context.theme.resolveAttribute(android.R.attr.selectableItemBackground, ripple, true)) {
|
||||
card.foreground = ContextCompat.getDrawable(context, ripple.resourceId)
|
||||
}
|
||||
card.setOnClickListener { onSelect?.invoke() }
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
private fun compactMeasureRow(
|
||||
context: Context,
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
isCurrent: Boolean,
|
||||
verticalPaddingDp: Int,
|
||||
labelTextSizeSp: Float,
|
||||
): View = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
setPadding(0, ViewUtils.dp(context, verticalPaddingDp), 0, 0)
|
||||
val indicatorSize = ViewUtils.dp(context, 22)
|
||||
addView(TextView(context).apply {
|
||||
gravity = Gravity.CENTER
|
||||
text = indicatorSymbol(checked, isCurrent)
|
||||
minWidth = indicatorSize
|
||||
minHeight = indicatorSize
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, if (checked) 14f else 16f)
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
setTextColor(
|
||||
color(
|
||||
context,
|
||||
when {
|
||||
checked -> R.color.brand_purple
|
||||
isCurrent -> R.color.brand_purple
|
||||
else -> R.color.brand_text_muted
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
addView(TextView(context).apply {
|
||||
text = label
|
||||
setPadding(ViewUtils.dp(context, 8), 0, 0, 0)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, labelTextSizeSp)
|
||||
setLineSpacing(0f, 1.2f)
|
||||
setTextColor(color(context, R.color.brand_text_dark))
|
||||
if (isCurrent) setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
})
|
||||
}
|
||||
|
||||
private fun color(context: Context, res: Int) = ContextCompat.getColor(context, res)
|
||||
}
|
||||
48
app/src/main/java/com/tomhempel/rbarometer/ui/SessionUi.kt
Normal file
48
app/src/main/java/com/tomhempel/rbarometer/ui/SessionUi.kt
Normal file
@ -0,0 +1,48 @@
|
||||
package com.tomhempel.rbarometer.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.RadioButton
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.tomhempel.rbarometer.R
|
||||
|
||||
object SessionUi {
|
||||
|
||||
fun applyQuestionScreen(context: Context, root: View?) {
|
||||
if (root == null) return
|
||||
root.setBackgroundResource(R.drawable.bg_question_screen)
|
||||
|
||||
root.findViewById<TextView>(R.id.textView)?.let { tv ->
|
||||
TextViewCompat.setTextAppearance(tv, R.style.TextAppearance_Barometer_QuestionHint)
|
||||
tv.setLineSpacing(0f, 1.25f)
|
||||
}
|
||||
root.findViewById<TextView>(R.id.question)?.let { tv ->
|
||||
TextViewCompat.setTextAppearance(tv, R.style.TextAppearance_Barometer_QuestionTitle)
|
||||
tv.setLineSpacing(0f, 1.3f)
|
||||
}
|
||||
|
||||
root.findViewById<MaterialButton>(R.id.Qfinish)?.let { btn ->
|
||||
btn.setTextAppearance(R.style.TextAppearance_Barometer_PrimaryButton)
|
||||
btn.elevation = context.resources.getDimension(R.dimen.session_btn_elevation)
|
||||
}
|
||||
}
|
||||
|
||||
fun styleAnswerChoice(context: Context, radioButton: RadioButton) {
|
||||
val density = context.resources.displayMetrics.density
|
||||
val padH = (18 * density).toInt()
|
||||
val padV = (14 * density).toInt()
|
||||
radioButton.setPadding(padH, padV, padH, padV)
|
||||
radioButton.background = ContextCompat.getDrawable(context, R.drawable.bg_answer_choice)
|
||||
radioButton.buttonTintList = ColorStateList.valueOf(
|
||||
ContextCompat.getColor(context, R.color.brand_purple),
|
||||
)
|
||||
radioButton.setTextColor(ContextCompat.getColor(context, R.color.brand_text_dark))
|
||||
radioButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
||||
radioButton.letterSpacing = 0.01f
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@ import com.tomhempel.rbarometer.R
|
||||
import com.tomhempel.rbarometer.data.AnswerRepository
|
||||
import com.tomhempel.rbarometer.data.CoachScoringReviewRepository
|
||||
import com.tomhempel.rbarometer.data.CompletedQuestionnaireRepository
|
||||
import com.tomhempel.rbarometer.data.PendingUploadRepository
|
||||
import com.tomhempel.rbarometer.data.Question
|
||||
import com.tomhempel.rbarometer.network.DatabaseUploader
|
||||
import com.tomhempel.rbarometer.network.QuestionnaireCache
|
||||
@ -159,8 +160,9 @@ class UploadOverviewHandler(
|
||||
uploadButton.setOnClickListener {
|
||||
uploadButton.isEnabled = false
|
||||
uploadButton.alpha = 0.5f
|
||||
setListLoading(listLoading, true, recycler, summaryTv, emptyView)
|
||||
backButton.isEnabled = false
|
||||
progressDeterminate.visibility = View.VISIBLE
|
||||
progressDeterminate.isIndeterminate = false
|
||||
progressDeterminate.progress = 0
|
||||
progressDeterminate.max = (qCount + sCount).coerceAtLeast(1)
|
||||
|
||||
@ -171,12 +173,15 @@ class UploadOverviewHandler(
|
||||
onProgress = { done, total ->
|
||||
if (!overviewActive) return@uploadDatabaseWithToken
|
||||
progressDeterminate.max = total.coerceAtLeast(1)
|
||||
progressDeterminate.progress = done
|
||||
progressDeterminate.progress = done.coerceAtMost(progressDeterminate.max)
|
||||
},
|
||||
) { result ->
|
||||
if (!overviewActive) return@uploadDatabaseWithToken
|
||||
setListLoading(listLoading, false, recycler, summaryTv, emptyView)
|
||||
progressDeterminate.progress = progressDeterminate.max
|
||||
progressDeterminate.visibility = View.GONE
|
||||
backButton.isEnabled = true
|
||||
uploadButton.isEnabled = true
|
||||
uploadButton.alpha = 1f
|
||||
showUploadResultDialog(result)
|
||||
}
|
||||
}
|
||||
@ -223,8 +228,8 @@ class UploadOverviewHandler(
|
||||
}
|
||||
|
||||
private suspend fun loadPendingListItems(): List<ListItem> = withContext(Dispatchers.IO) {
|
||||
val pending = CompletedQuestionnaireRepository.getAllPendingUploads()
|
||||
val pendingScoring = CoachScoringReviewRepository.getAllPendingUploads()
|
||||
val pending = PendingUploadRepository.getUploadReadyQuestionnaires(activity)
|
||||
val pendingScoring = PendingUploadRepository.getUploadReadyScoringReviews(activity)
|
||||
if (pending.isEmpty() && pendingScoring.isEmpty()) return@withContext emptyList()
|
||||
|
||||
val qRows = pending.map { entry ->
|
||||
|
||||
@ -1,14 +1,21 @@
|
||||
package com.tomhempel.rbarometer
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
* Resolves UI strings from API translations (synced from the website).
|
||||
* No embedded translation tables — configure strings in the admin Translations UI.
|
||||
* Falls back to bundled German defaults for app UI strings when the server
|
||||
* has not yet synced translations for the source language.
|
||||
*/
|
||||
object LanguageManager {
|
||||
/** Global app UI strings from GET ?translations=1 */
|
||||
private var apiTranslations: Map<String, Map<String, String>> = emptyMap()
|
||||
/** Questionnaire-specific strings from GET ?id=... */
|
||||
private var questionnaireTranslations: Map<String, Map<String, String>> = emptyMap()
|
||||
private val bundledGermanDefaults = AtomicReference<Map<String, String>?>(null)
|
||||
|
||||
fun setApiTranslations(translations: Map<String, Map<String, String>>) {
|
||||
apiTranslations = translations
|
||||
@ -22,6 +29,11 @@ object LanguageManager {
|
||||
questionnaireTranslations = emptyMap()
|
||||
}
|
||||
|
||||
fun warmBundledDefaults(context: Context) {
|
||||
if (bundledGermanDefaults.get() != null) return
|
||||
bundledGermanDefaults.set(loadBundledGermanDefaults(context))
|
||||
}
|
||||
|
||||
fun getTextFormatted(languageId: String, key: String, vararg args: Any): String {
|
||||
val raw = getText(languageId, key)
|
||||
return String.format(raw, *args)
|
||||
@ -29,13 +41,33 @@ object LanguageManager {
|
||||
|
||||
fun getText(languageCode: String, id: String): String {
|
||||
val lang = apiLanguageCode(languageCode)
|
||||
val raw = questionnaireTranslations[lang]?.get(id)
|
||||
?: apiTranslations[lang]?.get(id)
|
||||
?: apiTranslations["de"]?.get(id)
|
||||
?: id
|
||||
val fromQuestionnaire = questionnaireTranslations[lang]?.get(id)
|
||||
val fromApi = apiTranslations[lang]?.get(id)
|
||||
val fromQuestionnaireDe = questionnaireTranslations["de"]?.get(id)
|
||||
val fromApiDe = apiTranslations["de"]?.get(id)
|
||||
val fromBundledDe = bundledGermanDefaults.get()?.get(id)
|
||||
|
||||
val raw = when {
|
||||
!fromQuestionnaire.isNullOrBlank() -> fromQuestionnaire
|
||||
!fromApi.isNullOrBlank() -> fromApi
|
||||
!fromQuestionnaireDe.isNullOrBlank() -> fromQuestionnaireDe
|
||||
!fromApiDe.isNullOrBlank() -> fromApiDe
|
||||
!fromBundledDe.isNullOrBlank() -> fromBundledDe
|
||||
else -> id
|
||||
}
|
||||
return injectDynamicValues(raw)
|
||||
}
|
||||
|
||||
/** Returns the first key that resolves to translated text (not equal to the key itself). */
|
||||
fun resolvedText(languageCode: String, vararg keys: String?): String {
|
||||
for (key in keys) {
|
||||
if (key.isNullOrBlank()) continue
|
||||
val text = getText(languageCode, key)
|
||||
if (text != key) return text
|
||||
}
|
||||
return keys.filterNot { it.isNullOrBlank() }.lastOrNull()?.let { getText(languageCode, it) } ?: ""
|
||||
}
|
||||
|
||||
private fun apiLanguageCode(languageCode: String): String = when (languageCode) {
|
||||
"GERMAN" -> "de"
|
||||
"ENGLISH" -> "en"
|
||||
@ -47,8 +79,19 @@ object LanguageManager {
|
||||
"UKRAINIAN" -> "uk"
|
||||
"RUSSIAN" -> "ru"
|
||||
"SPANISH" -> "es"
|
||||
"CHINESE" -> "zh"
|
||||
"FARSI" -> "fa"
|
||||
else -> languageCode.lowercase()
|
||||
}
|
||||
|
||||
private fun loadBundledGermanDefaults(context: Context): Map<String, String> {
|
||||
return runCatching {
|
||||
context.assets.open("bundled_german_defaults.json").bufferedReader().use { reader ->
|
||||
val type = object : TypeToken<Map<String, String>>() {}.type
|
||||
Gson().fromJson<Map<String, String>>(reader, type) ?: emptyMap()
|
||||
}
|
||||
}.getOrDefault(emptyMap())
|
||||
}
|
||||
|
||||
private fun injectDynamicValues(text: String): String = text
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#FFFFFFFF"/>
|
||||
<item android:color="@color/brand_purple" />
|
||||
</selector>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.85" android:color="#334B5A" android:state_pressed="true"/>
|
||||
<item android:color="#385266"/>
|
||||
<item android:alpha="0.92" android:color="@color/brand_nav_secondary_pressed" android:state_pressed="true" />
|
||||
<item android:color="@color/brand_nav_secondary" />
|
||||
</selector>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.85" android:color="#2F855A" android:state_pressed="true"/>
|
||||
<item android:color="#339966"/>
|
||||
<item android:alpha="0.9" android:color="@color/brand_purple" android:state_pressed="true" />
|
||||
<item android:color="@color/brand_purple" />
|
||||
</selector>
|
||||
|
||||
13
app/src/main/res/drawable/bg_answer_choice.xml
Normal file
13
app/src/main/res/drawable/bg_answer_choice.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/brand_ripple_on_surface">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/brand_surface" />
|
||||
<corners android:radius="16dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/brand_card_stroke" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
9
app/src/main/res/drawable/bg_measure_row_current.xml
Normal file
9
app/src/main/res/drawable/bg_measure_row_current.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/brand_emphasis_bg" />
|
||||
<corners android:radius="14dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/brand_header_stroke" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/bg_measure_row_done.xml
Normal file
5
app/src/main/res/drawable/bg_measure_row_done.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/brand_purple" />
|
||||
</shape>
|
||||
8
app/src/main/res/drawable/bg_measure_row_pending.xml
Normal file
8
app/src/main/res/drawable/bg_measure_row_pending.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@color/brand_card_stroke" />
|
||||
</shape>
|
||||
9
app/src/main/res/drawable/bg_measure_row_selected.xml
Normal file
9
app/src/main/res/drawable/bg_measure_row_selected.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/brand_questionnaire_highlight_bg" />
|
||||
<corners android:radius="16dp" />
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@color/brand_purple" />
|
||||
</shape>
|
||||
9
app/src/main/res/drawable/bg_question_screen.xml
Normal file
9
app/src/main/res/drawable/bg_question_screen.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:endColor="@color/brand_session_bg_bottom"
|
||||
android:startColor="@color/brand_session_bg_top"
|
||||
android:type="linear" />
|
||||
</shape>
|
||||
6
app/src/main/res/drawable/bg_session_hero.xml
Normal file
6
app/src/main/res/drawable/bg_session_hero.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/brand_primary_container" />
|
||||
<corners android:radius="24dp" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/bg_session_icon_circle.xml
Normal file
5
app/src/main/res/drawable/bg_session_icon_circle.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/brand_purple" />
|
||||
</shape>
|
||||
@ -5,12 +5,12 @@
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#0F766E"
|
||||
android:fillColor="#0D9488"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#0E7490"
|
||||
android:pathData="M0,78 C26,62 48,65 73,49 C88,39 99,27 108,15 L108,108 L0,108 Z" />
|
||||
android:pathData="M0,72 C22,58 46,60 68,46 C84,36 96,24 108,12 L108,108 L0,108 Z" />
|
||||
<path
|
||||
android:fillColor="#14B8A6"
|
||||
android:pathData="M0,0h108v26 C86,36 64,39 44,32 C27,26 14,17 0,14 Z" />
|
||||
android:pathData="M0,0 C18,10 38,12 58,8 C78,4 94,10 108,18 L108,0 Z" />
|
||||
</vector>
|
||||
|
||||
@ -4,19 +4,33 @@
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<!-- Gauge arc -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M54,23C36.88,23 23,36.88 23,54C23,71.12 36.88,85 54,85C71.12,85 85,71.12 85,54C85,36.88 71.12,23 54,23ZM54,31C66.7,31 77,41.3 77,54C77,66.7 66.7,77 54,77C41.3,77 31,66.7 31,54C31,41.3 41.3,31 54,31Z" />
|
||||
android:pathData="M31,63 A23,23 0 1,1 77,63 L72,63 A18,18 0 1,0 36,63 Z" />
|
||||
<!-- Scale ticks -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M54,37C51.79,37 50,38.79 50,41C50,43.21 51.79,45 54,45C56.21,45 58,43.21 58,41C58,38.79 56.21,37 54,37ZM54,41C61.38,41 67.67,45.6 70,52H63.5C61.76,48.45 58.16,46 54,46C49.84,46 46.24,48.45 44.5,52H38C40.33,45.6 46.62,41 54,41Z" />
|
||||
android:pathData="M33,61 L37,57 L39,59 L35,63 Z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M52,57 L76,43 L58,64 Z" />
|
||||
android:pathData="M51,35 L55,35 L55,41 L51,41 Z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M31,70h46v7h-46z" />
|
||||
android:pathData="M69,59 L73,63 L71,65 L67,61 Z" />
|
||||
<!-- Needle -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M32,54h6v5h-6zM70,54h6v5h-6zM51,30h6v8h-6z" />
|
||||
android:pathData="M54,58 L71,42 L58,58 Z" />
|
||||
<!-- Pivot -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M54,58 m-4.5,0 a4.5,4.5 0 1,1 9,0 a4.5,4.5 0 1,1 -9,0" />
|
||||
<path
|
||||
android:fillColor="#0D9488"
|
||||
android:pathData="M54,58 m-2.5,0 a2.5,2.5 0 1,1 5,0 a2.5,2.5 0 1,1 -5,0" />
|
||||
<!-- Baseline -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M36,67 L72,67 L72,71 L36,71 Z" />
|
||||
</vector>
|
||||
|
||||
@ -14,6 +14,13 @@
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gPromptBottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.32" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
@ -22,15 +29,15 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_left_tint"
|
||||
app:rippleColor="@color/btn_nav_left_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="@color/btn_nav_left_ripple" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
@ -40,90 +47,108 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_right_tint"
|
||||
app:rippleColor="@color/btn_nav_right_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:rippleColor="@color/btn_nav_right_ripple" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
<ScrollView
|
||||
android:id="@+id/questionPromptScroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/gPromptBottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
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" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/client_code"
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/answerArea"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintWidth_percent="0.7"
|
||||
app:layout_constraintHeight_percent="0.08"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/Qprev"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/bg_edit_text"
|
||||
android:ems="10"
|
||||
android:inputType="text"
|
||||
android:tag="client_code"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textColorHint="@color/brand_text_muted"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:autoSizeMinTextSize="12sp"
|
||||
android:autoSizeMaxTextSize="36sp"
|
||||
android:autoSizeStepGranularity="2sp" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gPromptBottom"
|
||||
app:layout_constraintWidth_percent="0.7">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/coach_code"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintWidth_percent="0.7"
|
||||
app:layout_constraintHeight_percent="0.08"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/client_code"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/bg_edit_text"
|
||||
android:ems="10"
|
||||
android:inputType="text"
|
||||
android:tag="coach_code"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textColorHint="@color/brand_text_muted"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:autoSizeMinTextSize="12sp"
|
||||
android:autoSizeMaxTextSize="36sp"
|
||||
android:autoSizeStepGranularity="2sp" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/client_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:autoSizeMaxTextSize="36sp"
|
||||
android:autoSizeMinTextSize="12sp"
|
||||
android:autoSizeStepGranularity="2sp"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:background="@drawable/bg_edit_text"
|
||||
android:ems="10"
|
||||
android:gravity="center_vertical"
|
||||
android:inputType="text"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:tag="client_code"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textColorHint="@color/brand_text_muted" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/coach_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:autoSizeMaxTextSize="36sp"
|
||||
android:autoSizeMinTextSize="12sp"
|
||||
android:autoSizeStepGranularity="2sp"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:background="@drawable/bg_edit_text"
|
||||
android:ems="10"
|
||||
android:gravity="center_vertical"
|
||||
android:inputType="text"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:tag="coach_code"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textColorHint="@color/brand_text_muted" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -11,114 +11,12 @@
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gPromptBottom"
|
||||
android:layout_width="wrap_content"
|
||||
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" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date_spinner_day"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:tag="day"
|
||||
android:gravity="start|center_vertical"
|
||||
android:textAlignment="viewStart"
|
||||
android:paddingStart="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.07"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
app:layout_constraintWidth_percent="0.24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date_spinner_month"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:tag="month"
|
||||
android:gravity="start|center_vertical"
|
||||
android:textAlignment="viewStart"
|
||||
android:paddingStart="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.48"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
app:layout_constraintWidth_percent="0.38" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date_spinner_year"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:tag="year"
|
||||
android:textAlignment="viewStart"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
app:layout_constraintWidth_percent="0.30" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_value_day"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_field_dropdown"
|
||||
android:popupBackground="@drawable/bg_spinner_popup"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.07"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/date_spinner_day"
|
||||
app:layout_constraintWidth_percent="0.24" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_value_month"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_field_dropdown"
|
||||
android:popupBackground="@drawable/bg_spinner_popup"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.48"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/date_spinner_month"
|
||||
app:layout_constraintWidth_percent="0.38" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_value_year"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_field_dropdown"
|
||||
android:popupBackground="@drawable/bg_spinner_popup"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/date_spinner_year"
|
||||
app:layout_constraintWidth_percent="0.30" />
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.32" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
@ -128,15 +26,15 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_left_tint"
|
||||
app:rippleColor="@color/btn_nav_left_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="@color/btn_nav_left_ripple" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
@ -146,14 +44,143 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_right_tint"
|
||||
app:rippleColor="@color/btn_nav_right_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:rippleColor="@color/btn_nav_right_ripple" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/questionPromptScroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/gPromptBottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintWidth_percent="0.9">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/answerArea"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/Qprev"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gPromptBottom"
|
||||
app:layout_constraintWidth_percent="0.92">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date_spinner_day"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:tag="day"
|
||||
android:textAlignment="viewStart"
|
||||
app:layout_constraintEnd_toStartOf="@id/date_spinner_month"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date_spinner_month"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:tag="month"
|
||||
android:textAlignment="viewStart"
|
||||
app:layout_constraintEnd_toStartOf="@id/date_spinner_year"
|
||||
app:layout_constraintStart_toEndOf="@id/date_spinner_day"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.38" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date_spinner_year"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:tag="year"
|
||||
android:textAlignment="viewStart"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/date_spinner_month"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.30" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_value_day"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_field_dropdown"
|
||||
android:popupBackground="@drawable/bg_spinner_popup"
|
||||
app:layout_constraintEnd_toEndOf="@id/date_spinner_day"
|
||||
app:layout_constraintStart_toStartOf="@id/date_spinner_day"
|
||||
app:layout_constraintTop_toBottomOf="@id/date_spinner_day" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_value_month"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_field_dropdown"
|
||||
app:layout_constraintEnd_toEndOf="@id/date_spinner_month"
|
||||
app:layout_constraintStart_toStartOf="@id/date_spinner_month"
|
||||
app:layout_constraintTop_toBottomOf="@id/date_spinner_month"
|
||||
android:popupBackground="@drawable/bg_spinner_popup" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_value_year"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_field_dropdown"
|
||||
app:layout_constraintEnd_toEndOf="@id/date_spinner_year"
|
||||
app:layout_constraintStart_toStartOf="@id/date_spinner_year"
|
||||
app:layout_constraintTop_toBottomOf="@id/date_spinner_year"
|
||||
android:popupBackground="@drawable/bg_spinner_popup" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -91,21 +91,58 @@
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/dialogBtnPrimary"
|
||||
style="@style/Widget.Barometer.SessionPrimaryButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:textAllCaps="false"
|
||||
app:cornerRadius="14dp" />
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/dialogBtnSecondary"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
<LinearLayout
|
||||
android:id="@+id/dialogButtonRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:minHeight="44dp"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/brand_text_muted" />
|
||||
android:layout_marginTop="10dp"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/dialogBtnSecondary"
|
||||
style="@style/Widget.Barometer.SessionSecondaryButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/dialogBtnTertiary"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:minHeight="52dp"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/brand_text_muted"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/dialogBtnPrimaryInline"
|
||||
style="@style/Widget.Barometer.SessionPrimaryButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAllCaps="false"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
@ -13,6 +13,13 @@
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gPromptBottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.32" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
@ -21,15 +28,15 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_left_tint"
|
||||
app:rippleColor="@color/btn_nav_left_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="@color/btn_nav_left_ripple" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
@ -39,88 +46,94 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_right_tint"
|
||||
app:rippleColor="@color/btn_nav_right_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:rippleColor="@color/btn_nav_right_ripple" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
<ScrollView
|
||||
android:id="@+id/questionPromptScroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/gPromptBottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
app:layout_constraintWidth_percent="0.9">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/note_before"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<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/note_before"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/note_after"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
<TextView
|
||||
android:id="@+id/note_before"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/free_text_input"
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/note_after"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/answerArea"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/bg_edit_text"
|
||||
android:gravity="center_vertical|start"
|
||||
android:inputType="textCapSentences"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textColorHint="@color/brand_text_muted"
|
||||
android:maxLines="1"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
app:layout_constraintWidth_percent="0.85"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/Qprev"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/note_after" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gPromptBottom"
|
||||
app:layout_constraintWidth_percent="0.85">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/free_text_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/bg_edit_text"
|
||||
android:gravity="center_vertical|start"
|
||||
android:inputType="textCapSentences"
|
||||
android:maxLines="1"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textColorHint="@color/brand_text_muted" />
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gTop"
|
||||
@ -11,69 +14,12 @@
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<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" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/glass_header"
|
||||
android:layout_width="0dp"
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gPromptBottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:weightSum="9"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="1" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/glassScroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:fillViewport="true"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/glass_header"
|
||||
app:layout_constraintBottom_toTopOf="@+id/Qprev"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="1">
|
||||
|
||||
<TableLayout
|
||||
android:id="@+id/glass_table"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:stretchColumns="1,2,3,4,5"
|
||||
android:textAlignment="center"
|
||||
android:textStyle="bold" />
|
||||
</ScrollView>
|
||||
app:layout_constraintGuide_percent="0.28" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
@ -81,15 +27,15 @@
|
||||
android:layout_height="@dimen/nav_btn_size"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_left_tint"
|
||||
app:rippleColor="@color/btn_nav_left_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="@color/btn_nav_left_ripple" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
@ -97,14 +43,97 @@
|
||||
android:layout_height="@dimen/nav_btn_size"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_right_tint"
|
||||
app:rippleColor="@color/btn_nav_right_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:rippleColor="@color/btn_nav_right_ripple" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/questionPromptScroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/gPromptBottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintWidth_percent="0.9">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/answerArea"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/Qprev"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gPromptBottom">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/glassScroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/glass_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:weightSum="9" />
|
||||
|
||||
<TableLayout
|
||||
android:id="@+id/glass_table"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:stretchColumns="1,2,3,4,5"
|
||||
android:textAlignment="center"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
67
app/src/main/res/layout/item_session_measure_row.xml
Normal file
67
app/src/main/res/layout/item_session_measure_row.xml
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/sessionMeasureCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/session_measure_row_spacing"
|
||||
android:minHeight="@dimen/session_measure_row_min_height"
|
||||
app:cardBackgroundColor="@color/brand_header_surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="@color/brand_card_stroke"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="@dimen/session_measure_row_min_height"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/sessionMeasureIndicatorWrap"
|
||||
android:layout_width="@dimen/session_measure_indicator_size"
|
||||
android:layout_height="@dimen/session_measure_indicator_size">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionMeasureIndicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionMeasureTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionMeasureStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/brand_text_muted"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@ -1,80 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_question_screen">
|
||||
|
||||
<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="48dp" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/lastPageCard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
app:cardBackgroundColor="@color/brand_surface"
|
||||
app:cardCornerRadius="24dp"
|
||||
app:cardElevation="@dimen/session_btn_elevation"
|
||||
app:layout_constraintBottom_toTopOf="@+id/Qfinish"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintVertical_bias="0.35"
|
||||
app:strokeColor="@color/brand_card_stroke"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="28dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/TextAppearance.Barometer.QuestionHint" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.35"
|
||||
android:textAppearance="@style/TextAppearance.Barometer.QuestionTitle" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Zurück (links unten) -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
android:layout_height="@dimen/nav_btn_size"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_left_tint"
|
||||
app:rippleColor="@color/btn_nav_left_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="@color/btn_nav_left_ripple" />
|
||||
|
||||
<!-- Fertig/Speichern (rechts unten) – gleiche Farbe wie Qprev -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qfinish"
|
||||
android:layout_width="@dimen/finish_btn_width"
|
||||
style="@style/Widget.Barometer.SessionPrimaryButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/nav_btn_size"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textAllCaps="false"
|
||||
android:minWidth="0dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:insetLeft="0dp"
|
||||
android:insetRight="0dp"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:minWidth="0dp"
|
||||
android:paddingStart="28dp"
|
||||
android:paddingEnd="28dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_right_tint"
|
||||
app:rippleColor="@color/btn_nav_right_ripple"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="start|center_vertical"
|
||||
android:textAlignment="viewStart"
|
||||
android:paddingStart="8dp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
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="start|center_vertical"
|
||||
android:textAlignment="viewStart"
|
||||
android:paddingStart="8dp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
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" />
|
||||
app:layout_constraintStart_toEndOf="@+id/Qprev" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
@ -82,9 +96,9 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -14,6 +14,13 @@
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gPromptBottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.32" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
@ -22,15 +29,15 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_left_tint"
|
||||
app:rippleColor="@color/btn_nav_left_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="@color/btn_nav_left_ripple" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
@ -40,63 +47,88 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_right_tint"
|
||||
app:rippleColor="@color/btn_nav_right_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="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" />
|
||||
app:rippleColor="@color/btn_nav_right_ripple" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:id="@+id/questionPromptScroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/gPromptBottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
app:layout_constraintBottom_toTopOf="@id/Qnext"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintWidth_percent="0.9">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/CheckboxContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp" />
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/answerArea"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/Qprev"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gPromptBottom"
|
||||
app:layout_constraintWidth_percent="0.9">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/CheckboxContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_window_bg"
|
||||
android:background="@drawable/bg_question_screen"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
@ -77,21 +77,19 @@
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp"
|
||||
android:background="@color/brand_surface"
|
||||
android:elevation="8dp">
|
||||
android:elevation="4dp"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSave"
|
||||
style="@style/Widget.Barometer.SessionPrimaryButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/nav_btn_size"
|
||||
android:textAllCaps="false"
|
||||
android:minWidth="0dp"
|
||||
android:insetLeft="0dp"
|
||||
android:insetRight="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_right_tint"
|
||||
app:rippleColor="@color/btn_nav_right_ripple" />
|
||||
app:cornerRadius="999dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
|
||||
@ -14,6 +14,13 @@
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gPromptBottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.32" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
@ -22,15 +29,15 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_left_tint"
|
||||
app:rippleColor="@color/btn_nav_left_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="@color/btn_nav_left_ripple" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
@ -40,64 +47,88 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_right_tint"
|
||||
app:rippleColor="@color/btn_nav_right_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="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" />
|
||||
app:rippleColor="@color/btn_nav_right_ripple" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/radioScroll"
|
||||
android:id="@+id/questionPromptScroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:fillViewport="true"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
app:layout_constraintBottom_toTopOf="@id/Qnext"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:fillViewport="true"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/gPromptBottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.80">
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintWidth_percent="0.9">
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/RadioGroup"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp" />
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/answerArea"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/Qprev"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gPromptBottom"
|
||||
app:layout_constraintWidth_percent="0.85">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/radioScroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/RadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
142
app/src/main/res/layout/session_complete_screen.xml
Normal file
142
app/src/main/res/layout/session_complete_screen.xml
Normal file
@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_question_screen"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/session_content_padding">
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="0.35" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/sessionCompleteIconWrap"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:background="@drawable/bg_session_icon_circle">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_sync_complete"
|
||||
app:tint="@color/white" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionCompleteEyebrow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/TextAppearance.Barometer.SessionEyebrow" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionCompleteTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/TextAppearance.Barometer.SessionTitle"
|
||||
android:textSize="32sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionCompleteSubtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.25"
|
||||
android:textColor="@color/brand_text_muted"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionCompleteClient"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:background="@drawable/bg_control_chip"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="14dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/sessionCompleteCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="22dp"
|
||||
app:cardBackgroundColor="@color/brand_surface"
|
||||
app:cardCornerRadius="20dp"
|
||||
app:cardElevation="@dimen/session_btn_elevation"
|
||||
app:strokeColor="@color/brand_card_stroke"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="18dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionCompleteMeasureHeading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/brand_text_muted"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sessionCompleteMeasureList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:clipToPadding="false" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionCompleteBody"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="18dp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.35"
|
||||
android:textColor="@color/brand_text_muted"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="0.65" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/sessionCompleteContinue"
|
||||
style="@style/Widget.Barometer.SessionPrimaryButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconGravity="textEnd"
|
||||
app:iconPadding="12dp"
|
||||
app:iconTint="@color/white" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
209
app/src/main/res/layout/session_measure_interstitial.xml
Normal file
209
app/src/main/res/layout/session_measure_interstitial.xml
Normal file
@ -0,0 +1,209 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_question_screen"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/interstitialScroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/session_content_padding"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@android:color/transparent"
|
||||
app:cardCornerRadius="@dimen/session_card_radius"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_session_hero"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="20dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="@dimen/session_icon_circle"
|
||||
android:layout_height="@dimen/session_icon_circle"
|
||||
android:background="@drawable/bg_session_icon_circle">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_dialog_info_24"
|
||||
app:tint="@color/white" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interstitialEyebrow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Barometer.SessionEyebrow" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interstitialTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
android:textAppearance="@style/TextAppearance.Barometer.SessionTitle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interstitialSessionLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/interstitialProgressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
app:indicatorColor="@color/brand_purple"
|
||||
app:trackColor="@color/brand_header_stroke"
|
||||
app:trackCornerRadius="8dp"
|
||||
app:trackThickness="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interstitialProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:lineSpacingMultiplier="1.25"
|
||||
android:textColor="@color/brand_text_muted"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interstitialClient"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:background="@drawable/bg_control_chip"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="14dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="18dp"
|
||||
app:cardBackgroundColor="@color/brand_surface"
|
||||
app:cardCornerRadius="20dp"
|
||||
app:cardElevation="@dimen/session_btn_elevation"
|
||||
app:strokeColor="@color/brand_card_stroke"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="18dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interstitialMeasureHeading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/brand_text_muted"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/interstitialMeasureList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interstitialSummary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:lineSpacingMultiplier="1.3"
|
||||
android:textColor="@color/brand_text_muted"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/interstitialActions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/brand_surface"
|
||||
android:elevation="8dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="@dimen/session_content_padding"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="@dimen/session_content_padding"
|
||||
android:paddingBottom="@dimen/session_content_padding">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@color/brand_header_stroke" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/interstitialContinue"
|
||||
style="@style/Widget.Barometer.SessionPrimaryButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/session_action_btn_height"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconGravity="textEnd"
|
||||
app:iconPadding="12dp"
|
||||
app:iconTint="@color/white" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/interstitialPause"
|
||||
style="@style/Widget.Barometer.SessionSecondaryButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/session_action_btn_height"
|
||||
android:layout_marginTop="10dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@ -11,113 +11,135 @@
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gPromptBottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.32" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/questionPromptScroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/gPromptBottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
app:layout_constraintWidth_percent="0.9">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/note_before"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/note_before"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/note_after"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/answerArea"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/Qprev"
|
||||
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:textColor="@color/brand_text_dark"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/note_before"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
app:layout_constraintTop_toBottomOf="@id/gPromptBottom"
|
||||
app:layout_constraintWidth_percent="0.9">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/note_after"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slider_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textSize="20sp"
|
||||
android:minHeight="28dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/note_after"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
<TextView
|
||||
android:id="@+id/slider_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="28dp"
|
||||
android:textColor="@color/brand_text_dark"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slider_min"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:gravity="center"
|
||||
android:minWidth="28dp"
|
||||
android:textColor="@color/brand_text_muted"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/value_slider"
|
||||
app:layout_constraintEnd_toStartOf="@id/value_slider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/value_slider" />
|
||||
<TextView
|
||||
android:id="@+id/slider_min"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:gravity="center"
|
||||
android:minWidth="28dp"
|
||||
android:textColor="@color/brand_text_muted"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/value_slider"
|
||||
app:layout_constraintEnd_toStartOf="@id/value_slider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/value_slider" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/value_slider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/slider_max"
|
||||
app:layout_constraintStart_toEndOf="@id/slider_min"
|
||||
app:layout_constraintTop_toBottomOf="@id/slider_value" />
|
||||
<SeekBar
|
||||
android:id="@+id/value_slider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/slider_max"
|
||||
app:layout_constraintStart_toEndOf="@id/slider_min"
|
||||
app:layout_constraintTop_toBottomOf="@id/slider_value" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slider_max"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:gravity="center"
|
||||
android:minWidth="28dp"
|
||||
android:textColor="@color/brand_text_muted"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/value_slider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/value_slider"
|
||||
app:layout_constraintTop_toTopOf="@id/value_slider" />
|
||||
<TextView
|
||||
android:id="@+id/slider_max"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:gravity="center"
|
||||
android:minWidth="28dp"
|
||||
android:textColor="@color/brand_text_muted"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/value_slider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/value_slider"
|
||||
app:layout_constraintTop_toTopOf="@id/value_slider" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
@ -125,15 +147,15 @@
|
||||
android:layout_height="@dimen/nav_btn_size"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_left_tint"
|
||||
app:rippleColor="@color/btn_nav_left_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="@color/btn_nav_left_ripple" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
@ -141,14 +163,14 @@
|
||||
android:layout_height="@dimen/nav_btn_size"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_right_tint"
|
||||
app:rippleColor="@color/btn_nav_right_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:rippleColor="@color/btn_nav_right_ripple" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -12,17 +12,12 @@
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/string_spinner"
|
||||
android:layout_width="0dp"
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gPromptBottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/bg_field_dropdown"
|
||||
android:popupBackground="@drawable/bg_spinner_popup"
|
||||
app:layout_constraintWidth_percent="0.70"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/question" />
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.32" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
@ -32,15 +27,15 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_left_tint"
|
||||
app:rippleColor="@color/btn_nav_left_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="@color/btn_nav_left_ripple" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
@ -50,42 +45,72 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_right_tint"
|
||||
app:rippleColor="@color/btn_nav_right_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:rippleColor="@color/btn_nav_right_ripple" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
<ScrollView
|
||||
android:id="@+id/questionPromptScroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/gPromptBottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
app:layout_constraintWidth_percent="0.9">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/answerArea"
|
||||
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"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/Qprev"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gPromptBottom"
|
||||
app:layout_constraintWidth_percent="0.7">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/string_spinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/bg_field_dropdown"
|
||||
android:popupBackground="@drawable/bg_spinner_popup" />
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -12,30 +12,12 @@
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/value_spinner"
|
||||
android:layout_width="0dp"
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gPromptBottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_field_dropdown"
|
||||
android:popupBackground="@drawable/bg_spinner_popup"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.495"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/note_after"
|
||||
app:layout_constraintVertical_bias="0.027"
|
||||
app:layout_constraintWidth_percent="0.70" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/note_after"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.32" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
@ -45,15 +27,15 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_left_tint"
|
||||
app:rippleColor="@color/btn_nav_left_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="@color/btn_nav_left_ripple" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
@ -63,53 +45,86 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="999dp"
|
||||
app:backgroundTint="@color/btn_nav_right_tint"
|
||||
app:rippleColor="@color/btn_nav_right_ripple"
|
||||
app:cornerRadius="999dp"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:rippleColor="@color/btn_nav_right_ripple" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
<ScrollView
|
||||
android:id="@+id/questionPromptScroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/gPromptBottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
app:layout_constraintWidth_percent="0.9">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/note_before"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/note_before"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/note_after"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/answerArea"
|
||||
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/note_before"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/Qprev"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/gPromptBottom"
|
||||
app:layout_constraintWidth_percent="0.7">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/value_spinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/bg_field_dropdown"
|
||||
android:popupBackground="@drawable/bg_spinner_popup" />
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -63,4 +63,10 @@
|
||||
<color name="brand_destructive_fill">#4A2028</color>
|
||||
<color name="brand_destructive_text">#FFB4BE</color>
|
||||
<color name="brand_bottom_sheet_handle">#5B5A70</color>
|
||||
|
||||
<color name="brand_session_bg_top">#161820</color>
|
||||
<color name="brand_session_bg_bottom">#121218</color>
|
||||
<color name="brand_answer_choice_checked">#1A4A44</color>
|
||||
<color name="brand_nav_secondary">#123D39</color>
|
||||
<color name="brand_nav_secondary_pressed">#14534D</color>
|
||||
</resources>
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<color name="brand_purple">#0F766E</color>
|
||||
<color name="brand_window_bg">#F3F7F6</color>
|
||||
<color name="brand_purple">#0D9488</color>
|
||||
<color name="brand_window_bg">#F4FAF8</color>
|
||||
<color name="brand_loading_scrim">#66000000</color>
|
||||
<color name="brand_loading_track">#E8E4F5</color>
|
||||
<color name="brand_surface">#FFFFFFFF</color>
|
||||
@ -15,11 +15,11 @@
|
||||
<color name="brand_stroke">#D5E4E0</color>
|
||||
<color name="brand_field_locked_fill">#F4F4F6</color>
|
||||
<color name="brand_field_locked_stroke">#C7C7D0</color>
|
||||
<color name="brand_stroke_enabled">#0F766E</color>
|
||||
<color name="brand_stroke_enabled">#0D9488</color>
|
||||
<color name="brand_stroke_disabled">#D8E5E2</color>
|
||||
<color name="brand_primary_container">#DDF4EF</color>
|
||||
<color name="brand_emphasis_bg">#E7F5F1</color>
|
||||
<color name="brand_questionnaire_highlight_bg">#DDF4EF</color>
|
||||
<color name="brand_primary_container">#E6F7F3</color>
|
||||
<color name="brand_emphasis_bg">#EDF8F5</color>
|
||||
<color name="brand_questionnaire_highlight_bg">#E0F5F0</color>
|
||||
<color name="brand_card_locked">#243331</color>
|
||||
<color name="brand_card_locked_title">#FFFFFFFF</color>
|
||||
<color name="brand_card_locked_subtitle">#E0E0E0</color>
|
||||
@ -49,7 +49,7 @@
|
||||
<color name="brand_control_chip_bg">#FCFEFD</color>
|
||||
<color name="brand_control_chip_stroke">#DCE9E5</color>
|
||||
<color name="brand_control_dropdown_bg">#FFFFFFFF</color>
|
||||
<color name="brand_control_dropdown_stroke">#0F766E</color>
|
||||
<color name="brand_control_dropdown_stroke">#0D9488</color>
|
||||
<color name="brand_status_chip_online_bg">#E8F5E9</color>
|
||||
<color name="brand_status_chip_online_text">#1B5E20</color>
|
||||
<color name="brand_status_chip_offline_bg">#FFEBEE</color>
|
||||
@ -71,4 +71,10 @@
|
||||
<color name="brand_destructive_fill">#FCE8EA</color>
|
||||
<color name="brand_destructive_text">#A63345</color>
|
||||
<color name="brand_bottom_sheet_handle">#C7D9D4</color>
|
||||
|
||||
<color name="brand_session_bg_top">#F8FCFB</color>
|
||||
<color name="brand_session_bg_bottom">#EEF7F4</color>
|
||||
<color name="brand_answer_choice_checked">#D4F0EA</color>
|
||||
<color name="brand_nav_secondary">#E6F7F3</color>
|
||||
<color name="brand_nav_secondary_pressed">#D4F0EA</color>
|
||||
</resources>
|
||||
|
||||
@ -21,4 +21,13 @@
|
||||
<dimen name="language_picker_height">44dp</dimen>
|
||||
<dimen name="language_picker_min_width">128dp</dimen>
|
||||
<dimen name="language_picker_padding_horizontal">10dp</dimen>
|
||||
|
||||
<dimen name="session_card_radius">24dp</dimen>
|
||||
<dimen name="session_content_padding">24dp</dimen>
|
||||
<dimen name="session_btn_elevation">2dp</dimen>
|
||||
<dimen name="session_icon_circle">52dp</dimen>
|
||||
<dimen name="session_action_btn_height">56dp</dimen>
|
||||
<dimen name="session_measure_row_min_height">76dp</dimen>
|
||||
<dimen name="session_measure_indicator_size">44dp</dimen>
|
||||
<dimen name="session_measure_row_spacing">18dp</dimen>
|
||||
</resources>
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="system_bars_base_padding" type="id" />
|
||||
<item name="questionnaire_lang_overlay" type="id" />
|
||||
<item name="questionnaire_screen_wrapper" type="id" />
|
||||
<item name="questionnaire_content_root" type="id" />
|
||||
<item name="questionnaire_lang_toolbar" type="id" />
|
||||
</resources>
|
||||
|
||||
@ -11,4 +11,50 @@
|
||||
<item name="android:textColor">@color/brand_text_dark</item>
|
||||
<item name="android:textColorHint">@color/brand_text_muted</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Barometer.QuestionHint" parent="TextAppearance.Material3.BodyMedium">
|
||||
<item name="android:textColor">@color/brand_text_muted</item>
|
||||
<item name="android:textSize">15sp</item>
|
||||
<item name="android:textStyle">normal</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Barometer.QuestionTitle" parent="TextAppearance.Material3.TitleLarge">
|
||||
<item name="android:textColor">@color/brand_text_dark</item>
|
||||
<item name="android:textSize">22sp</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Barometer.SessionEyebrow" parent="TextAppearance.Material3.LabelLarge">
|
||||
<item name="android:textColor">@color/brand_purple</item>
|
||||
<item name="android:textSize">12sp</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:letterSpacing">0.06</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Barometer.SessionTitle" parent="TextAppearance.Material3.HeadlineMedium">
|
||||
<item name="android:textColor">@color/brand_text_dark</item>
|
||||
<item name="android:textSize">28sp</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Barometer.PrimaryButton" parent="TextAppearance.Material3.LabelLarge">
|
||||
<item name="android:textSize">16sp</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Barometer.SessionPrimaryButton" parent="Widget.Material3.Button">
|
||||
<item name="android:textAllCaps">false</item>
|
||||
<item name="android:minHeight">56dp</item>
|
||||
<item name="cornerRadius">28dp</item>
|
||||
<item name="backgroundTint">@color/brand_purple</item>
|
||||
<item name="android:textAppearance">@style/TextAppearance.Barometer.PrimaryButton</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Barometer.SessionSecondaryButton" parent="Widget.Material3.Button.OutlinedButton">
|
||||
<item name="android:textAllCaps">false</item>
|
||||
<item name="android:minHeight">52dp</item>
|
||||
<item name="cornerRadius">28dp</item>
|
||||
<item name="strokeColor">@color/brand_purple</item>
|
||||
<item name="android:textColor">@color/brand_purple</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@ -6,12 +6,14 @@
|
||||
<item name="colorOnPrimaryContainer">@color/brand_text_dark</item>
|
||||
<item name="colorSurface">@color/brand_surface</item>
|
||||
<item name="colorOnSurface">@color/brand_text_dark</item>
|
||||
<item name="colorSecondary">@color/brand_upload_accent</item>
|
||||
<item name="android:colorBackground">@color/brand_window_bg</item>
|
||||
<item name="android:windowBackground">@color/brand_window_bg</item>
|
||||
<item name="android:statusBarColor">@color/brand_window_bg</item>
|
||||
<item name="android:statusBarColor">@color/brand_session_bg_top</item>
|
||||
<item name="android:navigationBarColor">@color/brand_surface</item>
|
||||
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
|
||||
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
|
||||
<item name="materialButtonStyle">@style/Widget.Barometer.SessionPrimaryButton</item>
|
||||
<item name="textInputStyle">@style/Widget.App.TextInputLayout.OutlinedBox</item>
|
||||
<item name="editTextStyle">@style/Widget.App.TextInputEditText</item>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user