Fixes various issues with async writes

perftest
Laurent 3 years ago
parent 99cf2d465a
commit 3d8991baab
  1. 2
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  2. 6
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  3. 4
      app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt
  4. 56
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  5. 164
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  6. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  7. 10
      app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt
  8. 4
      app/src/main/java/net/pokeranalytics/android/ui/fragment/Top10Fragment.kt
  9. 3
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/RealmFragment.kt
  10. 12
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetDataListFragment.kt
  11. 21
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetDataMultiSelectionFragment.kt
  12. 11
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetFragment.kt
  13. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListGameFragment.kt
  14. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStaticListFragment.kt
  15. 2
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedSessionRowRepresentableAdapter.kt
  16. 43
      app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionFragment.kt
  17. 23
      app/src/main/java/net/pokeranalytics/android/ui/viewmodel/BottomSheetViewModel.kt
  18. 2
      app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt
  19. 19
      app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt

@ -618,7 +618,7 @@ class SSStats(sessionSet: SessionSet, query: Query) { // Session Set Stats
if (setSessions.size == filteredSessions.size) { if (setSessions.size == filteredSessions.size) {
this.initStatsWithSet(sessionSet) this.initStatsWithSet(sessionSet)
} else { } else {
ratedNet = filteredSessions.sumOf { it.managedComputableResult?.ratedNet ?: 0.0 } ratedNet = filteredSessions.sumOf { it.computableResult?.ratedNet ?: 0.0 }
bbSum = filteredSessions.sumOf { it.bbNet } bbSum = filteredSessions.sumOf { it.bbNet }
hourlyDuration = filteredSessions.hourlyDuration hourlyDuration = filteredSessions.hourlyDuration
estimatedHands = filteredSessions.sumOf { it.estimatedHands } estimatedHands = filteredSessions.sumOf { it.estimatedHands }

@ -337,7 +337,11 @@ class PokerAnalyticsMigration : RealmMigration {
// Migrate to version 15 // Migrate to version 15
if (currentVersion == 14) { if (currentVersion == 14) {
schema.get("ComputableResult")?.let { crs -> // schema.get("ComputableResult")?.let { crs ->
// crs.addField("id", String::class.java).setRequired("id", true)
// crs.addPrimaryKey("id")
// }
schema.get("Result")?.let { crs ->
crs.addField("id", String::class.java).setRequired("id", true) crs.addField("id", String::class.java).setRequired("id", true)
crs.addPrimaryKey("id") crs.addPrimaryKey("id")
} }

@ -8,8 +8,8 @@ import java.util.*
open class ComputableResult : RealmObject(), Filterable { open class ComputableResult : RealmObject(), Filterable {
@PrimaryKey // @PrimaryKey
var id = UUID.randomUUID().toString() // var id = UUID.randomUUID().toString()
var ratedNet: Double = 0.0 var ratedNet: Double = 0.0

@ -3,13 +3,13 @@ package net.pokeranalytics.android.model.realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass import io.realm.annotations.RealmClass
import net.pokeranalytics.android.exceptions.PADataModelException import net.pokeranalytics.android.exceptions.PADataModelException
import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import java.lang.ref.WeakReference import java.util.*
@RealmClass @RealmClass
open class Result : RealmObject(), Filterable { open class Result : RealmObject(), Filterable {
@ -24,13 +24,17 @@ open class Result : RealmObject(), Filterable {
} }
} }
@PrimaryKey
var id = UUID.randomUUID().toString()
/** /**
* The buyin amount * The buyin amount
*/ */
var buyin: Double? = null var buyin: Double? = null
set(value) { set(value) {
field = value field = value
this.computeNumberOfRebuy() // this.computeNumberOfRebuy()
this.computeNet(true) this.computeNet(true)
} }
@ -41,9 +45,6 @@ open class Result : RealmObject(), Filterable {
set(value) { set(value) {
field = value field = value
this.computeNet(true) this.computeNet(true)
if (value != null) {
this.session().end()
}
} }
/** /**
@ -53,9 +54,6 @@ open class Result : RealmObject(), Filterable {
set(value) { set(value) {
field = value field = value
this.computeNet(false) this.computeNet(false)
if (value != null) {
this.session().end()
}
} }
/** /**
@ -68,10 +66,6 @@ open class Result : RealmObject(), Filterable {
* Tips * Tips
*/ */
var tips: Double? = null var tips: Double? = null
set(value) {
field = value
this.session().computeStats()
}
// The transactions associated with the Result, impacting the result // The transactions associated with the Result, impacting the result
var transactions: RealmList<Transaction> = RealmList() var transactions: RealmList<Transaction> = RealmList()
@ -89,26 +83,26 @@ open class Result : RealmObject(), Filterable {
@LinkingObjects("result") @LinkingObjects("result")
private val sessions: RealmResults<Session>? = null private val sessions: RealmResults<Session>? = null
// @Ignore private val managedSession: Session
// private fun managedSession(): Session { get() {
// return this.sessions?.firstOrNull() ?: throw PAIllegalStateException("Unmanaged Result") return this.sessions?.firstOrNull() ?: throw PADataModelException("Unmanaged Result")
// } }
// Manually set session
@Ignore
var inverseSession: WeakReference<Session>? = null
// // Manually set session
// @Ignore // @Ignore
fun session() : Session { // var inverseSession: WeakReference<Session>? = null
return this.inverseSession?.get() ?: throw PADataModelException("Inverse session not set") //
} //// @Ignore
// fun session() : Session {
// return this.inverseSession?.get() ?: throw PADataModelException("Inverse session not set")
// }
/** /**
* Returns 1 if the session is positive * Returns 1 if the session is positive
*/ */
val isPositive: Int val isPositive: Int
get() { get() {
return if (session().isTournament()) { return if (managedSession.isTournament()) {
if ((this.cashout ?: -1.0) >= 0.0) 1 else 0 // if cashout is null we want to count a negative session if ((this.cashout ?: -1.0) >= 0.0) 1 else 0 // if cashout is null we want to count a negative session
} else { } else {
if (this.net >= 0.0) 1 else 0 if (this.net >= 0.0) 1 else 0
@ -128,7 +122,7 @@ open class Result : RealmObject(), Filterable {
} else if (buyin != null || cashout != null) { } else if (buyin != null || cashout != null) {
useBuyin = true useBuyin = true
} else { } else {
if (this.session().isCashGame() && !this.session().isLive) { if (this.managedSession.isCashGame() && !this.managedSession.isLive) {
useBuyin = false useBuyin = false
} }
} }
@ -144,14 +138,14 @@ open class Result : RealmObject(), Filterable {
} }
// Precompute results // Precompute results
this.session().computeStats() // this.managedSession.computeStats()
this.session().sessionSet?.computeStats() // this.managedSession.sessionSet?.computeStats()
} }
// Computes the number of rebuy // Computes the number of rebuy
fun computeNumberOfRebuy() { fun computeNumberOfRebuy() {
if (this.session().isCashGame()) { if (this.managedSession.isCashGame()) {
this.session().cgBiggestBet?.let { bb -> this.managedSession.cgBiggestBet?.let { bb ->
if (bb > 0.0) { if (bb > 0.0) {
this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0) this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0)
} else { } else {
@ -159,7 +153,7 @@ open class Result : RealmObject(), Filterable {
} }
} }
} else { } else {
this.session().tournamentEntryFee?.let { entryFee -> this.managedSession.tournamentEntryFee?.let { entryFee ->
if (entryFee > 0.0) { if (entryFee > 0.0) {
this.numberOfRebuy = (this.buyin ?: 0.0) / entryFee this.numberOfRebuy = (this.buyin ?: 0.0) / entryFee
} else { } else {

@ -33,8 +33,11 @@ import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.view.* import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.util.* import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.extensions.* import net.pokeranalytics.android.util.extensions.hourMinute
import java.lang.ref.WeakReference import net.pokeranalytics.android.util.extensions.shortDateTime
import net.pokeranalytics.android.util.extensions.toCurrency
import net.pokeranalytics.android.util.extensions.toMinutes
import timber.log.Timber
import java.text.DateFormat import java.text.DateFormat
import java.text.NumberFormat import java.text.NumberFormat
import java.text.ParseException import java.text.ParseException
@ -68,7 +71,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
fun newInstance(realm: Realm, isTournament: Boolean, bankroll: Bankroll? = null): Session { fun newInstance(realm: Realm, isTournament: Boolean, bankroll: Bankroll? = null): Session {
val session = Session() val session = Session()
val result = Result() val result = Result()
result.inverseSession = WeakReference(session) // result.inverseSession = WeakReference(session)
session.result = result session.result = result
if (bankroll != null) { if (bankroll != null) {
@ -160,18 +163,10 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
var result: Result? = null var result: Result? = null
@LinkingObjects("session") @LinkingObjects("session")
private val managedComputableResults: RealmResults<ComputableResult>? = null private val computableResults: RealmResults<ComputableResult>? = null
@Ignore @Ignore
val managedComputableResult: ComputableResult? = this.managedComputableResults?.firstOrNull() val computableResult: ComputableResult? = this.computableResults?.firstOrNull()
@Ignore
var inverseComputableResult: WeakReference<ComputableResult>? = null
// @Ignore
fun computableResult() : ComputableResult? {
return this.inverseComputableResult?.get()
}
// Timed interface // Timed interface
@ -225,8 +220,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
if (value != null && this.endDate != null && value.after(this.endDate)) { if (value != null && this.endDate != null && value.after(this.endDate)) {
this.endDate = null this.endDate = null
} }
// this.dateChanged() this.dateChanged()
this.computeStats() // this.computeStats()
} }
/** /**
@ -245,9 +240,9 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
} }
this.computeNetDuration() this.computeNetDuration()
// this.dateChanged() this.dateChanged()
this.defineDefaultTournamentBuyinIfNecessary() this.defineDefaultTournamentBuyinIfNecessary()
this.computeStats() // this.computeStats()
} }
/** /**
@ -257,7 +252,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
set(value) { set(value) {
field = value field = value
this.computeNetDuration() this.computeNetDuration()
this.computeStats() // this.computeStats()
} }
/** /**
@ -285,7 +280,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
set(value) { set(value) {
field = value field = value
this.generateStakes() this.generateStakes()
this.computeStats() // this.computeStats()
// this.updateRowRepresentation() // this.updateRowRepresentation()
} }
@ -313,7 +308,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
set(value) { set(value) {
if (value > 0) { if (value > 0) {
field = value field = value
this.computeStats() // this.computeStats()
} }
} }
@ -340,7 +335,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
set(value) { set(value) {
field = value field = value
this.computeStats() this.computeStats()
this.result?.computeNumberOfRebuy() // this.result?.computeNumberOfRebuy()
} }
// var blinds: String? = null // var blinds: String? = null
@ -351,8 +346,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
field = value field = value
this.generateStakes() this.generateStakes()
this.defineHighestBet() this.defineHighestBet()
this.computeStats() // this.computeStats()
this.result?.computeNumberOfRebuy() // this.result?.computeNumberOfRebuy()
} }
var cgBlinds: String? = null var cgBlinds: String? = null
@ -360,8 +355,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
field = cleanupBlinds(value) field = cleanupBlinds(value)
this.generateStakes() this.generateStakes()
this.defineHighestBet() this.defineHighestBet()
this.computeStats() // this.computeStats()
this.result?.computeNumberOfRebuy() // this.result?.computeNumberOfRebuy()
} }
var cgBiggestBet: Double? = null var cgBiggestBet: Double? = null
@ -374,7 +369,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
var tournamentEntryFee: Double? = null var tournamentEntryFee: Double? = null
set(value) { set(value) {
field = value field = value
this.result?.computeNumberOfRebuy() // this.result?.computeNumberOfRebuy()
} }
// The total number of players who participated in the tournament // The total number of players who participated in the tournament
@ -396,13 +391,17 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
var handsCount: Int? = null var handsCount: Int? = null
set(value) { set(value) {
field = value field = value
this.computeStats() // this.computeStats()
} }
fun bankrollHasBeenUpdated() { fun bankrollHasBeenUpdated() {
this.generateStakes() this.generateStakes()
} }
private fun dateChanged() {
SessionSetManager.dateChange = true
}
// /** // /**
// * Manages impacts on SessionSets // * Manages impacts on SessionSets
// * Should be called when the start / end date are changed // * Should be called when the start / end date are changed
@ -489,35 +488,35 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
return this.result?.net ?: 0.0 return this.result?.net ?: 0.0
} }
fun preCompute() {
this.computeStats()
this.sessionSet?.computeStats()
this.result?.computeNumberOfRebuy()
}
/** /**
* Pre-compute various statIds * Pre-compute various statIds
*/ */
fun computeStats() { fun computeStats() {
val realm = Realm.getDefaultInstance()
val currentComputableResult = realm.computableResult(this)
// ComputableResult are created only when a session is over // ComputableResult are created only when a session is over
if (this.startDate != null && this.endDate != null && currentComputableResult == null && this.inverseComputableResult?.get() == null) { if (this.startDate != null && this.endDate != null && this.computableResults?.size == 0) {
val computableResult = ComputableResult() val computableResult = realm.createObject(ComputableResult::class.java)
computableResult.session = this computableResult.session = this
this.inverseComputableResult = WeakReference(computableResult)
} // if a ComputableResult exists and the session is not completed, delete it } // if a ComputableResult exists and the session is not completed, delete it
else if ((this.startDate == null || this.endDate == null) && currentComputableResult != null && currentComputableResult.isValid) { else if ((this.startDate == null || this.endDate == null) && this.computableResult != null && this.computableResult.isValid) {
currentComputableResult.deleteFromRealm() this.computableResult.deleteFromRealm()
} }
realm.close()
this.computableResult()?.updateWith(this)
// Update the ComputableResult // Update the ComputableResult
// this.computableResult()?.forEachIndexed { index, computableResult -> this.computableResults?.forEachIndexed { index, computableResult ->
// computableResult.updateWith(this) computableResult.updateWith(this)
// if (index > 0) { if (index > 0) {
// throw PAIllegalStateException("Session cannot have more than one computable result") throw PAIllegalStateException("Session cannot have more than one computable result")
// } }
// } }
this.sessionSet?.computeStats() this.sessionSet?.computeStats()
} }
/** /**
@ -707,42 +706,9 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
} }
fun getFormattedStakes(): String { fun getFormattedStakes(): String {
return this.cgStakes?.let { StakesHolder.readableStakes(it) } ?: run { NULL_TEXT } return this.cgStakes?.let { StakesHolder.readableStakes(it) } ?: run { NULL_TEXT }
//
// val formattedBlinds = StakesHolder.formattedBlinds(this.cgBlinds, this.currency)
// val formattedAntes = StakesHolder.formattedAnte(this.cgAnte, this.currency)
//
// return StakesHolder.formattedStakes(formattedBlinds, formattedAntes)
//
//
// val components = arrayListOf<String>()
// this.formattedBlinds?.let { components.add(it) }
// this.formattedAnte?.let { components.add("($it)") }
//
// return if (components.isNotEmpty()) {
// components.joinToString(" ")
// } else {
// NULL_TEXT
// }
} }
// fun formatBlinds() {
// blinds = null
// if (cgBigBlind == null) return
// cgBigBlind?.let { bb ->
// val sb = cgSmallBlind ?: bb / 2.0
// val preFormattedBlinds = "${sb.formatted}/${bb.round()}"
// println("<<<<<< bb.toCurrency(currency) : ${bb.toCurrency(currency)}")
// println("<<<<<< preFormattedBlinds : $preFormattedBlinds")
// val regex = Regex("-?\\d+(\\.\\d+)?")
// blinds = bb.toCurrency(currency).replace(regex, preFormattedBlinds)
// println("<<<<<< blinds = $blinds")
// }
// }
// LifeCycle // LifeCycle
/** /**
@ -772,7 +738,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
} }
// cleanup unnecessary related objects // cleanup unnecessary related objects
this.result?.deleteFromRealm() this.result?.deleteFromRealm()
this.computableResult()?.deleteFromRealm() this.computableResult?.deleteFromRealm()
} }
@ -807,32 +773,12 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
when (row) { when (row) {
SessionPropertiesRow.BANKROLL -> bankroll = value as Bankroll? SessionPropertiesRow.BANKROLL -> bankroll = value as Bankroll?
SessionPropertiesRow.STAKES -> if (value is Stakes) { SessionPropertiesRow.STAKES -> if (value is Stakes) {
if (value.ante != null) { if (value.ante != null) {
this.cgAnte = value.ante this.cgAnte = value.ante
} }
if (value.blinds != null) { if (value.blinds != null) {
this.cgBlinds = value.blinds this.cgBlinds = value.blinds
} }
// cgSmallBlind = try {
// (value[0] as String? ?: "0").toDouble()
// } catch (e: Exception) {
// null
// }
//
// cgBigBlind = try {
// (value[1] as String? ?: "0").toDouble()
// } catch (e: Exception) {
// null
// }
//
// cgBigBlind?.let {
// if (cgSmallBlind == null || cgSmallBlind == 0.0) {
// cgSmallBlind = it / 2.0
// }
// }
} else if (value == null) { } else if (value == null) {
this.cgBlinds = null this.cgBlinds = null
this.cgAnte = null this.cgAnte = null
@ -843,15 +789,21 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.BUY_IN -> { SessionPropertiesRow.BUY_IN -> {
val localResult = getOrCreateResult() val localResult = getOrCreateResult()
localResult.buyin = value as Double? localResult.buyin = value as Double?
// this.updateRowRepresentation()
} }
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> { SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> {
val localResult = getOrCreateResult() val localResult = getOrCreateResult()
localResult.cashout = value as Double? localResult.cashout = value as Double?
Timber.d("localResult cashout = ${localResult.cashout}")
if (value != null) {
this.end()
}
} }
SessionPropertiesRow.NET_RESULT -> { SessionPropertiesRow.NET_RESULT -> {
val localResult = getOrCreateResult() val localResult = getOrCreateResult()
localResult.netResult = value as Double? localResult.netResult = value as Double?
if (value != null) {
this.end()
}
} }
SessionPropertiesRow.COMMENT -> comment = value as String? ?: "" SessionPropertiesRow.COMMENT -> comment = value as String? ?: ""
SessionPropertiesRow.END_DATE -> if (value is Date?) { SessionPropertiesRow.END_DATE -> if (value is Date?) {
@ -907,12 +859,10 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName = value as TournamentName? SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName = value as TournamentName?
SessionPropertiesRow.TOURNAMENT_TYPE -> tournamentType = (value as TournamentType?)?.ordinal SessionPropertiesRow.TOURNAMENT_TYPE -> tournamentType = (value as TournamentType?)?.ordinal
SessionPropertiesRow.TOURNAMENT_FEATURE -> { SessionPropertiesRow.TOURNAMENT_FEATURE -> {
this.tournamentFeatures.clear()
value?.let { value?.let {
tournamentFeatures = RealmList() tournamentFeatures = RealmList()
tournamentFeatures.addAll((it as ArrayList<TournamentFeature>)) tournamentFeatures.addAll((it as List<TournamentFeature>))
} ?: run {
tournamentFeatures.removeAll(this.tournamentFeatures)
} }
} }
SessionPropertiesRow.HANDS_COUNT -> handsCount = (value as Double?)?.toInt() SessionPropertiesRow.HANDS_COUNT -> handsCount = (value as Double?)?.toInt()
@ -945,8 +895,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
private fun getOrCreateResult(): Result { private fun getOrCreateResult(): Result {
return this.result return this.result
?: run { ?: run {
val result = realm.createObject(Result::class.java) val result = Result()
result.inverseSession = WeakReference(this) // result.inverseSession = WeakReference(this)
this.result = result this.result = result
result result
} }
@ -1030,7 +980,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
var right: TextFormat? = null var right: TextFormat? = null
if (!hasMainCurrencyCode) { if (!hasMainCurrencyCode) {
this.managedComputableResult?.ratedNet?.let { ratedNet -> this.computableResult?.ratedNet?.let { ratedNet ->
right = Stat.NET_RESULT.textFormat(ratedNet) right = Stat.NET_RESULT.textFormat(ratedNet)
} }
} }
@ -1060,7 +1010,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.NET_RESULT -> this.result?.netResult?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.NET_RESULT -> this.result?.netResult?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.COMMENT -> if (this.comment.isNotEmpty()) this.comment else NULL_TEXT SessionPropertiesRow.COMMENT -> this.comment.ifEmpty { NULL_TEXT }
SessionPropertiesRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT SessionPropertiesRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT
SessionPropertiesRow.GAME -> getFormattedGame() SessionPropertiesRow.GAME -> getFormattedGame()
SessionPropertiesRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(currency) ?: NULL_TEXT

@ -61,7 +61,7 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
override var netDuration: Long = 0L override var netDuration: Long = 0L
fun computeStats() { fun computeStats() {
this.ratedNet = this.sessions?.sumOf { it.managedComputableResult?.ratedNet ?: 0.0 } ?: 0.0 this.ratedNet = this.sessions?.sumOf { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0
this.estimatedHands = this.sessions?.sumOf { it.estimatedHands } ?: 0.0 this.estimatedHands = this.sessions?.sumOf { it.estimatedHands } ?: 0.0
this.bbNet = this.sessions?.sumOf { it.bbNet } ?: 0.0 this.bbNet = this.sessions?.sumOf { it.bbNet } ?: 0.0
this.breakDuration = this.sessions?.max("breakDuration")?.toLong() ?: 0L this.breakDuration = this.sessions?.max("breakDuration")?.toLong() ?: 0L

@ -22,12 +22,19 @@ object SessionSetManager {
private val sessionsToProcess = mutableSetOf<Session>() private val sessionsToProcess = mutableSetOf<Session>()
var dateChange = false
fun configure() {} // launch init
init { init {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
this.sessions = realm.where(Session::class.java).findAllAsync() this.sessions = realm.where(Session::class.java).findAllAsync()
this.sessions.addChangeListener { sessions, changeSet -> this.sessions.addChangeListener { sessions, changeSet ->
if (this.dateChange) {
this.dateChange = false
changeSet.insertions.forEach { index -> changeSet.insertions.forEach { index ->
sessions[index]?.let { this.sessionsToProcess.add(it) } sessions[index]?.let { this.sessionsToProcess.add(it) }
} }
@ -39,6 +46,7 @@ object SessionSetManager {
realm.executeTransactionAsync { asyncRealm -> realm.executeTransactionAsync { asyncRealm ->
processSessions(asyncRealm, sessionIds) processSessions(asyncRealm, sessionIds)
} }
}
} }
@ -51,6 +59,8 @@ object SessionSetManager {
private fun processSessions(realm: Realm, sessionIds: List<String>) { private fun processSessions(realm: Realm, sessionIds: List<String>) {
Timber.d("PROCESS SESSIONS")
for (sessionId in sessionIds) { for (sessionId in sessionIds) {
realm.findById<Session>(sessionId)?.let { session -> realm.findById<Session>(sessionId)?.let { session ->
if (session.startDate != null && session.endDate != null) { if (session.startDate != null && session.endDate != null) {

@ -141,10 +141,10 @@ class Top10Fragment : RealmFragment(), RowRepresentableDataSource, RowRepresenta
// Sort by rated net // Sort by rated net
val sortedCashGames = cashGames.sortedByDescending { val sortedCashGames = cashGames.sortedByDescending {
it.managedComputableResult?.ratedNet it.computableResult?.ratedNet
}.toMutableList() }.toMutableList()
val sortedTournaments = tournaments.sortedByDescending { val sortedTournaments = tournaments.sortedByDescending {
it.managedComputableResult?.ratedNet it.computableResult?.ratedNet
}.toMutableList() }.toMutableList()
// Keep 10 items // Keep 10 items

@ -36,8 +36,7 @@ open class RealmFragment : BaseFragment() {
realm = Realm.getDefaultInstance() realm = Realm.getDefaultInstance()
this.observedEntities.forEach { this.observedEntities.forEach {
val realmResults = realm.where(it).findAll() val realmResults = realm.where(it).findAllAsync()
realmResults.addChangeListener { t, _ -> realmResults.addChangeListener { t, _ ->
this.entitiesChanged(it, t) this.entitiesChanged(it, t)
} }

@ -13,7 +13,7 @@ import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
open class BottomSheetListFragment : BottomSheetFragment(), LiveRowRepresentableDataSource, RowRepresentableDelegate { open class BottomSheetDataListFragment : BottomSheetFragment(), LiveRowRepresentableDataSource, RowRepresentableDelegate {
private var _binding: BottomSheetListBinding? = null private var _binding: BottomSheetListBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -62,18 +62,8 @@ open class BottomSheetListFragment : BottomSheetFragment(), LiveRowRepresentable
} }
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
this.onRowSelected(position) this.onRowSelected(position)
dismiss() dismiss()
// this.viewModel.realmData?.let {
// val selectedData = it[position]
// selectedData?.let {data ->
// this.viewModel.onRowValueChanged(data)
//// this.delegate.onRowValueChanged(data, this.row)
// dismiss()
// }
// }
// super.onRowSelected(position, row, tag)
} }
/** /**

@ -2,22 +2,16 @@ package net.pokeranalytics.android.ui.fragment.components.bottomsheet
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import io.realm.RealmModel
import net.pokeranalytics.android.databinding.BottomSheetDoubleEditTextBinding
import net.pokeranalytics.android.databinding.BottomSheetListBinding
import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.ui.modules.data.EditableDataActivity
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.modules.data.EditableDataActivity
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
/** /**
* Manage multiple items selection in a bottom sheet list * Manage multiple items selection in a bottom sheet list
*/ */
open class BottomSheetMultiSelectionFragment : BottomSheetListFragment() { open class BottomSheetDataMultiSelectionFragment : BottomSheetDataListFragment() {
override fun viewTypeForPosition(position: Int): Int { override fun viewTypeForPosition(position: Int): Int {
return RowViewType.TITLE_CHECK.ordinal return RowViewType.TITLE_CHECK.ordinal
@ -27,11 +21,14 @@ open class BottomSheetMultiSelectionFragment : BottomSheetListFragment() {
if (requestCode == REQUEST_CODE_ADD_NEW_OBJECT && resultCode == Activity.RESULT_OK && data != null) { if (requestCode == REQUEST_CODE_ADD_NEW_OBJECT && resultCode == Activity.RESULT_OK && data != null) {
val dataType = data.getIntExtra(EditableDataActivity.IntentKey.DATA_TYPE.keyName, 0) val dataType = data.getIntExtra(EditableDataActivity.IntentKey.DATA_TYPE.keyName, 0)
val primaryKey = data.getStringExtra(EditableDataActivity.IntentKey.PRIMARY_KEY.keyName) val primaryKey = data.getStringExtra(EditableDataActivity.IntentKey.PRIMARY_KEY.keyName)
val pokerAnalyticsActivity = activity as BaseActivity
val liveDataType = LiveData.values()[dataType] val liveDataType = LiveData.values()[dataType]
val proxyItem: RealmModel? = liveDataType.getData(pokerAnalyticsActivity.getRealm(), primaryKey) val realm = (activity as BaseActivity).getRealm()
this.model.selectedRows.add(proxyItem as RowRepresentable) liveDataType.getData(realm, primaryKey)?.let { proxyItem ->
this.refreshRow(proxyItem as RowRepresentable) val copy = realm.copyFromRealm(proxyItem)
this.model.selectedRows.add(copy as RowRepresentable)
this.refreshRow(copy as RowRepresentable)
}
// dataAdapter.refreshRow(proxyItem as RowRepresentable) // dataAdapter.refreshRow(proxyItem as RowRepresentable)
} }
} }

@ -82,11 +82,11 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
private fun newInstance(bottomSheetType: BottomSheetType): BottomSheetFragment { private fun newInstance(bottomSheetType: BottomSheetType): BottomSheetFragment {
return when (bottomSheetType) { return when (bottomSheetType) {
BottomSheetType.NONE -> BottomSheetFragment() BottomSheetType.NONE -> BottomSheetFragment()
BottomSheetType.LIST -> BottomSheetListFragment() BottomSheetType.LIST -> BottomSheetDataListFragment()
BottomSheetType.LIST_STATIC -> BottomSheetStaticListFragment() BottomSheetType.LIST_STATIC -> BottomSheetStaticListFragment()
BottomSheetType.LIST_GAME -> BottomSheetListGameFragment() BottomSheetType.LIST_GAME -> BottomSheetListGameFragment()
BottomSheetType.DOUBLE_LIST -> BottomSheetListGameFragment() BottomSheetType.DOUBLE_LIST -> BottomSheetListGameFragment()
BottomSheetType.MULTI_SELECTION -> BottomSheetMultiSelectionFragment() BottomSheetType.MULTI_SELECTION -> BottomSheetDataMultiSelectionFragment()
BottomSheetType.GRID -> BottomSheetTableSizeGridFragment() BottomSheetType.GRID -> BottomSheetTableSizeGridFragment()
BottomSheetType.EDIT_TEXT -> BottomSheetEditTextFragment() BottomSheetType.EDIT_TEXT -> BottomSheetEditTextFragment()
BottomSheetType.EDIT_TEXT_MULTI_LINES -> BottomSheetEditTextMultiLinesFragment() BottomSheetType.EDIT_TEXT_MULTI_LINES -> BottomSheetEditTextMultiLinesFragment()
@ -102,6 +102,7 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
//TODO: When dependency 'com.google.android.material:material:1.1.0' will be available in stable version, upgrade and remove that //TODO: When dependency 'com.google.android.material:material:1.1.0' will be available in stable version, upgrade and remove that
activity?.setTheme(R.style.PokerAnalyticsTheme) activity?.setTheme(R.style.PokerAnalyticsTheme)
_binding = FragmentBottomSheetBinding.inflate(inflater, container, false) _binding = FragmentBottomSheetBinding.inflate(inflater, container, false)
inflateContentView(inflater, binding.root) inflateContentView(inflater, binding.root)
return binding.root return binding.root
@ -277,9 +278,9 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
return this.model.rowRepresentableEditDescriptors return this.model.rowRepresentableEditDescriptors
} }
private fun getValue(): Any? { // private fun getValue(): Any? {
return this.model.getValue() // return this.model.getValue()
} // }
private fun onClear() { private fun onClear() {
this.delegate?.onRowValueChanged(null, this.model.row) this.delegate?.onRowValueChanged(null, this.model.row)

@ -18,7 +18,7 @@ import net.pokeranalytics.android.ui.view.RowRepresentable
* Bottom Sheet List Game Fragment * Bottom Sheet List Game Fragment
* Display a list of game + chips to choose the game limit * Display a list of game + chips to choose the game limit
*/ */
class BottomSheetListGameFragment : BottomSheetListFragment() { class BottomSheetListGameFragment : BottomSheetDataListFragment() {
private var _binding: BottomSheetGameListBinding? = null private var _binding: BottomSheetGameListBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!

@ -45,9 +45,7 @@ class BottomSheetStaticListFragment : BottomSheetFragment(), StaticRowRepresenta
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
this.model.selectedRows.add(row) this.model.selectedRows.add(row)
this.onRowValueChanged() this.onRowValueChanged()
// this.delegate.onRowValueChanged(row, this.row)
dismiss() dismiss()
// super.onRowSelected(position, row, tag)
} }
/** /**

@ -182,7 +182,7 @@ class FeedSessionRowRepresentableAdapter(
allSessions.clear() allSessions.clear()
allSessions.addAll(this.pendingSessions) allSessions.addAll(this.pendingSessions)
allSessions.addAll(this.startedSessions) allSessions.addAll(this.startedSessions)
Timber.d("Update session list, total count = ${allSessions.size}") // Timber.d("Update session list, total count = ${allSessions.size}")
val headersPositions = HashMap<Int, Date?>() val headersPositions = HashMap<Int, Date?>()

@ -11,7 +11,10 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import kotlinx.coroutines.* import io.realm.RealmModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.bankroll.BankrollReportManager import net.pokeranalytics.android.calculus.bankroll.BankrollReportManager
import net.pokeranalytics.android.calculus.optimalduration.CashGameOptimalDurationCalculator import net.pokeranalytics.android.calculus.optimalduration.CashGameOptimalDurationCalculator
@ -40,8 +43,8 @@ import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.extensions.* import net.pokeranalytics.android.util.extensions.*
import timber.log.Timber import timber.log.Timber
import java.lang.Runnable
import java.util.* import java.util.*
import kotlin.collections.ArrayList
class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, ResultCaptureTypeDelegate { class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, ResultCaptureTypeDelegate {
@ -190,7 +193,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
val sessionRealm = realm.findById<Session>(sessionId) val sessionRealm = realm.findById<Session>(sessionId)
sessionRealm?.let { sessionRealm?.let {
val copy = realm.copySessionFromRealm(it) val copy = realm.copyFromRealm(it)
if (this.model.duplicate) { // duplicate session if (this.model.duplicate) { // duplicate session
// realm.executeTransaction { // realm.executeTransaction {
@ -307,13 +310,10 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
return return
} }
getRealm().executeTransactionAsync { realm ->
realm.copyToRealmOrUpdate(this.currentSession)
this.currentSession.inverseComputableResult?.get()?.let { computableResult ->
realm.copyToRealmOrUpdate(computableResult)
}
getRealm().executeTransactionAsync { realm ->
val session = realm.copyToRealmOrUpdate(this.currentSession)
session.preCompute()
} }
} }
@ -536,9 +536,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
val sessionId = this.currentSession.id val sessionId = this.currentSession.id
getRealm().executeTransactionAsync { asyncRealm -> getRealm().executeTransactionAsync { asyncRealm ->
asyncRealm.findById<Session>(sessionId)?.let { session -> asyncRealm.findById<Session>(sessionId)?.delete()
session.delete()
}
} }
// this.currentSession.delete() // this.currentSession.delete()
@ -548,15 +546,18 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
activity?.finish() activity?.finish()
} }
/** // /**
* Called when the user pressed back on the parent activity // * Called when the user pressed back on the parent activity
*/ // */
override fun onBackPressed() { // override fun onBackPressed() {
super.onBackPressed() // super.onBackPressed()
if (!sessionHasBeenUserCustomized) { // if (!sessionHasBeenUserCustomized) {
currentSession.delete() // val sessionId = currentSession.id
} // getRealm().executeTransactionAsync { asyncRealm ->
} // asyncRealm.findById<Session>(sessionId)?.delete()
// }
// }
// }
//// Static Data Source //// Static Data Source

@ -8,11 +8,13 @@ import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException
import net.pokeranalytics.android.model.Stakes import net.pokeranalytics.android.model.Stakes
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.util.extensions.findById
import java.util.* import java.util.*
class BottomSheetViewModelFactory(var row: RowRepresentable, var delegate: RowRepresentableDelegate): ViewModelProvider.Factory { class BottomSheetViewModelFactory(var row: RowRepresentable, var delegate: RowRepresentableDelegate): ViewModelProvider.Factory {
@ -85,7 +87,7 @@ class BottomSheetViewModel(var row: RowRepresentable) : ViewModel() {
var alternativeLabels: Boolean = false var alternativeLabels: Boolean = false
/** /**
* Multiselection * Multi-selection
*/ */
val selectedRows: ArrayList<RowRepresentable> = ArrayList() val selectedRows: ArrayList<RowRepresentable> = ArrayList()
@ -225,12 +227,31 @@ class BottomSheetViewModel(var row: RowRepresentable) : ViewModel() {
fun isSelected(row: RowRepresentable): Boolean { fun isSelected(row: RowRepresentable): Boolean {
return this.selectedRows.contains(row) return this.selectedRows.contains(row)
// return when(row.bottomSheetType) {
// BottomSheetType.MULTI_SELECTION -> {
// val identifiables = this.selectedRows as List<Identifiable>
// val ids = identifiables.map { it.id }
// val id = (row as Identifiable).id
// ids.contains(id)
// }
// else -> this.selectedRows.contains(row)
// }
} }
fun changedValue(): Any? { fun changedValue(): Any? {
return when(row.bottomSheetType) { return when(row.bottomSheetType) {
BottomSheetType.DOUBLE_EDIT_TEXT -> arrayListOf(this.stringValue, this.secondStringValue) BottomSheetType.DOUBLE_EDIT_TEXT -> arrayListOf(this.stringValue, this.secondStringValue)
BottomSheetType.DOUBLE_LIST, BottomSheetType.LIST_GAME -> arrayListOf(this.someValues[0], this.someValues[1]) BottomSheetType.DOUBLE_LIST, BottomSheetType.LIST_GAME -> arrayListOf(this.someValues[0], this.someValues[1])
BottomSheetType.MULTI_SELECTION -> {
this.realmData?.realm?.let { realm ->
val identifiables = this.selectedRows as List<Identifiable>
identifiables.firstOrNull()?.realmObjectClass?.let { clazz ->
val ids = identifiables.map { it.id }
val objects = ids.mapNotNull { realm.findById(clazz, it) }
objects.map { realm.copyFromRealm(it) }
} ?: kotlin.run { this.selectedRows }
} ?: kotlin.run { this.selectedRows }
}
else -> getValue() else -> getValue()
} }
} }

@ -276,6 +276,8 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
this.addAdditionallyCreatedIdentifiable(transaction) this.addAdditionallyCreatedIdentifiable(transaction)
} }
managedSession.preCompute()
return managedSession return managedSession
} else { } else {
Timber.d("Session already exists(count=$count): sd=$startDate, ed=$endDate, net=$net") Timber.d("Session already exists(count=$count): sd=$startDate, ed=$endDate, net=$net")

@ -9,7 +9,6 @@ import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.UsageCountable import net.pokeranalytics.android.model.interfaces.UsageCountable
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import java.lang.ref.WeakReference
fun <T : RealmModel>Realm.count(clazz: Class<T>) : Long { fun <T : RealmModel>Realm.count(clazz: Class<T>) : Long {
return this.where(clazz).count() return this.where(clazz).count()
@ -159,12 +158,12 @@ fun Realm.computableResult(session: Session): ComputableResult? {
return crs.firstOrNull() return crs.firstOrNull()
} }
fun Realm.copySessionFromRealm(session: Session): Session { //fun Realm.copySessionFromRealm(session: Session): Session {
val copy = this.copyFromRealm(session) // val copy = this.copyFromRealm(session)
copy.result?.inverseSession = WeakReference(copy) // copy.result?.inverseSession = WeakReference(copy)
this.computableResult(session)?.let { //// this.computableResult(session)?.let {
val computableResult = this.copyFromRealm(it) //// val computableResult = this.copyFromRealm(it)
copy.inverseComputableResult = WeakReference(computableResult) //// copy.inverseComputableResult = WeakReference(computableResult)
} //// }
return copy // return copy
} //}
Loading…
Cancel
Save