Merge branch 'master' of gitlab.com:stax-river/poker-analytics

dev_raz_wip
Aurelien Hubert 7 years ago
commit 02471ed095
  1. 341
      app/src/androidTest/java/net/pokeranalytics/android/ExampleInstrumentedUnitTest.kt
  2. 33
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  3. 56
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  4. 15
      app/src/main/java/net/pokeranalytics/android/calculus/Computable.kt
  5. 52
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  6. 46
      app/src/main/java/net/pokeranalytics/android/model/LiveData.kt
  7. 3
      app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt
  8. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/Game.kt
  9. 28
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  10. 20
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  11. 46
      app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrame.kt
  12. 30
      app/src/main/java/net/pokeranalytics/android/ui/adapter/components/RowRepresentableAdapter.kt
  13. 48
      app/src/main/java/net/pokeranalytics/android/ui/fragment/EditableDataFragment.kt
  14. 70
      app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt
  15. 13
      app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt
  16. 20
      app/src/main/res/layout/fragment_editable_data.xml
  17. 2
      app/src/main/res/values/strings.xml

@ -1,6 +1,7 @@
package net.pokeranalytics.android
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.realm.RealmResults
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.SessionGroup
@ -37,13 +38,6 @@ class ExampleInstrumentedUnitTest : RealmInstrumentedUnitTest() {
val realm = this.mockRealm
realm.beginTransaction()
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 10:00")
val ed1 = sdf.parse("01/1/2019 11:00")
val sd2 = sdf.parse("02/1/2019 08:00")
val ed2 = sdf.parse("02/1/2019 11:00")
var s1 = realm.createObject(Session::class.java, "1")
var s2 = realm.createObject(Session::class.java, "2")
@ -53,19 +47,28 @@ class ExampleInstrumentedUnitTest : RealmInstrumentedUnitTest() {
s1.result = realm.createObject(net.pokeranalytics.android.model.realm.Result::class.java)
s2.result = realm.createObject(net.pokeranalytics.android.model.realm.Result::class.java)
// var s1: Session = Session.newInstance()
// var s2: Session = Session.newInstance()
s1.result?.buyin = 100.0 // net result = -100
s2.result?.buyin = 200.0
s2.result?.cashout = 500.0 // net result = 300
s1.result?.netResult = -100.0
s2.result?.netResult = 300.0
s1.cgBigBlind = 0.5 // bb net result = -200bb
s2.cgBigBlind = 2.0 // bb net result = 150bb
realm.insert(s1)
realm.insert(s2)
realm.commitTransaction()
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 10:00")
val ed1 = sdf.parse("01/1/2019 11:00")
val sd2 = sdf.parse("02/1/2019 08:00")
val ed2 = sdf.parse("02/1/2019 11:00")
realm.beginTransaction()
s1.timeFrame?.setDate(sd1, ed1)
s2.timeFrame?.setDate(sd2, ed2)
s1.timeFrame?.setDate(sd1, ed1) // duration = 1h, hourly = -100, bb100 = -200bb / 25hands * 100 = -800
s2.timeFrame?.setDate(sd2, ed2) // duration = 3h, hourly = 100, bb100 = 150 / 75 * 100 = +200
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
@ -75,7 +78,10 @@ class ExampleInstrumentedUnitTest : RealmInstrumentedUnitTest() {
val sessions = realm.where(Session::class.java).findAll()
val group = SessionGroup(name = "test", sessions = sessions)
val results: ComputedResults = Calculator.compute(group, Calculator.Options())
var options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val delta = 0.01
val sum = results.computedStat(Stat.NETRESULT)
@ -99,8 +105,315 @@ class ExampleInstrumentedUnitTest : RealmInstrumentedUnitTest() {
Assert.fail("No duration stat")
}
val hourlyRate = results.computedStat(Stat.HOURLY_RATE)
if (hourlyRate != null) {
assertEquals(50.0, hourlyRate.value, delta)
} else {
Assert.fail("No houry rate stat")
}
val handsPlayed = results.computedStat(Stat.HANDS_PLAYED)
if (handsPlayed != null) {
assertEquals(100.0, handsPlayed.value, delta)
} else {
Assert.fail("No hands played stat")
}
val numberOfGames = results.computedStat(Stat.NUMBER_OF_GAMES)
if (numberOfGames != null) {
assertEquals(2, numberOfGames.value.toInt())
} else {
Assert.fail("No numberOfGames stat")
}
val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
if (numberOfSets != null) {
assertEquals(2, numberOfSets.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
val avgBuyin = results.computedStat(Stat.AVERAGE_BUYIN)
if (avgBuyin != null) {
assertEquals(150.0, avgBuyin.value, delta)
} else {
Assert.fail("No avgBuyin stat")
}
val avgDuration = results.computedStat(Stat.AVERAGE_DURATION)
if (avgDuration != null) {
assertEquals(2.0, avgDuration.value, delta)
} else {
Assert.fail("No avgDuration stat")
}
val roi = results.computedStat(Stat.ROI)
if (roi != null) {
assertEquals(200 / 300.0, roi.value, delta)
} else {
Assert.fail("No roi stat")
}
val avgBBNet = results.computedStat(Stat.AVERAGE_NET_BB)
if (avgBBNet != null) {
assertEquals(-25.0, avgBBNet.value, delta)
} else {
Assert.fail("No avgBBNet stat")
}
val bbHourlyRate = results.computedStat(Stat.HOURLY_RATE_BB)
if (bbHourlyRate != null) {
assertEquals(-12.5, bbHourlyRate.value, delta)
} else {
Assert.fail("No bbHourlyRate stat")
}
val netbbPer100Hands = results.computedStat(Stat.NET_BB_PER_100_HANDS)
if (netbbPer100Hands != null) {
assertEquals(-50.0, netbbPer100Hands.value, delta)
} else {
Assert.fail("No netbbPer100Hands stat")
}
// val stdHourly = results.computedStat(Stat.STANDARD_DEVIATION_HOURLY)
// if (stdHourly != null) {
// assertEquals(111.8, stdHourly.value, delta)
// } else {
// Assert.fail("No stdHourly stat")
// }
//
// val std = results.computedStat(Stat.STANDARD_DEVIATION)
// if (std != null) {
// assertEquals(200.0, std.value, delta)
// } else {
// Assert.fail("No std stat")
// }
//
// val std100 = results.computedStat(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS)
// if (std100 != null) {
// assertEquals(503.12, std100.value, delta)
// } else {
// Assert.fail("No std100 stat")
// }
}
@Test
fun testOverlappingSessions1() {
val realm = this.mockRealm
realm.beginTransaction()
var s1 = realm.createObject(Session::class.java, "1")
var s2 = realm.createObject(Session::class.java, "2")
s1.timeFrame = realm.createObject(TimeFrame::class.java)
s2.timeFrame = realm.createObject(TimeFrame::class.java)
s1.result = realm.createObject(net.pokeranalytics.android.model.realm.Result::class.java)
s2.result = realm.createObject(net.pokeranalytics.android.model.realm.Result::class.java)
realm.insert(s1)
realm.insert(s2)
realm.commitTransaction()
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 09:00")
val ed1 = sdf.parse("01/1/2019 10:00")
val sd2 = sdf.parse("01/1/2019 08:00")
val ed2 = sdf.parse("01/1/2019 11:00")
realm.beginTransaction()
s1.timeFrame?.setDate(sd1, ed1) // duration = 1h, hourly = -100, bb100 = -200bb / 25hands * 100 = -800
s2.timeFrame?.setDate(sd2, ed2) // duration = 4h, hourly = 100, bb100 = 150 / 75 * 100 = +200
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
realm.commitTransaction()
val sessions = realm.where(Session::class.java).findAll()
val group = SessionGroup(name = "test", sessions = sessions)
var options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val delta = 0.01
val duration = results.computedStat(Stat.DURATION)
if (duration != null) {
assertEquals(3.0, duration.value, delta)
} else {
Assert.fail("No Net result stat")
}
val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
if (numberOfSets != null) {
assertEquals(1, numberOfSets.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
val numberOfGames = results.computedStat(Stat.NUMBER_OF_GAMES)
if (numberOfGames != null) {
assertEquals(2, numberOfGames.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
}
@Test
fun testOverlappingSessions2() {
val realm = this.mockRealm
realm.beginTransaction()
var s1 = realm.createObject(Session::class.java, "1")
var s2 = realm.createObject(Session::class.java, "2")
var s3 = realm.createObject(Session::class.java, "3")
s1.timeFrame = realm.createObject(TimeFrame::class.java)
s2.timeFrame = realm.createObject(TimeFrame::class.java)
s3.timeFrame = realm.createObject(TimeFrame::class.java)
realm.insert(s1)
realm.insert(s2)
realm.insert(s3)
realm.commitTransaction()
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 05:00")
val ed1 = sdf.parse("01/1/2019 09:00")
val sd2 = sdf.parse("01/1/2019 07:00")
val ed2 = sdf.parse("01/1/2019 11:00")
val sd3 = sdf.parse("01/1/2019 03:00")
val ed3 = sdf.parse("01/1/2019 06:00")
realm.beginTransaction()
s1.timeFrame?.setDate(sd1, ed1) // duration = 4h
s2.timeFrame?.setDate(sd2, ed2) // duration = 4h
s3.timeFrame?.setDate(sd3, ed3) // duration = 3h
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
realm.copyToRealmOrUpdate(s3)
realm.commitTransaction()
val sessions = realm.where(Session::class.java).findAll()
val group = SessionGroup(name = "test", sessions = sessions)
var options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val delta = 0.01
val duration = results.computedStat(Stat.DURATION)
if (duration != null) {
assertEquals(8.0, duration.value, delta)
} else {
Assert.fail("No Net result stat")
}
val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
if (numberOfSets != null) {
assertEquals(1, numberOfSets.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
val numberOfGames = results.computedStat(Stat.NUMBER_OF_GAMES)
if (numberOfGames != null) {
assertEquals(3, numberOfGames.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
}
var sessions: RealmResults<Session>? = null
@Test
fun testOverlappingSessionDeletion() {
val realm = this.mockRealm
// this.sessions = realm.where(Session::class.java).findAll() // monitor session deletions
// Looper.prepare()
// this.sessions?.addChangeListener { t, changeSet ->
//
// val deletedSessions = realm.where(Session::class.java).`in`("id", changeSet.deletions.toTypedArray()).findAll()
// deletedSessions.forEach { it.cleanup() }
//
// }
// Looper.loop()
realm.beginTransaction()
var s1 = realm.createObject(Session::class.java, "1")
var s2 = realm.createObject(Session::class.java, "2")
var s3 = realm.createObject(Session::class.java, "3")
s1.timeFrame = realm.createObject(TimeFrame::class.java)
s2.timeFrame = realm.createObject(TimeFrame::class.java)
s3.timeFrame = realm.createObject(TimeFrame::class.java)
realm.insert(s1)
realm.insert(s2)
realm.insert(s3)
realm.commitTransaction()
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 05:00")
val ed1 = sdf.parse("01/1/2019 09:00")
val sd2 = sdf.parse("01/1/2019 07:00")
val ed2 = sdf.parse("01/1/2019 11:00")
val sd3 = sdf.parse("01/1/2019 03:00")
val ed3 = sdf.parse("01/1/2019 06:00")
realm.beginTransaction()
s1.timeFrame?.setDate(sd1, ed1) // duration = 4h
s2.timeFrame?.setDate(sd2, ed2) // duration = 4h
s3.timeFrame?.setDate(sd3, ed3) // duration = 3h
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
realm.copyToRealmOrUpdate(s3)
realm.commitTransaction()
val sessions = realm.where(Session::class.java).findAll()
val group = SessionGroup(name = "test", sessions = sessions)
var options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val delta = 0.01
val duration = results.computedStat(Stat.DURATION)
if (duration != null) {
assertEquals(8.0, duration.value, delta)
} else {
Assert.fail("No duration stat")
}
realm.beginTransaction()
s1.deleteFromRealm()
realm.commitTransaction()
// realm.executeTransaction {
// s1.deleteFromRealm()
// }
val group2 = SessionGroup(name = "test", sessions = sessions)
val results2: ComputedResults = Calculator.compute(group2, options)
val duration2 = results2.computedStat(Stat.DURATION)
if (duration2 != null) {
assertEquals(7.0, duration2.value, delta)
} else {
Assert.fail("No duration2 stat")
}
}
}

@ -3,13 +3,29 @@ package net.pokeranalytics.android
import android.app.Application
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmResults
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.util.PokerAnalyticsLogs
import timber.log.Timber
class PokerAnalyticsApplication: Application() {
// var timeFrames: RealmResults<Session>? = null
var sessions: RealmResults<Session>? = null
// private val listener: OrderedRealmCollectionChangeListener<RealmResults<Session>> =
// OrderedRealmCollectionChangeListener() { realmResults: RealmResults<Session>, changeSet: OrderedCollectionChangeSet ->
//
// if (changeSet == null) {
// return@OrderedRealmCollectionChangeListener
// }
//
// val realm: Realm = Realm.getDefaultInstance()
//
// val deletedSessions = realm.where(Session::class.java).`in`("id", changeSet.deletions.toTypedArray()).findAll()
// deletedSessions.forEach { it.cleanup() }
//
// }
override fun onCreate() {
super.onCreate()
@ -22,12 +38,15 @@ class PokerAnalyticsApplication: Application() {
.build()
Realm.setDefaultConfiguration(realmConfiguration)
// val realm: Realm = Realm.getDefaultInstance()
// // Add observer on session time frame changes
// this.timeFrames = realm.where(Session::class.java).findAllAsync()
// this.timeFrames?.addChangeListener { t, changeSet -> // @todo check if main thread has running Looper, cf Realm doc
// changeSet.deletions
// }
val realm: Realm = Realm.getDefaultInstance()
// Add observer on session time frame changes
this.sessions = realm.where(Session::class.java).findAll() // monitor session deletions
this.sessions?.addChangeListener { t, changeSet ->
val deletedSessions = realm.where(Session::class.java).`in`("id", changeSet.deletions.toTypedArray()).findAll()
deletedSessions.forEach { it.cleanup() }
}
if (BuildConfig.DEBUG) {
// Logs

@ -3,11 +3,19 @@ package net.pokeranalytics.android.calculus
import net.pokeranalytics.android.calculus.Stat.*
import net.pokeranalytics.android.model.realm.SessionSet
/**
* The class performing stats computation
*/
class Calculator {
/**
* The options used for calculations or display
*/
class Options {
/**
* The way the stats are going to be displayed
*/
enum class Display {
TABLE,
EVOLUTION,
@ -16,6 +24,9 @@ class Calculator {
POLYNOMIAL
}
/**
* The type of evolution values
*/
enum class EvolutionValues {
NONE,
STANDARD,
@ -26,6 +37,9 @@ class Calculator {
var evolutionValues: EvolutionValues = EvolutionValues.NONE
var displayedStats: List<Stat> = listOf()
/**
* This function determines whether the standard deviation should be computed
*/
fun shouldComputeStandardDeviation() : Boolean {
this.displayedStats.forEach { stat ->
return when (stat) {
@ -41,14 +55,16 @@ class Calculator {
companion object {
fun computePreAggregation(sets: List<SessionSet>, options: Options): List<ComputedGroup> {
fun computePreAggregation(sets: List<SessionSet>, options: Options): List<ComputedResults> {
return listOf()
}
// Computes all stats for list of Session sessionGroup
fun computeGroups(groups: List<SessionGroup>, options: Options): List<ComputedGroup> {
/**
* Computes all stats for list of Session sessionGroup
*/
fun computeGroups(groups: List<SessionGroup>, options: Options): List<ComputedResults> {
var computedGroups: MutableList<ComputedGroup> = mutableListOf()
var computedResults: MutableList<ComputedResults> = mutableListOf()
groups.forEach { group ->
// Computes actual sessionGroup stats
val results: ComputedResults = Calculator.compute(group, options = options)
@ -57,19 +73,20 @@ class Calculator {
val comparedGroup = group.comparedSessions
if (comparedGroup != null) {
val comparedResults = Calculator.compute(comparedGroup, options = options)
group.comparedComputedGroup = ComputedGroup(comparedGroup, comparedResults)
group.comparedComputedResults = comparedResults
results.computeStatVariations(comparedResults)
}
results.finalize(options)
computedGroups.add(ComputedGroup(group, results))
results.finalize(options) // later treatment, such as evolution values sorting
computedResults.add(results)
}
return computedGroups
return computedResults
}
// Computes stats for a SessionSet
/**
* Computes stats for a SessionSet
*/
fun compute(sessionGroup: SessionGroup, options: Options) : ComputedResults {
val sessions: List<SessionInterface> = sessionGroup.sessions
@ -124,15 +141,15 @@ class Calculator {
gTotalHands += sessionSet.estimatedHands
gBBSum += sessionSet.bbNetResult
hourlyRate = gSum / duration * 3600.0
hourlyRateBB = gBBSum / duration * 3600.0
hourlyRate = gSum / duration
hourlyRateBB = gBBSum / duration
if (options.evolutionValues == Options.EvolutionValues.DATED) {
results.addEvolutionValue(gSum, duration, NETRESULT)
results.addEvolutionValue(gSum / duration * 3600.0, duration, HOURLY_RATE)
results.addEvolutionValue(gSum / duration, duration, HOURLY_RATE)
results.addEvolutionValue(Stat.netBBPer100Hands(gBBSum, gTotalHands), duration, NET_BB_PER_100_HANDS)
results.addEvolutionValue(hourlyRate, duration, HOURLY_RATE)
results.addEvolutionValue(gIndex.toDouble(), duration, NUMBER_OF_GROUPS)
results.addEvolutionValue(gIndex.toDouble(), duration, NUMBER_OF_SETS)
results.addEvolutionValue(sessionSet.duration.toDouble(), duration, DURATION)
results.addEvolutionValue(duration / gIndex, duration, AVERAGE_DURATION)
results.addEvolutionValue(hourlyRateBB, duration, HOURLY_RATE_BB)
@ -147,11 +164,11 @@ class Calculator {
ComputedStat(HOURLY_RATE, hourlyRate),
ComputedStat(AVERAGE, average),
ComputedStat(DURATION, duration),
ComputedStat(NUMBER_OF_GROUPS, sessionSets.size.toDouble()),
ComputedStat(NUMBER_OF_SETS, sessionSets.size.toDouble()),
ComputedStat(NUMBER_OF_GAMES, sessions.size.toDouble()),
ComputedStat(AVERAGE_DURATION, (duration / 3600.0) / sessions.size),
ComputedStat(AVERAGE_DURATION, duration / sessions.size),
ComputedStat(NET_BB_PER_100_HANDS, Stat.netBBPer100Hands(bbSum, totalHands)),
ComputedStat(HOURLY_RATE_BB, bbSum / duration * 3600.0),
ComputedStat(HOURLY_RATE_BB, bbSum / duration),
ComputedStat(AVERAGE_NET_BB, bbSum / bbSessionCount),
ComputedStat(WIN_RATIO, (winningSessionCount / sessions.size).toDouble()),
ComputedStat(AVERAGE_BUYIN, totalBuyin / sessions.size),
@ -160,7 +177,6 @@ class Calculator {
))
// Standard Deviation
if (options.shouldComputeStandardDeviation()) {
@ -178,7 +194,7 @@ class Calculator {
results.addStats(setOf(
ComputedStat(STANDARD_DEVIATION, standardDeviation),
ComputedStat(Stat.STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation)
ComputedStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation)
))
}

@ -32,20 +32,7 @@ class SessionGroup(name: String, sessions: List<SessionInterface>) {
var comparedSessions: SessionGroup? = null
// The computed stats of the comparable sessionGroup
var comparedComputedGroup: ComputedGroup? = null
}
class ComputedGroup(sessionGroup: SessionGroup, computedResults: ComputedResults) {
// A computable sessionGroup
var sessionGroup: SessionGroup = sessionGroup
// The computed stats of the sessionGroup
var computedResults: ComputedResults = computedResults
fun statValue(stat: Stat) : Double? {
return computedResults.computedStat(stat)?.value
}
var comparedComputedResults: ComputedResults? = null
}

@ -4,16 +4,15 @@ import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
interface AnyStat {
}
enum class Stat : AnyStat, RowRepresentable {
/**
* An enum representing all the types of Session statistics
*/
enum class Stat : RowRepresentable {
NETRESULT,
HOURLY_RATE,
AVERAGE,
NUMBER_OF_GROUPS,
NUMBER_OF_SETS,
NUMBER_OF_GAMES,
DURATION,
AVERAGE_DURATION,
@ -28,12 +27,9 @@ enum class Stat : AnyStat, RowRepresentable {
STANDARD_DEVIATION_BB_PER_100_HANDS,
HANDS_PLAYED;
fun label() : String = when (this) {
NETRESULT -> ""
HOURLY_RATE -> ""
else -> throw IllegalArgumentException("Label not defined")
}
/**
* Returns whether the stat evolution values requires a distribution sorting
*/
fun hasDistributionSorting() : Boolean {
when (this) {
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> return true
@ -59,7 +55,7 @@ enum class Stat : AnyStat, RowRepresentable {
NETRESULT -> R.string.net_result
HOURLY_RATE -> R.string.average_hour_rate
AVERAGE -> R.string.average
NUMBER_OF_GROUPS -> R.string.number_of_groups
NUMBER_OF_SETS -> R.string.number_of_groups
NUMBER_OF_GAMES -> R.string.number_of_games
DURATION -> R.string.duration
AVERAGE_DURATION -> R.string.average_duration
@ -79,11 +75,9 @@ enum class Stat : AnyStat, RowRepresentable {
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
}
enum class CashSessionStat : AnyStat {
NETBB,
AVERAGEBB
}
/**
* ComputedStat contains a [stat] and their associated [value]
*/
class ComputedStat(stat: Stat, value: Double) {
constructor(stat: Stat, value: Double, previousValue: Double?) : this(stat, value) {
@ -92,23 +86,31 @@ class ComputedStat(stat: Stat, value: Double) {
}
}
// The statistic type
/**
* The statistic type
*/
var stat: Stat = stat
// The stat value
/**
* The stat value
*/
var value: Double = value
// The variation of the stat
/**
* The variation of the stat
*/
var variation: Double? = null
// The data points leading to the current stat value
// var points: List<Point> = mutableListOf()
// Formats the value of the stat to be suitable for display
/**
* Formats the value of the stat to be suitable for display
*/
fun format() : StatFormat {
return StatFormat()
}
/**
* Returns a StatFormat instance for an evolution value located at the specified [index]
*/
fun evolutionValueFormat(index: Int) : StatFormat {
return StatFormat()
}

@ -4,14 +4,25 @@ import io.realm.Realm
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.Sort
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.realm.*
import java.util.*
import net.pokeranalytics.android.ui.view.Localizable
enum class LiveData {
/**
* An interface to easily handle the validity of any object we want to save
*/
interface ObjectSavable {
fun isValidForSave(): Boolean { return true }
}
/**
* An enum managing the business objects related to a realm results
*/
enum class LiveData : Localizable {
BANKROLL,
GAME,
LOCATION,
TOURNAMENT_TYPE,
TOURNAMENT_FEATURE,
TRANSACTION_TYPE;
fun items(realm: Realm, fieldName: String? = null, sortOrder: Sort? = null): RealmResults<*> {
@ -28,11 +39,21 @@ enum class LiveData {
BANKROLL -> Bankroll::class.java
GAME -> Game::class.java
LOCATION -> Location::class.java
TOURNAMENT_TYPE -> TournamentFeature::class.java
TOURNAMENT_FEATURE -> TournamentFeature::class.java
TRANSACTION_TYPE -> TransactionType::class.java
}
}
fun newEntity(): RealmObject {
return when (this) {
BANKROLL -> Bankroll()
GAME -> Game()
LOCATION -> Location()
TOURNAMENT_FEATURE -> TournamentFeature()
TRANSACTION_TYPE -> TransactionType()
}
}
fun getData(realm:Realm, primaryKey:String?): RealmObject? {
var proxyItem: RealmObject? = null
primaryKey?.let {
@ -49,13 +70,24 @@ enum class LiveData {
proxyItem?.let {
return realm.copyFromRealm(it)
} ?: run {
realm.beginTransaction()
return this.newEntity()
/* realm.beginTransaction()
val t = realm.createObject(this.relatedEntity, UUID.randomUUID().toString())
realm.commitTransaction()
return realm.copyFromRealm(t)
return realm.copyFromRealm(t)*/
}
}
}
override val resId: Int?
get() {
return when (this) {
BANKROLL -> R.string.bankroll
GAME -> R.string.game
LOCATION -> R.string.location
TOURNAMENT_FEATURE -> R.string.tournament_type
TRANSACTION_TYPE -> R.string.operation_types
}
}}
/*
interface ListableDataSource {

@ -4,6 +4,7 @@ import android.text.InputType
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.model.ObjectSavable
import net.pokeranalytics.android.ui.adapter.components.LiveDataDataSource
import net.pokeranalytics.android.ui.adapter.components.RowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetData
@ -15,7 +16,7 @@ import java.util.*
import kotlin.collections.ArrayList
open class Bankroll(name: String = "") : RealmObject(), RowRepresentableDataSource, LiveDataDataSource,
RowEditable {
RowEditable, ObjectSavable {
companion object {
fun newInstance() : Bankroll {

@ -3,6 +3,7 @@ package net.pokeranalytics.android.model.realm
import android.text.InputType
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.model.ObjectSavable
import net.pokeranalytics.android.ui.adapter.components.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetData
import net.pokeranalytics.android.ui.view.RowEditable
@ -11,7 +12,7 @@ import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.SimpleRow
import java.util.*
open class Game : RealmObject(), RowRepresentableDataSource, LiveDataDataSource, RowEditable {
open class Game : RealmObject(), RowRepresentableDataSource, LiveDataDataSource, RowEditable, ObjectSavable {
@PrimaryKey
var id = UUID.randomUUID().toString()
@ -35,6 +36,7 @@ open class Game : RealmObject(), RowRepresentableDataSource, LiveDataDataSource,
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
SimpleRow.NAME -> this.name
GameRow.SHORT_NAME -> this.shortName?:""
else -> return super.stringForRow(row)
}
}
@ -43,6 +45,7 @@ open class Game : RealmObject(), RowRepresentableDataSource, LiveDataDataSource,
val data = java.util.ArrayList<BottomSheetData>()
when (row) {
SimpleRow.NAME -> data.add(BottomSheetData(this.name, SimpleRow.NAME.resId, InputType.TYPE_CLASS_TEXT))
GameRow.SHORT_NAME -> data.add(BottomSheetData(this.shortName, GameRow.SHORT_NAME.resId, InputType.TYPE_CLASS_TEXT))
}
return data
}
@ -50,6 +53,11 @@ open class Game : RealmObject(), RowRepresentableDataSource, LiveDataDataSource,
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
GameRow.SHORT_NAME -> this.shortName = value as String
}
}
override fun isValidForSave(): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

@ -164,7 +164,7 @@ open class Session : RealmObject(), SessionInterface, RowRepresentableDataSource
}
@Ignore
override var estimatedHands: Double = 0.0
override var estimatedHands: Double = 25.0 * (this.timeFrame?.hourlyDuration?.toDouble() ?: 0.0)
@Ignore
override var bbNetResult: Double = 0.0
@ -191,6 +191,32 @@ open class Session : RealmObject(), SessionInterface, RowRepresentableDataSource
return 0.0
}
/**
* This method is called whenever a session is about to be deleted
*/
fun cleanup() {
this.sessionSet?.let { set ->
// get all sessions part of the deleted session set
val sessionsFromSet = set.sessions
// cleanup unecessary related objects
set.deleteFromRealm()
this.timeFrame?.deleteFromRealm()
this.result?.deleteFromRealm()
// make sessions recreate/find their session set
sessionsFromSet?.let { sessions ->
sessions.forEach { session ->
session.timeFrame?.notifySessionDateChange()
}
}
}
}
override fun adapterRows(): ArrayList<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
rows.addAll(SessionRow.getRowsForState(getState()))

@ -1,18 +1,25 @@
package net.pokeranalytics.android.model.realm
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects
open class SessionSet() : RealmObject() {
// The timeframe of the set, i.e. its start & end date
/**
* The timeframe of the set, i.e. its start & end date
*/
var timeFrame: TimeFrame? = null
// The list of Session played within the set, i.e. played within the same time frame
var sessions: RealmList<Session> = RealmList()
/**
* The list of sessions associated with this set
*/
@LinkingObjects("sessionSet")
val sessions: RealmResults<Session>? = null
@Ignore // a duration shortcut
var duration: Long = 0L
@ -28,12 +35,15 @@ open class SessionSet() : RealmObject() {
@Ignore // a netResult shortcut
var netResult: Double = 0.0
get () {
return this.sessions?.sumByDouble { it.value } ?: 0.0
}
@Ignore // a duration shortcut
var hourlyRate: Double = 0.0
@Ignore
var estimatedHands: Double = 0.0
var estimatedHands: Double = 25.0 * (this.timeFrame?.hourlyDuration?.toDouble() ?: 0.0)
@Ignore
var bbNetResult: Double = 0.0

@ -7,6 +7,7 @@ import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects
import net.pokeranalytics.android.exceptions.ModelException
import timber.log.Timber
import java.util.*
open class TimeFrame : RealmObject() {
@ -81,24 +82,26 @@ open class TimeFrame : RealmObject() {
this.duration = netDuration
}
private fun notifySessionDateChange() {
fun notifySessionDateChange() {
val realm = Realm.getDefaultInstance()
var query: RealmQuery<SessionSet> = realm.where(SessionSet::class.java)
query.isNotNull("timeFrame")
// Timber.d("this> sd = : ${this.startDate}, ed = ${this.endDate}")
if (this.endDate == null) {
query.greaterThan("timeFrame.startDate", this.startDate.time).or().greaterThan("timeFrame.endDate", this.startDate.time)
} else {
val endDate = this.endDate!!
query
.greaterThan("timeFrame.startDate", this.startDate)
.lessThan("timeFrame.endDate", this.startDate)
.or()
.greaterThan("timeFrame.startDate", endDate)
.lessThan("timeFrame.endDate", endDate)
.or()
.lessThan("timeFrame.startDate", this.startDate)
.greaterThan("timeFrame.endDate", this.startDate)
.or()
.lessThan("timeFrame.startDate", endDate)
.greaterThan("timeFrame.endDate", endDate)
.or()
.greaterThan("timeFrame.startDate", this.startDate)
.lessThan("timeFrame.endDate", endDate)
}
val sessionGroups = query.findAll()
@ -127,7 +130,6 @@ open class TimeFrame : RealmObject() {
private fun createSessionGroup() {
val realm = Realm.getDefaultInstance()
// realm.beginTransaction()
val set: SessionSet = SessionSet.newInstance(realm)
set.timeFrame?.let {
@ -142,9 +144,9 @@ open class TimeFrame : RealmObject() {
} ?: run {
throw ModelException("Session should never be null here")
}
// this.session?.sessionSet = set
// set.sessions.add(this.session)
// realm.commitTransaction()
Timber.d("sd = : ${set.timeFrame?.startDate}, ed = ${set.timeFrame?.endDate}")
}
/**
@ -165,14 +167,7 @@ open class TimeFrame : RealmObject() {
groupTimeFrame.endDate = null
}
// Realm Update
// val realm = Realm.getDefaultInstance()
realm.beginTransaction()
if (!sessionSet.sessions.contains(this.session)) {
sessionSet.sessions.add(this.session)
}
// realm.copyToRealmOrUpdate(groupTimeFrame)
// realm.commitTransaction()
this.session?.sessionSet = sessionSet
}
@ -185,6 +180,7 @@ open class TimeFrame : RealmObject() {
var startDate: Date = this.startDate
var endDate: Date? = this.endDate
// find earlier and later dates from all sets
val timeFrames = sessionSets.mapNotNull { it.timeFrame }
timeFrames.forEach { tf ->
if (tf.startDate.before(startDate)) {
@ -204,11 +200,8 @@ open class TimeFrame : RealmObject() {
}
// get all sessions from sets
var sessions = sessionSets.flatMap { it.sessions }
// Start Realm updates
// val realm = Realm.getDefaultInstance()
// realm.beginTransaction()
var sessions = mutableSetOf<Session>()
sessionSets.forEach { it.sessions?.asIterable()?.let { it1 -> sessions.addAll(it1) } }
// delete all sets
sessionSets.deleteAllFromRealm()
@ -224,15 +217,14 @@ open class TimeFrame : RealmObject() {
// Add the session linked to this timeframe to the new sessionGroup
this.sessions?.first()?.let {
set.sessions.add(it)
it.sessionSet = set
} ?: run {
throw ModelException("TimeFrame should never be null here")
}
// Add all orphan sessions
set.sessions.addAll(sessions)
sessions.forEach { it.sessionSet = set }
// realm.commitTransaction()
}
}

@ -3,22 +3,37 @@ package net.pokeranalytics.android.ui.adapter.components
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import net.pokeranalytics.android.ui.view.DynamicHolder
import net.pokeranalytics.android.ui.view.BindableHolder
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
/**
* An interface used to provide RowRepresentableAdapter content and value in the form of rows
*/
interface RowRepresentableDataSource {
/**
* Returns a list of rows
*/
fun adapterRows(): ArrayList<RowRepresentable>
/**
* Returns a boolean for a specific row
*/
fun boolForRow(row: RowRepresentable): Boolean {
return false
}
/**
* Returns a string for a specific row
*/
fun stringForRow(row: RowRepresentable): String {
return ""
}
/**
* Returns an action icon identifier for a specific row
*/
fun actionIconForRow(row: RowRepresentable): Int? {
return 0
}
@ -34,14 +49,25 @@ interface RowRepresentableDataSource {
}
/**
* A delegate used to propagate UI actions
*/
interface RowRepresentableDelegate {
fun onRowSelected(row: RowRepresentable) {}
fun onActionSelected(row: RowRepresentable) {}
}
/**
* An adapter capable of displaying a list of RowRepresentables
* @param rowRepresentableDataSource the datasource providing rows
* @param rowRepresentableDelegate the delegate, notified of UI actions
*/
class RowRepresentableAdapter(var rowRepresentableDataSource: RowRepresentableDataSource, var rowRepresentableDelegate: RowRepresentableDelegate? = null) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
/**
* The list of rows to display
*/
private var rows: ArrayList<RowRepresentable> = ArrayList()
init {
@ -72,7 +98,7 @@ class RowRepresentableAdapter(var rowRepresentableDataSource: RowRepresentableDa
rowRepresentableDelegate?.onActionSelected(dynamicRow)
}
(holder as DynamicHolder).bind(dynamicRow, this.rowRepresentableDataSource, listener, actionListener)
(holder as BindableHolder).bind(dynamicRow, this.rowRepresentableDataSource, listener, actionListener)
}
/**

@ -1,17 +1,19 @@
package net.pokeranalytics.android.ui.fragment
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm
import io.realm.RealmObject
import kotlinx.android.synthetic.main.fragment_editable_data.*
import kotlinx.android.synthetic.main.fragment_editable_data.view.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.model.ObjectSavable
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.adapter.components.*
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
@ -78,26 +80,56 @@ class EditableDataFragment : PokerAnalyticsFragment(), RowRepresentableDelegate,
setHasFixedSize(true)
layoutManager = viewManager
}
this.saveButton.text = this.saveButton.context.getString(R.string.save)
this.saveButton.setOnClickListener {
if ((this.item as ObjectSavable).isValidForSave()) {
this.getRealm().executeTransaction {
it.copyToRealmOrUpdate(this.item)
}
this.activity?.let {
it.finish()
}
} else {
val builder = AlertDialog.Builder(it.context)
builder.setTitle(R.string.warning)
.setNegativeButton(R.string.ok, null)
builder.show()
}
}
this.deleteButton.text = this.deleteButton.context.getString(R.string.delete)
this.deleteButton.setOnClickListener {
val builder = AlertDialog.Builder(it.context)
builder.setTitle(R.string.warning)
.setMessage(R.string.are_you_sure_you_want_to_do_that_)
.setNeutralButton(R.string.no, null)
.setNegativeButton(R.string.yes, DialogInterface.OnClickListener { dialog, id ->
this.getRealm().executeTransaction {
this.item.deleteFromRealm()
}
this.activity?.let {
it.finish()
}
})
builder.show()
}
}
/**
* Set fragment data
*/
fun setData(dataType: Int, primaryKey: String?) {
this.liveDataType = LiveData.values()[dataType]
val realm = Realm.getDefaultInstance()
var proxyItem : RealmObject? = this.liveDataType.getData(realm, primaryKey)
var proxyItem : RealmObject? = this.liveDataType.getData(this.getRealm(), primaryKey)
proxyItem?.let {
this.appBar.toolbar.title = "Update ${this.liveDataType.name.toLowerCase().capitalize()}"
} ?: run {
this.appBar.toolbar.title = "New ${this.liveDataType.name.toLowerCase().capitalize()}"
}
this.item = this.liveDataType.updateOrCreate(realm, primaryKey)
this.item = this.liveDataType.updateOrCreate(this.getRealm(), primaryKey)
this.rowRepresentableAdapter = RowRepresentableAdapter((this.item as RowRepresentableDataSource), this)
this.recyclerView.adapter = rowRepresentableAdapter
}
}

@ -7,11 +7,9 @@ import net.pokeranalytics.android.model.extensions.SessionState
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
/**
* An interface used so that enums values can be represented visually
* as rows in RecyclerViews
* An interface to easily localize any object
*/
interface RowRepresentable {
interface Localizable {
/**
* The resource identifier of the localized title
*/
@ -29,7 +27,13 @@ interface RowRepresentable {
}
return "LOCALISATION NOT FOUND"
}
}
/**
* An interface used so that enums values can be represented visually
* as rows in RecyclerViews
*/
interface RowRepresentable : Localizable {
/**
* The type of view associated with the row
*/
@ -190,9 +194,53 @@ enum class BankrollRow : RowRepresentable {
}
enum class GameRow : RowRepresentable {
SHORT_NAME;
override val resId: Int?
get() {
return when (this) {
SHORT_NAME -> R.string.short_name
}
}
override val viewType: Int
get() {
return when (this) {
SHORT_NAME -> RowViewType.TITLE_VALUE.ordinal
}
}
override val bottomSheetType: BottomSheetType
get() {
return when (this) {
SHORT_NAME -> BottomSheetType.EDIT_TEXT
}
}
}
enum class LocationRow : RowRepresentable {
LOCATION_STATUS;
override val resId: Int?
get() {
return when (this) {
LOCATION_STATUS -> R.string.short_name
}
}
override val viewType: Int
get() {
return when (this) {
LOCATION_STATUS -> RowViewType.TITLE.ordinal
}
}
override val bottomSheetType: BottomSheetType
get() {
return when (this) {
LOCATION_STATUS -> BottomSheetType.NONE
}
}
}
enum class TransactionTypeRow : RowRepresentable {
@ -205,17 +253,15 @@ enum class SettingRow : RowRepresentable {
BANKROLL,
GAME,
LOCATION,
TOURNAMENT_TYPE,
TOURNAMENT_FEATURE,
TRANSACTION_TYPE;
override val resId: Int?
get() {
return when (this) {
BANKROLL -> R.string.bankroll
GAME -> R.string.game
LOCATION -> R.string.location
TOURNAMENT_TYPE -> R.string.tournament_type
TRANSACTION_TYPE -> R.string.operation_types
this.relatedResultsRepresentable?. let {
return it.resId
} ?: run {
return super.resId
}
}
@ -227,7 +273,7 @@ enum class SettingRow : RowRepresentable {
BANKROLL -> LiveData.BANKROLL
GAME -> LiveData.GAME
LOCATION -> LiveData.LOCATION
TOURNAMENT_TYPE -> LiveData.TOURNAMENT_TYPE
TOURNAMENT_FEATURE -> LiveData.TOURNAMENT_FEATURE
TRANSACTION_TYPE -> LiveData.TRANSACTION_TYPE
}
}

@ -8,7 +8,10 @@ import kotlinx.android.synthetic.main.row_title_value_action.view.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.adapter.components.RowRepresentableDataSource
interface DynamicHolder {
/**
* An interface used to factor the configuration of RecyclerView.ViewHolder
*/
interface BindableHolder {
fun bind(row: RowRepresentable, rowRepresentableDataSource: RowRepresentableDataSource? = null, listener: View.OnClickListener, actionListener: View.OnClickListener? = null) {}
@ -22,13 +25,13 @@ enum class RowViewType {
TITLE_VALUE_ACTION;
inner class FakeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
DynamicHolder {
BindableHolder {
override fun bind(row: RowRepresentable, rowRepresentableDataSource: RowRepresentableDataSource?, listener: View.OnClickListener, actionListener: View.OnClickListener?) {
}
}
inner class TitleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
DynamicHolder {
BindableHolder {
override fun bind(row: RowRepresentable, rowRepresentableDataSource: RowRepresentableDataSource?, listener: View.OnClickListener, actionListener: View.OnClickListener?) {
itemView.title.text = row.localizedTitle(itemView.context)
itemView.container.setOnClickListener(listener)
@ -36,7 +39,7 @@ enum class RowViewType {
}
inner class TitleValueViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
DynamicHolder {
BindableHolder {
override fun bind(row: RowRepresentable, rowRepresentableDataSource: RowRepresentableDataSource?, listener: View.OnClickListener, actionListener: View.OnClickListener?) {
itemView.title.text = row.localizedTitle(itemView.context)
@ -48,7 +51,7 @@ enum class RowViewType {
}
inner class TitleValueActionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
DynamicHolder {
BindableHolder {
override fun bind(row: RowRepresentable, rowRepresentableDataSource: RowRepresentableDataSource?, listener: View.OnClickListener, actionListener: View.OnClickListener?) {
itemView.title.text = row.localizedTitle(itemView.context)
rowRepresentableDataSource?.let { rowDelegate ->

@ -9,7 +9,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
app:layout_behavior="@string/appbar_scrolling_view_behavior" android:id="@+id/nestedScrollView">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@ -22,7 +22,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
@ -51,10 +51,24 @@
android:layout_height="?attr/actionBarSize"
app:title="Poker Analytics"
app:titleTextColor="@color/white"
app:layout_collapseMode="pin" />
app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<Button
tools:text="Save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/saveButton"
app:layout_anchorGravity="right|top"
app:layout_anchor="@+id/appBar"/>
<Button
tools:text="Delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/deleteButton"
app:layout_anchorGravity="bottom|right"
app:layout_anchor="@+id/nestedScrollView"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -18,6 +18,8 @@
<string name="standard_deviation_hourly">Standard deviation hourly</string>
<string name="hands_played">Hands played</string>
<string name="save">Save</string>
<!--
<string name="bankroll">Bankroll</string>
<string name="blinds">Blinds</string>

Loading…
Cancel
Save