diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d534f814..77eeaac0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -183,6 +183,11 @@ android:launchMode="singleTop" android:screenOrientation="portrait" /> + + + if (isChecked && this.model.selectedTimeUnit != TimeUnit.DAY) { + this.model.selectedTimeUnit = TimeUnit.DAY + this.launch() + } + } + + this.binding.monthUnit.setOnCheckedChangeListener { _, isChecked -> + if (isChecked && this.model.selectedTimeUnit != TimeUnit.MONTH) { + this.model.selectedTimeUnit = TimeUnit.MONTH + this.launch() + } + } + + } + + private fun initData() { + this.launch() + } + + private fun launch() { + this.model.buildDataStructure() + this.dataAdapter.notifyDataSetChanged() } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/GridCalendarViewModel.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/GridCalendarViewModel.kt new file mode 100644 index 00000000..f123087e --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/GridCalendarViewModel.kt @@ -0,0 +1,223 @@ +package net.pokeranalytics.android.ui.modules.calendar + +import androidx.lifecycle.ViewModel +import io.realm.Realm +import net.pokeranalytics.android.R +import net.pokeranalytics.android.exceptions.PAIllegalStateException +import net.pokeranalytics.android.model.filter.Query +import net.pokeranalytics.android.model.filter.QueryCondition +import net.pokeranalytics.android.model.realm.Session +import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowViewType +import net.pokeranalytics.android.util.extensions.* +import java.util.* +import kotlin.collections.HashMap + +class GridCalendarViewModel : ViewModel(), RowRepresentableDataSource { + + var selectedTimeUnit: TimeUnit = TimeUnit.DAY + + private var groups: TreeSet = TreeSet() + + private var results: HashMap = HashMap() + + private fun addGroup(date: Date) { + this.groups.add(date) + } + + private fun result(index: Int): CalendarItemCell { + val group = this.groups.elementAt(index) + return this.results[group] ?: throw PAIllegalStateException("item not found") + } + + private fun addResult(group: Date, result: CellResult, unit: TimeUnit) { + this.addGroup(group) + this.results[group]?.add(result) ?: run { + this.results[group] = CalendarItemCell(group, result, unit) + } + + } + + private fun clear() { + this.groups.clear() + this.results.clear() + } + + fun buildDataStructure() { + +// Timber.d(">>> Start buildDataStructure: ${this.selectedTimeUnit}") + + this.clear() + + val realm = Realm.getDefaultInstance() + val query = Query(QueryCondition.DateNotNull, QueryCondition.EndDateNotNull) + val sessions = realm.findAll(query, "startDate") + + val groupedSessions = when (this.selectedTimeUnit) { + TimeUnit.DAY -> sessions.groupBy { it.startDate!!.startOfDay() } + TimeUnit.MONTH -> sessions.groupBy { it.startDate!!.startOfMonth() } + } + + val firstDate = sessions.firstOrNull()?.startDate?.startOf(this.selectedTimeUnit.groupingUnit) + val lastDate = sessions.lastOrNull()?.startDate?.endOf(this.selectedTimeUnit.groupingUnit) + +// Timber.d("f = $firstDate, e = $lastDate") + + if (firstDate != null && lastDate != null) { + var tmpDate: Date = firstDate.startOfDay() + + val calendar = Calendar.getInstance() + while (tmpDate.time <= lastDate.time) { + + val result = groupedSessions[tmpDate]?.let { bucket -> + if (bucket.sumByDouble { + it.result?.net ?: 0.0 + } > 0.0) CellResult.POSITIVE else CellResult.NEGATIVE + } ?: run { + CellResult.EMPTY + } + + val groupingDate = tmpDate.startOf(this.selectedTimeUnit.groupingUnit) + this.addResult(groupingDate, result, this.selectedTimeUnit) + + // change tmp date + calendar.time = tmpDate + calendar.add(selectedTimeUnit.calendarUnit, 1) + tmpDate = calendar.time + } + + } + + } + + // RowRepresentableDataSource + + override fun adapterRows(): List { + return this.groups.descendingSet().map { this.results[it]!! } + } + + override fun rowRepresentableForPosition(position: Int): RowRepresentable { + return this.result(position) + } + + override fun numberOfRows(): Int { + return this.results.size + } + + override fun viewTypeForPosition(position: Int): Int { + return RowViewType.CALENDAR_GRID_CELL.ordinal + } + +} + +class CalendarItemCell(var date: Date, result: CellResult, var unit: TimeUnit) : RowRepresentable, RowRepresentableDataSource { + + private var results: MutableList = mutableListOf(result) + private set + + fun add(result: CellResult) { + this.results.add(result) + } + + val size: Int = this.results.size + + override val viewType: Int = RowViewType.CALENDAR_GRID_CELL.ordinal + + val title: String + get() { + return when (unit) { + TimeUnit.DAY -> date.getMonthAndYear() + TimeUnit.MONTH -> date.getDateYear() + } + } + + // RowRepresentableDataSource + + override fun adapterRows(): List { + return this.results + } + + override fun rowRepresentableForPosition(position: Int): RowRepresentable { + return this.results[position] + } + + override fun numberOfRows(): Int { + return this.results.size + } + + override fun viewTypeForPosition(position: Int): Int { + return RowViewType.CALENDAR_TIME_UNIT_CELL.ordinal + } + +} + +enum class TimeUnit { + DAY, + MONTH; + + val calendarUnit: Int + get() { + return when (this) { + DAY -> Calendar.DAY_OF_MONTH + MONTH -> Calendar.MONTH + } + } + + val groupingUnit: Int + get() { + return when (this) { + DAY -> Calendar.MONTH + MONTH -> Calendar.YEAR + } + } + + val spanCount: Int + get() { + return when (this) { + DAY -> 7 + MONTH -> 4 + } + } + +} + +enum class CellResult : RowRepresentable { + POSITIVE, + NEGATIVE, + EMPTY; + + val background: Int + get() { + return when (this) { + POSITIVE -> R.drawable.rounded_green_rect + NEGATIVE -> R.drawable.rounded_red_rect + EMPTY -> R.drawable.rounded_grey_rect + } + } + + override val viewType: Int = RowViewType.CALENDAR_TIME_UNIT_CELL.ordinal + +} + +private fun Date.startOf(unit: Int): Date { + + return when (unit) { + Calendar.DAY_OF_MONTH -> this.startOfDay() + Calendar.MONTH -> this.startOfMonth() + Calendar.YEAR -> this.startOfYear() + else -> throw PAIllegalStateException("Un-managed case: $unit") + } + +} + +private fun Date.endOf(unit: Int): Date { + + return when (unit) { + Calendar.DAY_OF_MONTH -> this.endOfDay() + Calendar.MONTH -> this.endOfMonth() + Calendar.YEAR -> this.endOfYear() + else -> throw PAIllegalStateException("Un-managed case: $unit") + } + +} diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/views/KeyboardActionView.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/views/KeyboardActionView.kt index 6ea9db84..447ab4cd 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/views/KeyboardActionView.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/views/KeyboardActionView.kt @@ -41,12 +41,12 @@ class KeyboardActionView(context: Context) : AbstractKeyboardView(context), // Action recycler val spanCount = 3 - val viewManager = GridLayoutManager(context, spanCount) this.dataAdapter = RowRepresentableAdapter(this, this) val spacing = 2.px val includeEdge = false + val viewManager = GridLayoutManager(context, spanCount) actionRecyclerView.apply { setHasFixedSize(true) layoutManager = viewManager diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt index 43886371..29d3fa71 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt @@ -2,7 +2,6 @@ package net.pokeranalytics.android.ui.view import android.content.Context import net.pokeranalytics.android.model.LiveData -import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.util.NULL_TEXT @@ -14,6 +13,7 @@ interface RowRepresentable : Displayable, EditDataSource, ImageDecorator { fun getDisplayName(context: Context): String { return NULL_TEXT } + } interface EditDataSource { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt index d357fcb1..5bbd4ca1 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt @@ -7,14 +7,18 @@ import android.widget.FrameLayout import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatTextView import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.charts.LineChart import com.github.mikephil.charting.data.* import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup +import kotlinx.android.synthetic.main.cell_calendar_time_unit.view.* +import kotlinx.android.synthetic.main.row_recycler.view.* import net.pokeranalytics.android.R import net.pokeranalytics.android.calculus.ComputedStat import net.pokeranalytics.android.calculus.Stat @@ -27,12 +31,16 @@ import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.ui.adapter.BindableHolder import net.pokeranalytics.android.ui.adapter.RecyclerAdapter +import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.extensions.ChipGroupExtension +import net.pokeranalytics.android.ui.extensions.dp import net.pokeranalytics.android.ui.extensions.px import net.pokeranalytics.android.ui.extensions.setTextFormat import net.pokeranalytics.android.ui.graph.Graph import net.pokeranalytics.android.ui.modules.bankroll.BankrollRowRepresentable import net.pokeranalytics.android.ui.graph.setStyle +import net.pokeranalytics.android.ui.modules.calendar.CalendarItemCell +import net.pokeranalytics.android.ui.modules.calendar.CellResult import net.pokeranalytics.android.ui.modules.handhistory.views.RowHandHistoryViewHolder import net.pokeranalytics.android.ui.view.holder.RowViewHolder import net.pokeranalytics.android.ui.view.rows.* @@ -86,6 +94,8 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier { ROW_PLAYER(R.layout.row_player), ROW_PLAYER_IMAGE(R.layout.row_player_image), HAND_HISTORY(R.layout.row_hand_history_view), + CALENDAR_GRID_CELL(R.layout.cell_calendar_grid), + CALENDAR_TIME_UNIT_CELL(R.layout.cell_calendar_time_unit), // ROW_HAND_ACTION(R.layout.row_hand_action), // ROW_HAND_STREET(R.layout.row_hand_cards), @@ -147,6 +157,9 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier { // ROW_HAND_ACTION -> RowHandAction(layout) // ROW_HAND_STREET -> RowHandStreet(layout) + CALENDAR_GRID_CELL -> CalendarGridCellHolder(layout) + CALENDAR_TIME_UNIT_CELL -> CalendarTimeUnitCellHolder(layout) + // Separator SEPARATOR -> SeparatorViewHolder(layout) @@ -526,7 +539,6 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier { } } - /** * Display a player image view */ @@ -569,7 +581,6 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier { } } - /** * Display a separator */ @@ -579,4 +590,94 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier { } } + inner class CalendarGridCellHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + BindableHolder { + + private var spanCount = 7 + + init { + + itemView.findViewById(R.id.recyclerView)?.let { recyclerView -> + +// val spanCount = row.unit.spanCount + val spacing = 4.px + val includeEdge = true + + val viewManager = object : GridLayoutManager(itemView.context, spanCount) { + override fun checkLayoutParams(lp: RecyclerView.LayoutParams?): Boolean { + val side = width / this.spanCount - 4.px + lp?.let { params -> + params.width = side + params.height = side + } + return true + } + } + + recyclerView.apply { + setHasFixedSize(true) + layoutManager = viewManager + addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, includeEdge)) + } + + } + } + + private fun setLayoutManager(spanCount: Int) { + itemView.findViewById(R.id.recyclerView)?.let { recyclerView -> + + val viewManager = object : GridLayoutManager(itemView.context, spanCount) { + override fun checkLayoutParams(lp: RecyclerView.LayoutParams?): Boolean { + val side = width / spanCount - 4.px + lp?.let { params -> + params.width = side + params.height = side + } + return true + } + } + recyclerView.layoutManager = viewManager + } + } + + override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { + + if (row is CalendarItemCell) { + val sc = row.unit.spanCount + + if (this.spanCount != sc) { + setLayoutManager(sc) + this.spanCount = sc + } + + itemView.findViewById(R.id.recyclerView)?.let { recyclerView -> + if (recyclerView.adapter == null) { + recyclerView.adapter = RowRepresentableAdapter(row) + } else { + (recyclerView.adapter as RowRepresentableAdapter).changeDataSource(row) + } + recyclerView.adapter?.notifyDataSetChanged() + } + + itemView.findViewById(R.id.title)?.let { textView -> + textView.text = row.title + } + + } + } + + } + + inner class CalendarTimeUnitCellHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + BindableHolder { + + override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { + + if (row is CellResult) { + itemView.timeUnit.background = ContextCompat.getDrawable(itemView.context, row.background) + } + + } + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt index e0befb16..312ac9f3 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt @@ -121,6 +121,7 @@ abstract class PACSVDescriptor(source: DataSource, is SessionField.Addon -> additionalBuyins += field.parse(value) ?: 0.0 is SessionField.Rebuy -> additionalBuyins += field.parse(value) ?: 0.0 is SessionField.Tips -> session.result?.tips = field.parse(value) + is SessionField.HandsCount -> session.handsCount = field.parse(value) is SessionField.Break -> { field.parse(value)?.let { session.breakDuration = it.toLong() diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt index 3a137468..7f17162d 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt @@ -220,6 +220,7 @@ class ProductCSVDescriptors { SessionField.CashedOut("Cashed Out"), SessionField.NetResult("Online Net"), SessionField.Tips("Tips"), + SessionField.HandsCount("Hands Count"), SessionField.LimitType("Limit"), SessionField.Game("Game"), SessionField.TableSize("Table Size"), diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt index 3e4b49fd..52af5f00 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt @@ -51,6 +51,7 @@ class SessionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg el is SessionField.CashedOut -> field.format(data.result?.cashout) is SessionField.NetResult -> field.format(data.result?.netResult) is SessionField.Tips -> field.format(data.result?.tips) + is SessionField.HandsCount -> field.format(data.handsCount) is SessionField.LimitType -> { data.limit?.let { limit -> Limit.values()[limit].longName diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt index 842c92c4..c306c4d0 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt @@ -121,6 +121,11 @@ sealed class SessionField { override var callback: ((String) -> Double?)? = null ) : NumberCSVField + data class HandsCount( + override var header: String, + override var callback: ((String) -> Int?)? = null + ) : IntCSVField + data class SmallBlind( override var header: String, override var callback: ((String) -> Double?)? = null diff --git a/app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt b/app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt index 490b750a..c80cc2ef 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt @@ -165,6 +165,16 @@ fun Date.startOfMonth(): Date { return calendar.time } +// Return the date of the end of the current date +fun Date.endOfMonth(): Date { + val calendar = Calendar.getInstance() + calendar.time = this + calendar.add(Calendar.MONTH, 1) + calendar.set(Calendar.DAY_OF_MONTH, 1) + calendar.add(Calendar.DATE, -1) + return calendar.time +} + // Return the date of the beginning of the current year fun Date.startOfYear(): Date { val calendar = Calendar.getInstance() @@ -173,6 +183,15 @@ fun Date.startOfYear(): Date { return calendar.time } +// Return the date of the end of the current date +fun Date.endOfYear(): Date { + val calendar = Calendar.getInstance() + calendar.time = this + calendar.set(Calendar.MONTH, 11) + calendar.set(Calendar.DAY_OF_MONTH, 31) + return calendar.time +} + // Return the number of seconds until the next minute fun Date.getNextMinuteInseconds(): Int { return (getNextMinuteInMilliseconds() / 1000).toInt() diff --git a/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt b/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt index 8bf53edf..0ebf60fd 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt @@ -1,6 +1,8 @@ package net.pokeranalytics.android.util.extensions import io.realm.* +import net.pokeranalytics.android.model.filter.Filterable +import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.interfaces.UsageCountable import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.model.interfaces.NameManageable @@ -119,4 +121,8 @@ fun < T : RealmModel> Realm.find(clazz: Class, searchContent: String?) : Real val sortField = arrayOf("name") val resultSort = arrayOf(Sort.ASCENDING) return items.sort(sortField, resultSort) -} \ No newline at end of file +} + +inline fun Realm.findAll(query: Query, sortField: String? = null): RealmResults { + return Filter.queryOn(this, query, sortField) +} diff --git a/app/src/main/res/drawable/rounded_grey_rect.xml b/app/src/main/res/drawable/rounded_grey_rect.xml new file mode 100644 index 00000000..67e39ad4 --- /dev/null +++ b/app/src/main/res/drawable/rounded_grey_rect.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw320dp/fragment_calendar.xml b/app/src/main/res/layout-sw320dp/fragment_calendar.xml index 96386cec..b4ff7686 100644 --- a/app/src/main/res/layout-sw320dp/fragment_calendar.xml +++ b/app/src/main/res/layout-sw320dp/fragment_calendar.xml @@ -123,4 +123,20 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/appBar" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw400dp/fragment_calendar.xml b/app/src/main/res/layout-sw400dp/fragment_calendar.xml index 96386cec..b4ff7686 100644 --- a/app/src/main/res/layout-sw400dp/fragment_calendar.xml +++ b/app/src/main/res/layout-sw400dp/fragment_calendar.xml @@ -123,4 +123,20 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/appBar" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_grid_calendar.xml b/app/src/main/res/layout/activity_grid_calendar.xml index d4c76fd8..8a3d48dc 100644 --- a/app/src/main/res/layout/activity_grid_calendar.xml +++ b/app/src/main/res/layout/activity_grid_calendar.xml @@ -6,7 +6,7 @@ android:orientation="vertical"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cell_calendar_time_unit.xml b/app/src/main/res/layout/cell_calendar_time_unit.xml new file mode 100644 index 00000000..5ac88985 --- /dev/null +++ b/app/src/main/res/layout/cell_calendar_time_unit.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_calendar.xml b/app/src/main/res/layout/fragment_calendar.xml index a7bbd39b..9096ec72 100644 --- a/app/src/main/res/layout/fragment_calendar.xml +++ b/app/src/main/res/layout/fragment_calendar.xml @@ -127,4 +127,20 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/appBar" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_grid_calendar.xml b/app/src/main/res/layout/fragment_grid_calendar.xml index 61a4490a..95b6a1f4 100644 --- a/app/src/main/res/layout/fragment_grid_calendar.xml +++ b/app/src/main/res/layout/fragment_grid_calendar.xml @@ -1,6 +1,77 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file