commit
1ce9ee3ee3
@ -1,80 +0,0 @@ |
|||||||
package net.pokeranalytics.android.api |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import net.pokeranalytics.android.BuildConfig |
|
||||||
import net.pokeranalytics.android.R |
|
||||||
import net.pokeranalytics.android.model.retrofit.CurrencyConverterValue |
|
||||||
import net.pokeranalytics.android.util.URL |
|
||||||
import okhttp3.Interceptor |
|
||||||
import okhttp3.OkHttpClient |
|
||||||
import okhttp3.logging.HttpLoggingInterceptor |
|
||||||
import retrofit2.Call |
|
||||||
import retrofit2.Retrofit |
|
||||||
import retrofit2.converter.gson.GsonConverterFactory |
|
||||||
import retrofit2.http.GET |
|
||||||
import retrofit2.http.Query |
|
||||||
import java.util.concurrent.TimeUnit |
|
||||||
|
|
||||||
/** |
|
||||||
* CurrencyCode Converter API |
|
||||||
*/ |
|
||||||
interface CurrencyConverterApi { |
|
||||||
|
|
||||||
companion object { |
|
||||||
|
|
||||||
private var currencyConverterApi: CurrencyConverterApi? = null |
|
||||||
|
|
||||||
fun getApi(context: Context): CurrencyConverterApi? { |
|
||||||
|
|
||||||
if (currencyConverterApi == null) { |
|
||||||
|
|
||||||
var serviceEndpoint = URL.API_CURRENCY_CONVERTER |
|
||||||
|
|
||||||
val httpClient = OkHttpClient.Builder() |
|
||||||
|
|
||||||
// Logging interceptor |
|
||||||
if (BuildConfig.DEBUG) { |
|
||||||
val interceptor = HttpLoggingInterceptor() |
|
||||||
interceptor.level = HttpLoggingInterceptor.Level.BASIC |
|
||||||
httpClient.addInterceptor(interceptor) |
|
||||||
} |
|
||||||
|
|
||||||
// Add headers |
|
||||||
val interceptor = Interceptor { chain -> |
|
||||||
val original = chain.request() |
|
||||||
val originalHttpUrl = original.url() |
|
||||||
|
|
||||||
val url = originalHttpUrl.newBuilder() |
|
||||||
.addQueryParameter("apiKey", context.getString(R.string.currency_converter_api)) |
|
||||||
.build() |
|
||||||
|
|
||||||
val requestBuilder = original.newBuilder() |
|
||||||
.url(url) |
|
||||||
|
|
||||||
chain.proceed(requestBuilder.build()) |
|
||||||
} |
|
||||||
httpClient.addInterceptor(interceptor) |
|
||||||
|
|
||||||
val client = httpClient |
|
||||||
.readTimeout(60, TimeUnit.SECONDS) |
|
||||||
.connectTimeout(60, TimeUnit.SECONDS) |
|
||||||
.build() |
|
||||||
|
|
||||||
val retrofit = Retrofit.Builder() |
|
||||||
.addConverterFactory(GsonConverterFactory.create()) |
|
||||||
.baseUrl(serviceEndpoint.value) |
|
||||||
.client(client) |
|
||||||
.build() |
|
||||||
|
|
||||||
currencyConverterApi = retrofit.create(CurrencyConverterApi::class.java) |
|
||||||
} |
|
||||||
|
|
||||||
return currencyConverterApi |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@GET("convert") |
|
||||||
fun convert(@Query("q") currencies: String, @Query("compact") compact: String = "y"): Call<Map<String, CurrencyConverterValue>> |
|
||||||
|
|
||||||
} |
|
||||||
@ -0,0 +1,47 @@ |
|||||||
|
package net.pokeranalytics.android.api |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import com.android.volley.Request |
||||||
|
import com.android.volley.Response |
||||||
|
import com.android.volley.toolbox.StringRequest |
||||||
|
import com.android.volley.toolbox.Volley |
||||||
|
import kotlinx.serialization.json.Json |
||||||
|
import kotlinx.serialization.json.JsonConfiguration |
||||||
|
import timber.log.Timber |
||||||
|
|
||||||
|
class FreeConverterApi { |
||||||
|
|
||||||
|
companion object { |
||||||
|
|
||||||
|
fun currencyRate(pair: String, context: Context, callback: (Double) -> (Unit)) { |
||||||
|
|
||||||
|
val queue = Volley.newRequestQueue(context) |
||||||
|
val url = "https://free.currconv.com/api/v7/convert?q=${pair}&compact=ultra&apiKey=5ba8d38995282fe8b1c8" |
||||||
|
|
||||||
|
// https://free.currconv.com/api/v7/convert?q=GBP_USD&compact=ultra&apiKey=5ba8d38995282fe8b1c8 |
||||||
|
// { "USD_PHP": 44.1105, "PHP_USD": 0.0227 } |
||||||
|
|
||||||
|
val stringRequest = StringRequest( |
||||||
|
Request.Method.GET, url, |
||||||
|
Response.Listener { response -> |
||||||
|
|
||||||
|
val json = Json(JsonConfiguration.Stable) |
||||||
|
val f = json.parseJson(response) |
||||||
|
f.jsonObject[pair]?.primitive?.double?.let { rate -> |
||||||
|
callback(rate) |
||||||
|
} ?: run { |
||||||
|
Timber.d("no rate: $response") |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
Response.ErrorListener { |
||||||
|
Timber.d("Api call failed: ${it.message}") |
||||||
|
}) |
||||||
|
|
||||||
|
queue.add(stringRequest) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,170 @@ |
|||||||
|
package net.pokeranalytics.android.calculus.optimalduration |
||||||
|
|
||||||
|
import io.realm.Realm |
||||||
|
import net.pokeranalytics.android.calculus.Calculator |
||||||
|
import net.pokeranalytics.android.calculus.Stat |
||||||
|
import net.pokeranalytics.android.model.filter.Query |
||||||
|
import net.pokeranalytics.android.model.filter.QueryCondition |
||||||
|
import net.pokeranalytics.android.model.realm.Session |
||||||
|
import org.apache.commons.math3.fitting.PolynomialCurveFitter |
||||||
|
import org.apache.commons.math3.fitting.WeightedObservedPoints |
||||||
|
import timber.log.Timber |
||||||
|
import java.util.* |
||||||
|
import kotlin.math.pow |
||||||
|
import kotlin.math.round |
||||||
|
|
||||||
|
/*** |
||||||
|
* This class attempts to find the optimal game duration, |
||||||
|
* meaning the duration where the player will maximize its results, based on his history. |
||||||
|
* The results stands for cash game, and are separated between live and online. |
||||||
|
* Various reasons can prevent the algorithm to find a duration, see below. |
||||||
|
*/ |
||||||
|
class CashGameOptimalDurationCalculator { |
||||||
|
|
||||||
|
companion object { |
||||||
|
|
||||||
|
private const val bucket = 60 * 60 * 1000L // the duration of bucket |
||||||
|
private const val bucketInterval = 4 // number of duration tests inside the bucket to find the best duration |
||||||
|
private const val minimumValidityCount = 10 // the number of sessions inside a bucket to start having a reasonable average |
||||||
|
private const val intervalValidity = 3 // the minimum number of unit between the shortest & longest valid buckets |
||||||
|
private const val polynomialDegree = 7 // the degree of the computed polynomial |
||||||
|
|
||||||
|
/*** |
||||||
|
* Starts the calculation |
||||||
|
* [isLive] is a boolean to indicate if we're looking at live or online games |
||||||
|
* return a duration or null if it could not be computed |
||||||
|
*/ |
||||||
|
fun start(isLive: Boolean): Double? { |
||||||
|
|
||||||
|
val realm = Realm.getDefaultInstance() |
||||||
|
|
||||||
|
val query = Query().add(QueryCondition.IsCash) // cash game |
||||||
|
query.add(if (isLive) { QueryCondition.IsLive } else { QueryCondition.IsOnline }) // live / online |
||||||
|
query.add(QueryCondition.EndDateNotNull) // ended |
||||||
|
query.add(QueryCondition.BigBlindNotNull) // has BB value |
||||||
|
|
||||||
|
val sessions = query.queryWith(realm.where(Session::class.java)).findAll() |
||||||
|
val sessionsByDuration = sessions.groupBy { |
||||||
|
val dur = round((it.netDuration / bucket).toDouble()) * bucket |
||||||
|
Timber.d("Stop notif > key: $dur") |
||||||
|
dur |
||||||
|
} |
||||||
|
|
||||||
|
// define validity interval |
||||||
|
var start: Double? = null |
||||||
|
var end: Double? = null |
||||||
|
var validBuckets = 0 |
||||||
|
|
||||||
|
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted() |
||||||
|
Timber.d("Stop notif > keys: $hkeys ") |
||||||
|
for (key in sessionsByDuration.keys.sorted()) { |
||||||
|
val sessionCount = sessionsByDuration[key]?.size ?: 0 |
||||||
|
if (start == null && sessionCount >= minimumValidityCount) { |
||||||
|
start = key |
||||||
|
} |
||||||
|
if (sessionCount >= minimumValidityCount) { |
||||||
|
end = key |
||||||
|
validBuckets++ |
||||||
|
} |
||||||
|
} |
||||||
|
Timber.d("Stop notif > validBuckets: $validBuckets ") |
||||||
|
if (!(start != null && end != null && (end - start) >= intervalValidity)) { |
||||||
|
Timber.d("Stop notif > invalid setup: $start / $end ") |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
// define if we have enough sessions |
||||||
|
if (sessions.size < 50) { |
||||||
|
Timber.d("Stop notif > not enough sessions: ${sessions.size} ") |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
val options = Calculator.Options() |
||||||
|
options.query = query |
||||||
|
val report = Calculator.computeStats(realm, options) |
||||||
|
val stdBB = report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value |
||||||
|
|
||||||
|
val p = polynomialRegression(sessions, stdBB) |
||||||
|
|
||||||
|
var bestAverage = 0.0 |
||||||
|
var bestHourlyRate = 0.0 |
||||||
|
var bestDuration = 0.0 |
||||||
|
var maxDuration = 0.0 |
||||||
|
|
||||||
|
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted() |
||||||
|
|
||||||
|
for (key in keys) { |
||||||
|
|
||||||
|
val sessionCount = sessionsByDuration[key]?.size ?: 0 |
||||||
|
|
||||||
|
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid |
||||||
|
|
||||||
|
for (i in 0 until bucketInterval) { |
||||||
|
|
||||||
|
val duration = key + i * bucket / bucketInterval |
||||||
|
|
||||||
|
val averageResult = getBB(duration, p) |
||||||
|
val hourly = averageResult / duration |
||||||
|
if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) { |
||||||
|
bestAverage = averageResult |
||||||
|
bestDuration = duration |
||||||
|
} |
||||||
|
|
||||||
|
if (duration > 0 && hourly > bestHourlyRate) { |
||||||
|
bestHourlyRate = hourly |
||||||
|
} |
||||||
|
if (duration > maxDuration){ |
||||||
|
maxDuration = duration |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
if (bestDuration > 0.0) { |
||||||
|
return bestDuration |
||||||
|
} |
||||||
|
|
||||||
|
Timber.d("Stop notif > not found, best duration: $bestDuration") |
||||||
|
realm.close() |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
private fun getBB(netDuration: Double, polynomial: DoubleArray): Double { |
||||||
|
var y = 0.0 |
||||||
|
for (i in polynomial.indices) { |
||||||
|
y += polynomial[i] * netDuration.pow(i) |
||||||
|
} |
||||||
|
return y |
||||||
|
} |
||||||
|
|
||||||
|
private fun polynomialRegression(sessions: List<Session>, bbStandardDeviation: Double?): DoubleArray { |
||||||
|
|
||||||
|
val stdBB = bbStandardDeviation ?: Double.MAX_VALUE |
||||||
|
|
||||||
|
val points = WeightedObservedPoints() |
||||||
|
val now = Date().time |
||||||
|
|
||||||
|
sessions.forEach { |
||||||
|
var weight = 5.0 |
||||||
|
|
||||||
|
val endTime = it.endDate?.time ?: 0L |
||||||
|
|
||||||
|
val age = now - endTime |
||||||
|
if (age > 2 * 365 * 24 * 3600 * 1000L) { // if more than 2 years loses 1 point |
||||||
|
weight -= 1.0 |
||||||
|
} |
||||||
|
if (it.bbNet > 2 * stdBB) { // if very big result loses 3 points |
||||||
|
weight -= 3.0 |
||||||
|
} |
||||||
|
|
||||||
|
points.add(weight, it.netDuration.toDouble(), it.bbNet) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// polynomial of 7 degree, same as iOS |
||||||
|
return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList()) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,80 @@ |
|||||||
|
package net.pokeranalytics.android.model.handhistory |
||||||
|
|
||||||
|
import net.pokeranalytics.android.exceptions.PAIllegalStateException |
||||||
|
import net.pokeranalytics.android.model.realm.handhistory.Card |
||||||
|
|
||||||
|
/*** |
||||||
|
* An interface used for board changes notifications |
||||||
|
*/ |
||||||
|
interface BoardChangedListener { |
||||||
|
fun boardChanged() |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* The BoardManager purpose is to manage the cards from a hand history board |
||||||
|
* and notify its listener when a change occurs |
||||||
|
*/ |
||||||
|
class BoardManager(cards: List<Card>, var listener: BoardChangedListener) { |
||||||
|
|
||||||
|
/*** |
||||||
|
* The sorted list of cards |
||||||
|
*/ |
||||||
|
private var sortedBoardCards: MutableList<Card> = mutableListOf() |
||||||
|
|
||||||
|
/*** |
||||||
|
* All cards |
||||||
|
*/ |
||||||
|
val allCards: List<Card> |
||||||
|
get() { |
||||||
|
return this.sortedBoardCards |
||||||
|
} |
||||||
|
|
||||||
|
init { |
||||||
|
this.sortedBoardCards = cards.sortedBy { it.index }.toMutableList() |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Adds a card to the board, notifies the listener |
||||||
|
*/ |
||||||
|
fun add(card: Card) { |
||||||
|
|
||||||
|
this.sortedBoardCards.lastOrNull()?.let { |
||||||
|
if (it.suit == null) { |
||||||
|
it.suit = Card.Suit.UNDEFINED |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (this.sortedBoardCards.size == 5) { |
||||||
|
throw PAIllegalStateException("Can't add anymore cards") |
||||||
|
} |
||||||
|
|
||||||
|
card.index = this.sortedBoardCards.size |
||||||
|
|
||||||
|
this.sortedBoardCards.add(card) |
||||||
|
this.listener.boardChanged() |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Clears the street's cards, notifies the listener |
||||||
|
*/ |
||||||
|
fun clearStreet(street: Street) { |
||||||
|
this.sortedBoardCards.removeAll { it.street == street } |
||||||
|
this.listener.boardChanged() |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns the last card of a given [street] |
||||||
|
*/ |
||||||
|
fun lastCard(street: Street) : Card? { |
||||||
|
return this.sortedBoardCards.lastOrNull { it.street == street } |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Remove the given [card], notifies the listener |
||||||
|
*/ |
||||||
|
fun remove(card: Card) { |
||||||
|
this.sortedBoardCards.remove(card) |
||||||
|
this.listener.boardChanged() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,104 @@ |
|||||||
|
package net.pokeranalytics.android.model.handhistory |
||||||
|
|
||||||
|
import io.realm.Realm |
||||||
|
import net.pokeranalytics.android.model.realm.Game |
||||||
|
import net.pokeranalytics.android.model.realm.Session |
||||||
|
import net.pokeranalytics.android.model.realm.handhistory.HandHistory |
||||||
|
import net.pokeranalytics.android.util.extensions.findById |
||||||
|
import timber.log.Timber |
||||||
|
import java.util.* |
||||||
|
|
||||||
|
class HandSetup { |
||||||
|
|
||||||
|
companion object { |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns a HandSetup instance using a [configurationId], the id of a Realm object, |
||||||
|
* Session or HandHistory, used to later configure the HandHistory |
||||||
|
*/ |
||||||
|
fun from(configurationId: String?, attached: Boolean, realm: Realm): HandSetup { |
||||||
|
|
||||||
|
return if (configurationId != null) { |
||||||
|
val handSetup = HandSetup() |
||||||
|
val hh = realm.findById(HandHistory::class.java, configurationId) |
||||||
|
if (hh != null) { |
||||||
|
handSetup.configure(hh) |
||||||
|
} |
||||||
|
val session = realm.findById(Session::class.java, configurationId) |
||||||
|
if (session != null) { |
||||||
|
handSetup.configure(session, attached) |
||||||
|
} |
||||||
|
handSetup |
||||||
|
} else { |
||||||
|
HandSetup() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private fun configure(handHistory: HandHistory) { |
||||||
|
this.smallBlind = handHistory.smallBlind |
||||||
|
this.bigBlind = handHistory.bigBlind |
||||||
|
this.bigBlindAnte = handHistory.bigBlindAnte |
||||||
|
this.ante = handHistory.ante |
||||||
|
this.tableSize = handHistory.numberOfPlayers |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Configures the Hand Setup with a [session] |
||||||
|
* [attached] denotes if the HandHistory must be directly linked to the session |
||||||
|
*/ |
||||||
|
private fun configure(session: Session, attached: Boolean) { |
||||||
|
if (attached) { |
||||||
|
this.session = session |
||||||
|
} |
||||||
|
if (session.endDate == null) { |
||||||
|
this.game = session.game // we don't want to force the max number of cards if unsure |
||||||
|
} |
||||||
|
this.type = session.sessionType |
||||||
|
this.smallBlind = session.cgSmallBlind |
||||||
|
this.bigBlind = session.cgBigBlind |
||||||
|
this.tableSize = session.tableSize |
||||||
|
} |
||||||
|
|
||||||
|
var type: Session.Type? = null |
||||||
|
|
||||||
|
var smallBlind: Double? = null |
||||||
|
|
||||||
|
var bigBlind: Double? = null |
||||||
|
|
||||||
|
var ante: Double? = null |
||||||
|
|
||||||
|
var tableSize: Int? = null |
||||||
|
|
||||||
|
var bigBlindAnte: Boolean = false |
||||||
|
|
||||||
|
var game: Game? = null |
||||||
|
|
||||||
|
var session: Session? = null |
||||||
|
|
||||||
|
var straddlePositions: MutableList<Position> = mutableListOf() |
||||||
|
private set |
||||||
|
|
||||||
|
fun clearStraddles() { |
||||||
|
this.straddlePositions.clear() |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* This method sorts the straddle positions in their natural order |
||||||
|
* If the straddle position contains the button, we're usually in a Mississipi straddle, |
||||||
|
* meaning the BUT straddles, then CO, then HJ... |
||||||
|
* Except if it goes to UTG, in which case we don't know if we're in standard straddle, or Mississipi |
||||||
|
* We use the first straddled position to sort out this case |
||||||
|
*/ |
||||||
|
fun setStraddlePositions(firstStraddlePosition: Position, positions: LinkedHashSet<Position>) { |
||||||
|
var sortedPosition = positions.sortedBy { it.ordinal } |
||||||
|
if (positions.contains(Position.BUT) && firstStraddlePosition != Position.UTG) { |
||||||
|
sortedPosition = sortedPosition.reversed() |
||||||
|
} |
||||||
|
Timber.d("sortedPosition = $sortedPosition") |
||||||
|
this.straddlePositions = sortedPosition.toMutableList() |
||||||
|
Timber.d("this.straddlePositions = ${this.straddlePositions}") |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
package net.pokeranalytics.android.model.handhistory |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import net.pokeranalytics.android.exceptions.PAIllegalStateException |
||||||
|
import net.pokeranalytics.android.ui.view.RowRepresentable |
||||||
|
import java.util.* |
||||||
|
|
||||||
|
enum class Position(var value: String) : RowRepresentable { |
||||||
|
SB("SB"), |
||||||
|
BB("BB"), |
||||||
|
UTG("UTG"), |
||||||
|
UTG1("UTG+1"), |
||||||
|
UTG2("UTG+2"), |
||||||
|
UTG3("UTG+3"), |
||||||
|
MP("MP"), |
||||||
|
HJ("HJ"), |
||||||
|
CO("CO"), |
||||||
|
BUT("BUT"); |
||||||
|
|
||||||
|
companion object { |
||||||
|
|
||||||
|
fun positionsPerPlayers(playerCount: Int) : LinkedHashSet<Position> { |
||||||
|
return when(playerCount) { |
||||||
|
2 -> linkedSetOf(SB, BB) |
||||||
|
3 -> linkedSetOf(SB, BB, BUT) |
||||||
|
4 -> linkedSetOf(SB, BB, UTG, BUT) |
||||||
|
5 -> linkedSetOf(SB, BB, UTG, CO, BUT) |
||||||
|
6 -> linkedSetOf(SB, BB, UTG, HJ, CO, BUT) |
||||||
|
7 -> linkedSetOf(SB, BB, UTG, MP, HJ, CO, BUT) |
||||||
|
8 -> linkedSetOf(SB, BB, UTG, UTG1, MP, HJ, CO, BUT) |
||||||
|
9 -> linkedSetOf(SB, BB, UTG, UTG1, UTG2, MP, HJ, CO, BUT) |
||||||
|
10 -> linkedSetOf(SB, BB, UTG, UTG1, UTG2, UTG3, MP, HJ, CO, BUT) |
||||||
|
else -> throw PAIllegalStateException("Unmanaged number of players") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
val shortValue: String |
||||||
|
get() { |
||||||
|
return when (this) { |
||||||
|
UTG1 -> "+1" |
||||||
|
UTG2 -> "+2" |
||||||
|
UTG3 -> "+3" |
||||||
|
else -> this.value |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun getDisplayName(context: Context): String { |
||||||
|
return this.value |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
package net.pokeranalytics.android.model.handhistory |
||||||
|
|
||||||
|
import net.pokeranalytics.android.R |
||||||
|
import net.pokeranalytics.android.ui.modules.handhistory.replayer.HandStep |
||||||
|
|
||||||
|
enum class Street : HandStep { |
||||||
|
PREFLOP, |
||||||
|
FLOP, |
||||||
|
TURN, |
||||||
|
RIVER, |
||||||
|
SUMMARY; |
||||||
|
|
||||||
|
override val street: Street = this |
||||||
|
|
||||||
|
val totalBoardCards: Int |
||||||
|
get() { |
||||||
|
return when (this) { |
||||||
|
PREFLOP -> 0 |
||||||
|
FLOP -> 3 |
||||||
|
TURN -> 4 |
||||||
|
RIVER, SUMMARY -> 5 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
val resId: Int |
||||||
|
get() { |
||||||
|
return when (this) { |
||||||
|
PREFLOP -> R.string.street_preflop |
||||||
|
FLOP -> R.string.street_flop |
||||||
|
TURN -> R.string.street_turn |
||||||
|
RIVER -> R.string.street_river |
||||||
|
SUMMARY -> R.string.summary |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
val next: Street |
||||||
|
get() { |
||||||
|
return values()[this.ordinal + 1] |
||||||
|
} |
||||||
|
|
||||||
|
val previous: Street? |
||||||
|
get() { |
||||||
|
return when (this) { |
||||||
|
PREFLOP -> null |
||||||
|
else -> values()[this.ordinal - 1] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
@ -1,16 +0,0 @@ |
|||||||
package net.pokeranalytics.android.model.realm |
|
||||||
|
|
||||||
import io.realm.RealmObject |
|
||||||
import io.realm.annotations.PrimaryKey |
|
||||||
import java.util.* |
|
||||||
|
|
||||||
|
|
||||||
open class HandHistory : RealmObject() { |
|
||||||
|
|
||||||
@PrimaryKey |
|
||||||
var id = UUID.randomUUID().toString() |
|
||||||
|
|
||||||
// the date of the hand history |
|
||||||
var date: Date = Date() |
|
||||||
|
|
||||||
} |
|
||||||
@ -0,0 +1,252 @@ |
|||||||
|
package net.pokeranalytics.android.model.realm.handhistory |
||||||
|
|
||||||
|
import io.realm.RealmObject |
||||||
|
import net.pokeranalytics.android.R |
||||||
|
import net.pokeranalytics.android.exceptions.PAIllegalStateException |
||||||
|
import net.pokeranalytics.android.model.handhistory.Position |
||||||
|
import net.pokeranalytics.android.model.handhistory.Street |
||||||
|
import net.pokeranalytics.android.ui.modules.handhistory.model.ActionReadRow |
||||||
|
import net.pokeranalytics.android.ui.view.RowRepresentable |
||||||
|
import net.pokeranalytics.android.ui.view.RowViewType |
||||||
|
import net.pokeranalytics.android.util.extensions.formatted |
||||||
|
|
||||||
|
|
||||||
|
/*** |
||||||
|
* An extension to transform a list of ComputedAction into |
||||||
|
* a more compact and read-friendly list of ActionReadRow |
||||||
|
*/ |
||||||
|
fun List<Action>.compact(positions: LinkedHashSet<Position>, heroIndex: Int?): List<ActionReadRow> { |
||||||
|
val rows = mutableListOf<ActionReadRow>() |
||||||
|
this.forEach { |
||||||
|
if (it.type == Action.Type.FOLD && rows.lastOrNull()?.action == Action.Type.FOLD) { |
||||||
|
rows.lastOrNull()?.positions?.add(positions.elementAt(it.position)) |
||||||
|
} else { |
||||||
|
rows.add(it.toReadRow(positions, heroIndex, null)) // TODO stack. The method is used for text export only atm |
||||||
|
} |
||||||
|
} |
||||||
|
return rows |
||||||
|
} |
||||||
|
|
||||||
|
fun Action.toReadRow(positions: LinkedHashSet<Position>, heroIndex: Int?, stack: Double?): ActionReadRow { |
||||||
|
val pos = positions.elementAt(this.position) |
||||||
|
val isHero = (heroIndex == this.position) |
||||||
|
|
||||||
|
var amount = this.amount |
||||||
|
if (this.type?.isCall == true) { |
||||||
|
amount = this.effectiveAmount |
||||||
|
} |
||||||
|
|
||||||
|
return ActionReadRow(mutableListOf(pos), this.position, this.type, amount, stack, isHero) |
||||||
|
} |
||||||
|
|
||||||
|
open class Action : RealmObject() { |
||||||
|
|
||||||
|
enum class Type(override var resId: Int) : RowRepresentable { |
||||||
|
|
||||||
|
POST_SB(R.string.posts_sb), |
||||||
|
POST_BB(R.string.post_bb), |
||||||
|
STRADDLE(R.string.straddle), |
||||||
|
FOLD(R.string.fold), |
||||||
|
CHECK(R.string.check), |
||||||
|
CALL(R.string.call), |
||||||
|
BET(R.string.bet), |
||||||
|
POT(R.string.pot), |
||||||
|
RAISE(R.string.raise), |
||||||
|
UNDEFINED_ALLIN(R.string.allin), |
||||||
|
CALL_ALLIN(R.string.callin), |
||||||
|
BET_ALLIN(R.string.ballin), |
||||||
|
RAISE_ALLIN(R.string.rallin); |
||||||
|
|
||||||
|
val isBlind: Boolean |
||||||
|
get() { |
||||||
|
return when (this) { |
||||||
|
POST_SB, POST_BB -> true |
||||||
|
else -> false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
val isSignificant: Boolean |
||||||
|
get() { |
||||||
|
return when (this) { |
||||||
|
POST_SB, POST_BB, STRADDLE, BET, POT, RAISE, BET_ALLIN, RAISE_ALLIN -> true |
||||||
|
UNDEFINED_ALLIN -> throw PAIllegalStateException("Can't ask for UNDEFINED_ALLIN") |
||||||
|
else -> false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Tells if the action pulls the player out from any new decision |
||||||
|
*/ |
||||||
|
val isPullOut: Boolean |
||||||
|
get() { |
||||||
|
return when (this) { |
||||||
|
FOLD, BET_ALLIN, RAISE_ALLIN, CALL_ALLIN, UNDEFINED_ALLIN -> true |
||||||
|
else -> false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns if the action is passive |
||||||
|
*/ |
||||||
|
val isPassive: Boolean |
||||||
|
get() { |
||||||
|
return when (this) { |
||||||
|
FOLD, CHECK -> true |
||||||
|
else -> false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
val isCall: Boolean |
||||||
|
get() { |
||||||
|
return when (this) { |
||||||
|
CALL, CALL_ALLIN -> true |
||||||
|
else -> false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
val isAllin: Boolean |
||||||
|
get() { |
||||||
|
return when (this) { |
||||||
|
UNDEFINED_ALLIN, BET_ALLIN, RAISE_ALLIN, CALL_ALLIN -> true |
||||||
|
else -> false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
val color: Int |
||||||
|
get() { |
||||||
|
return when (this) { |
||||||
|
POST_SB, POST_BB, CALL, CHECK, CALL_ALLIN -> R.color.kaki_lighter |
||||||
|
FOLD -> R.color.red |
||||||
|
else -> R.color.green |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
val background: Int |
||||||
|
get() { |
||||||
|
return when (this) { |
||||||
|
POST_SB, POST_BB, CALL, CHECK, CALL_ALLIN -> R.drawable.rounded_kaki_medium_rect |
||||||
|
FOLD -> R.drawable.rounded_red_rect |
||||||
|
else -> R.drawable.rounded_green_rect |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override val viewType: Int = RowViewType.TITLE_GRID.ordinal |
||||||
|
|
||||||
|
companion object { |
||||||
|
|
||||||
|
val defaultTypes: List<Type> by lazy { |
||||||
|
listOf(FOLD, CHECK, BET, POT, CALL, RAISE, UNDEFINED_ALLIN) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* The street of the action |
||||||
|
*/ |
||||||
|
private var streetIdentifier: Int = 0 |
||||||
|
var street: Street |
||||||
|
set(value) { |
||||||
|
this.streetIdentifier = value.ordinal |
||||||
|
} |
||||||
|
get() { |
||||||
|
return streetIdentifier.let { Street.values()[it] } |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* The index of the action |
||||||
|
*/ |
||||||
|
var index: Int = 0 |
||||||
|
|
||||||
|
/*** |
||||||
|
* The position of the user making the action |
||||||
|
*/ |
||||||
|
var position: Int = 0 |
||||||
|
|
||||||
|
/*** |
||||||
|
* The type of action: check, fold, raise... |
||||||
|
*/ |
||||||
|
private var typeIdentifier: Int? = null |
||||||
|
|
||||||
|
var type: Type? |
||||||
|
set(value) { |
||||||
|
this.typeIdentifier = value?.ordinal |
||||||
|
} |
||||||
|
get() { |
||||||
|
return typeIdentifier?.let { Type.values()[it] } |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* The amount linked for a bet, raise... |
||||||
|
*/ |
||||||
|
var amount: Double? = null |
||||||
|
set(value) { |
||||||
|
field = value |
||||||
|
// Timber.d("/// set value = $value") |
||||||
|
} |
||||||
|
|
||||||
|
var effectiveAmount: Double = 0.0 |
||||||
|
|
||||||
|
var positionRemainingStack: Double? = null |
||||||
|
|
||||||
|
val isActionSignificant: Boolean |
||||||
|
get() { |
||||||
|
return this.type?.isSignificant ?: false |
||||||
|
} |
||||||
|
|
||||||
|
val formattedAmount: String? |
||||||
|
get() { |
||||||
|
val amount = when (this.type) { |
||||||
|
Type.CALL, Type.CALL_ALLIN -> this.effectiveAmount |
||||||
|
else -> this.amount |
||||||
|
} |
||||||
|
return amount?.formatted |
||||||
|
} |
||||||
|
|
||||||
|
fun toggleType(remainingStack: Double) { |
||||||
|
|
||||||
|
when (this.type) { |
||||||
|
Type.BET -> { |
||||||
|
if (remainingStack == 0.0) { |
||||||
|
this.type = Type.BET_ALLIN |
||||||
|
} |
||||||
|
} |
||||||
|
Type.BET_ALLIN -> { |
||||||
|
if (remainingStack > 0.0) { |
||||||
|
this.type = Type.BET |
||||||
|
} |
||||||
|
} |
||||||
|
Type.RAISE -> { |
||||||
|
if (remainingStack == 0.0) { |
||||||
|
this.type = Type.RAISE_ALLIN |
||||||
|
} |
||||||
|
} |
||||||
|
Type.RAISE_ALLIN -> { |
||||||
|
if (remainingStack > 0.0) { |
||||||
|
this.type = Type.RAISE |
||||||
|
} |
||||||
|
} |
||||||
|
Type.CALL -> { |
||||||
|
if (remainingStack == 0.0) { |
||||||
|
this.type = Type.CALL_ALLIN |
||||||
|
} |
||||||
|
} |
||||||
|
Type.CALL_ALLIN -> { |
||||||
|
if (remainingStack > 0.0) { |
||||||
|
this.type = Type.CALL |
||||||
|
} |
||||||
|
} |
||||||
|
else -> {} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
val displayedAmount: Double? |
||||||
|
get() { |
||||||
|
if (this.type?.isCall == true) { |
||||||
|
return this.effectiveAmount |
||||||
|
} |
||||||
|
return this.amount |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,662 @@ |
|||||||
|
package net.pokeranalytics.android.model.realm.handhistory |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import android.view.LayoutInflater |
||||||
|
import android.view.View |
||||||
|
import android.view.ViewGroup |
||||||
|
import androidx.appcompat.widget.AppCompatTextView |
||||||
|
import io.realm.Realm |
||||||
|
import io.realm.RealmList |
||||||
|
import io.realm.RealmObject |
||||||
|
import io.realm.annotations.Ignore |
||||||
|
import io.realm.annotations.PrimaryKey |
||||||
|
import net.pokeranalytics.android.R |
||||||
|
import net.pokeranalytics.android.model.filter.Filterable |
||||||
|
import net.pokeranalytics.android.model.handhistory.HandSetup |
||||||
|
import net.pokeranalytics.android.model.handhistory.Position |
||||||
|
import net.pokeranalytics.android.model.handhistory.Street |
||||||
|
import net.pokeranalytics.android.model.interfaces.Deletable |
||||||
|
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus |
||||||
|
import net.pokeranalytics.android.model.interfaces.Identifiable |
||||||
|
import net.pokeranalytics.android.model.interfaces.TimeFilterable |
||||||
|
import net.pokeranalytics.android.model.realm.Session |
||||||
|
import net.pokeranalytics.android.ui.modules.handhistory.evaluator.EvaluatorBridge |
||||||
|
import net.pokeranalytics.android.ui.modules.handhistory.model.ActionReadRow |
||||||
|
import net.pokeranalytics.android.ui.modules.handhistory.model.CardHolder |
||||||
|
import net.pokeranalytics.android.ui.view.RowRepresentable |
||||||
|
import net.pokeranalytics.android.ui.view.RowViewType |
||||||
|
import net.pokeranalytics.android.util.extensions.addLineReturn |
||||||
|
import net.pokeranalytics.android.util.extensions.formatted |
||||||
|
import net.pokeranalytics.android.util.extensions.fullDate |
||||||
|
import java.util.* |
||||||
|
import kotlin.math.max |
||||||
|
|
||||||
|
data class PositionAmount(var position: Int, var amount: Double, var isAllin: Boolean) |
||||||
|
|
||||||
|
open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable, TimeFilterable, |
||||||
|
CardHolder, Comparator<PositionAmount> { |
||||||
|
|
||||||
|
@PrimaryKey |
||||||
|
override var id = UUID.randomUUID().toString() |
||||||
|
|
||||||
|
@Ignore |
||||||
|
override val realmObjectClass: Class<out Identifiable> = HandHistory::class.java |
||||||
|
|
||||||
|
/*** |
||||||
|
* The date of the hand history |
||||||
|
*/ |
||||||
|
var date: Date = Date() |
||||||
|
set(value) { |
||||||
|
field = value |
||||||
|
this.updateTimeParameter(value) |
||||||
|
} |
||||||
|
|
||||||
|
init { |
||||||
|
this.date = Date() // force the custom setter call |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* The session whose hand was played |
||||||
|
*/ |
||||||
|
var session: Session? = null |
||||||
|
|
||||||
|
/*** |
||||||
|
* The small blind |
||||||
|
*/ |
||||||
|
var smallBlind: Double? = null |
||||||
|
set(value) { |
||||||
|
field = value |
||||||
|
if (this.bigBlind == null && value != null) { |
||||||
|
this.bigBlind = value * 2 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* The big blind |
||||||
|
*/ |
||||||
|
var bigBlind: Double? = null |
||||||
|
set(value) { |
||||||
|
field = value |
||||||
|
if (this.smallBlind == null && value != null) { |
||||||
|
this.smallBlind = value / 2 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* The ante |
||||||
|
*/ |
||||||
|
var ante: Double = 0.0 |
||||||
|
|
||||||
|
/*** |
||||||
|
* Big blind ante |
||||||
|
*/ |
||||||
|
var bigBlindAnte: Boolean = false |
||||||
|
|
||||||
|
/*** |
||||||
|
* Number of players in the hand |
||||||
|
*/ |
||||||
|
var numberOfPlayers: Int = 9 |
||||||
|
|
||||||
|
/*** |
||||||
|
* Number of players in the hand |
||||||
|
*/ |
||||||
|
var comment: String? = null |
||||||
|
|
||||||
|
/*** |
||||||
|
* The position index of the hero |
||||||
|
*/ |
||||||
|
var heroIndex: Int? = null |
||||||
|
|
||||||
|
/*** |
||||||
|
* Indicates if the hero wins the hand |
||||||
|
*/ |
||||||
|
var winnerPots: RealmList<WonPot> = RealmList() |
||||||
|
|
||||||
|
/*** |
||||||
|
* The board |
||||||
|
*/ |
||||||
|
var board: RealmList<Card> = RealmList() |
||||||
|
|
||||||
|
/*** |
||||||
|
* The players actions |
||||||
|
*/ |
||||||
|
var actions: RealmList<Action> = RealmList() |
||||||
|
|
||||||
|
/*** |
||||||
|
* A list of players initial data: user, position, hand, stack |
||||||
|
*/ |
||||||
|
var playerSetups: RealmList<PlayerSetup> = RealmList() |
||||||
|
|
||||||
|
// Timed interface |
||||||
|
override var dayOfWeek: Int? = null |
||||||
|
override var month: Int? = null |
||||||
|
override var year: Int? = null |
||||||
|
override var dayOfMonth: Int? = null |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns the indexes of all players |
||||||
|
*/ |
||||||
|
val positionIndexes: IntRange |
||||||
|
get() { return (0 until this.numberOfPlayers) } |
||||||
|
|
||||||
|
// Deletable |
||||||
|
|
||||||
|
override fun isValidForDelete(realm: Realm): Boolean { |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int { |
||||||
|
TODO("not implemented") //To change body of created functions use File | Settings | File Templates. |
||||||
|
} |
||||||
|
|
||||||
|
override fun deleteDependencies(realm: Realm) { |
||||||
|
this.board.deleteAllFromRealm() |
||||||
|
this.playerSetups.deleteAllFromRealm() |
||||||
|
this.actions.deleteAllFromRealm() |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Configures a hand history with a [handSetup] |
||||||
|
*/ |
||||||
|
fun configure(handSetup: HandSetup) { |
||||||
|
|
||||||
|
this.playerSetups.removeAll(this.playerSetups) |
||||||
|
|
||||||
|
handSetup.tableSize?.let { this.numberOfPlayers = it } |
||||||
|
handSetup.smallBlind?.let { this.smallBlind = it } |
||||||
|
handSetup.bigBlind?.let { this.bigBlind = it } |
||||||
|
|
||||||
|
this.session = handSetup.session |
||||||
|
this.date = this.session?.handHistoryAutomaticDate ?: Date() |
||||||
|
|
||||||
|
this.createActions(handSetup) |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Creates the initial actions of the hand history |
||||||
|
*/ |
||||||
|
private fun createActions(handSetup: HandSetup) { |
||||||
|
|
||||||
|
this.actions.clear() |
||||||
|
|
||||||
|
this.addAction(0, Action.Type.POST_SB, this.smallBlind) |
||||||
|
this.addAction(1, Action.Type.POST_BB, this.bigBlind) |
||||||
|
|
||||||
|
// var lastStraddler: Int? = null |
||||||
|
|
||||||
|
val positions = Position.positionsPerPlayers(this.numberOfPlayers) |
||||||
|
handSetup.straddlePositions.forEach { position -> // position are sorted here |
||||||
|
val positionIndex = positions.indexOf(position) |
||||||
|
this.addAction(positionIndex, Action.Type.STRADDLE) |
||||||
|
// lastStraddler = positionIndex |
||||||
|
} |
||||||
|
|
||||||
|
// val totalActions = this.actions.size |
||||||
|
// val startingPosition = lastStraddler?.let { it + 1 } ?: totalActions |
||||||
|
|
||||||
|
// for (i in this.positionIndexes - 1) { // we don't add the BB / straddler by default in case of a walk |
||||||
|
// this.addAction((startingPosition + i) % this.numberOfPlayers) |
||||||
|
// } |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Adds an action with the given [position], [type] and [amount] to the actions list |
||||||
|
*/ |
||||||
|
private fun addAction(position: Int, type: Action.Type? = null, amount: Double? = null) { |
||||||
|
val action = Action() |
||||||
|
action.index = this.actions.size |
||||||
|
action.position = position |
||||||
|
action.type = type |
||||||
|
action.amount = amount |
||||||
|
action.effectiveAmount = amount ?: 0.0 |
||||||
|
this.actions.add(action) |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns the board cards for a given [street] |
||||||
|
*/ |
||||||
|
fun cardsForStreet(street: Street): MutableList<Card> { |
||||||
|
return this.board.sortedBy { it.index }.take(street.totalBoardCards).toMutableList() |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns the optional PlayerSetup object at the [position] |
||||||
|
*/ |
||||||
|
fun playerSetupForPosition(position: Int): PlayerSetup? { |
||||||
|
return this.playerSetups.firstOrNull { it.position == position } |
||||||
|
} |
||||||
|
|
||||||
|
override val cards: RealmList<Card> |
||||||
|
get() { return this.board } |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns the ante sum |
||||||
|
*/ |
||||||
|
val anteSum: Double |
||||||
|
get() { |
||||||
|
return if (bigBlindAnte) { |
||||||
|
this.bigBlind ?: 0.0 |
||||||
|
} else { |
||||||
|
this.ante * this.numberOfPlayers |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns the sorted list of actions by index |
||||||
|
*/ |
||||||
|
private val sortedActions: List<Action> |
||||||
|
get() { |
||||||
|
return this.actions.sortedBy { it.index } |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns the list of undefined positions, |
||||||
|
* meaning the positions where no PlayerSetup has been created |
||||||
|
*/ |
||||||
|
fun undefinedPositions(): List<Position> { |
||||||
|
val positions = Position.positionsPerPlayers(this.numberOfPlayers) |
||||||
|
val copy = positions.clone() as LinkedHashSet<Position> |
||||||
|
this.playerSetups.forEach { |
||||||
|
copy.remove(positions.elementAt(it.position)) |
||||||
|
} |
||||||
|
return copy.toList() |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Creates and affect a PlayerSetup at the given [positionIndex] |
||||||
|
*/ |
||||||
|
fun createPlayerSetup(positionIndex: Int): PlayerSetup { |
||||||
|
|
||||||
|
val playerSetup = if (this.realm != null) { |
||||||
|
this.realm.createObject(PlayerSetup::class.java) } |
||||||
|
else { |
||||||
|
PlayerSetup() |
||||||
|
} |
||||||
|
|
||||||
|
playerSetup.position = positionIndex |
||||||
|
this.playerSetups.add(playerSetup) |
||||||
|
return playerSetup |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns the pot size at the start of the given [street] |
||||||
|
*/ |
||||||
|
fun potSizeForStreet(street: Street): Double { |
||||||
|
val sortedActions = this.sortedActions |
||||||
|
val firstIndexOfStreet = sortedActions.firstOrNull { it.street == street }?.index |
||||||
|
?: sortedActions.size |
||||||
|
return this.anteSum + sortedActions.take(firstIndexOfStreet).sumByDouble { it.effectiveAmount } |
||||||
|
} |
||||||
|
|
||||||
|
@Ignore |
||||||
|
override val viewType: Int = RowViewType.HAND_HISTORY.ordinal |
||||||
|
|
||||||
|
override fun localizedString(context: Context): CharSequence { |
||||||
|
|
||||||
|
val positions = Position.positionsPerPlayers(this.numberOfPlayers) |
||||||
|
|
||||||
|
var string = "" |
||||||
|
|
||||||
|
// Settings |
||||||
|
val players = "${this.numberOfPlayers} ${context.getString(R.string.players)}" |
||||||
|
val firstLineComponents = mutableListOf(this.date.fullDate(), players) |
||||||
|
|
||||||
|
this.smallBlind?.let { sb -> |
||||||
|
this.bigBlind?.let { bb -> |
||||||
|
firstLineComponents.add("${sb.formatted}/${bb.formatted}") |
||||||
|
} |
||||||
|
} |
||||||
|
if (this.ante > 0.0) { |
||||||
|
firstLineComponents.add("ante ${this.ante}") |
||||||
|
} |
||||||
|
string = string.plus(firstLineComponents.joinToString(" - ")) |
||||||
|
string = string.addLineReturn(2) |
||||||
|
|
||||||
|
// Comment |
||||||
|
this.comment?.let { comment -> |
||||||
|
string = string.plus(comment) |
||||||
|
string = string.addLineReturn(2) |
||||||
|
} |
||||||
|
|
||||||
|
// Players |
||||||
|
this.playerSetups.sortedBy { it.position }.forEach { |
||||||
|
string = string.plus(localizedPlayerSetup(it, positions, context)) |
||||||
|
string = string.addLineReturn() |
||||||
|
} |
||||||
|
|
||||||
|
// Actions per street |
||||||
|
val sortedActions = this.actions.sortedBy { it.index } |
||||||
|
|
||||||
|
// val actionList = ActionList() |
||||||
|
// actionList.load(this) |
||||||
|
|
||||||
|
Street.values().forEach { street -> |
||||||
|
|
||||||
|
string = string.addLineReturn(2) |
||||||
|
|
||||||
|
val streetActions = sortedActions.filter { it.street == street }.compact(positions, this.heroIndex) |
||||||
|
if (streetActions.isNotEmpty()) { |
||||||
|
|
||||||
|
val streetItems = mutableListOf<CharSequence>(context.getString(street.resId)) |
||||||
|
|
||||||
|
val potSize = this.potSizeForStreet(street) |
||||||
|
if (potSize > 0) { |
||||||
|
// streetItems.add(context.getString(R.string.pot_size)) |
||||||
|
streetItems.add("(" + potSize.formatted + ")") |
||||||
|
} |
||||||
|
string = string.plus(streetItems.joinToString(" ")) |
||||||
|
|
||||||
|
val streetCards = this.cardsForStreet(street) |
||||||
|
if (streetCards.isNotEmpty()) { |
||||||
|
string = string.addLineReturn() |
||||||
|
string = string.plus(streetCards.formatted(context) ?: "") |
||||||
|
} |
||||||
|
string = string.addLineReturn() |
||||||
|
string = string.plus("-----------") |
||||||
|
|
||||||
|
string = string.addLineReturn() |
||||||
|
|
||||||
|
streetActions.forEach { action -> |
||||||
|
string = string.plus(localizedAction(action, context)) |
||||||
|
string = string.addLineReturn() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
return string |
||||||
|
} |
||||||
|
|
||||||
|
fun anteForPosition(position: Position): Double { |
||||||
|
return if (this.bigBlindAnte) { |
||||||
|
if (position == Position.BB) { |
||||||
|
this.bigBlind ?: 0.0 |
||||||
|
} else { |
||||||
|
0.0 |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.ante |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns a string representation of the [playerSetup] |
||||||
|
*/ |
||||||
|
private fun localizedPlayerSetup(playerSetup: PlayerSetup, positions: LinkedHashSet<Position>, context: Context): String { |
||||||
|
|
||||||
|
val playerItems = mutableListOf(positions.elementAt(playerSetup.position).value) |
||||||
|
|
||||||
|
val isHero = (playerSetup.position == this.heroIndex) |
||||||
|
if (isHero) { |
||||||
|
val heroString = context.getString(R.string.hero) |
||||||
|
playerItems.add("- $heroString") |
||||||
|
} |
||||||
|
playerItems.add("[${playerSetup.cards.formatted(context)}]") |
||||||
|
playerSetup.stack?.let { stack -> |
||||||
|
playerItems.add("- $stack") |
||||||
|
} |
||||||
|
return playerItems.joinToString(" ") |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns a string representation of the [actionReadRow] |
||||||
|
*/ |
||||||
|
private fun localizedAction(actionReadRow: ActionReadRow, context: Context): String { |
||||||
|
val formattedPositions = actionReadRow.positions.joinToString(", ") { it.value } |
||||||
|
val actionItems = mutableListOf(formattedPositions) |
||||||
|
actionReadRow.action?.let { type -> |
||||||
|
actionItems.add(context.getString(type.resId)) |
||||||
|
} |
||||||
|
actionReadRow.amount?.let { amount -> |
||||||
|
actionItems.add(amount.formatted) |
||||||
|
} |
||||||
|
return actionItems.joinToString(" ") |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns if the hero has won the hand, or part of the pot |
||||||
|
*/ |
||||||
|
val heroWins: Boolean? |
||||||
|
get() { |
||||||
|
return this.heroIndex?.let { heroIndex -> |
||||||
|
this.winnerPots.any { it.position == heroIndex } |
||||||
|
} ?: run { |
||||||
|
null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Creates a list of cards for the hand history to give a representation of the hand |
||||||
|
* We will try to add a minimum of 5 cards using by priority: |
||||||
|
* - the hero hand |
||||||
|
* - the opponents hands |
||||||
|
* - the board |
||||||
|
*/ |
||||||
|
fun cardViews(context: Context, viewGroup: ViewGroup): List<View> { |
||||||
|
|
||||||
|
val views = mutableListOf<View>() |
||||||
|
val layoutInflater = LayoutInflater.from(context) |
||||||
|
|
||||||
|
// Create all the possible cards list: hero, opponents, board |
||||||
|
val cardSets = mutableListOf<List<Card>>() |
||||||
|
|
||||||
|
// Hero |
||||||
|
this.heroIndex?.let { hIndex -> |
||||||
|
this.playerSetupForPosition(hIndex)?.cards?.let { |
||||||
|
cardSets.add(it) |
||||||
|
} |
||||||
|
} |
||||||
|
// Opponents |
||||||
|
this.playerSetups.filter { it.cards.isNotEmpty() && it.position != this.heroIndex }.forEach { |
||||||
|
cardSets.add(it.cards) |
||||||
|
} |
||||||
|
// Board |
||||||
|
cardSets.add(this.board) |
||||||
|
|
||||||
|
// Try to add cards but not too much |
||||||
|
while (views.size < 5 && cardSets.isNotEmpty()) { |
||||||
|
|
||||||
|
val cardSet = cardSets.removeAt(0) |
||||||
|
|
||||||
|
if (views.isNotEmpty() && cardSet.isNotEmpty()) { // add separator with previous set of cards |
||||||
|
val view = layoutInflater.inflate(R.layout.view_card_separator, viewGroup, false) as AppCompatTextView |
||||||
|
views.add(view) |
||||||
|
} |
||||||
|
cardSet.forEach { views.add(it.view(context, layoutInflater, viewGroup)) } |
||||||
|
} |
||||||
|
|
||||||
|
// Add 5 blank cards if no card has been added |
||||||
|
if (views.isEmpty()) { |
||||||
|
val blankCard = Card() |
||||||
|
(1..5).forEach { _ -> |
||||||
|
val view = blankCard.view(context, layoutInflater, viewGroup, true) |
||||||
|
view.setBackgroundResource(R.drawable.rounded_kaki_medium_rect) |
||||||
|
views.add(view) |
||||||
|
} |
||||||
|
} |
||||||
|
return views |
||||||
|
} |
||||||
|
|
||||||
|
data class Pot(var amount: Double, var level: Double, var positions: MutableSet<Int> = mutableSetOf()) |
||||||
|
|
||||||
|
/*** |
||||||
|
* Defines which positions win the hand |
||||||
|
*/ |
||||||
|
fun defineWinnerPositions() { |
||||||
|
|
||||||
|
val folds = this.sortedActions.filter { it.type == Action.Type.FOLD }.map { it.position } |
||||||
|
val activePositions = this.positionIndexes.toMutableList() |
||||||
|
activePositions.removeAll(folds) |
||||||
|
|
||||||
|
val wonPots = when (activePositions.size) { |
||||||
|
0 -> listOf() // no winner, everyone has fold. Should not happen |
||||||
|
1 -> { // One player has not fold, typically BET / FOLD |
||||||
|
val pot = WonPot() |
||||||
|
pot.position = activePositions.first() |
||||||
|
pot.amount = potSizeForStreet(Street.SUMMARY) |
||||||
|
listOf(pot) |
||||||
|
} |
||||||
|
else -> { // Several players remains, typically BET/FOLD or CHECKS |
||||||
|
this.wonPots(getPots(activePositions)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.winnerPots.clear() |
||||||
|
this.winnerPots.addAll(wonPots) |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Returns a list with all the different pots with the appropriate eligible players |
||||||
|
*/ |
||||||
|
fun getPots(eligiblePositions: List<Int>): List<Pot> { |
||||||
|
|
||||||
|
var runningPotAmount = 0.0 |
||||||
|
val pots = mutableListOf<Pot>() |
||||||
|
Street.values().forEach { street -> |
||||||
|
|
||||||
|
val streetActions = this.actions.filter { it.street == street } |
||||||
|
|
||||||
|
val allinPositions = streetActions.filter { it.type?.isAllin == true }.map { it.position } |
||||||
|
|
||||||
|
if (allinPositions.isEmpty()) { |
||||||
|
runningPotAmount += streetActions.sumByDouble { it.effectiveAmount } |
||||||
|
} else { |
||||||
|
val amountsPerPosition = mutableListOf<PositionAmount>() |
||||||
|
|
||||||
|
// get all committed amounts for the street by player, by allin |
||||||
|
this.positionIndexes.map { position -> |
||||||
|
val playerActions = streetActions.filter { it.position == position } |
||||||
|
val sum = playerActions.sumByDouble { it.effectiveAmount } |
||||||
|
amountsPerPosition.add(PositionAmount(position, sum, allinPositions.contains(position))) |
||||||
|
} |
||||||
|
amountsPerPosition.sortWith(this) // sort by value, then allin. Allin must be first of equal values sequence |
||||||
|
|
||||||
|
// for each player |
||||||
|
val streetPots = mutableListOf<Pot>() |
||||||
|
amountsPerPosition.forEach { positionAmount -> |
||||||
|
val amount = positionAmount.amount |
||||||
|
val position = positionAmount.position |
||||||
|
|
||||||
|
var rest = amount |
||||||
|
var lastPotLevel = 0.0 |
||||||
|
// put invested amount in smaller pots |
||||||
|
streetPots.forEach { pot -> |
||||||
|
val added = pot.level - lastPotLevel |
||||||
|
pot.amount += added |
||||||
|
if (eligiblePositions.contains(position)) { |
||||||
|
pot.positions.add(position) |
||||||
|
} |
||||||
|
rest -= added |
||||||
|
lastPotLevel = pot.level |
||||||
|
} |
||||||
|
// Adds remaining chips to the running Pot |
||||||
|
runningPotAmount += rest |
||||||
|
|
||||||
|
// If the player is allin, create a new pot for the relevant amount |
||||||
|
val isAllin = allinPositions.contains(position) |
||||||
|
if (isAllin) { |
||||||
|
streetPots.add(Pot(runningPotAmount, amount, mutableSetOf(position))) |
||||||
|
runningPotAmount = 0.0 |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
pots.addAll(streetPots) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// Create a pot with the remaining chips |
||||||
|
if (runningPotAmount > 0.0) { |
||||||
|
pots.add(Pot(runningPotAmount, 0.0, eligiblePositions.toMutableSet())) |
||||||
|
} |
||||||
|
|
||||||
|
return pots |
||||||
|
} |
||||||
|
|
||||||
|
private fun wonPots(pots: List<Pot>): Collection<WonPot> { |
||||||
|
|
||||||
|
val wonPots = hashMapOf<Int, WonPot>() |
||||||
|
|
||||||
|
pots.forEach { pot -> |
||||||
|
|
||||||
|
val winningPositions = compareHands(pot.positions.toList()) |
||||||
|
|
||||||
|
// Distributes the pot for each winners |
||||||
|
val share = pot.amount / winningPositions.size |
||||||
|
winningPositions.forEach { p -> |
||||||
|
val wp = wonPots[p] |
||||||
|
if (wp == null) { |
||||||
|
val wonPot = WonPot() |
||||||
|
wonPot.position = p |
||||||
|
wonPot.amount = share |
||||||
|
wonPots[p] = wonPot |
||||||
|
} else { |
||||||
|
wp.amount += share |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
return wonPots.values |
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* Compares the hands of the players at the given [activePositions] |
||||||
|
* Returns the list of winning hands by position |
||||||
|
*/ |
||||||
|
private fun compareHands(activePositions: List<Int>): List<Int> { |
||||||
|
|
||||||
|
// Evaluate all hands |
||||||
|
val results = activePositions.map { |
||||||
|
this.playerSetupForPosition(it)?.cards?.let { hand -> |
||||||
|
EvaluatorBridge.playerHand(hand, this.board) |
||||||
|
} ?: run { |
||||||
|
Int.MAX_VALUE |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Check who has best score (EvaluatorBridge gives a lowest score for a better hand) |
||||||
|
return results.min()?.let { best -> |
||||||
|
val winners = mutableListOf<Int>() |
||||||
|
results.forEachIndexed { index, i -> |
||||||
|
if (i == best && i != Int.MAX_VALUE) { |
||||||
|
winners.add(activePositions[index]) |
||||||
|
} |
||||||
|
} |
||||||
|
winners |
||||||
|
} ?: run { |
||||||
|
listOf<Int>() // type needed for build |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/*** |
||||||
|
* return a negative integer, zero, or a positive integer as the |
||||||
|
* first argument is less than, equal to, or greater than the |
||||||
|
* second. |
||||||
|
*/ |
||||||
|
override fun compare(o1: PositionAmount, o2: PositionAmount): Int { |
||||||
|
return if (o1.amount == o2.amount) { |
||||||
|
if (o1.isAllin) -1 else 1 |
||||||
|
} else { |
||||||
|
(o1.amount - o2.amount).toInt() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
val maxPlayerCards: Int |
||||||
|
get() { |
||||||
|
var max = 0 |
||||||
|
this.playerSetups.forEach { |
||||||
|
max = max(it.cards.size, max) |
||||||
|
} |
||||||
|
return max |
||||||
|
} |
||||||
|
|
||||||
|
val usesWildcards: Boolean |
||||||
|
get() { |
||||||
|
val boardHasWildCard = this.cards.any { it.isWildCard } |
||||||
|
val playerCardHasWildCard = this.playerSetups.any { it.cards.any { it.isWildCard } } |
||||||
|
return boardHasWildCard || playerCardHasWildCard |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
package net.pokeranalytics.android.model.realm.handhistory |
||||||
|
|
||||||
|
import io.realm.RealmList |
||||||
|
import io.realm.RealmObject |
||||||
|
import net.pokeranalytics.android.model.realm.Player |
||||||
|
import net.pokeranalytics.android.ui.modules.handhistory.model.CardHolder |
||||||
|
import net.pokeranalytics.android.ui.view.Localizable |
||||||
|
|
||||||
|
open class PlayerSetup : RealmObject(), CardHolder, Localizable { |
||||||
|
|
||||||
|
/*** |
||||||
|
* The player |
||||||
|
*/ |
||||||
|
var player: Player? = null |
||||||
|
|
||||||
|
/*** |
||||||
|
* The position at the table |
||||||
|
*/ |
||||||
|
var position: Int = 0 |
||||||
|
|
||||||
|
/*** |
||||||
|
* The initial stack of the player |
||||||
|
*/ |
||||||
|
var stack: Double? = null |
||||||
|
|
||||||
|
/*** |
||||||
|
* The cards of the player |
||||||
|
*/ |
||||||
|
override var cards: RealmList<Card> = RealmList() |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
package net.pokeranalytics.android.model.realm.handhistory |
||||||
|
|
||||||
|
import io.realm.RealmObject |
||||||
|
|
||||||
|
open class WonPot: RealmObject() { |
||||||
|
|
||||||
|
/*** |
||||||
|
* The position of the player |
||||||
|
*/ |
||||||
|
var position: Int = 0 |
||||||
|
|
||||||
|
/*** |
||||||
|
* The amount won |
||||||
|
*/ |
||||||
|
var amount: Double = 0.0 |
||||||
|
|
||||||
|
} |
||||||
@ -1,58 +0,0 @@ |
|||||||
package net.pokeranalytics.android.ui.activity |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import android.content.Intent |
|
||||||
import android.os.Bundle |
|
||||||
import androidx.fragment.app.Fragment |
|
||||||
import kotlinx.android.synthetic.main.activity_data_list.* |
|
||||||
import net.pokeranalytics.android.R |
|
||||||
import net.pokeranalytics.android.ui.activity.components.BaseActivity |
|
||||||
import net.pokeranalytics.android.ui.fragment.DataListFragment |
|
||||||
import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode |
|
||||||
|
|
||||||
class DataListActivity : BaseActivity() { |
|
||||||
|
|
||||||
enum class IntentKey(val keyName: String) { |
|
||||||
DATA_TYPE("DATA_TYPE"), |
|
||||||
LIVE_DATA_TYPE("LIVE_DATA_TYPE"), |
|
||||||
ITEM_DELETED("ITEM_DELETED"), |
|
||||||
SHOW_ADD_BUTTON("SHOW_ADD_BUTTON"), |
|
||||||
} |
|
||||||
|
|
||||||
companion object { |
|
||||||
fun newInstance(context: Context, dataType: Int) { |
|
||||||
context.startActivity(getIntent(context, dataType)) |
|
||||||
} |
|
||||||
|
|
||||||
fun newSelectInstance(fragment: Fragment, dataType: Int, showAddButton: Boolean = true) { |
|
||||||
val context = fragment.requireContext() |
|
||||||
fragment.startActivityForResult(getIntent(context, dataType, showAddButton), FilterActivityRequestCode.SELECT_FILTER.ordinal) |
|
||||||
} |
|
||||||
|
|
||||||
private fun getIntent(context: Context, dataType: Int, showAddButton: Boolean = true): Intent { |
|
||||||
val intent = Intent(context, DataListActivity::class.java) |
|
||||||
intent.putExtra(IntentKey.DATA_TYPE.keyName, dataType) |
|
||||||
intent.putExtra(IntentKey.SHOW_ADD_BUTTON.keyName, showAddButton) |
|
||||||
return intent |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||||
super.onCreate(savedInstanceState) |
|
||||||
setContentView(R.layout.activity_data_list) |
|
||||||
|
|
||||||
initUI() |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Init UI |
|
||||||
*/ |
|
||||||
private fun initUI() { |
|
||||||
val dataType = intent.getIntExtra(IntentKey.DATA_TYPE.keyName, 0) |
|
||||||
val showAddButton = intent.getBooleanExtra(IntentKey.SHOW_ADD_BUTTON.keyName, true) |
|
||||||
val fragment = dataListFragment as DataListFragment |
|
||||||
fragment.setData(dataType) |
|
||||||
fragment.updateUI(showAddButton) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,71 +0,0 @@ |
|||||||
package net.pokeranalytics.android.ui.activity |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import android.content.Intent |
|
||||||
import android.os.Bundle |
|
||||||
import androidx.fragment.app.Fragment |
|
||||||
import net.pokeranalytics.android.R |
|
||||||
import net.pokeranalytics.android.model.realm.Filter |
|
||||||
import net.pokeranalytics.android.ui.activity.components.BaseActivity |
|
||||||
import net.pokeranalytics.android.ui.fragment.FilterDetailsFragment |
|
||||||
|
|
||||||
class FilterDetailsActivity : BaseActivity() { |
|
||||||
|
|
||||||
enum class IntentKey(val keyName: String) { |
|
||||||
FILTER_ID("FILTER_ID"), |
|
||||||
FILTER_CATEGORY_ORDINAL("FILTER_CATEGORY_ORDINAL") |
|
||||||
} |
|
||||||
|
|
||||||
companion object { |
|
||||||
|
|
||||||
/** |
|
||||||
* Default constructor |
|
||||||
*/ |
|
||||||
fun newInstance(context: Context, filterId: String, filterCategoryOrdinal: Int) { |
|
||||||
val intent = Intent(context, FilterDetailsActivity::class.java) |
|
||||||
intent.putExtra(IntentKey.FILTER_ID.keyName, filterId) |
|
||||||
intent.putExtra(IntentKey.FILTER_CATEGORY_ORDINAL.keyName, filterCategoryOrdinal) |
|
||||||
context.startActivity(intent) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Create a new instance for result |
|
||||||
*/ |
|
||||||
fun newInstanceForResult(fragment: Fragment, filterId: String, filterCategoryOrdinal: Int, requestCode: Int, filter: Filter? = null) { |
|
||||||
|
|
||||||
val intent = Intent(fragment.requireContext(), FilterDetailsActivity::class.java) |
|
||||||
intent.putExtra(IntentKey.FILTER_ID.keyName, filterId) |
|
||||||
intent.putExtra(IntentKey.FILTER_CATEGORY_ORDINAL.keyName, filterCategoryOrdinal) |
|
||||||
fragment.startActivityForResult(intent, requestCode) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private lateinit var fragment: FilterDetailsFragment |
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||||
super.onCreate(savedInstanceState) |
|
||||||
setContentView(R.layout.activity_filter_details) |
|
||||||
initUI() |
|
||||||
} |
|
||||||
|
|
||||||
override fun onBackPressed() { |
|
||||||
fragment.onBackPressed() |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Init UI |
|
||||||
*/ |
|
||||||
private fun initUI() { |
|
||||||
|
|
||||||
val fragmentManager = supportFragmentManager |
|
||||||
val fragmentTransaction = fragmentManager.beginTransaction() |
|
||||||
val filterId = intent.getStringExtra(IntentKey.FILTER_ID.keyName) |
|
||||||
val filterCategoryOrdinal = intent.getIntExtra(IntentKey.FILTER_CATEGORY_ORDINAL.keyName, 0) |
|
||||||
|
|
||||||
fragment = FilterDetailsFragment() |
|
||||||
fragmentTransaction.add(R.id.container, fragment) |
|
||||||
fragmentTransaction.commit() |
|
||||||
fragment.setData(filterId, filterCategoryOrdinal) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,74 +0,0 @@ |
|||||||
package net.pokeranalytics.android.ui.activity |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import android.content.Intent |
|
||||||
import android.os.Bundle |
|
||||||
import androidx.fragment.app.Fragment |
|
||||||
import net.pokeranalytics.android.R |
|
||||||
import net.pokeranalytics.android.ui.activity.components.BaseActivity |
|
||||||
import net.pokeranalytics.android.ui.fragment.FiltersFragment |
|
||||||
import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode |
|
||||||
import net.pokeranalytics.android.ui.interfaces.FilterableType |
|
||||||
|
|
||||||
class FiltersActivity : BaseActivity() { |
|
||||||
|
|
||||||
enum class IntentKey(val keyName: String) { |
|
||||||
FILTER_ID("FILTER_ID"), |
|
||||||
FILTERABLE_TYPE("FILTERABLE_TYPE"), |
|
||||||
HIDE_MOST_USED_FILTERS("HIDE_MOST_USED_FILTERS"), |
|
||||||
; |
|
||||||
} |
|
||||||
|
|
||||||
private lateinit var fragment: FiltersFragment |
|
||||||
|
|
||||||
companion object { |
|
||||||
/** |
|
||||||
* Create a new instance for result |
|
||||||
*/ |
|
||||||
fun newInstanceForResult(fragment: Fragment, filterId: String? = null, currentFilterable: FilterableType, hideMostUsedFilters: Boolean = false) { |
|
||||||
val intent = getIntent(fragment.requireContext(), filterId, currentFilterable, hideMostUsedFilters) |
|
||||||
fragment.startActivityForResult(intent, FilterActivityRequestCode.CREATE_FILTER.ordinal) |
|
||||||
} |
|
||||||
|
|
||||||
private fun getIntent(context: Context, filterId: String?, currentFilterable: FilterableType, hideMostUsedFilters: Boolean = false): Intent { |
|
||||||
val intent = Intent(context, FiltersActivity::class.java) |
|
||||||
intent.putExtra(IntentKey.FILTERABLE_TYPE.keyName, currentFilterable.uniqueIdentifier) |
|
||||||
intent.putExtra(IntentKey.HIDE_MOST_USED_FILTERS.keyName, hideMostUsedFilters) |
|
||||||
filterId?.let { |
|
||||||
intent.putExtra(IntentKey.FILTER_ID.keyName, it) |
|
||||||
} |
|
||||||
return intent |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||||
super.onCreate(savedInstanceState) |
|
||||||
setContentView(R.layout.activity_filters) |
|
||||||
initUI() |
|
||||||
} |
|
||||||
|
|
||||||
override fun onBackPressed() { |
|
||||||
fragment.onBackPressed() |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Init UI |
|
||||||
*/ |
|
||||||
private fun initUI() { |
|
||||||
|
|
||||||
val fragmentManager = supportFragmentManager |
|
||||||
val fragmentTransaction = fragmentManager.beginTransaction() |
|
||||||
val filterId = intent.getStringExtra(IntentKey.FILTER_ID.keyName) |
|
||||||
val uniqueIdentifier= intent.getIntExtra(IntentKey.FILTERABLE_TYPE.keyName, 0) |
|
||||||
val hideMostUsedFilters = intent.getBooleanExtra(IntentKey.HIDE_MOST_USED_FILTERS.keyName, false) |
|
||||||
val filterableType = FilterableType.valueByIdentifier(uniqueIdentifier) |
|
||||||
|
|
||||||
fragment = FiltersFragment() |
|
||||||
fragment.setData(filterId, filterableType) |
|
||||||
fragmentTransaction.add(R.id.container, fragment) |
|
||||||
fragmentTransaction.commit() |
|
||||||
fragment.updateMostUsedFiltersVisibility(!hideMostUsedFilters) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,58 +0,0 @@ |
|||||||
package net.pokeranalytics.android.ui.activity |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import android.content.Intent |
|
||||||
import android.os.Bundle |
|
||||||
import androidx.fragment.app.Fragment |
|
||||||
import kotlinx.android.synthetic.main.activity_filters_list.* |
|
||||||
import net.pokeranalytics.android.R |
|
||||||
import net.pokeranalytics.android.ui.activity.components.BaseActivity |
|
||||||
import net.pokeranalytics.android.ui.fragment.FiltersListFragment |
|
||||||
import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode |
|
||||||
|
|
||||||
class FiltersListActivity : BaseActivity() { |
|
||||||
|
|
||||||
enum class IntentKey(val keyName: String) { |
|
||||||
DATA_TYPE("DATA_TYPE"), |
|
||||||
LIVE_DATA_TYPE("LIVE_DATA_TYPE"), |
|
||||||
ITEM_DELETED("ITEM_DELETED"), |
|
||||||
SHOW_ADD_BUTTON("SHOW_ADD_BUTTON"), |
|
||||||
} |
|
||||||
|
|
||||||
companion object { |
|
||||||
fun newInstance(context: Context, dataType: Int) { |
|
||||||
context.startActivity(getIntent(context, dataType)) |
|
||||||
} |
|
||||||
|
|
||||||
fun newSelectInstance(fragment: Fragment, dataType: Int, showAddButton: Boolean = true) { |
|
||||||
val context = fragment.requireContext() |
|
||||||
fragment.startActivityForResult(getIntent(context, dataType, showAddButton), FilterActivityRequestCode.SELECT_FILTER.ordinal) |
|
||||||
} |
|
||||||
|
|
||||||
private fun getIntent(context: Context, dataType: Int, showAddButton: Boolean = true): Intent { |
|
||||||
val intent = Intent(context, FiltersListActivity::class.java) |
|
||||||
intent.putExtra(IntentKey.DATA_TYPE.keyName, dataType) |
|
||||||
intent.putExtra(IntentKey.SHOW_ADD_BUTTON.keyName, showAddButton) |
|
||||||
return intent |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||||
super.onCreate(savedInstanceState) |
|
||||||
setContentView(R.layout.activity_filters_list) |
|
||||||
|
|
||||||
initUI() |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Init UI |
|
||||||
*/ |
|
||||||
private fun initUI() { |
|
||||||
val dataType = intent.getIntExtra(IntentKey.DATA_TYPE.keyName, 0) |
|
||||||
val showAddButton = intent.getBooleanExtra(IntentKey.SHOW_ADD_BUTTON.keyName, true) |
|
||||||
val fragment = filtersListFragment as FiltersListFragment |
|
||||||
fragment.setData(dataType) |
|
||||||
fragment.updateUI(showAddButton) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -0,0 +1,240 @@ |
|||||||
|
package net.pokeranalytics.android.ui.adapter |
||||||
|
|
||||||
|
import android.view.LayoutInflater |
||||||
|
import android.view.View |
||||||
|
import android.view.ViewGroup |
||||||
|
import androidx.appcompat.widget.AppCompatTextView |
||||||
|
import androidx.recyclerview.widget.RecyclerView |
||||||
|
import io.realm.RealmModel |
||||||
|
import io.realm.RealmQuery |
||||||
|
import io.realm.RealmResults |
||||||
|
import net.pokeranalytics.android.R |
||||||
|
import net.pokeranalytics.android.exceptions.PAIllegalStateException |
||||||
|
import net.pokeranalytics.android.model.filter.Filterable |
||||||
|
import net.pokeranalytics.android.model.realm.Filter |
||||||
|
import net.pokeranalytics.android.ui.modules.feed.FeedTransactionRowRepresentableAdapter |
||||||
|
import net.pokeranalytics.android.ui.view.RowRepresentable |
||||||
|
import net.pokeranalytics.android.ui.view.RowViewType |
||||||
|
import net.pokeranalytics.android.util.NULL_TEXT |
||||||
|
import net.pokeranalytics.android.util.extensions.getMonthAndYear |
||||||
|
import java.util.* |
||||||
|
import kotlin.collections.HashMap |
||||||
|
|
||||||
|
interface DateModel : RowRepresentable, RealmModel, Filterable { |
||||||
|
var date: Date |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionED : EntityDescriptor { |
||||||
|
override fun bindableHolder(view: View): RecyclerView.ViewHolder { |
||||||
|
TODO() |
||||||
|
} |
||||||
|
|
||||||
|
override val layout: Int = R.layout.row_transaction |
||||||
|
|
||||||
|
override val viewType: Int = RowViewType.ROW_TRANSACTION.ordinal |
||||||
|
|
||||||
|
override val sortFieldName: String = "date" |
||||||
|
|
||||||
|
override fun distinctHeaders(realmQuery: RealmQuery<out DateModel>): RealmQuery<out DateModel> { |
||||||
|
return realmQuery.distinct("year", "month") |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
interface EntityDescriptor { |
||||||
|
|
||||||
|
fun bindableHolder(view: View): RecyclerView.ViewHolder |
||||||
|
|
||||||
|
val layout: Int |
||||||
|
|
||||||
|
val viewType: Int |
||||||
|
|
||||||
|
val sortFieldName: String |
||||||
|
|
||||||
|
fun distinctHeaders(realmQuery: RealmQuery<out DateModel>): RealmQuery<out DateModel> |
||||||
|
|
||||||
|
// val clazz: Class<T> |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* An adapter capable of displaying a list of RowRepresentables |
||||||
|
* @param dataSource the datasource providing rows |
||||||
|
* @param delegate the delegate, notified of UI actions |
||||||
|
*/ |
||||||
|
class FilterSectionAdapter( |
||||||
|
override var delegate: RowRepresentableDelegate? = null, |
||||||
|
override var dataSource: RowRepresentableDataSource, |
||||||
|
var descriptor: EntityDescriptor, |
||||||
|
var realmQuery: RealmQuery<out DateModel> |
||||||
|
// var distinctTransactionsHeaders: RealmResults<Transaction> |
||||||
|
) : |
||||||
|
RecyclerView.Adapter<RecyclerView.ViewHolder>(), RecyclerAdapter { |
||||||
|
|
||||||
|
private var headersPositions = HashMap<Int, Date?>() |
||||||
|
private lateinit var sortedHeaders: SortedMap<Int, Date?> |
||||||
|
|
||||||
|
private var realmEntities: RealmResults<out DateModel> |
||||||
|
private var realmHeaders: RealmResults<out DateModel> |
||||||
|
|
||||||
|
var filter: Filter? = null |
||||||
|
// |
||||||
|
// companion object { |
||||||
|
// |
||||||
|
// inline fun <reified T : DateModel> build(delegate: RowRepresentableDelegate?, dataSource: RowRepresentableDataSource, descriptor: EntityDescriptor<T>, realmQuery: RealmQuery<T>) : FilterSectionAdapter<T> { |
||||||
|
// val adapter = FilterSectionAdapter(delegate, dataSource, descriptor, realmQuery) |
||||||
|
// adapter.load() |
||||||
|
// return adapter |
||||||
|
// } |
||||||
|
// |
||||||
|
// } |
||||||
|
|
||||||
|
init { |
||||||
|
|
||||||
|
// this.realmEntities = this.realmQuery.findAll().sort(this.descriptor.sortFieldName) |
||||||
|
this.realmEntities = this.filter?.results() ?: this.realmQuery.findAll().sort(this.descriptor.sortFieldName) |
||||||
|
this.realmHeaders = this.descriptor.distinctHeaders(this.realmQuery).findAll() |
||||||
|
|
||||||
|
|
||||||
|
refreshData() |
||||||
|
} |
||||||
|
|
||||||
|
// fun load() { |
||||||
|
// |
||||||
|
// val f = this.filter?.results()?: this.realmQuery.findAll().sort(this.descriptor.sortFieldName) |
||||||
|
// this.realmEntities = f |
||||||
|
// |
||||||
|
// } |
||||||
|
|
||||||
|
|
||||||
|
// /** |
||||||
|
// * Display a transaction view |
||||||
|
// */ |
||||||
|
// inner class RowEntityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { |
||||||
|
// |
||||||
|
// fun bind(position: Int, row: DateModel?, adapter: FilterSectionAdapter) { |
||||||
|
// |
||||||
|
//// itemView.transactionRow.setData(row as Transaction) |
||||||
|
//// val listener = View.OnClickListener { |
||||||
|
//// adapter.delegate?.onRowSelected(position, row) |
||||||
|
//// } |
||||||
|
//// itemView.transactionRow.setOnClickListener(listener) |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
/** |
||||||
|
* Display a header |
||||||
|
*/ |
||||||
|
inner class HeaderTitleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { |
||||||
|
|
||||||
|
fun bind(title: String) { |
||||||
|
// Title |
||||||
|
itemView.findViewById<AppCompatTextView>(R.id.title)?.let { |
||||||
|
it.text = title |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { |
||||||
|
val inflater = LayoutInflater.from(parent.context) |
||||||
|
return if (viewType == this.descriptor.viewType) { // TODO layout |
||||||
|
val layout = inflater.inflate(this.descriptor.layout, parent, false) |
||||||
|
this.descriptor.bindableHolder(layout) |
||||||
|
// RowEntityViewHolder(layout) |
||||||
|
} else { |
||||||
|
val layout = inflater.inflate(R.layout.row_header_title, parent, false) |
||||||
|
HeaderTitleViewHolder(layout) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int { |
||||||
|
return if (sortedHeaders.containsKey(position)) { |
||||||
|
RowViewType.HEADER_TITLE.ordinal |
||||||
|
} else { |
||||||
|
this.descriptor.viewType // RowViewType.ROW_TRANSACTION.ordinal |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
override fun getItemCount(): Int { |
||||||
|
return realmEntities.size + realmHeaders.size |
||||||
|
} |
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { |
||||||
|
|
||||||
|
if (holder is BindableHolder) { |
||||||
|
holder.onBind(position, getEntityForPosition(position), this) |
||||||
|
} else if (holder is FeedTransactionRowRepresentableAdapter.HeaderTitleViewHolder) { |
||||||
|
holder.bind(getHeaderForPosition(position)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the header |
||||||
|
*/ |
||||||
|
private fun getHeaderForPosition(position: Int): String { |
||||||
|
if (sortedHeaders.containsKey(position)) { |
||||||
|
val realmHeaderPosition = sortedHeaders.keys.indexOf(position) |
||||||
|
return realmHeaders[realmHeaderPosition]?.date?.getMonthAndYear() ?: "" |
||||||
|
} |
||||||
|
return NULL_TEXT |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get real index |
||||||
|
*/ |
||||||
|
private fun getEntityForPosition(position: Int): DateModel { |
||||||
|
|
||||||
|
// Row position |
||||||
|
var headersBefore = 0 |
||||||
|
for (key in sortedHeaders.keys) { |
||||||
|
if (position > key) { |
||||||
|
headersBefore++ |
||||||
|
} else { |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return realmEntities[position - headersBefore] ?: throw PAIllegalStateException("Nasty problem!") |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Refresh headers positions |
||||||
|
*/ |
||||||
|
fun refreshData() { |
||||||
|
|
||||||
|
headersPositions.clear() |
||||||
|
|
||||||
|
var previousYear = Int.MAX_VALUE |
||||||
|
var previousMonth = Int.MAX_VALUE |
||||||
|
|
||||||
|
val calendar = Calendar.getInstance() |
||||||
|
|
||||||
|
// Add headers if the date doesn't exist yet |
||||||
|
for ((index, transaction) in realmEntities.withIndex()) { |
||||||
|
calendar.time = transaction.date |
||||||
|
if (checkHeaderCondition(calendar, previousYear, previousMonth)) { |
||||||
|
headersPositions[index + headersPositions.size] = transaction.date |
||||||
|
previousYear = calendar.get(Calendar.YEAR) |
||||||
|
previousMonth = calendar.get(Calendar.MONTH) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
sortedHeaders = headersPositions.toSortedMap() |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Check if we need to add a header |
||||||
|
* Can be change to manage different condition |
||||||
|
*/ |
||||||
|
private fun checkHeaderCondition(currentCalendar: Calendar, previousYear: Int, previousMonth: Int): Boolean { |
||||||
|
return currentCalendar.get(Calendar.YEAR) == previousYear && currentCalendar.get(Calendar.MONTH) < previousMonth || (currentCalendar.get(Calendar.YEAR) < previousYear) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
package net.pokeranalytics.android.ui.adapter |
||||||
|
|
||||||
|
import android.view.View |
||||||
|
import net.pokeranalytics.android.ui.view.RowRepresentable |
||||||
|
|
||||||
|
interface RecyclerAdapter { |
||||||
|
var dataSource: RowRepresentableDataSource |
||||||
|
var delegate: RowRepresentableDelegate? |
||||||
|
} |
||||||
|
|
||||||
|
interface RowRepresentableDelegate { |
||||||
|
fun onRowSelected(position: Int, row: RowRepresentable, tag: Int = 0) {} |
||||||
|
fun onRowDeselected(position: Int, row: RowRepresentable) {} |
||||||
|
fun onRowValueChanged(value: Any?, row: RowRepresentable) {} |
||||||
|
fun onRowDeleted(row: RowRepresentable) {} |
||||||
|
fun onRowLongClick(itemView: View, row: RowRepresentable, position: Int) {} |
||||||
|
fun onItemClick(position: Int, row: RowRepresentable, tag: Int = 0) {} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* An interface used to factor the configuration of RecyclerView.ViewHolder |
||||||
|
*/ |
||||||
|
interface BindableHolder { |
||||||
|
fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { |
||||||
|
} |
||||||
|
} |
||||||
@ -1,201 +0,0 @@ |
|||||||
package net.pokeranalytics.android.ui.fragment |
|
||||||
|
|
||||||
import android.app.Activity |
|
||||||
import android.content.Intent |
|
||||||
import android.os.Bundle |
|
||||||
import android.view.* |
|
||||||
import androidx.appcompat.widget.SearchView |
|
||||||
import androidx.core.view.isVisible |
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper |
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager |
|
||||||
import io.realm.Realm |
|
||||||
import io.realm.RealmResults |
|
||||||
import kotlinx.android.synthetic.main.fragment_data_list.* |
|
||||||
import net.pokeranalytics.android.R |
|
||||||
import net.pokeranalytics.android.exceptions.PAIllegalStateException |
|
||||||
import net.pokeranalytics.android.model.LiveData |
|
||||||
import net.pokeranalytics.android.model.interfaces.Deletable |
|
||||||
import net.pokeranalytics.android.model.interfaces.Identifiable |
|
||||||
import net.pokeranalytics.android.model.realm.Filter |
|
||||||
import net.pokeranalytics.android.ui.activity.EditableDataActivity |
|
||||||
import net.pokeranalytics.android.ui.activity.FiltersActivity |
|
||||||
import net.pokeranalytics.android.ui.adapter.LiveRowRepresentableDataSource |
|
||||||
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter |
|
||||||
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate |
|
||||||
import net.pokeranalytics.android.ui.extensions.removeMargins |
|
||||||
import net.pokeranalytics.android.ui.fragment.components.DeletableItemFragment |
|
||||||
import net.pokeranalytics.android.ui.helpers.SwipeToDeleteCallback |
|
||||||
import net.pokeranalytics.android.ui.view.RowRepresentable |
|
||||||
import net.pokeranalytics.android.ui.view.RowViewType |
|
||||||
import net.pokeranalytics.android.util.extensions.find |
|
||||||
import net.pokeranalytics.android.util.extensions.sorted |
|
||||||
|
|
||||||
|
|
||||||
open class DataListFragment : DeletableItemFragment(), LiveRowRepresentableDataSource, RowRepresentableDelegate { |
|
||||||
|
|
||||||
companion object { |
|
||||||
const val REQUEST_CODE_DETAILS = 1000 |
|
||||||
} |
|
||||||
|
|
||||||
private lateinit var identifiableClass: Class<out Deletable> |
|
||||||
|
|
||||||
private lateinit var dataType: LiveData |
|
||||||
private lateinit var items: RealmResults<out Deletable> |
|
||||||
private var dataListMenu: Menu? = null |
|
||||||
private var searchView: SearchView? = null |
|
||||||
|
|
||||||
var isSearchable: Boolean = false |
|
||||||
set(value) { |
|
||||||
field = value |
|
||||||
val searchMenuItem = dataListMenu?.findItem(R.id.action_search) |
|
||||||
searchMenuItem?.isVisible = value |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Set fragment data |
|
||||||
*/ |
|
||||||
open fun setData(dataType: Int) { |
|
||||||
|
|
||||||
this.dataType = LiveData.values()[dataType] |
|
||||||
this.identifiableClass = this.dataType.relatedEntity |
|
||||||
setToolbarTitle(this.dataType.pluralLocalizedTitle(requireContext())) |
|
||||||
|
|
||||||
this.items = this.retrieveItems(getRealm()) |
|
||||||
|
|
||||||
this.isSearchable = when (this.dataType) { |
|
||||||
LiveData.PLAYER, LiveData.LOCATION -> true |
|
||||||
else -> false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
open fun retrieveItems(realm: Realm): RealmResults<out Deletable> { |
|
||||||
return realm.sorted(this.identifiableClass, editableOnly = true, filterableTypeUniqueIdentifier = dataType.subType) |
|
||||||
} |
|
||||||
|
|
||||||
override fun deletableItems() : List<Deletable> { |
|
||||||
return this.items |
|
||||||
} |
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { |
|
||||||
super.onCreateView(inflater, container, savedInstanceState) |
|
||||||
return inflater.inflate(R.layout.fragment_data_list, container, false) |
|
||||||
} |
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
|
||||||
super.onViewCreated(view, savedInstanceState) |
|
||||||
initUI() |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Init UI |
|
||||||
*/ |
|
||||||
private fun initUI() { |
|
||||||
|
|
||||||
setDisplayHomeAsUpEnabled(true) |
|
||||||
|
|
||||||
val viewManager = LinearLayoutManager(requireContext()) |
|
||||||
dataListAdapter = RowRepresentableAdapter(this, this) |
|
||||||
|
|
||||||
val swipeToDelete = SwipeToDeleteCallback(dataListAdapter) { position -> |
|
||||||
val item = this.items[position] |
|
||||||
if (item != null) { |
|
||||||
val itemId = item.id |
|
||||||
deleteItem(dataListAdapter, items, itemId) |
|
||||||
} else { |
|
||||||
throw PAIllegalStateException("Item with position $position not found") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
val itemTouchHelper = ItemTouchHelper(swipeToDelete) |
|
||||||
|
|
||||||
recyclerView.apply { |
|
||||||
setHasFixedSize(true) |
|
||||||
layoutManager = viewManager |
|
||||||
adapter = dataListAdapter |
|
||||||
itemTouchHelper.attachToRecyclerView(this) |
|
||||||
} |
|
||||||
|
|
||||||
this.addButton.setOnClickListener { |
|
||||||
EditableDataActivity.newInstance( |
|
||||||
requireContext(), |
|
||||||
dataType = this.dataType.ordinal, |
|
||||||
primaryKey = null |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun onResume() { |
|
||||||
super.onResume() |
|
||||||
this.recyclerView?.adapter?.notifyDataSetChanged() |
|
||||||
} |
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { |
|
||||||
|
|
||||||
menu.clear() |
|
||||||
inflater.inflate(R.menu.toolbar_data_list, menu) |
|
||||||
this.dataListMenu = menu |
|
||||||
|
|
||||||
val searchMenuItem = menu.findItem(R.id.action_search) |
|
||||||
searchMenuItem.isVisible = isSearchable |
|
||||||
|
|
||||||
searchView = searchMenuItem.actionView as SearchView? |
|
||||||
searchView?.removeMargins() |
|
||||||
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { |
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean { |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean { |
|
||||||
filterItemsWithSearch(newText) |
|
||||||
return false |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu, inflater) |
|
||||||
} |
|
||||||
|
|
||||||
override fun rowRepresentableForPosition(position: Int): RowRepresentable? { |
|
||||||
return this.items[position] as RowRepresentable |
|
||||||
} |
|
||||||
|
|
||||||
override fun numberOfRows(): Int { |
|
||||||
return this.items.size |
|
||||||
} |
|
||||||
|
|
||||||
override fun viewTypeForPosition(position: Int): Int { |
|
||||||
val viewType = (this.items[position] as RowRepresentable).viewType |
|
||||||
return if (viewType != -1) viewType else RowViewType.DATA.ordinal |
|
||||||
} |
|
||||||
|
|
||||||
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { |
|
||||||
|
|
||||||
when (this.dataType) { |
|
||||||
LiveData.FILTER -> { |
|
||||||
val intent = Intent() |
|
||||||
intent.putExtra(FiltersActivity.IntentKey.FILTER_ID.keyName, (row as Filter).id) |
|
||||||
activity?.setResult(Activity.RESULT_OK, intent) |
|
||||||
activity?.finish() |
|
||||||
} |
|
||||||
else -> { |
|
||||||
val identifier = (row as Identifiable).id |
|
||||||
EditableDataActivity.newInstanceForResult(this, this.dataType, identifier, REQUEST_CODE_DETAILS) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Update UI |
|
||||||
*/ |
|
||||||
fun updateUI(showAddButton: Boolean) { |
|
||||||
this.addButton.isVisible = showAddButton |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Filter the items list with the given search content |
|
||||||
*/ |
|
||||||
private fun filterItemsWithSearch(searchContent: String?) { |
|
||||||
this.items = getRealm().find(this.identifiableClass, searchContent) |
|
||||||
dataListAdapter.notifyDataSetChanged() |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,122 +0,0 @@ |
|||||||
package net.pokeranalytics.android.ui.fragment |
|
||||||
|
|
||||||
import android.app.Activity |
|
||||||
import android.content.Context |
|
||||||
import android.content.Intent |
|
||||||
import io.realm.RealmResults |
|
||||||
import net.pokeranalytics.android.model.LiveData |
|
||||||
import net.pokeranalytics.android.model.interfaces.Deletable |
|
||||||
import net.pokeranalytics.android.model.interfaces.Identifiable |
|
||||||
import net.pokeranalytics.android.model.realm.Filter |
|
||||||
import net.pokeranalytics.android.ui.activity.EditableDataActivity |
|
||||||
import net.pokeranalytics.android.ui.activity.FiltersActivity |
|
||||||
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetFragment |
|
||||||
import net.pokeranalytics.android.ui.interfaces.FilterHandler.Companion.INTENT_FILTER_UPDATE_FILTER_UI |
|
||||||
import net.pokeranalytics.android.ui.view.RowRepresentable |
|
||||||
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor |
|
||||||
import net.pokeranalytics.android.ui.view.RowViewType |
|
||||||
import net.pokeranalytics.android.util.Preferences |
|
||||||
import timber.log.Timber |
|
||||||
|
|
||||||
|
|
||||||
open class FiltersListFragment : DataListFragment() { |
|
||||||
|
|
||||||
private var identifiableClass: Class<out Deletable> = Filter::class.java |
|
||||||
private var dataType: LiveData = LiveData.FILTER |
|
||||||
private lateinit var items: RealmResults<Filter> |
|
||||||
|
|
||||||
/** |
|
||||||
* Set fragment data |
|
||||||
*/ |
|
||||||
override fun setData(dataType: Int) { |
|
||||||
super.setData(dataType) |
|
||||||
|
|
||||||
this.dataType = LiveData.FILTER |
|
||||||
this.identifiableClass = Filter::class.java |
|
||||||
setToolbarTitle(this.dataType.pluralLocalizedTitle(requireContext())) |
|
||||||
this.items = this.retrieveItems(getRealm()) as RealmResults<Filter> |
|
||||||
} |
|
||||||
|
|
||||||
override fun rowRepresentableForPosition(position: Int): RowRepresentable? { |
|
||||||
Timber.d("rowRepresentableForPosition: ${this.items[position] as RowRepresentable}") |
|
||||||
return this.items[position] as RowRepresentable |
|
||||||
} |
|
||||||
|
|
||||||
override fun numberOfRows(): Int { |
|
||||||
return this.items.size |
|
||||||
} |
|
||||||
|
|
||||||
override fun adapterRows(): List<RowRepresentable>? { |
|
||||||
return items |
|
||||||
} |
|
||||||
|
|
||||||
override fun viewTypeForPosition(position: Int): Int { |
|
||||||
val viewType = (this.items[position] as RowRepresentable).viewType |
|
||||||
return if (viewType != -1) viewType else RowViewType.DATA.ordinal |
|
||||||
} |
|
||||||
|
|
||||||
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? { |
|
||||||
return when (row) { |
|
||||||
is Filter -> row.editingDescriptors(mapOf("defaultValue" to row.name)) |
|
||||||
else -> super.editDescriptors(row) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun onRowValueChanged(value: Any?, row: RowRepresentable) { |
|
||||||
when (row) { |
|
||||||
is Filter -> { |
|
||||||
row.updateValue(value, row) |
|
||||||
dataListAdapter.refreshRow(row) |
|
||||||
updateFilterUIIfNecessary(requireContext(), row.id) |
|
||||||
} |
|
||||||
else -> super.onRowValueChanged(value, row) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun onRowDeleted(row: RowRepresentable) { |
|
||||||
when (row) { |
|
||||||
is Filter -> { |
|
||||||
val filterId = row.id |
|
||||||
deleteItem(dataListAdapter, items, filterId) |
|
||||||
if (filterId == Preferences.getActiveFilterId(requireContext())) { |
|
||||||
Preferences.setActiveFilterId("", requireContext()) |
|
||||||
updateFilterUIIfNecessary(requireContext(), "") |
|
||||||
} |
|
||||||
} |
|
||||||
else -> super.onRowDeleted(row) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { |
|
||||||
when (row) { |
|
||||||
is Filter -> { |
|
||||||
if (fromAction) { |
|
||||||
val data = row.editingDescriptors(mapOf("defaultValue" to row.name)) |
|
||||||
BottomSheetFragment.create(fragmentManager, row, this, data, false, isDeletable = true, valueHasPlaceholder = false) |
|
||||||
} else { |
|
||||||
val intent = Intent() |
|
||||||
intent.putExtra(FiltersActivity.IntentKey.FILTER_ID.keyName, row.id) |
|
||||||
activity?.setResult(Activity.RESULT_OK, intent) |
|
||||||
activity?.finish() |
|
||||||
} |
|
||||||
} |
|
||||||
else -> { |
|
||||||
val identifier = (row as Identifiable).id |
|
||||||
EditableDataActivity.newInstanceForResult(this, this.dataType, identifier, REQUEST_CODE_DETAILS) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Update filter UI |
|
||||||
*/ |
|
||||||
fun updateFilterUIIfNecessary(context: Context, filterId: String) { |
|
||||||
if (filterId == Preferences.getActiveFilterId(context)) { |
|
||||||
// Send broadcast |
|
||||||
val intent = Intent() |
|
||||||
intent.action = INTENT_FILTER_UPDATE_FILTER_UI |
|
||||||
context.sendBroadcast(intent) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,4 +1,4 @@ |
|||||||
package net.pokeranalytics.android.ui.activity |
package net.pokeranalytics.android.ui.modules.bankroll |
||||||
|
|
||||||
import android.content.Context |
import android.content.Context |
||||||
import android.content.Intent |
import android.content.Intent |
||||||
@ -1,4 +1,4 @@ |
|||||||
package net.pokeranalytics.android.ui.viewmodel |
package net.pokeranalytics.android.ui.modules.calendar |
||||||
|
|
||||||
import androidx.lifecycle.ViewModel |
import androidx.lifecycle.ViewModel |
||||||
import net.pokeranalytics.android.calculus.ComputedResults |
import net.pokeranalytics.android.calculus.ComputedResults |
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue