From 51d44c7d04998794bd8ef71c28e9f9c0e82001c3 Mon Sep 17 00:00:00 2001 From: chaneylc Date: Thu, 12 Mar 2026 16:48:38 -0500 Subject: [PATCH 01/11] renamed commutative crossing setting to reciprocal crossing --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64ad92d..5e3258c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -517,7 +517,7 @@ Notification sound when a cross is made. Open Cross After Creating Opens the cross immediately after creating. - Commutative Crossing + Reciprocal Crossing When enabled, cross AxB and cross BxA are considered the same. From 937bb210468c8e1ffc85199b07df586a6c9e913f Mon Sep 17 00:00:00 2001 From: chaneylc Date: Fri, 13 Mar 2026 11:45:17 -0500 Subject: [PATCH 02/11] added gradle plugin management added v4 database migration to only allow unique male/female/wishTypes in wishlist --- .../3.json | 438 ++++++++++++++++++ .../intercross/data/IntercrossDatabase.kt | 5 +- .../intercross/data/dao/WishlistDao.kt | 5 + .../migrations/MigrationV4UniqueWishType.kt | 31 ++ .../intercross/data/models/Wishlist.kt | 6 +- settings.gradle | 8 + 6 files changed, 490 insertions(+), 3 deletions(-) create mode 100644 app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/3.json create mode 100644 app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV4UniqueWishType.kt diff --git a/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/3.json b/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/3.json new file mode 100644 index 0000000..69fda1d --- /dev/null +++ b/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/3.json @@ -0,0 +1,438 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "cea50807ded802b922e1950728a86e94", + "entities": [ + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `mom` TEXT NOT NULL, `dad` TEXT NOT NULL, `name` TEXT NOT NULL, `date` TEXT NOT NULL, `person` TEXT NOT NULL, `experiment` TEXT NOT NULL, `type` INTEGER NOT NULL, `sex` INTEGER NOT NULL, `eid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "eventDbId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "femaleObsUnitDbId", + "columnName": "mom", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleObsUnitDbId", + "columnName": "dad", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readableName", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "person", + "columnName": "person", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "experiment", + "columnName": "experiment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sex", + "columnName": "sex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "eid" + ] + }, + "indices": [ + { + "name": "index_events_codeId", + "unique": true, + "columnNames": [ + "codeId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_events_codeId` ON `${TABLE_NAME}` (`codeId`)" + } + ] + }, + { + "tableName": "parents", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `sex` INTEGER NOT NULL, `selected` INTEGER NOT NULL, `pid` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `isPoly` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "codeId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sex", + "columnName": "sex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selected", + "columnName": "selected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "pid", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPoly", + "columnName": "isPoly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pid" + ] + }, + "indices": [ + { + "name": "index_parents_codeId", + "unique": true, + "columnNames": [ + "codeId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_parents_codeId` ON `${TABLE_NAME}` (`codeId`)" + } + ] + }, + { + "tableName": "wishlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`femaleDbId` TEXT NOT NULL, `maleDbId` TEXT NOT NULL, `femaleName` TEXT NOT NULL, `maleName` TEXT NOT NULL, `wishType` TEXT NOT NULL, `wishMin` INTEGER NOT NULL, `wishMax` INTEGER, `wid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "femaleDbId", + "columnName": "femaleDbId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleDbId", + "columnName": "maleDbId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "femaleName", + "columnName": "femaleName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleName", + "columnName": "maleName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wishType", + "columnName": "wishType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wishMin", + "columnName": "wishMin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wishMax", + "columnName": "wishMax", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "wid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "wid" + ] + }, + "indices": [ + { + "name": "index_wishlist_femaleDbId_maleDbId_wishType", + "unique": true, + "columnNames": [ + "femaleDbId", + "maleDbId", + "wishType" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_wishlist_femaleDbId_maleDbId_wishType` ON `${TABLE_NAME}` (`femaleDbId`, `maleDbId`, `wishType`)" + } + ] + }, + { + "tableName": "settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`isPattern` INTEGER NOT NULL, `isUUID` INTEGER NOT NULL, `startFrom` INTEGER NOT NULL, `isAutoIncrement` INTEGER NOT NULL, `pad` INTEGER NOT NULL, `number` INTEGER NOT NULL, `prefix` TEXT NOT NULL, `suffix` TEXT NOT NULL, `allowBlank` INTEGER NOT NULL, `order` INTEGER NOT NULL, `collectData` INTEGER NOT NULL, `sid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "isPattern", + "columnName": "isPattern", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUUID", + "columnName": "isUUID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startFrom", + "columnName": "startFrom", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAutoIncrement", + "columnName": "isAutoIncrement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pad", + "columnName": "pad", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prefix", + "columnName": "prefix", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "allowBlank", + "columnName": "allowBlank", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectData", + "columnName": "collectData", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "sid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "sid" + ] + } + }, + { + "tableName": "pollen_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `name` TEXT NOT NULL, `maleId` INTEGER, `gid` INTEGER PRIMARY KEY AUTOINCREMENT, `selected` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "codeId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleId", + "columnName": "maleId", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "gid", + "affinity": "INTEGER" + }, + { + "fieldPath": "selected", + "columnName": "selected", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "gid" + ] + } + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`property` TEXT NOT NULL, `defaultValue` INTEGER, `mid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "property", + "columnName": "property", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultValue", + "columnName": "defaultValue", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "mid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "mid" + ] + }, + "indices": [ + { + "name": "index_metadata_property", + "unique": true, + "columnNames": [ + "property" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metadata_property` ON `${TABLE_NAME}` (`property`)" + } + ] + }, + { + "tableName": "metaValues", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eid` INTEGER NOT NULL, `metaId` INTEGER NOT NULL, `value` INTEGER, `mvId` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "eid", + "columnName": "eid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "metaId", + "columnName": "metaId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "mvId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "mvId" + ] + }, + "indices": [ + { + "name": "index_metaValues_eid_metaId", + "unique": true, + "columnNames": [ + "eid", + "metaId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metaValues_eid_metaId` ON `${TABLE_NAME}` (`eid`, `metaId`)" + } + ] + } + ], + "views": [ + { + "viewName": "WishlistView", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT DISTINCT femaleDbId as momId, femaleName as momName, maleDbId as dadId, maleName as dadName, wishMin, wishMax, wishType,\n\t(SELECT COUNT(*) \n\tFROM events as child\n\tWHERE (w.femaleDbId = child.mom and ((w.maleDbId = child.dad) or (child.dad = \"blank\" and w.maleDbId = \"-1\")))) as wishProgress\nfrom wishlist as w" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cea50807ded802b922e1950728a86e94')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/IntercrossDatabase.kt b/app/src/main/java/org/phenoapps/intercross/data/IntercrossDatabase.kt index cacaec8..99dfa10 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/IntercrossDatabase.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/IntercrossDatabase.kt @@ -5,12 +5,12 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase import org.phenoapps.intercross.data.dao.* import org.phenoapps.intercross.data.migrations.MigrationV2MetaData import org.phenoapps.intercross.data.migrations.MigrationV3WishlistView +import org.phenoapps.intercross.data.migrations.MigrationV4UniqueWishType import org.phenoapps.intercross.data.models.* +import kotlin.jvm.java @Database(entities = [Event::class, Parent::class, Wishlist::class, Settings::class, PollenGroup::class, @@ -46,6 +46,7 @@ abstract class IntercrossDatabase : RoomDatabase() { return Room.databaseBuilder(ctx, IntercrossDatabase::class.java, DATABASE_NAME) .addMigrations(MigrationV2MetaData()) //v1 -> v2 migration added JSON based metadata .addMigrations(MigrationV3WishlistView()) // v2 -> v3 migration for WishlistView + .addMigrations(MigrationV4UniqueWishType()) // v3 -> v4 migration for unique wishlist type .setJournalMode(JournalMode.TRUNCATE) //truncate mode makes it easier to export/import database w/o having to manage WAL files. .build() } diff --git a/app/src/main/java/org/phenoapps/intercross/data/dao/WishlistDao.kt b/app/src/main/java/org/phenoapps/intercross/data/dao/WishlistDao.kt index dd0f419..b3ed431 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/dao/WishlistDao.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/dao/WishlistDao.kt @@ -2,6 +2,8 @@ package org.phenoapps.intercross.data.dao import androidx.lifecycle.LiveData import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import org.phenoapps.intercross.data.models.Wishlist @@ -52,4 +54,7 @@ interface WishlistDao : BaseDao { @Transaction @Query("DELETE FROM wishlist") fun drop() + + @Insert(onConflict = OnConflictStrategy.IGNORE) + override fun insert(vararg items: Wishlist) } \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV4UniqueWishType.kt b/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV4UniqueWishType.kt new file mode 100644 index 0000000..1787867 --- /dev/null +++ b/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV4UniqueWishType.kt @@ -0,0 +1,31 @@ +package org.phenoapps.intercross.data.migrations + +import android.database.sqlite.SQLiteException +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class MigrationV4UniqueWishType : Migration(3, 4) { + + override fun migrate(db: SupportSQLiteDatabase) { + with(db) { + try { + beginTransaction() + + createWishTypeIndex() + + setTransactionSuccessful() + } catch (e: SQLiteException) { + e.printStackTrace() + } finally { + endTransaction() + } + } + } + + private fun SupportSQLiteDatabase.createWishTypeIndex() { + execSQL( + "CREATE UNIQUE INDEX IF NOT EXISTS index_intercross_maleDbId_femaleDbId_wishType " + + "ON wishlist(maleDbId, femaleDbId, wishType)" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/models/Wishlist.kt b/app/src/main/java/org/phenoapps/intercross/data/models/Wishlist.kt index 8fd4541..231cf94 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/models/Wishlist.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/models/Wishlist.kt @@ -2,9 +2,13 @@ package org.phenoapps.intercross.data.models import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.Index import androidx.room.PrimaryKey -@Entity(tableName = "wishlist") +@Entity(tableName = "wishlist", + indices = [ + Index(value = ["femaleDbId", "maleDbId", "wishType"], unique = true) + ]) data class Wishlist(var femaleDbId: String, var maleDbId: String, var femaleName: String=femaleDbId, diff --git a/settings.gradle b/settings.gradle index e7b4def..bd5d53b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,9 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + include ':app' From 2a5c6046da2980997171b7a9fa7179c67814789b Mon Sep 17 00:00:00 2001 From: chaneylc Date: Fri, 13 Mar 2026 12:19:04 -0500 Subject: [PATCH 03/11] added manual search for crossID --- .../intercross/fragments/EventsFragment.kt | 69 +++++++++++++++++++ .../res/layout/dialog_manual_cross_search.xml | 34 +++++++++ app/src/main/res/values/strings.xml | 5 ++ 3 files changed, 108 insertions(+) create mode 100644 app/src/main/res/layout/dialog_manual_cross_search.xml diff --git a/app/src/main/java/org/phenoapps/intercross/fragments/EventsFragment.kt b/app/src/main/java/org/phenoapps/intercross/fragments/EventsFragment.kt index 1eb3446..718bae9 100644 --- a/app/src/main/java/org/phenoapps/intercross/fragments/EventsFragment.kt +++ b/app/src/main/java/org/phenoapps/intercross/fragments/EventsFragment.kt @@ -10,10 +10,14 @@ import android.text.TextWatcher import android.util.TypedValue import android.view.* import android.view.inputmethod.EditorInfo +import android.widget.ArrayAdapter +import android.widget.AutoCompleteTextView import android.widget.EditText import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputLayout import androidx.core.content.ContextCompat import androidx.core.view.MenuProvider import androidx.core.widget.addTextChangedListener @@ -697,6 +701,11 @@ class EventsFragment : IntercrossBaseFragment(R.layout.fr } + fragmentEventsSearchButton.setOnLongClickListener { + showManualCrossSearchDialog() + true + } + saveButton.setOnClickListener { askUserNewExperimentName() @@ -714,6 +723,66 @@ class EventsFragment : IntercrossBaseFragment(R.layout.fr } } + private fun showManualCrossSearchDialog() { + + if (mEvents.isEmpty()) { + mSnackbar.push(SnackbarQueue.SnackJob(mBinding.root, getString(R.string.manual_cross_search_no_data))) + return + } + + val dialogView = layoutInflater.inflate(R.layout.dialog_manual_cross_search, null) + + val inputLayout = dialogView.findViewById(R.id.manual_cross_search_input_layout) + val inputView = dialogView.findViewById(R.id.manual_cross_search_input) + + val crossIds = mEvents.map { it.eventDbId } + .filter { it.isNotBlank() } + .distinct() + .sorted() + + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, crossIds) + inputView.setAdapter(adapter) + inputView.threshold = 0 + inputView.setOnClickListener { inputView.showDropDown() } + + val dialog = MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.manual_cross_search_title) + .setView(dialogView) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.go, null) + .create() + + dialog.setOnShowListener { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val enteredCrossId = inputView.text?.toString()?.trim().orEmpty() + + if (enteredCrossId.isBlank()) { + inputLayout.error = getString(R.string.manual_cross_search_required) + return@setOnClickListener + } + + val eventId = mEvents.firstOrNull { + it.eventDbId.equals(enteredCrossId, ignoreCase = true) + }?.id + + if (eventId == null) { + inputLayout.error = getString(R.string.manual_cross_search_not_found) + return@setOnClickListener + } + + inputLayout.error = null + dialog.dismiss() + findNavController().navigate(EventsFragmentDirections.actionToEventFragment(eventId)) + } + } + + inputView.setOnItemClickListener { _, _, _, _ -> + inputLayout.error = null + } + + dialog.show() + } + private fun FragmentEventsBinding.isInputValid(): Boolean { val maleFirst = mPref.getBoolean(mKeyUtil.crossOrderKey, false) diff --git a/app/src/main/res/layout/dialog_manual_cross_search.xml b/app/src/main/res/layout/dialog_manual_cross_search.xml new file mode 100644 index 0000000..4e99981 --- /dev/null +++ b/app/src/main/res/layout/dialog_manual_cross_search.xml @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e3258c..2ff6c80 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -673,4 +673,9 @@ Parents Settings + Find Cross + Cross ID + Please enter a cross ID. + No cross found for that ID. + No saved crosses are available to search. From 9581d137bee8e022e1ee8e840e58a5e93dca8b9d Mon Sep 17 00:00:00 2001 From: chaneylc Date: Fri, 13 Mar 2026 13:39:11 -0500 Subject: [PATCH 04/11] improved person management added optional person entry in main form --- .../intercross/fragments/EventsFragment.kt | 99 ++++++- .../fragments/preferences/ProfileFragment.kt | 274 +++++++++++++++--- .../org/phenoapps/intercross/util/KeyUtil.kt | 4 + .../res/drawable/account_group_outline.xml | 9 + app/src/main/res/drawable/form_dropdown.xml | 9 + app/src/main/res/layout/fragment_events.xml | 28 +- app/src/main/res/values/keys.xml | 4 + app/src/main/res/values/strings.xml | 15 + app/src/main/res/xml/profile_preferences.xml | 14 +- 9 files changed, 411 insertions(+), 45 deletions(-) create mode 100644 app/src/main/res/drawable/account_group_outline.xml create mode 100644 app/src/main/res/drawable/form_dropdown.xml diff --git a/app/src/main/java/org/phenoapps/intercross/fragments/EventsFragment.kt b/app/src/main/java/org/phenoapps/intercross/fragments/EventsFragment.kt index 718bae9..6d00cc7 100644 --- a/app/src/main/java/org/phenoapps/intercross/fragments/EventsFragment.kt +++ b/app/src/main/java/org/phenoapps/intercross/fragments/EventsFragment.kt @@ -20,6 +20,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputLayout import androidx.core.content.ContextCompat import androidx.core.view.MenuProvider +import androidx.core.view.isVisible import androidx.core.widget.addTextChangedListener import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels @@ -105,6 +106,8 @@ class EventsFragment : IntercrossBaseFragment(R.layout.fr private var mFocused: View? = null + private var mPersons: List = emptyList() + private val scope = CoroutineScope(Dispatchers.IO) @Inject @@ -183,6 +186,7 @@ class EventsFragment : IntercrossBaseFragment(R.layout.fr setupUI() + setupPersonInput() } override fun onResume() { @@ -193,6 +197,7 @@ class EventsFragment : IntercrossBaseFragment(R.layout.fr mBinding.bottomNavBar.selectedItemId = R.id.action_nav_home + setupPersonInput() } private fun setMenuItems() { @@ -321,8 +326,6 @@ class EventsFragment : IntercrossBaseFragment(R.layout.fr if (it.isNotBlank()) { - //Log.d("IntercrossNextScan", mFocused?.id.toString()) - when (mFocused?.id) { mBinding.firstText.id -> { @@ -532,6 +535,10 @@ class EventsFragment : IntercrossBaseFragment(R.layout.fr } } + if (mBinding.personTextHolder.isVisible) { + mBinding.personText.setText(selectedPersonFromPrefs(), false) + } + mBinding.firstText.requestFocus() } @@ -575,6 +582,8 @@ class EventsFragment : IntercrossBaseFragment(R.layout.fr firstText.addTextChangedListener(emptyGuard) editTextCross.addTextChangedListener(emptyGuard) + personText.addTextChangedListener(emptyGuard) + firstText.onFocusChangeListener = focusListener secondText.onFocusChangeListener = focusListener editTextCross.onFocusChangeListener = focusListener @@ -626,6 +635,18 @@ class EventsFragment : IntercrossBaseFragment(R.layout.fr false }) + personText.setOnEditorActionListener(TextView.OnEditorActionListener { _, i, _ -> + + if (i == EditorInfo.IME_ACTION_DONE) { + + askUserNewExperimentName() + + return@OnEditorActionListener true + } + + false + }) + } open class KeyboardToggleListener( @@ -841,6 +862,8 @@ class EventsFragment : IntercrossBaseFragment(R.layout.fr if (value.isNotBlank() && (male.isNotBlank() || blank) && female.isNotBlank()) { + persistPersonSelectionFromInput() + if (male.isBlank()) male = "blank" val crossIds = mEvents.map { event -> event.eventDbId } @@ -983,4 +1006,76 @@ class EventsFragment : IntercrossBaseFragment(R.layout.fr override fun onEventClick(eventId: Long) { findNavController().navigate(EventsFragmentDirections.actionToEventFragment(eventId)) } + + private fun setupPersonInput() { + val showPersonInput = mPref.getBoolean(mKeyUtil.profileShowPersonInputKey, false) + mBinding.personTextHolder.isVisible = showPersonInput + + if (!showPersonInput) { + return + } + + mPersons = loadPersons() + + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, mPersons) + mBinding.personText.setAdapter(adapter) + mBinding.personText.threshold = 0 + mBinding.personText.setOnClickListener { mBinding.personText.showDropDown() } + + val selectedPerson = selectedPersonFromPrefs() + if (selectedPerson.isNotBlank()) { + mBinding.personText.setText(selectedPerson, false) + } + } + + private fun loadPersons(): List { + return mPref.getStringSet(mKeyUtil.profilePersonListKey, emptySet()) + .orEmpty() + .map { it.trim() } + .filter { it.isNotBlank() } + .distinctBy { it.lowercase(Locale.getDefault()) } + .sortedBy { it.lowercase(Locale.getDefault()) } + } + + private fun selectedPersonFromPrefs(): String { + val selected = mPref.getString(mKeyUtil.profileSelectedPersonKey, "").orEmpty().trim() + if (selected.isNotBlank()) { + return selected + } + + val first = mPref.getString(mKeyUtil.personFirstNameKey, "").orEmpty().trim() + val last = mPref.getString(mKeyUtil.personLastNameKey, "").orEmpty().trim() + return "$first $last".trim() + } + + private fun persistPersonSelectionFromInput() { + if (!mBinding.personTextHolder.isVisible) { + return + } + + val entered = mBinding.personText.text?.toString()?.trim().orEmpty() + if (entered.isBlank()) { + return + } + + val updatedPersons = mPersons.toMutableList() + if (updatedPersons.none { it.equals(entered, ignoreCase = true) }) { + updatedPersons.add(entered) + updatedPersons.sortBy { it.lowercase(Locale.getDefault()) } + mPref.edit { + putStringSet(mKeyUtil.profilePersonListKey, updatedPersons.toSet()) + } + mPersons = updatedPersons + } + + val tokens = entered.split("\\s+".toRegex(), limit = 2) + val first = tokens.firstOrNull().orEmpty() + val last = if (tokens.size > 1) tokens[1] else "" + + mPref.edit { + putString(mKeyUtil.profileSelectedPersonKey, entered) + putString(mKeyUtil.personFirstNameKey, first) + putString(mKeyUtil.personLastNameKey, last) + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/fragments/preferences/ProfileFragment.kt b/app/src/main/java/org/phenoapps/intercross/fragments/preferences/ProfileFragment.kt index 645653b..3949356 100644 --- a/app/src/main/java/org/phenoapps/intercross/fragments/preferences/ProfileFragment.kt +++ b/app/src/main/java/org/phenoapps/intercross/fragments/preferences/ProfileFragment.kt @@ -2,16 +2,21 @@ package org.phenoapps.intercross.fragments.preferences import android.app.AlertDialog import android.os.Bundle -import android.view.View -import android.widget.EditText +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.AutoCompleteTextView import android.widget.LinearLayout +import android.widget.ListView +import android.widget.Toast +import androidx.core.content.edit import androidx.preference.Preference import org.phenoapps.intercross.R -import androidx.core.content.edit +import java.util.Locale class ProfileFragment : BasePreferenceFragment(R.xml.profile_preferences) { private var profilePerson: Preference? = null + private var profileManagePersons: Preference? = null private var profileReset: Preference? = null private var personDialog: AlertDialog? = null @@ -27,26 +32,27 @@ class ProfileFragment : BasePreferenceFragment(R.xml.profile_preferences) { mPrefs.edit { putLong(mKeyUtil.lastTimeAskedKey, System.nanoTime()) } profilePerson = findPreference(mKeyUtil.profilePersonKey) + profileManagePersons = findPreference(mKeyUtil.profileManagePersonsKey) profileReset = findPreference(mKeyUtil.profileResetKey) + migrateLegacyPersonIfNeeded() updatePersonSummary() - setPreferenceClickListeners() - val arguments = arguments - - if (arguments != null) { - val updatePerson = arguments.getBoolean(mKeyUtil.personUpdateKey, false) - - if (updatePerson) { - showPersonDialog() - } + val updatePerson = arguments?.getBoolean(mKeyUtil.personUpdateKey, false) == true + if (updatePerson) { + showPersonSelectionDialog() } } private fun setPreferenceClickListeners() { profilePerson?.setOnPreferenceClickListener { - showPersonDialog() + showPersonSelectionDialog() + true + } + + profileManagePersons?.setOnPreferenceClickListener { + showManagePersonsDialog() true } @@ -56,38 +62,167 @@ class ProfileFragment : BasePreferenceFragment(R.xml.profile_preferences) { } } - private fun showPersonDialog() { - val inflater = this.layoutInflater - val layout: View = inflater.inflate(R.layout.dialog_person, null) - val firstName = layout.findViewById(R.id.firstName) - val lastName = layout.findViewById(R.id.lastName) + private fun showPersonSelectionDialog() { + val persons = loadPersons() - firstName.setText(mPrefs.getString(mKeyUtil.personFirstNameKey, "")) - lastName.setText(mPrefs.getString(mKeyUtil.personLastNameKey, "")) + if (persons.isEmpty()) { + showManagePersonsDialog() + return + } - firstName.setSelectAllOnFocus(true) - lastName.setSelectAllOnFocus(true) + val current = selectedPerson() + var selectedIndex = persons.indexOfFirst { it.equals(current, ignoreCase = true) } val builder = AlertDialog.Builder(context) - .setTitle(R.string.profile_person_title) - .setCancelable(true) - .setView(layout) + .setTitle(R.string.profile_person_select_title) + .setSingleChoiceItems(persons.toTypedArray(), selectedIndex) { _, which -> + selectedIndex = which + } .setNegativeButton(getString(R.string.dialog_cancel)) { dialog, _ -> dialog.dismiss() } + .setNeutralButton(getString(R.string.profile_person_manage)) { _, _ -> + showManagePersonsDialog() + } .setPositiveButton(getString(R.string.dialog_save)) { _, _ -> - mPrefs.edit { - putString(mKeyUtil.personFirstNameKey, firstName.text.toString()) - putString(mKeyUtil.personLastNameKey, lastName.text.toString()) + if (selectedIndex in persons.indices) { + setSelectedPerson(persons[selectedIndex]) } - updatePersonSummary() } personDialog = builder.create() personDialog?.show() - val langParams = personDialog?.window?.attributes - langParams?.width = LinearLayout.LayoutParams.MATCH_PARENT - personDialog?.window?.attributes = langParams + val params = personDialog?.window?.attributes + params?.width = LinearLayout.LayoutParams.MATCH_PARENT + personDialog?.window?.attributes = params + } + + private fun showManagePersonsDialog() { + val persons = loadPersons().toMutableList() + + val container = LinearLayout(requireContext()).apply { + orientation = LinearLayout.VERTICAL + setPadding(32, 24, 32, 24) + } + + val input = AutoCompleteTextView(requireContext()).apply { + hint = getString(R.string.profile_person_input_hint) + isSingleLine = true + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + val listView = ListView(requireContext()).apply { + choiceMode = ListView.CHOICE_MODE_SINGLE + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + 420 + ) + } + + val inputAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, persons.toMutableList()) + val listAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_list_item_single_choice, persons.toMutableList()) + input.setAdapter(inputAdapter) + input.threshold = 0 + input.setOnClickListener { input.showDropDown() } + listView.adapter = listAdapter + + container.addView(input) + container.addView(listView) + + var selectedIndex = persons.indexOfFirst { it.equals(selectedPerson(), ignoreCase = true) } + if (selectedIndex >= 0) { + listView.setItemChecked(selectedIndex, true) + input.setText(persons[selectedIndex]) + input.setSelection(persons[selectedIndex].length) + } + + listView.setOnItemClickListener { _, _, position, _ -> + selectedIndex = position + val selected = persons[position] + input.setText(selected) + input.setSelection(selected.length) + } + + val dialog = AlertDialog.Builder(context) + .setTitle(R.string.profile_person_manage_title) + .setMessage(R.string.profile_person_manage_summary) + .setView(container) + .setNegativeButton(getString(R.string.dialog_cancel)) { d, _ -> d.dismiss() } + .setNeutralButton(getString(R.string.profile_person_remove), null) + .setPositiveButton(getString(R.string.add), null) + .create() + + fun refreshAdapters() { + val snapshot = persons.toList() + + inputAdapter.clear() + inputAdapter.addAll(snapshot) + inputAdapter.notifyDataSetChanged() + + listAdapter.clear() + listAdapter.addAll(snapshot) + listAdapter.notifyDataSetChanged() + } + + dialog.setOnShowListener { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val entered = input.text?.toString()?.trim().orEmpty() + if (entered.isBlank()) { + input.error = getString(R.string.profile_person_name_required) + return@setOnClickListener + } + + val existingIndex = persons.indexOfFirst { it.equals(entered, ignoreCase = true) } + if (existingIndex == -1) { + persons.add(entered) + persons.sortBy { it.lowercase(Locale.getDefault()) } + savePersons(persons) + refreshAdapters() + } + + selectedIndex = persons.indexOfFirst { it.equals(entered, ignoreCase = true) } + if (selectedIndex >= 0) { + listView.setItemChecked(selectedIndex, true) + setSelectedPerson(persons[selectedIndex]) + } + + updatePersonSummary() + } + + dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener { + if (selectedIndex !in persons.indices) { + Toast.makeText(requireContext(), getString(R.string.profile_person_not_found), Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + + val removedPerson = persons.removeAt(selectedIndex) + savePersons(persons) + + if (selectedPerson().equals(removedPerson, ignoreCase = true)) { + setSelectedPerson(persons.firstOrNull().orEmpty()) + } + + refreshAdapters() + + if (persons.isNotEmpty()) { + selectedIndex = selectedIndex.coerceAtMost(persons.lastIndex) + listView.setItemChecked(selectedIndex, true) + input.setText(persons[selectedIndex]) + input.setSelection(persons[selectedIndex].length) + } else { + selectedIndex = -1 + input.setText("") + } + + updatePersonSummary() + Toast.makeText(requireContext(), getString(R.string.profile_person_removed), Toast.LENGTH_SHORT).show() + } + } + + dialog.show() } private fun showClearSettingsDialog() { @@ -98,8 +233,11 @@ class ProfileFragment : BasePreferenceFragment(R.xml.profile_preferences) { .setPositiveButton(getString(R.string.dialog_yes)) { dialog, _ -> dialog.dismiss() mPrefs.edit { + putStringSet(mKeyUtil.profilePersonListKey, emptySet()) + putString(mKeyUtil.profileSelectedPersonKey, "") putString(mKeyUtil.personFirstNameKey, "") putString(mKeyUtil.personLastNameKey, "") + putBoolean(mKeyUtil.profileShowPersonInputKey, false) } updatePersonSummary() } @@ -108,18 +246,76 @@ class ProfileFragment : BasePreferenceFragment(R.xml.profile_preferences) { } private fun updatePersonSummary() { - profilePerson?.setSummary(personSummary()) + profilePerson?.summary = personSummary() } private fun personSummary(): String { - var tagName = "" + val selected = selectedPerson() + if (selected.isNotBlank()) { + return selected + } - val firstNameLength = mPrefs.getString(mKeyUtil.personFirstNameKey, "")?.length ?: 0 - val lastNameLength = mPrefs.getString(mKeyUtil.personLastNameKey, "")?.length ?: 0 + val first = mPrefs.getString(mKeyUtil.personFirstNameKey, "").orEmpty() + val last = mPrefs.getString(mKeyUtil.personLastNameKey, "").orEmpty() + return "$first $last".trim() + } + + private fun selectedPerson(): String = + mPrefs.getString(mKeyUtil.profileSelectedPersonKey, "").orEmpty().trim() + + private fun loadPersons(): List { + return mPrefs.getStringSet(mKeyUtil.profilePersonListKey, emptySet()) + .orEmpty() + .map { it.trim() } + .filter { it.isNotBlank() } + .distinctBy { it.lowercase(Locale.getDefault()) } + .sortedBy { it.lowercase(Locale.getDefault()) } + } + + private fun savePersons(persons: List) { + mPrefs.edit { + putStringSet(mKeyUtil.profilePersonListKey, persons.toSet()) + } + } + + private fun setSelectedPerson(person: String) { + val cleaned = person.trim() + val first: String + val last: String + + if (cleaned.isBlank()) { + first = "" + last = "" + } else { + val tokens = cleaned.split("\\s+".toRegex(), limit = 2) + first = tokens.firstOrNull().orEmpty() + last = if (tokens.size > 1) tokens[1] else "" + } + + mPrefs.edit { + putString(mKeyUtil.profileSelectedPersonKey, cleaned) + putString(mKeyUtil.personFirstNameKey, first) + putString(mKeyUtil.personLastNameKey, last) + } + } + + private fun migrateLegacyPersonIfNeeded() { + val first = mPrefs.getString(mKeyUtil.personFirstNameKey, "").orEmpty().trim() + val last = mPrefs.getString(mKeyUtil.personLastNameKey, "").orEmpty().trim() + val legacyPerson = "$first $last".trim() + + if (legacyPerson.isBlank()) { + return + } + + val currentPersons = loadPersons().toMutableList() + if (currentPersons.none { it.equals(legacyPerson, ignoreCase = true) }) { + currentPersons.add(legacyPerson) + savePersons(currentPersons) + } - if ((firstNameLength > 0) or (lastNameLength > 0)) { - tagName += mPrefs.getString(mKeyUtil.personFirstNameKey, "") + " " + mPrefs.getString(mKeyUtil.personLastNameKey, "") + if (selectedPerson().isBlank()) { + setSelectedPerson(legacyPerson) } - return tagName } } \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/util/KeyUtil.kt b/app/src/main/java/org/phenoapps/intercross/util/KeyUtil.kt index 5ab25c7..fb66b4f 100644 --- a/app/src/main/java/org/phenoapps/intercross/util/KeyUtil.kt +++ b/app/src/main/java/org/phenoapps/intercross/util/KeyUtil.kt @@ -50,7 +50,11 @@ class KeyUtil @Inject constructor( // person and experiment preferences val profileRootKey by key(R.string.root_profile) val profilePersonKey by key(R.string.key_pref_profile_person) + val profileManagePersonsKey by key(R.string.key_pref_profile_manage_persons) + val profileShowPersonInputKey by key(R.string.key_pref_profile_show_person_input) val profileResetKey by key(R.string.key_pref_profile_reset) + val profilePersonListKey by key(R.string.key_pref_profile_person_list) + val profileSelectedPersonKey by key(R.string.key_pref_profile_selected_person) val personFirstNameKey by key(R.string.key_pref_person_first_name) val personLastNameKey by key(R.string.key_pref_person_last_name) val experimentNameKey by key(R.string.key_pref_experiment_name) diff --git a/app/src/main/res/drawable/account_group_outline.xml b/app/src/main/res/drawable/account_group_outline.xml new file mode 100644 index 0000000..141eba5 --- /dev/null +++ b/app/src/main/res/drawable/account_group_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/form_dropdown.xml b/app/src/main/res/drawable/form_dropdown.xml new file mode 100644 index 0000000..36b21e0 --- /dev/null +++ b/app/src/main/res/drawable/form_dropdown.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_events.xml b/app/src/main/res/layout/fragment_events.xml index ba41e06..caea052 100644 --- a/app/src/main/res/layout/fragment_events.xml +++ b/app/src/main/res/layout/fragment_events.xml @@ -103,11 +103,35 @@ + + + + + + &package;PROFILE_PERSON + &package;PROFILE_MANAGE_PERSONS + &package;PROFILE_SHOW_PERSON_INPUT &package;PROFILE_RESET + &package;PROFILE_PERSON_LIST + &package;PROFILE_SELECTED_PERSON &package;FIRST_NAME &package;LAST_NAME &package;EXPERIMENT_NAME diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ff6c80..1ccf4b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -678,4 +678,19 @@ Please enter a cross ID. No cross found for that ID. No saved crosses are available to search. + + + Manage Persons + Show person field on Events screen + Select Person + Manage Persons + Person name + Type a name to add/update, or tap Remove to pick from your saved persons list. + Remove + Manage + Please enter a person name. + Person removed. + Person not found in list. + Person + Add diff --git a/app/src/main/res/xml/profile_preferences.xml b/app/src/main/res/xml/profile_preferences.xml index d704ef8..1aac8d7 100644 --- a/app/src/main/res/xml/profile_preferences.xml +++ b/app/src/main/res/xml/profile_preferences.xml @@ -1,8 +1,7 @@ + xmlns:android="http://schemas.android.com/apk/res/android"> + + + + From 9fe173dbe1d57030b915d3db3b273fd832b7bc7d Mon Sep 17 00:00:00 2001 From: chaneylc Date: Fri, 13 Mar 2026 16:35:06 -0500 Subject: [PATCH 05/11] parent fragment UI update --- .../intercross/adapters/BindingAdapters.kt | 13 +- .../intercross/adapters/ParentsAdapter.kt | 46 +++-- .../intercross/fragments/ParentsFragment.kt | 166 ++++++++++-------- app/src/main/res/layout/fragment_parents.xml | 112 ++++++++---- .../list_item_selectable_parent_row.xml | 158 ++++++++++++++--- app/src/main/res/values/strings.xml | 1 + 6 files changed, 346 insertions(+), 150 deletions(-) diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/BindingAdapters.kt b/app/src/main/java/org/phenoapps/intercross/adapters/BindingAdapters.kt index d262cdf..430722c 100644 --- a/app/src/main/java/org/phenoapps/intercross/adapters/BindingAdapters.kt +++ b/app/src/main/java/org/phenoapps/intercross/adapters/BindingAdapters.kt @@ -39,13 +39,14 @@ fun bindCrossTypeImage(view: ImageView, event: Event?) { @BindingAdapter("setQRCode") fun bindQRCodeImage(view: ImageView, code: String?) { - - code?.let { - - view.tag = code - - AsyncLoadBarcode(view, code).execute(code) + if (code.isNullOrBlank()) { + view.tag = null + view.setImageDrawable(null) + return } + + view.tag = code + AsyncLoadBarcode(view, code).execute(code) } @BindingAdapter("layoutMarginStart") diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/ParentsAdapter.kt b/app/src/main/java/org/phenoapps/intercross/adapters/ParentsAdapter.kt index d070ed8..32b1985 100644 --- a/app/src/main/java/org/phenoapps/intercross/adapters/ParentsAdapter.kt +++ b/app/src/main/java/org/phenoapps/intercross/adapters/ParentsAdapter.kt @@ -1,9 +1,9 @@ package org.phenoapps.intercross.adapters -import android.graphics.Color import android.view.LayoutInflater import android.view.ViewGroup import androidx.databinding.DataBindingUtil +import androidx.core.view.isVisible import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.phenoapps.intercross.R @@ -15,7 +15,8 @@ import org.phenoapps.intercross.data.viewmodels.PollenGroupListViewModel import org.phenoapps.intercross.databinding.ListItemSelectableParentRowBinding class ParentsAdapter(private val listModel: ParentsListViewModel, - private val groupModel: PollenGroupListViewModel) + private val groupModel: PollenGroupListViewModel, + private val crossCountProvider: (BaseParent) -> Int = { 0 }) : ListAdapter(BaseParent.Companion.DiffCallback()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -46,25 +47,36 @@ class ParentsAdapter(private val listModel: ParentsListViewModel, fun bind(p: BaseParent) { with(binding) { + val context = root.context + val isPollenGroup = p is PollenGroup + val crossCount = crossCountProvider(p) + val isSelected = when (p) { + is Parent -> p.selected + is PollenGroup -> p.selected + else -> false + } - if (p is Parent) { - - name = p.name - - checked = p.selected - - } else if (p is PollenGroup) { - - this.textField.setBackgroundColor(Color.parseColor("#42FF5722")) - - name = p.name + name = when (p) { + is Parent -> p.name + is PollenGroup -> p.name + else -> "" + } - checked = p.selected + code = when (p) { + is Parent -> p.codeId + is PollenGroup -> p.codeId + else -> null } - linearLayout.setOnClickListener { + checked = isSelected - doneCheckBox.isChecked=!doneCheckBox.isChecked + doneCheckBox.isChecked = isSelected + pollenGroupChip.isVisible = isPollenGroup + pollenGroupChip.text = context.getString(R.string.parent_pollen_group_chip) + crossCountChip.text = crossCount.toString() + + parentCard.setOnClickListener { + doneCheckBox.isChecked = !doneCheckBox.isChecked if (p is Parent) { @@ -82,6 +94,8 @@ class ParentsAdapter(private val listModel: ParentsListViewModel, }) } } + + executePendingBindings() } } } diff --git a/app/src/main/java/org/phenoapps/intercross/fragments/ParentsFragment.kt b/app/src/main/java/org/phenoapps/intercross/fragments/ParentsFragment.kt index 6fa1af9..0cda57d 100644 --- a/app/src/main/java/org/phenoapps/intercross/fragments/ParentsFragment.kt +++ b/app/src/main/java/org/phenoapps/intercross/fragments/ParentsFragment.kt @@ -6,7 +6,6 @@ import android.os.Build import android.os.Bundle import android.view.MenuItem import android.view.View -import android.view.WindowManager import android.widget.AdapterView import android.widget.TextView import android.widget.Toast @@ -16,7 +15,7 @@ import androidx.fragment.app.viewModels import androidx.navigation.Navigation import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.tabs.TabLayout +import com.google.android.material.chip.ChipGroup import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope @@ -82,6 +81,8 @@ class ParentsFragment: IntercrossBaseFragment(R.layout.f lateinit var mKeyUtil: KeyUtil private var mCrosses: List = ArrayList() + private var femaleCrossCounts: Map = emptyMap() + private var maleCrossCounts: Map = emptyMap() private lateinit var mMaleAdapter: ParentsAdapter @@ -126,6 +127,19 @@ class ParentsFragment: IntercrossBaseFragment(R.layout.f // } // } + private fun updateCrossCounts(events: List) { + femaleCrossCounts = events.groupingBy { it.femaleObsUnitDbId }.eachCount() + maleCrossCounts = events.groupingBy { it.maleObsUnitDbId }.eachCount() + + if (::mFemaleAdapter.isInitialized) { + mFemaleAdapter.notifyDataSetChanged() + } + + if (::mMaleAdapter.isInitialized) { + mMaleAdapter.notifyDataSetChanged() + } + } + override fun FragmentParentsBinding.afterCreateView() { val ctx = requireContext() @@ -137,8 +151,20 @@ class ParentsFragment: IntercrossBaseFragment(R.layout.f viewModel.updateSelection(0) groupList.updateSelection(0) - mMaleAdapter = ParentsAdapter(viewModel, groupList) - mFemaleAdapter = ParentsAdapter(viewModel, groupList) + mMaleAdapter = ParentsAdapter(viewModel, groupList) { parent -> + when (parent) { + is Parent -> maleCrossCounts[parent.codeId] ?: 0 + is PollenGroup -> maleCrossCounts[parent.codeId] ?: 0 + else -> 0 + } + } + mFemaleAdapter = ParentsAdapter(viewModel, groupList) { parent -> + when (parent) { + is Parent -> femaleCrossCounts[parent.codeId] ?: 0 + is PollenGroup -> femaleCrossCounts[parent.codeId] ?: 0 + else -> 0 + } + } femaleRecycler.adapter = mFemaleAdapter femaleRecycler.layoutManager = LinearLayoutManager(ctx) @@ -146,57 +172,40 @@ class ParentsFragment: IntercrossBaseFragment(R.layout.f maleRecycler.adapter = mMaleAdapter maleRecycler.layoutManager = LinearLayoutManager(ctx) - tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { - - override fun onTabReselected(tab: TabLayout.Tab?) { - } - - override fun onTabUnselected(tab: TabLayout.Tab?) { + filterChipGroup.setOnCheckedStateChangeListener { _: ChipGroup, checkedIds: List -> + when (checkedIds.firstOrNull()) { + R.id.filter_female -> { + femaleRecycler.visibility = View.VISIBLE + maleRecycler.visibility = View.GONE + fragParentsSelectAllCb.isChecked = !mNextFemaleSelection + } + R.id.filter_male -> { + maleRecycler.visibility = View.VISIBLE + femaleRecycler.visibility = View.GONE + fragParentsSelectAllCb.isChecked = !mNextMaleSelection + } } - - override fun onTabSelected(tab: TabLayout.Tab?) { - - tab?.let { - - when(it.text) { - - //TODO string resources - "Female" -> { - femaleRecycler.visibility=View.VISIBLE - maleRecycler.visibility=View.GONE - fragParentsSelectAllCb.isChecked = !mNextFemaleSelection - } - "Male" -> { - maleRecycler.visibility=View.VISIBLE - femaleRecycler.visibility=View.GONE - fragParentsSelectAllCb.isChecked = !mNextMaleSelection - } - } - - viewModel.parents.observe(viewLifecycleOwner) { parents -> - - groupList.groups.observe(viewLifecycleOwner) { groups -> - - mBinding.updateSelectionText( - parents.filter { it.selected }, - groups.filter { it.selected }) - - } - } + viewModel.parents.observe(viewLifecycleOwner) { parents -> + groupList.groups.observe(viewLifecycleOwner) { groups -> + mBinding.updateSelectionText( + parents.filter { it.selected }, + groups.filter { it.selected }) } } - }) + updateNoDataVisibility() + } /* - On startup, read arguments and determine the tab + On startup, select the chip matching the tabFocus argument (0 = female, 1 = male) */ - tabLayout.getTabAt(tabFocus)?.select() + if (tabFocus == 1) filterMale.isChecked = true eventsModel.events.observe(viewLifecycleOwner) { parents -> parents?.let { mCrosses = it + updateCrossCounts(it) } } @@ -232,6 +241,8 @@ class ParentsFragment: IntercrossBaseFragment(R.layout.f mBinding.updateSelectionText(parents.filter { it.selected }) + updateNoDataVisibility() + } fragParentsNewParentBtn.setOnClickListener { @@ -240,6 +251,14 @@ class ParentsFragment: IntercrossBaseFragment(R.layout.f } + fragParentsDeleteFab.setOnClickListener { + mBinding.deleteParents() + } + + fragParentsPrintFab.setOnClickListener { + mBinding.printParents() + } + fragParentsSelectAllCb.setOnClickListener { mBinding.selectAll() @@ -275,7 +294,7 @@ class ParentsFragment: IntercrossBaseFragment(R.layout.f private fun FragmentParentsBinding.updateSelectionText(parents: List, groups: List? = null) { - val selectedSex = if (tabLayout.getTabAt(0)?.isSelected != false) 0 else 1 + val selectedSex = if (filterFemale.isChecked) 0 else 1 var count = parents.count { it.sex == selectedSex } if (selectedSex == 1) count += groups?.count() ?: 0 @@ -297,21 +316,28 @@ class ParentsFragment: IntercrossBaseFragment(R.layout.f private fun updateMenuButtons(expanded: Boolean = false) { arrayOf(R.id.action_parents_delete, R.id.action_parents_print).forEach { - mBinding.fragParentsTb.menu?.findItem(it)?.isVisible = expanded - mBinding.fragParentsTb.invalidateMenu() + mBinding.fragParentsTb.menu?.findItem(it)?.isVisible = false } + mBinding.fragParentsTb.invalidateMenu() + mBinding.fragParentsDeleteFab.visibility = if (expanded) View.VISIBLE else View.GONE + mBinding.fragParentsPrintFab.visibility = if (expanded) View.VISIBLE else View.GONE } private fun FragmentParentsBinding.swipeLeft() { - - tabLayout.getTabAt(1)?.select() - + filterMale.isChecked = true } private fun FragmentParentsBinding.swipeRight() { + filterFemale.isChecked = true + } - tabLayout.getTabAt(0)?.select() - + private fun FragmentParentsBinding.updateNoDataVisibility() { + val currentListEmpty = if (filterFemale.isChecked) { + mFemaleAdapter.currentList.isEmpty() + } else { + mMaleAdapter.currentList.isEmpty() + } + noDataText.visibility = if (currentListEmpty) View.VISIBLE else View.GONE } /** @@ -319,7 +345,7 @@ class ParentsFragment: IntercrossBaseFragment(R.layout.f */ private fun FragmentParentsBinding.selectAll() { - if (tabLayout.getTabAt(0)?.isSelected == true) { + if (filterFemale.isChecked) { parentList.update( *(mFemaleAdapter.currentList @@ -375,7 +401,7 @@ class ParentsFragment: IntercrossBaseFragment(R.layout.f getString(R.string.frag_parent_confirm_delete_message) ) { - if (tabLayout.getTabAt(0)?.isSelected == true) { + if (filterFemale.isChecked) { val out: List = mFemaleAdapter.currentList.filterIsInstance() @@ -496,7 +522,9 @@ class ParentsFragment: IntercrossBaseFragment(R.layout.f private fun FragmentParentsBinding.printParents() { - if (tabLayout.getTabAt(0)?.isSelected == true) { + if (!checkBluetoothRuntimePermission()) return + + if (filterFemale.isChecked) { val outParents = mFemaleAdapter.currentList.filterIsInstance(Parent::class.java) @@ -504,27 +532,21 @@ class ParentsFragment: IntercrossBaseFragment(R.layout.f requireContext(), outParents.filter { p -> p.selected }.toTypedArray() ) + } else { + + val outParents = mMaleAdapter.currentList + .filterIsInstance() + .filter { p -> p.selected } + + val outAll = outParents + mMaleAdapter.currentList + .filterIsInstance() + .filter { p -> p.selected } + .map { group -> Parent(group.codeId, 1, group.name) } + + BluetoothUtil().print(requireContext(), outAll.toTypedArray()) } } -// override fun onCreate(savedInstanceState: Bundle?) { -// -// } else { -// -// val outParents = mMaleAdapter.currentList -// .filterIsInstance(Parent::class.java) -// .filter { p -> p.selected } -// -// val outAll = outParents + mMaleAdapter.currentList -// .filterIsInstance(PollenGroup::class.java) -// .filter { p -> p.selected } -// .map { group -> Parent(group.codeId, 1, group.name) } -// -// BluetoothUtil().print(requireContext(), outAll.toTypedArray()) -// -// } -// } - override fun onResume() { super.onResume() diff --git a/app/src/main/res/layout/fragment_parents.xml b/app/src/main/res/layout/fragment_parents.xml index 22b399a..3d6f068 100644 --- a/app/src/main/res/layout/fragment_parents.xml +++ b/app/src/main/res/layout/fragment_parents.xml @@ -33,68 +33,118 @@ - - - + app:singleSelection="true" + app:selectionRequired="true" + app:layout_constraintTop_toBottomOf="@id/frag_parents_tb" + app:layout_constraintStart_toStartOf="parent"> - + android:text="@string/female" + android:checked="true" + app:chipIcon="@drawable/ic_female_black_24dp" + app:chipIconSize="18dp"/> - + android:text="@string/male" + app:chipIcon="@drawable/ic_male_black_24dp" + app:chipIconSize="18dp"/> + + - + + + + + + + - - - + + + - - - - - + + - - + android:paddingStart="10dp" + android:paddingTop="10dp" + android:paddingEnd="10dp" + android:paddingBottom="10dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ccf4b9..efb633b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -114,6 +114,7 @@ Parents table is empty. No crosses have been made. Parent does not exist as a cross event. + Pollen group Go Cancel From 924342ae52d171542749ba5a11386f18f533be94 Mon Sep 17 00:00:00 2001 From: chaneylc Date: Mon, 16 Mar 2026 16:20:36 -0500 Subject: [PATCH 06/11] fixed parent chip selection --- app/src/main/res/layout/fragment_parents.xml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/fragment_parents.xml b/app/src/main/res/layout/fragment_parents.xml index 3d6f068..31f5a8c 100644 --- a/app/src/main/res/layout/fragment_parents.xml +++ b/app/src/main/res/layout/fragment_parents.xml @@ -51,7 +51,9 @@ android:text="@string/female" android:checked="true" app:chipIcon="@drawable/ic_female_black_24dp" - app:chipIconSize="18dp"/> + app:chipIconSize="18dp" + app:chipIconVisible="true" + style="@style/Widget.MaterialComponents.Chip.Choice"/> + app:chipIconSize="18dp" + app:chipIconVisible="true" + style="@style/Widget.MaterialComponents.Chip.Choice"/> From 7adbc9abb7fbcb423f3bc29bf22e83ee5479c240 Mon Sep 17 00:00:00 2001 From: chaneylc Date: Tue, 17 Mar 2026 11:17:26 -0500 Subject: [PATCH 07/11] added chip to parents list item showing sex --- .../intercross/adapters/ParentsAdapter.kt | 10 ++++++++++ .../layout/list_item_selectable_parent_row.xml | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/ParentsAdapter.kt b/app/src/main/java/org/phenoapps/intercross/adapters/ParentsAdapter.kt index 32b1985..9bc5a69 100644 --- a/app/src/main/java/org/phenoapps/intercross/adapters/ParentsAdapter.kt +++ b/app/src/main/java/org/phenoapps/intercross/adapters/ParentsAdapter.kt @@ -1,11 +1,14 @@ package org.phenoapps.intercross.adapters import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import androidx.appcompat.content.res.AppCompatResources import androidx.databinding.DataBindingUtil import androidx.core.view.isVisible import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import androidx.sqlite.throwSQLiteException import org.phenoapps.intercross.R import org.phenoapps.intercross.data.models.BaseParent import org.phenoapps.intercross.data.models.Parent @@ -75,6 +78,13 @@ class ParentsAdapter(private val listModel: ParentsListViewModel, pollenGroupChip.text = context.getString(R.string.parent_pollen_group_chip) crossCountChip.text = crossCount.toString() + crossCountChip.visibility = if (crossCount > 0) View.VISIBLE else View.GONE + + parentTypeChip.chipIcon = when((p as? Parent)?.sex) { + 0 -> AppCompatResources.getDrawable(context, R.drawable.ic_female_black_24dp) + else -> AppCompatResources.getDrawable(context, R.drawable.ic_male_black_24dp) + } + parentCard.setOnClickListener { doneCheckBox.isChecked = !doneCheckBox.isChecked diff --git a/app/src/main/res/layout/list_item_selectable_parent_row.xml b/app/src/main/res/layout/list_item_selectable_parent_row.xml index 0d458fe..fd7d589 100644 --- a/app/src/main/res/layout/list_item_selectable_parent_row.xml +++ b/app/src/main/res/layout/list_item_selectable_parent_row.xml @@ -110,7 +110,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="4dp" - app:constraint_referenced_ids="pollenGroupChip,crossCountChip" + app:constraint_referenced_ids="parentTypeChip,pollenGroupChip,crossCountChip" app:flow_horizontalAlign="start" app:flow_horizontalBias="0" app:flow_horizontalGap="6dp" @@ -120,6 +120,17 @@ app:layout_constraintStart_toStartOf="@id/headerLayout" app:layout_constraintTop_toBottomOf="@id/headerLayout" /> + + + tools:text="4"/> From b02c29e9c63baefe022b027cc809a05df30dcc40 Mon Sep 17 00:00:00 2001 From: chaneylc Date: Tue, 17 Mar 2026 12:16:43 -0500 Subject: [PATCH 08/11] updated process to add bulk males into two steps --- .../intercross/activities/MainActivity.kt | 3 +- .../fragments/ParentCreatorFragment.kt | 212 ++++++++++++++++++ .../fragments/PollenManagerFragment.kt | 186 +++------------ .../res/layout/fragment_parent_creator.xml | 114 ++++++++++ .../res/layout/fragment_pollen_manager.xml | 72 +----- app/src/main/res/navigation/navigation.xml | 71 ++++-- app/src/main/res/values/strings.xml | 3 + 7 files changed, 427 insertions(+), 234 deletions(-) create mode 100644 app/src/main/java/org/phenoapps/intercross/fragments/ParentCreatorFragment.kt create mode 100644 app/src/main/res/layout/fragment_parent_creator.xml diff --git a/app/src/main/java/org/phenoapps/intercross/activities/MainActivity.kt b/app/src/main/java/org/phenoapps/intercross/activities/MainActivity.kt index e9a5d29..70cd7d5 100644 --- a/app/src/main/java/org/phenoapps/intercross/activities/MainActivity.kt +++ b/app/src/main/java/org/phenoapps/intercross/activities/MainActivity.kt @@ -680,7 +680,8 @@ class MainActivity : AppCompatActivity(), SearchPreferenceResultListener { } doubleBackToExitPressedOnce = true - Toast.makeText(this@MainActivity, "Press back again to exit", Toast.LENGTH_SHORT).show() + Toast.makeText(this@MainActivity, + getString(R.string.press_back_again_to_exit), Toast.LENGTH_SHORT).show() Handler(Looper.getMainLooper()).postDelayed( { doubleBackToExitPressedOnce = false }, diff --git a/app/src/main/java/org/phenoapps/intercross/fragments/ParentCreatorFragment.kt b/app/src/main/java/org/phenoapps/intercross/fragments/ParentCreatorFragment.kt new file mode 100644 index 0000000..18c8b37 --- /dev/null +++ b/app/src/main/java/org/phenoapps/intercross/fragments/ParentCreatorFragment.kt @@ -0,0 +1,212 @@ +package org.phenoapps.intercross.fragments + +import androidx.appcompat.app.AlertDialog +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.navigation.findNavController +import androidx.navigation.fragment.navArgs +import org.phenoapps.intercross.R +import org.phenoapps.intercross.activities.MainActivity +import org.phenoapps.intercross.adapters.ParentsAdapter +import org.phenoapps.intercross.data.EventsRepository +import org.phenoapps.intercross.data.ParentsRepository +import org.phenoapps.intercross.data.PollenGroupRepository +import org.phenoapps.intercross.data.models.Event +import org.phenoapps.intercross.data.models.Parent +import org.phenoapps.intercross.data.viewmodels.EventListViewModel +import org.phenoapps.intercross.data.viewmodels.ParentsListViewModel +import org.phenoapps.intercross.data.viewmodels.PollenGroupListViewModel +import org.phenoapps.intercross.data.viewmodels.factory.EventsListViewModelFactory +import org.phenoapps.intercross.data.viewmodels.factory.ParentsListViewModelFactory +import org.phenoapps.intercross.data.viewmodels.factory.PollenGroupListViewModelFactory +import org.phenoapps.intercross.databinding.FragmentParentCreatorBinding +import org.phenoapps.intercross.util.Dialogs +import java.util.* + +class ParentCreatorFragment : IntercrossBaseFragment(R.layout.fragment_parent_creator) { + + private val args: ParentCreatorFragmentArgs by navArgs() + + private lateinit var mAdapter: ParentsAdapter + + private var mEvents: List = ArrayList() + + private var mParents: List = ArrayList() + + private var mMales: List = ArrayList() + + private val eventList: EventListViewModel by viewModels { + + EventsListViewModelFactory( + EventsRepository.getInstance(db.eventsDao())) + } + + private val parentList: ParentsListViewModel by viewModels { + + ParentsListViewModelFactory( + ParentsRepository.getInstance(db.parentsDao()) + ) + } + + private val groupList: PollenGroupListViewModel by viewModels { + + PollenGroupListViewModelFactory( + PollenGroupRepository.getInstance(db.pollenGroupDao()) + ) + } + + override fun FragmentParentCreatorBinding.afterCreateView() { + + (activity as? MainActivity)?.applyBottomInsets(root) + + (activity as MainActivity).setBackButtonToolbar() + (activity as MainActivity).supportActionBar?.apply { + title = getString(R.string.frag_parents_new_parent_title) + show() + } + + parentList.updateSelection(0) + + groupList.updateSelection(0) + + /*** + * When the safe args = 0 we are creating females, otherwise we are creating males/groups + */ + //an error is shown when a barcode already exists in the database + val error = getString(R.string.ErrorCodeExists) + + parentList.parents.observe(viewLifecycleOwner, Observer { parents -> + + parents?.let { + + mParents = it + + } + }) + + if (args.mode == 1) { + + mAdapter = ParentsAdapter(parentList, groupList) + + parentList.males.observe(viewLifecycleOwner, Observer { + + it?.let { males -> + + mMales = males + + } + + updateButtonText() + }) + + mode = args.mode + + bulkCheckBox.setOnCheckedChangeListener { _, checked -> + + updateButtonText(checked) + } + } + + /** + * Error check, ensure that the entered code does not exist in the events table. + */ + eventList.events.observe(viewLifecycleOwner, Observer { + + it?.let { + + mEvents = it + + codeEditText.addTextChangedListener { + + val codes = mEvents.map { event -> event.eventDbId } + mParents.map { parent -> parent.codeId }.distinct() + + if (codeEditText.text.toString() in codes) { + + if (codeTextHolder.error == null) codeTextHolder.error = error + + } else codeTextHolder.error = null + + } + } + }) + + codeEditText.setText(UUID.randomUUID().toString()) + + newButton.setOnClickListener { + + val codeText = codeEditText.text.toString() + + if (codeText.isNotBlank()) { + + var readableName = nameEditText.text.toString() + + if (readableName.isBlank()) { + + readableName = codeText + + } + + if (mParents.any { parent -> parent.codeId == codeEditText.text.toString() }) { + + Dialogs.notify(AlertDialog.Builder(requireContext()), getString(R.string.parent_already_exists)) + + } else when (args.mode) { + /** + * Insert a single female into the database based on the data entry. + */ + 0 -> { + + parentList.insert(Parent(codeText, 0, readableName)) + + mBinding.root.findNavController().navigate( + PollenManagerFragmentDirections + .actionReturnToParentsFragment(0)) + } + 1 -> { + + if (bulkCheckBox.isChecked) { + mBinding.root.findNavController().navigate( + ParentCreatorFragmentDirections + .actionToPollenManagerFragment(codeText, readableName)) + } else { + + parentList.insert(Parent(codeText, 1, readableName)) + + mBinding.root.findNavController().navigate( + PollenManagerFragmentDirections + .actionReturnToParentsFragment(1)) + } + } + } + } else { + + Dialogs.notify(AlertDialog.Builder(requireContext()), getString(R.string.cross_id_cannot_be_blank)) + + } + } + } + + private fun FragmentParentCreatorBinding.updateButtonText(isBulk: Boolean = false) { + + if (isAdded) { + + val act = requireActivity() + + newButton.text = + when { + isBulk -> { + + act.getString(R.string.add_male_group) + + } + args.mode == 0 -> { + + act.getString(R.string.add_female) + + } + else -> act.getString(R.string.add_male) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/fragments/PollenManagerFragment.kt b/app/src/main/java/org/phenoapps/intercross/fragments/PollenManagerFragment.kt index e16aa33..39fe621 100644 --- a/app/src/main/java/org/phenoapps/intercross/fragments/PollenManagerFragment.kt +++ b/app/src/main/java/org/phenoapps/intercross/fragments/PollenManagerFragment.kt @@ -1,7 +1,6 @@ package org.phenoapps.intercross.fragments import androidx.appcompat.app.AlertDialog -import androidx.core.widget.addTextChangedListener import androidx.fragment.app.viewModels import androidx.lifecycle.Observer import androidx.navigation.findNavController @@ -10,21 +9,16 @@ import androidx.recyclerview.widget.LinearLayoutManager import org.phenoapps.intercross.R import org.phenoapps.intercross.activities.MainActivity import org.phenoapps.intercross.adapters.ParentsAdapter -import org.phenoapps.intercross.data.EventsRepository import org.phenoapps.intercross.data.ParentsRepository import org.phenoapps.intercross.data.PollenGroupRepository -import org.phenoapps.intercross.data.models.Event import org.phenoapps.intercross.data.models.Parent import org.phenoapps.intercross.data.models.PollenGroup -import org.phenoapps.intercross.data.viewmodels.EventListViewModel import org.phenoapps.intercross.data.viewmodels.ParentsListViewModel import org.phenoapps.intercross.data.viewmodels.PollenGroupListViewModel -import org.phenoapps.intercross.data.viewmodels.factory.EventsListViewModelFactory import org.phenoapps.intercross.data.viewmodels.factory.ParentsListViewModelFactory import org.phenoapps.intercross.data.viewmodels.factory.PollenGroupListViewModelFactory import org.phenoapps.intercross.databinding.FragmentPollenManagerBinding import org.phenoapps.intercross.util.Dialogs -import java.util.* class PollenManagerFragment : IntercrossBaseFragment(R.layout.fragment_pollen_manager) { @@ -32,20 +26,8 @@ class PollenManagerFragment : IntercrossBaseFragment = ArrayList() - - private var mParents: List = ArrayList() - private var mMales: List = ArrayList() - private var mGroups: List = ArrayList() - - private val eventList: EventListViewModel by viewModels { - - EventsListViewModelFactory( - EventsRepository.getInstance(db.eventsDao())) - } - private val parentList: ParentsListViewModel by viewModels { ParentsListViewModelFactory( @@ -74,89 +56,39 @@ class PollenManagerFragment : IntercrossBaseFragment + parentList.males.observe(viewLifecycleOwner, Observer { - parents?.let { + it?.let { males -> - mParents = it + mMales = males - } - }) + groupList.groups.observeForever { groups -> - if (args.mode == 1) { + groups?.let { _ -> - mAdapter = ParentsAdapter(parentList, groupList) - - parentList.males.observe(viewLifecycleOwner, Observer { - - it?.let { males -> - - mMales = males - - groupList.groups.observeForever { groups -> - - groups?.let { gs -> - - mGroups = gs - /** - * Transform polycrosses to simple parent object before submitting to parent adapter. - */ - mAdapter.submitList(males.distinctBy { m -> m.codeId }) + /** + * Transform poly crosses to simple parent object before submitting to parent adapter. + */ + mAdapter.submitList(males.distinctBy { m -> m.codeId }) - updateButtonText() - } } } - - updateButtonText() - }) - - recyclerView.layoutManager = LinearLayoutManager(requireContext()) - - recyclerView.adapter = mAdapter - - mode = args.mode - - } - - /** - * Error check, ensure that the entered code does not exist in the events table. - */ - eventList.events.observe(viewLifecycleOwner, Observer { - - it?.let { - - mEvents = it - - codeEditText.addTextChangedListener { - - val codes = mEvents.map { event -> event.eventDbId } + mParents.map { parent -> parent.codeId }.distinct() - - if (codeEditText.text.toString() in codes) { - - if (codeTextHolder.error == null) codeTextHolder.error = error - - } else codeTextHolder.error = null - - } } }) - codeEditText.setText(UUID.randomUUID().toString()) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + + recyclerView.adapter = mAdapter newButton.setOnClickListener { - val codeText = codeEditText.text.toString() + val codeText = args.codeId if (codeText.isNotBlank()) { - var readableName = nameEditText.text.toString() + var readableName = args.readableName if (readableName.isBlank()) { @@ -164,58 +96,36 @@ class PollenManagerFragment : IntercrossBaseFragment parent.codeId == codeEditText.text.toString() }) { + val addedMales = ArrayList() - Dialogs.notify(AlertDialog.Builder(requireContext()), getString(R.string.parent_already_exists)) + for (p: Parent in mMales) { - } else when (args.mode) { - /** - * Insert a single female into the database based on the data entry. - */ - 0 -> { + if (p.selected) { - parentList.insert(Parent(codeText, 0, readableName)) + p.id?.let { id -> - mBinding.root.findNavController().navigate( - PollenManagerFragmentDirections - .actionReturnToParentsFragment(0)) + addedMales.add(buildGroup(id)) + } } - /** - * Either enter a group if a list is selected, or a single male with the - * entered data - */ - 1 -> { - val addedMales = ArrayList() - - for (p: Parent in mMales) { + } - if (p.selected) { + /*** + * check if list has been created, otherwise insert a single male + */ + if (addedMales.isEmpty()) { - p.id?.let { id -> + parentList.insert(Parent(codeText, 1, readableName)) - addedMales.add(buildGroup(id)) - } - } - } + } else { - /*** - * check if list has been created, otherwise insert a single male - */ - if (addedMales.isEmpty()) { + groupList.insert(*addedMales.toTypedArray()) - parentList.insert(Parent(codeText, 1, readableName)) - - } else { - - groupList.insert(*addedMales.toTypedArray()) + } - } + mBinding.root.findNavController().navigate( + PollenManagerFragmentDirections + .actionReturnToParentsFragment(1)) - mBinding.root.findNavController().navigate( - PollenManagerFragmentDirections - .actionReturnToParentsFragment(1)) - } - } } else { Dialogs.notify(AlertDialog.Builder(requireContext()), getString(R.string.cross_id_cannot_be_blank)) @@ -224,36 +134,10 @@ class PollenManagerFragment : IntercrossBaseFragment male.selected } -> { - - act.getString(R.string.add_male_group) - - } - args.mode == 0 -> { - - act.getString(R.string.add_female) - - } - else -> act.getString(R.string.add_male) - } - } - - } - /** * This function initializes and returns a PollenGroup object with the elements of the UI. */ - private fun FragmentPollenManagerBinding.buildGroup(id: Long) = - PollenGroup(codeEditText.text.toString(), - nameEditText.text.toString(), id) - + private fun buildGroup(id: Long) = + PollenGroup(args.codeId, args.readableName, id) } diff --git a/app/src/main/res/layout/fragment_parent_creator.xml b/app/src/main/res/layout/fragment_parent_creator.xml new file mode 100644 index 0000000..79ef886 --- /dev/null +++ b/app/src/main/res/layout/fragment_parent_creator.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +