diff --git a/app/build.gradle b/app/build.gradle index 7a60e77b..9654b5a3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,6 +9,7 @@ apply plugin: 'com.google.gms.google-services' // Crashlytics repositories { maven { url 'https://maven.fabric.io/public' } maven { url 'https://jitpack.io' } // required for MPAndroidChart + jcenter() // for kotlin serialization } android { @@ -33,8 +34,8 @@ android { applicationId "net.pokeranalytics.android" minSdkVersion 23 targetSdkVersion 28 - versionCode 84 - versionName "2.4.3" + versionCode 94 + versionName "5.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -97,23 +98,18 @@ dependencies { // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") // JVM dependency // Android - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.core:core-ktx:1.2.0-alpha03' - implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.3.0' + implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' - implementation 'androidx.browser:browser:1.0.0' - - // Retrofit - implementation 'com.squareup.retrofit2:retrofit:2.5.0' - implementation 'com.squareup.retrofit2:converter-gson:2.5.0' - implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1' - implementation 'com.google.code.gson:gson:2.8.5' + implementation 'androidx.work:work-runtime-ktx:2.3.4' // Places implementation 'com.google.android.libraries.places:places:1.1.0' @@ -124,7 +120,7 @@ dependencies { implementation 'com.android.billingclient:billing:1.2.2' // Firebase - implementation 'com.google.firebase:firebase-core:17.0.0' + implementation 'com.google.firebase:firebase-core:17.4.2' // Crashlytics implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' @@ -138,6 +134,9 @@ dependencies { // CSV Parser: https://mvnrepository.com/artifact/org.apache.commons/commons-csv implementation 'org.apache.commons:commons-csv:1.7' + // Polynomial Regression + implementation 'org.apache.commons:commons-math3:3.6.1' + // Instrumented Tests androidTestImplementation 'androidx.test:core:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0' @@ -149,5 +148,8 @@ dependencies { testImplementation 'com.android.support.test:runner:1.0.2' testImplementation 'com.android.support.test:rules:1.0.2' + // gross, somehow needed to make the stop notif work + implementation 'com.google.guava:guava:27.0.1-android' + } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2145a55a..8ffe53ad 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" + android:largeHeap="true" android:theme="@style/PokerAnalyticsTheme"> + + @@ -102,7 +109,7 @@ android:screenOrientation="portrait" /> @@ -112,17 +119,17 @@ android:screenOrientation="portrait" /> @@ -132,12 +139,7 @@ android:screenOrientation="portrait" /> - - @@ -167,6 +169,8 @@ android:theme="@style/PokerAnalyticsTheme.AlertDialog" android:launchMode="singleTop"/> + + diff --git a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt index e6c52dc3..fa64d55f 100644 --- a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt +++ b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt @@ -20,6 +20,7 @@ import net.pokeranalytics.android.util.PokerAnalyticsLogs import net.pokeranalytics.android.util.UserDefaults import net.pokeranalytics.android.util.billing.AppGuard import timber.log.Timber +import java.util.* class PokerAnalyticsApplication : Application() { @@ -44,12 +45,13 @@ class PokerAnalyticsApplication : Application() { Realm.init(this) val realmConfiguration = RealmConfiguration.Builder() .name(Realm.DEFAULT_REALM_NAME) - .schemaVersion(8) + .schemaVersion(9) .migration(PokerAnalyticsMigration()) .initialData(Seed(this)) .build() Realm.setDefaultConfiguration(realmConfiguration) + // val realm = Realm.getDefaultInstance() // realm.executeTransaction { // realm.where(Session::class.java).findAll().deleteAllFromRealm() @@ -75,11 +77,16 @@ class PokerAnalyticsApplication : Application() { if (BuildConfig.DEBUG) { Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}") + Timber.d("Realm path = ${Realm.getDefaultInstance().path}") + // this.createFakeSessions() } Patcher.patchAll(this.applicationContext) + val locale = Locale.getDefault() + Crashlytics.log("Country: ${locale.country}, language: ${locale.language}") + } /** @@ -93,7 +100,7 @@ class PokerAnalyticsApplication : Application() { if (sessionsCount < 10) { GlobalScope.launch { - FakeDataManager.createFakeSessions(200) + FakeDataManager.createFakeSessions(500) } } diff --git a/app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt b/app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt deleted file mode 100644 index 888a4a63..00000000 --- a/app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt +++ /dev/null @@ -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> - -} diff --git a/app/src/main/java/net/pokeranalytics/android/api/FreeConverterApi.kt b/app/src/main/java/net/pokeranalytics/android/api/FreeConverterApi.kt new file mode 100644 index 00000000..87595c93 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/api/FreeConverterApi.kt @@ -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) + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt b/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt index 7185f0d9..b934a185 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt @@ -21,6 +21,8 @@ import net.pokeranalytics.android.util.extensions.startOfDay import java.util.* import kotlin.math.max import kotlin.math.min +import kotlin.math.pow +import kotlin.math.sqrt /** * The class performing statIds computation @@ -119,7 +121,7 @@ class Calculator { val computeStandardDeviation: Boolean get() { this.stats.forEach { - if (it == STANDARD_DEVIATION_BB_PER_100_HANDS || it == STANDARD_DEVIATION || it == STANDARD_DEVIATION_HOURLY) { + if (it.isStandardDeviation) { return true } } @@ -335,6 +337,11 @@ class Calculator { ) ) } + var averageBB = 0.0 + if (bbSessionCount > 0) { + averageBB = bbSum / bbSessionCount + results.addStat(AVERAGE_NET_BB, averageBB) + } val shouldIterateOverComputables = (options.progressValues == Options.ProgressValues.STANDARD || options.computeLongestStreak) @@ -561,15 +568,19 @@ class Calculator { // Session var stdSum = 0.0 + var stdBBSum = 0.0 var stdBBper100HandsSum = 0.0 computables.forEach { session -> - stdSum += Math.pow(session.ratedNet - average, 2.0) - stdBBper100HandsSum += Math.pow(session.bbPer100Hands - bbPer100Hands, 2.0) + stdSum += (session.ratedNet - average).pow(2.0) + stdBBSum += (session.bbNet - averageBB).pow(2.0) + stdBBper100HandsSum += (session.bbPer100Hands - bbPer100Hands).pow(2.0) } - val standardDeviation = Math.sqrt(stdSum / computables.size) - val standardDeviationBBper100Hands = Math.sqrt(stdBBper100HandsSum / computables.size) + val standardDeviation = sqrt(stdSum / computables.size) + val standardDeviationBB = sqrt(stdBBSum / computables.size) + val standardDeviationBBper100Hands = sqrt(stdBBper100HandsSum / computables.size) results.addStat(STANDARD_DEVIATION, standardDeviation) + results.addStat(STANDARD_DEVIATION_BB, standardDeviationBB) results.addStat(STANDARD_DEVIATION_BB_PER_100_HANDS, standardDeviationBBper100Hands) // Session Set @@ -578,9 +589,9 @@ class Calculator { sessionSets.forEach { set -> val ssStats = SSStats(set, computableGroup.query) val sHourlyRate = ssStats.hourlyRate - hourlyStdSum += Math.pow(sHourlyRate - hourlyRate, 2.0) + hourlyStdSum += (sHourlyRate - hourlyRate).pow(2.0) } - val hourlyStandardDeviation = Math.sqrt(hourlyStdSum / sessionSets.size) + val hourlyStandardDeviation = sqrt(hourlyStdSum / sessionSets.size) results.addStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation) } diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/ComputedStat.kt b/app/src/main/java/net/pokeranalytics/android/calculus/ComputedStat.kt index 7058e349..f54677b8 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/ComputedStat.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/ComputedStat.kt @@ -27,8 +27,9 @@ class ComputedStat(var stat: Stat, var value: Double, var secondValue: Double? = /** * Formats the value of the stat to be suitable for display */ - fun format(): TextFormat { - return this.stat.format(this.value, this.secondValue, this.currency) - } + val textFormat: TextFormat + get() { + return this.stat.textFormat(this.value, this.secondValue, this.currency) + } } diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/Report.kt b/app/src/main/java/net/pokeranalytics/android/calculus/Report.kt index 3d241513..3d7c06cd 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Report.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Report.kt @@ -441,7 +441,7 @@ class ComputedResults(group: ComputableGroup, override fun formattedValue(stat: Stat): TextFormat { this.computedStat(stat)?.let { - return it.format() + return it.textFormat } ?: run { throw PAIllegalStateException("Missing stat in results") } @@ -460,12 +460,12 @@ class ComputedResults(group: ComputableGroup, GraphFragment.Style.BAR -> { return when (stat) { Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> { - val totalStatValue = stat.format(entry.y.toDouble(), currency = null) + val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null) DefaultLegendValues(this.entryTitle(context), totalStatValue) } else -> { val entryValue = this.formattedValue(stat) - val countValue = this.computedStat(Stat.NUMBER_OF_GAMES)?.format() + val countValue = this.computedStat(Stat.NUMBER_OF_GAMES)?.textFormat DefaultLegendValues(this.entryTitle(context), entryValue, countValue) } } @@ -474,12 +474,12 @@ class ComputedResults(group: ComputableGroup, return when (stat) { Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> { - val totalStatValue = stat.format(entry.y.toDouble(), currency = null) + val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null) DefaultLegendValues(this.entryTitle(context), totalStatValue) } else -> { val entryValue = this.formattedValue(stat) - val totalStatValue = stat.format(entry.y.toDouble(), currency = null) + val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null) DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue) } } diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt b/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt index 1043c434..e05ebb6a 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt @@ -52,6 +52,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres BB_SESSION_COUNT(26), TOTAL_BUYIN(27), RISK_OF_RUIN(28), + STANDARD_DEVIATION_BB(29), ; companion object : IntSearchable { @@ -119,6 +120,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres STANDARD_DEVIATION -> R.string.standard_deviation STANDARD_DEVIATION_HOURLY -> R.string.standard_deviation_per_hour STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.standard_deviation_bb_per_100_hands + STANDARD_DEVIATION_BB -> R.string.bb_standard_deviation HANDS_PLAYED -> R.string.number_of_hands LOCATIONS_PLAYED -> R.string.locations_played LONGEST_STREAKS -> R.string.longest_streaks @@ -135,7 +137,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres /** * Formats the value of the stat to be suitable for display */ - fun format(value: Double, secondValue: Double? = null, currency: Currency? = null): TextFormat { + fun textFormat(value: Double, secondValue: Double? = null, currency: Currency? = null): TextFormat { if (value.isNaN()) { return TextFormat(NULL_TEXT, R.color.white) @@ -150,7 +152,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres // Red/green numericValues HOURLY_RATE_BB, AVERAGE_NET_BB, NET_BB_PER_100_HANDS, BB_NET_RESULT -> { val color = if (value >= this.threshold) R.color.green else R.color.red - return TextFormat(value.formatted(), color) + return TextFormat(value.formatted, color) } // white integers NUMBER_OF_SETS, NUMBER_OF_GAMES, HANDS_PLAYED, LOCATIONS_PLAYED, DAYS_PLAYED -> { @@ -161,15 +163,15 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres } // red/green percentages WIN_RATIO, ROI -> { val color = if (value * 100 >= this.threshold) R.color.green else R.color.red - return TextFormat("${(value * 100).formatted()}%", color) + return TextFormat("${(value * 100).formatted}%", color) } RISK_OF_RUIN -> { val color = if (value * 100 <= this.threshold) R.color.green else R.color.red - return TextFormat("${(value * 100).formatted()}%", color) + return TextFormat("${(value * 100).formatted}%", color) } // white amountsr AVERAGE_BUYIN, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, - STANDARD_DEVIATION_BB_PER_100_HANDS, TOTAL_BUYIN -> { + STANDARD_DEVIATION_BB_PER_100_HANDS, STANDARD_DEVIATION_BB, TOTAL_BUYIN -> { return TextFormat(value.toCurrency(currency)) } LONGEST_STREAKS -> { @@ -200,6 +202,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres NUMBER_OF_GAMES -> R.string.number_of_records NET_RESULT -> R.string.total STANDARD_DEVIATION -> R.string.net_result + STANDARD_DEVIATION_BB -> R.string.average_net_result_bb_ STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands WIN_RATIO, HOURLY_DURATION -> return this.localizedTitle(context) @@ -235,12 +238,21 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres val hasProgressGraph: Boolean get() { return when (this) { - HOURLY_DURATION, AVERAGE_HOURLY_DURATION, - STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> false + HOURLY_DURATION, AVERAGE_HOURLY_DURATION, STANDARD_DEVIATION_BB, + STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, + STANDARD_DEVIATION_BB_PER_100_HANDS -> false else -> true } } + val isStandardDeviation: Boolean + get() { + return when (this) { + STANDARD_DEVIATION, STANDARD_DEVIATION_BB, + STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> true + else -> false + } + } val legendHideRightValue: Boolean get() { diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt b/app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt new file mode 100644 index 00000000..935e273f --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt @@ -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, 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()) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/LiveData.kt b/app/src/main/java/net/pokeranalytics/android/model/LiveData.kt index 322ea5c5..e31ef53a 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/LiveData.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/LiveData.kt @@ -1,10 +1,15 @@ package net.pokeranalytics.android.model import android.content.Context +import androidx.fragment.app.Fragment import io.realm.Realm +import io.realm.Sort import net.pokeranalytics.android.R import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.realm.* +import net.pokeranalytics.android.model.realm.handhistory.HandHistory +import net.pokeranalytics.android.ui.modules.data.EditableDataActivity +import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity import net.pokeranalytics.android.ui.view.Localizable import net.pokeranalytics.android.util.extensions.findById @@ -22,7 +27,8 @@ enum class LiveData : Localizable { FILTER, CUSTOM_FIELD, REPORT_SETUP, - PLAYER; + PLAYER, + HAND_HISTORY; var subType:Int? = null @@ -40,6 +46,7 @@ enum class LiveData : Localizable { CUSTOM_FIELD -> CustomField::class.java REPORT_SETUP -> ReportSetup::class.java PLAYER -> Player::class.java + HAND_HISTORY -> HandHistory::class.java } } @@ -81,6 +88,7 @@ enum class LiveData : Localizable { CUSTOM_FIELD -> R.string.custom_field REPORT_SETUP -> R.string.custom PLAYER -> R.string.player + HAND_HISTORY -> R.string.hand_history } } @@ -98,6 +106,7 @@ enum class LiveData : Localizable { CUSTOM_FIELD -> R.string.custom_fields REPORT_SETUP -> R.string.custom PLAYER -> R.string.players + HAND_HISTORY -> R.string.hands_history } } @@ -115,10 +124,17 @@ enum class LiveData : Localizable { CUSTOM_FIELD -> R.string.new_custom_field REPORT_SETUP -> R.string.new_report PLAYER -> R.string.new_friend + HAND_HISTORY -> R.string.new_hand } } - + val isSearchable: Boolean + get() { + return when (this) { + PLAYER, LOCATION -> true + else -> false + } + } /** * Return the new entity titleResId @@ -141,4 +157,33 @@ enum class LiveData : Localizable { return context.getString(this.pluralResId, context) } + fun openEditActivity(fragment: Fragment, primaryKey: String? = null, requestCode: Int) { + when (this) { + HAND_HISTORY -> { + HandHistoryActivity.newInstance(fragment, primaryKey) + } + else -> { + EditableDataActivity.newInstanceForResult(fragment, this, primaryKey, requestCode) + } + } + } + + val sortFields: Array + get() { + return when (this) { + TRANSACTION, HAND_HISTORY -> arrayOf("date") + FILTER -> arrayOf("useCount", "name") + else -> arrayOf("name") + } + } + + val sortOrders: Array + get() { + return when (this) { + TRANSACTION, HAND_HISTORY -> arrayOf(Sort.DESCENDING) + FILTER -> arrayOf(Sort.DESCENDING, Sort.ASCENDING) + else -> arrayOf(Sort.ASCENDING) + } + } + } diff --git a/app/src/main/java/net/pokeranalytics/android/model/TableSize.kt b/app/src/main/java/net/pokeranalytics/android/model/TableSize.kt index 144396f3..96b32bea 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/TableSize.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/TableSize.kt @@ -6,15 +6,19 @@ import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.util.Parser -class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITLE_GRID.ordinal) : RowRepresentable { +class TableSize( + var numberOfPlayer: Int, + var rowViewType: Int = RowViewType.TITLE_GRID.ordinal, + var alternativeLabels: Boolean = false +) : RowRepresentable { companion object { - val all: List - get() { - return Array(9, init = - { index -> TableSize(index + 2) }).toList() - } + fun all(alternativeLabels: Boolean): List { + return Array(9, init = { index -> + TableSize(index + 2, alternativeLabels = alternativeLabels) + }).toList() + } fun valueForLabel(label: String) : Int? { @@ -32,6 +36,10 @@ class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITL } override fun getDisplayName(context: Context): String { + if (this.alternativeLabels) { + return this.numberOfPlayer.toString() + } + return if (this.numberOfPlayer == 2) { return "HU" } else { @@ -49,6 +57,10 @@ class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITL } override fun localizedTitle(context: Context): String { + if (this.alternativeLabels) { + return this.numberOfPlayer.toString() + } + this.resId?.let { return if (this.numberOfPlayer == 2) { context.getString(it) diff --git a/app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt b/app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt index ac7deb47..8fc72845 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt @@ -12,7 +12,7 @@ enum class TournamentType(val label: String) : RowRepresentable { companion object { val all : List get() { - return values() as List + return values().toList() } fun getValueForLabel(label: String) : TournamentType? { diff --git a/app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt b/app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt index ba21ebea..93f07352 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt @@ -1,11 +1,17 @@ package net.pokeranalytics.android.model.extensions import android.content.Context +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager import net.pokeranalytics.android.R +import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.TournamentType import net.pokeranalytics.android.model.realm.Session +import net.pokeranalytics.android.util.NotificationSchedule import net.pokeranalytics.android.util.extensions.toCurrency import java.util.* +import java.util.concurrent.TimeUnit enum class SessionState { PENDING, @@ -14,7 +20,7 @@ enum class SessionState { PAUSED, FINISHED; - var hasStarted: Boolean = false + val hasStarted: Boolean get() { return when (this) { STARTED, PAUSED, FINISHED -> true @@ -46,11 +52,11 @@ fun Session.getState(): SessionState { } /** - * Formate the session game type + * Format the session game type */ fun Session.getFormattedGameType(context: Context): String { - var parameters = mutableListOf() + val parameters = mutableListOf() if (isTournament()) { tournamentEntryFee?.let { @@ -84,6 +90,39 @@ fun Session.getFormattedGameType(context: Context): String { return parameters.joinToString(separator = " ") } +fun Session.currentDuration(): Long { + return this.startDate?.let { + this.endDate().time - it.time + } ?: 0L +} + +fun Session.cancelStopNotification(context: Context) { + WorkManager.getInstance(context).cancelAllWorkByTag(this.id) +} + +fun Session.scheduleStopNotification(context: Context, optimalDuration: Long) { + + val timeDelay = optimalDuration - this.currentDuration() + if (timeDelay <= 0) throw PAIllegalStateException("A stop notif has been setup for the past: start=${this.startDate}, end=${this.endDate}, optimalDuration=$optimalDuration") + + val title = context.getString(R.string.stop_notification_title) + val body = context.getString(R.string.stop_notification_body) + + val data = Data.Builder() + .putString(NotificationSchedule.ParamKeys.TITLE.value, title) + .putString(NotificationSchedule.ParamKeys.BODY.value, body) + + val work = OneTimeWorkRequestBuilder() + .setInitialDelay(timeDelay, TimeUnit.MILLISECONDS) + .setInputData(data.build()) + .addTag(this.id) + .build() + + WorkManager.getInstance(context).enqueue(work) + +} + + val AbstractList.hourlyDuration: Double get() { val intervals = mutableListOf() @@ -113,7 +152,7 @@ fun MutableList.update(timeInterval: TimeInterval): MutableList.mapFirstCondition() : List { class Query { constructor(vararg elements: QueryCondition) { - if (elements.size > 0) { + if (elements.isNotEmpty()) { this.add(elements.asList()) } } @@ -23,22 +23,25 @@ class Query { return this._conditions } - fun add(vararg elements: QueryCondition) { - if (elements.size > 0) { + fun add(vararg elements: QueryCondition): Query { + if (elements.isNotEmpty()) { this.add(elements.asList()) } + return this } - fun add(queryCondition: QueryCondition) { + fun add(queryCondition: QueryCondition): Query { this._conditions.add(queryCondition) + return this } - fun remove(queryCondition: QueryCondition) { - this._conditions.remove(queryCondition) + fun add(queryConditions: List): Query{ + this._conditions.addAll(queryConditions) + return this } - fun add(queryConditions: List) { - this._conditions.addAll(queryConditions) + fun remove(queryCondition: QueryCondition) { + this._conditions.remove(queryCondition) } val defaultName: String @@ -59,28 +62,32 @@ class Query { inline fun queryWith(query: RealmQuery): RealmQuery { var realmQuery = query - val queryFromTime = this.conditions.filter { - it is QueryCondition.StartedFromTime - }.firstOrNull() - val queryToTime = this.conditions.filter { - it is QueryCondition.EndedToTime - }.firstOrNull() - - this.conditions.forEach { - if (it is QueryCondition.StartedFromTime) { - realmQuery = it.queryWith(realmQuery, queryToTime) - } else if (it is QueryCondition.EndedToTime) { - realmQuery = it.queryWith(realmQuery, queryFromTime) - } else { - realmQuery = it.queryWith(realmQuery) - } + val queryFromTime = this.conditions.firstOrNull { + it is QueryCondition.StartedFromTime + } + val queryToTime = this.conditions.firstOrNull { + it is QueryCondition.EndedToTime + } + + this.conditions.forEach { + realmQuery = when (it) { + is QueryCondition.StartedFromTime -> { + it.queryWith(realmQuery, queryToTime) + } + is QueryCondition.EndedToTime -> { + it.queryWith(realmQuery, queryFromTime) + } + else -> { + it.queryWith(realmQuery) + } + } } // println("<<<<<< ${realmQuery.description}") - val queryLast = this.conditions.filter { - it is QueryCondition.Last - }.firstOrNull() - queryLast?.let {qc -> + val queryLast = this.conditions.firstOrNull { + it is QueryCondition.Last + } + queryLast?.let {qc -> (qc as QueryCondition.Last).singleValue?.let { return realmQuery.limit(it.toLong()) } diff --git a/app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt b/app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt index ac94450e..4417b496 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt @@ -96,16 +96,17 @@ sealed class QueryCondition : FilterElementRow { val groupId: String get() { - when (this.operator) { + return when (this.operator) { Operator.MORE, Operator.LESS -> return "${this.operator.name.toLowerCase().capitalize()}$baseId" + else -> this.baseId } - return baseId } val id: List get() { when (this.operator) { Operator.MORE, Operator.LESS -> return listOf("$baseId+${this.operator.name}") + else -> {} } return when (this) { @@ -306,6 +307,9 @@ sealed class QueryCondition : FilterElementRow { abstract class TrueQueryCondition : QueryCondition() { override var operator: Operator = Operator.TRUE } + abstract class NotNullQueryCondition : QueryCondition() { + override var operator: Operator = Operator.NOTNULL + } object IsLive : TrueQueryCondition() @@ -601,9 +605,9 @@ sealed class QueryCondition : FilterElementRow { } } - object DateNotNull : QueryCondition() { - override var operator = Operator.NOTNULL - } + object DateNotNull : NotNullQueryCondition() + object EndDateNotNull : NotNullQueryCondition() + object BigBlindNotNull : NotNullQueryCondition() class StartedFromTime() : TimeQuery() { override var operator = Operator.MORE diff --git a/app/src/main/java/net/pokeranalytics/android/model/handhistory/BoardManager.kt b/app/src/main/java/net/pokeranalytics/android/model/handhistory/BoardManager.kt new file mode 100644 index 00000000..1a4080b1 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/handhistory/BoardManager.kt @@ -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, var listener: BoardChangedListener) { + + /*** + * The sorted list of cards + */ + private var sortedBoardCards: MutableList = mutableListOf() + + /*** + * All cards + */ + val allCards: List + 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() + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/handhistory/HandSetup.kt b/app/src/main/java/net/pokeranalytics/android/model/handhistory/HandSetup.kt new file mode 100644 index 00000000..af8c7897 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/handhistory/HandSetup.kt @@ -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 = 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) { + 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}") + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/handhistory/Position.kt b/app/src/main/java/net/pokeranalytics/android/model/handhistory/Position.kt new file mode 100644 index 00000000..a4f39a08 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/handhistory/Position.kt @@ -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 { + 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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/handhistory/Street.kt b/app/src/main/java/net/pokeranalytics/android/model/handhistory/Street.kt new file mode 100644 index 00000000..2b49c7c3 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/handhistory/Street.kt @@ -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] + } + } + +} + diff --git a/app/src/main/java/net/pokeranalytics/android/model/interfaces/TimeFilterable.kt b/app/src/main/java/net/pokeranalytics/android/model/interfaces/TimeFilterable.kt index a83cc2e1..d9b5ce8b 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/interfaces/TimeFilterable.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/interfaces/TimeFilterable.kt @@ -19,10 +19,10 @@ interface TimeFilterable { startDate?.let { val cal = Calendar.getInstance() cal.time = it - dayOfWeek = cal.get(Calendar.DAY_OF_WEEK) - dayOfMonth = cal.get(Calendar.DAY_OF_MONTH) - month = cal.get(Calendar.MONTH) - year = cal.get(Calendar.YEAR) + this.dayOfWeek = cal.get(Calendar.DAY_OF_WEEK) + this.dayOfMonth = cal.get(Calendar.DAY_OF_MONTH) + this.month = cal.get(Calendar.MONTH) + this.year = cal.get(Calendar.YEAR) } } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt b/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt index 32bebc81..e9ade914 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt @@ -2,6 +2,7 @@ package net.pokeranalytics.android.model.migrations import io.realm.DynamicRealm import io.realm.RealmMigration +import net.pokeranalytics.android.exceptions.PAIllegalStateException import timber.log.Timber import java.util.* @@ -171,6 +172,61 @@ class PokerAnalyticsMigration : RealmMigration { currentVersion++ } + + // Migrate to version 9 + if (currentVersion == 8) { + schema.get("HandHistory")?.let { hhSchema -> + + schema.get("Session")?.let { sessionSchema -> + sessionSchema.removeField("hands") + hhSchema.addRealmObjectField("session", sessionSchema) + } ?: throw PAIllegalStateException("Session schema not found") + hhSchema.addField("smallBlind", Double::class.java).setRequired("smallBlind", false) + hhSchema.addField("bigBlind", Double::class.java).setRequired("bigBlind", false) + hhSchema.addField("ante", Double::class.java) + hhSchema.addField("bigBlindAnte", Boolean::class.java) + hhSchema.addField("numberOfPlayers", Int::class.java) + hhSchema.addField("comment", String::class.java) + hhSchema.addField("heroIndex", Int::class.java).setRequired("heroIndex", false) + hhSchema.addField("dayOfWeek", Integer::class.java) + hhSchema.addField("month", Integer::class.java) + hhSchema.addField("year", Integer::class.java) + hhSchema.addField("dayOfMonth", Integer::class.java) + + val cardSchema = schema.create("Card") + cardSchema.addField("value", Int::class.java).setRequired("value", false) + cardSchema.addField("suitIdentifier", Int::class.java).setRequired("suitIdentifier", false) + cardSchema.addField("index", Int::class.java) + hhSchema.addRealmListField("board", cardSchema) + + val actionSchema = schema.create("Action") + actionSchema.addField("streetIdentifier", Int::class.java) + actionSchema.addField("index", Int::class.java) + actionSchema.addField("position", Int::class.java) + actionSchema.addField("typeIdentifier", Int::class.java).setRequired("typeIdentifier", false) + actionSchema.addField("amount", Double::class.java).setRequired("amount", false) + actionSchema.addField("effectiveAmount", Double::class.java) + actionSchema.addField("positionRemainingStack", Double::class.java).setRequired("positionRemainingStack", false) + hhSchema.addRealmListField("actions", actionSchema) + + val playerSetupSchema = schema.create("PlayerSetup") + schema.get("Player")?.let { + playerSetupSchema.addRealmObjectField("player", it) + } ?: throw PAIllegalStateException("Session schema not found") + playerSetupSchema.addField("position", Int::class.java) + playerSetupSchema.addField("stack", Double::class.java).setRequired("stack", false) + playerSetupSchema.addRealmListField("cards", cardSchema) + hhSchema.addRealmListField("playerSetups", playerSetupSchema) + + val wonPotSchema = schema.create("WonPot") + wonPotSchema.addField("position", Int::class.java) + wonPotSchema.addField("amount", Double::class.java) + hhSchema.addRealmListField("winnerPots", wonPotSchema) + + } + + currentVersion++ + } } override fun equals(other: Any?): Boolean { diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt index ccca2ccb..5beffd8b 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt @@ -6,19 +6,19 @@ import net.pokeranalytics.android.model.filter.QueryCondition open class ComputableResult() : RealmObject(), Filterable { - var ratedNet: Double = 0.0 + var ratedNet: Double = 0.0 - var bbNet: BB = 0.0 + var bbNet: BB = 0.0 - var hasBigBlind: Int = 0 + var hasBigBlind: Int = 0 - var isPositive: Int = 0 + var isPositive: Int = 0 - var ratedBuyin: Double = 0.0 + var ratedBuyin: Double = 0.0 - var estimatedHands: Double = 0.0 + var estimatedHands: Double = 0.0 - var bbPer100Hands: BB = 0.0 + var bbPer100Hands: BB = 0.0 var session: Session? = null @@ -35,7 +35,8 @@ open class ComputableResult() : RealmObject(), Filterable { this.bbNet = session.bbNet this.hasBigBlind = if (session.cgBigBlind != null) 1 else 0 this.estimatedHands = session.estimatedHands - this.bbPer100Hands = session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100 + this.bbPer100Hands = + session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100 } @@ -51,11 +52,11 @@ open class ComputableResult() : RealmObject(), Filterable { companion object { - fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? { - Session.fieldNameForQueryType(queryCondition)?.let { - return "session.$it" - } - return null + fun fieldNameForQueryType(queryCondition: Class): String? { + Session.fieldNameForQueryType(queryCondition)?.let { + return "session.$it" + } + return null } } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt index 97aac8c7..0371c160 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt @@ -193,7 +193,7 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa } } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { return when (row) { is CustomFieldEntry -> row.editingDescriptors( mapOf( @@ -290,7 +290,6 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa fun cleanupEntries() { // called when saving the custom field val realm = Realm.getDefaultInstance() - realm.executeTransaction { this.entriesToDelete.forEach { // entries are out of realm realm.where().equalTo("id", it.id).findFirst()?.deleteFromRealm() diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt index 00f9ff51..1d53b89d 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt @@ -11,7 +11,7 @@ import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.interfaces.* import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType -import net.pokeranalytics.android.ui.interfaces.FilterableType +import net.pokeranalytics.android.ui.modules.filter.FilterableType import net.pokeranalytics.android.ui.view.ImageDecorator import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor @@ -105,11 +105,8 @@ open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, Counta it.groupId == groupId } .apply { - Timber.d("list of querys: ${this.map { it.id }}") - val casted = arrayListOf() - casted.addAll(this) - val newFilterCondition = FilterCondition(casted) + val newFilterCondition = FilterCondition(this) val previousCondition = filterConditions.filter { it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator } @@ -153,22 +150,22 @@ open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, Counta } } - inline fun query(firstField: String? = null, secondField: String? = null): RealmQuery { + inline fun query(firstField: String? = null, vararg remainingFields: String): RealmQuery { val realmQuery = realm.where() - if (firstField != null && secondField != null) { - return this.query.queryWith(realmQuery).distinct(firstField, secondField) - } +// if (firstField != null && secondField != null) { +// return this.query.queryWith(realmQuery).distinct(firstField, secondField) +// } if (firstField != null) { - return this.query.queryWith(realmQuery).distinct(firstField) + return this.query.queryWith(realmQuery).distinct(firstField, *remainingFields) } return this.query.queryWith(realmQuery) } - inline fun results(firstField: String? = null, secondField: String? = null): RealmResults { - return this.query(firstField, secondField).findAll() + inline fun results(firstField: String? = null, vararg remainingFields: String): RealmResults { + return this.query(firstField, *remainingFields).findAll() } val query: Query diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt index 46c4fc5b..3a4f1538 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt @@ -14,7 +14,7 @@ open class FilterCondition() : RealmObject() { this.sectionName = sectionName } - constructor(filterElementRows: ArrayList) : this(filterElementRows.first().baseId, filterElementRows.first().filterSectionRow.name) { + constructor(filterElementRows: List) : this(filterElementRows.first().baseId, filterElementRows.first().filterSectionRow.name) { val row = filterElementRows.first() this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName this.operator = row.operator.ordinal diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Game.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Game.kt index da713f05..0a6df1f3 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Game.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Game.kt @@ -30,7 +30,6 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc val rowRepresentation : List by lazy { val rows = ArrayList() rows.add(SimpleRow.NAME) -// rows.addAll(GameRow.values()) rows } } @@ -62,18 +61,22 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc } override fun adapterRows(): List? { - return Game.rowRepresentation + return rowRepresentation } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return when (row) { SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT GameRow.SHORT_NAME -> this.shortName ?: NULL_TEXT - else -> return super.stringForRow(row) + else -> return super.charSequenceForRow(row, context, 0) } } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { return when (row) { SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.name)) GameRow.SHORT_NAME -> row.editingDescriptors(mapOf("defaultValue" to this.shortName)) @@ -103,4 +106,30 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc override fun isValidForDelete(realm: Realm): Boolean { return realm.where().equalTo("game.id", id).findAll().isEmpty() } + + val playerHandMaxCards: Int? + get() { + return when { + isHoldem -> 2 + isOmaha5 -> 5 + isOmaha4 -> 4 + else -> null + } + } + + private val isHoldem: Boolean + get() { + return name.contains("texas", ignoreCase = true) || name.contains("holdem", ignoreCase = true) || name.contains("hold'em", ignoreCase = true) || name.contains("HE") + } + + private val isOmaha4: Boolean + get() { + return name.contains("omaha", ignoreCase = true) || name.contains("PLO", ignoreCase = true) + } + + private val isOmaha5: Boolean + get() { + return name.contains("5") + } + } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/HandHistory.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/HandHistory.kt deleted file mode 100644 index c65264f9..00000000 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/HandHistory.kt +++ /dev/null @@ -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() - -} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt index 1e098e6d..71edbd31 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt @@ -17,12 +17,13 @@ import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepres import net.pokeranalytics.android.ui.view.rowrepresentable.PlayerRow import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRow import net.pokeranalytics.android.util.NULL_TEXT +import net.pokeranalytics.android.util.RANDOM_PLAYER import net.pokeranalytics.android.util.extensions.isSameDay import net.pokeranalytics.android.util.extensions.mediumDate import java.util.* import kotlin.collections.ArrayList -open class Player : RealmObject(), NameManageable, Deletable, StaticRowRepresentableDataSource, RowRepresentable { +open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRowRepresentableDataSource, RowRepresentable { @PrimaryKey override var id = UUID.randomUUID().toString() @@ -75,10 +76,14 @@ open class Player : RealmObject(), NameManageable, Deletable, StaticRowRepresent return this.name } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return when (row) { PlayerRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT - else -> return super.stringForRow(row) + else -> return super.charSequenceForRow(row, context, 0) } } @@ -90,7 +95,6 @@ open class Player : RealmObject(), NameManageable, Deletable, StaticRowRepresent } } - /** * Update the row representation */ @@ -178,7 +182,7 @@ open class Player : RealmObject(), NameManageable, Deletable, StaticRowRepresent this.commentsToDelete.clear() } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { when (row) { PlayerRow.NAME -> return row.editingDescriptors(mapOf("defaultValue" to this.name)) @@ -188,5 +192,24 @@ open class Player : RealmObject(), NameManageable, Deletable, StaticRowRepresent return null } + val initials: String + get() { + return if (this.name.isNotEmpty()) { + val playerData = this.name.split(" ") + when { + playerData.size > 1 -> { + playerData[0].first().toString() + playerData[1].first().toString() + } + this.name.length > 1 -> { + this.name.substring(0, 2) + } + else -> { + this.name.substring(0, this.name.length) + } + } + } else { + RANDOM_PLAYER + } + } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt index f813dd75..b48d8927 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt @@ -64,15 +64,15 @@ open class ReportSetup : RealmObject(), RowRepresentable, Deletable { /** * Returns the Options based on the ReportSetup parameters */ - val options: Calculator.Options - get() { + fun options(realm: Realm): Calculator.Options { - val realm = Realm.getDefaultInstance() val stats = this.statIds.map { Stat.valueByIdentifier(it) } // Comparison criteria val criteria = this.criteriaIds.map { Criteria.valueByIdentifier(it) } + val customFields = this.criteriaCustomFieldIds.mapNotNull { realm.findById(it) } + val cfCriteria = customFields.map { it.criteria } val allCriteria = mutableListOf() diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt index d887521b..faf740ab 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt @@ -1,6 +1,7 @@ package net.pokeranalytics.android.model.realm import android.content.Context +import com.crashlytics.android.Crashlytics import com.github.mikephil.charting.data.Entry import io.realm.Realm import io.realm.RealmList @@ -21,11 +22,13 @@ import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.TableSize import net.pokeranalytics.android.model.TournamentType import net.pokeranalytics.android.model.extensions.SessionState +import net.pokeranalytics.android.model.extensions.cancelStopNotification import net.pokeranalytics.android.model.extensions.getState import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition.* import net.pokeranalytics.android.model.interfaces.* +import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.model.utils.SessionSetManager import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.UnmanagedRowRepresentableException @@ -116,11 +119,22 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat CustomFieldAmountQuery::class.java, CustomFieldNumberQuery::class.java -> "customFieldEntries.numericValue" CustomFieldQuery::class.java -> "customFieldEntries.customFields.id" DateNotNull::class.java -> "startDate" + EndDateNotNull::class.java -> "endDate" + BigBlindNotNull::class.java -> "cgBigBlind" else -> null } } } + /*** + * Returns an automatic date for a new hand history + */ + val handHistoryAutomaticDate: Date + get() { + val hhCount = this.handHistories?.size ?: 0 + return Date(this.date.time + hhCount * 1000 * 60) // one HH per minute + } + @PrimaryKey override var id = UUID.randomUUID().toString() @@ -130,6 +144,9 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat @Index var type: Int = Type.CASH_GAME.ordinal + val sessionType: Type + get() { return Type.values()[this.type] } + // The result of the main user var result: Result? = null @@ -264,8 +281,11 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat // The number of tables played at the same time var numberOfTables: Int = 1 - // The hands list associated with the Session - var hands: RealmList = RealmList() +// var hands: RealmList = RealmList() + + // The hand histories of the session + @LinkingObjects("session") + val handHistories: RealmResults? = null // The list of opponents who participated to the session var opponents: RealmList = RealmList() @@ -496,7 +516,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat */ fun startOrContinue() { realm.executeTransaction { - when (getState()) { + when (val state = getState()) { SessionState.PENDING, SessionState.PLANNED -> { this.startDate = Date() this.defineDefaultTournamentBuyinIfNecessary() @@ -511,7 +531,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat this.pauseDate = null } else -> { - throw PAIllegalStateException("unmanaged session state") + throw PAIllegalStateException("unmanaged session state: $state") } } } @@ -528,11 +548,11 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat */ fun pause() { realm.executeTransaction { - when (getState()) { + when (val state = getState()) { SessionState.STARTED -> { this.pauseDate = Date() } - else -> throw PAIllegalStateException("Pausing a session in an unmanaged state") + else -> throw PAIllegalStateException("Pausing a session in an unmanaged state: $state") } } } @@ -540,15 +560,16 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat /** * Stop a session */ - fun stop() { + fun stop(context: Context) { realm.executeTransaction { - when (getState()) { + when (val state = getState()) { SessionState.STARTED, SessionState.PAUSED -> { this.end() } - else -> throw Exception("Stopping session in unmanaged state") + else -> throw Exception("Stopping session in unmanaged state: $state") } } + cancelStopNotification(context) } /** @@ -621,7 +642,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat if (cgBigBlind == null) return cgBigBlind?.let { bb -> val sb = cgSmallBlind ?: bb / 2.0 - val preFormattedBlinds = "${sb.formatted()}/${bb.round()}" + val preFormattedBlinds = "${sb.formatted}/${bb.round()}" // println("<<<<<< bb.toCurrency(currency) : ${bb.toCurrency(currency)}") // println("<<<<<< preFormattedBlinds : $preFormattedBlinds") val regex = Regex("-?\\d+(\\.\\d+)?") @@ -636,11 +657,15 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat * Delete the object from realm */ fun delete() { + + Crashlytics.log("Deletes session. Id = ${this.id}") if (isValid) { realm.executeTransaction { cleanup() deleteFromRealm() } + } else { + Crashlytics.log("Attempt to delete an invalid session") } } @@ -707,7 +732,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat CustomizableRowRepresentable( RowViewType.HEADER_TITLE_AMOUNT_BIG, title = getFormattedDuration(), - computedStat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency) + valueTextFormat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency).textFormat ) ) rows.add(SeparatorRow()) @@ -717,7 +742,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat CustomizableRowRepresentable( RowViewType.HEADER_TITLE_AMOUNT_BIG, resId = R.string.pause, - computedStat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency) + valueTextFormat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency).textFormat ) ) rows.add(SeparatorRow()) @@ -727,14 +752,14 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat CustomizableRowRepresentable( RowViewType.HEADER_TITLE_AMOUNT_BIG, title = getFormattedDuration(), - computedStat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency) + valueTextFormat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency).textFormat ) ) rows.add( CustomizableRowRepresentable( RowViewType.HEADER_TITLE_AMOUNT, resId = R.string.hour_rate_without_pauses, - computedStat = ComputedStat(Stat.HOURLY_RATE, this.hourlyRate, currency = currency) + valueTextFormat = ComputedStat(Stat.HOURLY_RATE, this.hourlyRate, currency = currency).textFormat ) ) @@ -777,7 +802,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat return false } - override fun stringForRow(row: RowRepresentable, context: Context): String { + override fun charSequenceForRow(row: RowRepresentable, context: Context): String { return when (row) { SessionRow.BANKROLL -> bankroll?.name ?: NULL_TEXT SessionRow.BLINDS -> getFormattedBlinds() @@ -816,6 +841,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat } } SessionRow.TOURNAMENT_NAME -> tournamentName?.name ?: NULL_TEXT + SessionRow.HANDS -> this.handHistories?.size.toString() is CustomField -> { customFieldEntries.find { it.customField?.id == row.id }?.let { customFieldEntry -> return customFieldEntry.getFormattedValue(currency) @@ -835,7 +861,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat } } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { return when (row) { SessionRow.BANKROLL -> row.editingDescriptors( mapOf( @@ -1123,7 +1149,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat } value?.let { - return stat.format(it, currency = currency) + return stat.textFormat(it, currency = currency) } ?: run { return TextFormat(NULL_TEXT) } @@ -1162,7 +1188,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat if (!hasMainCurrencyCode) { this.computableResult?.ratedNet?.let { ratedNet -> - right = Stat.NET_RESULT.format(ratedNet) + right = Stat.NET_RESULT.textFormat(ratedNet) } } @@ -1178,6 +1204,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat } + @Ignore override val realmObjectClass: Class = Session::class.java diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt index c447de69..32998dd8 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt @@ -122,15 +122,15 @@ open class SessionSet() : RealmObject(), Timed, Filterable { override fun formattedValue(stat: Stat) : TextFormat { return when (stat) { - Stat.NET_RESULT, Stat.AVERAGE -> stat.format(this.ratedNet, currency = null) - Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> stat.format(this.hourlyDuration, currency = null) - Stat.HOURLY_RATE -> stat.format(this.hourlyRate, currency = null) - Stat.HANDS_PLAYED -> stat.format(this.estimatedHands, currency = null) - Stat.HOURLY_RATE_BB -> stat.format(this.bbHourlyRate, currency = null) + Stat.NET_RESULT, Stat.AVERAGE -> stat.textFormat(this.ratedNet, currency = null) + Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> stat.textFormat(this.hourlyDuration, currency = null) + Stat.HOURLY_RATE -> stat.textFormat(this.hourlyRate, currency = null) + Stat.HANDS_PLAYED -> stat.textFormat(this.estimatedHands, currency = null) + Stat.HOURLY_RATE_BB -> stat.textFormat(this.bbHourlyRate, currency = null) Stat.NET_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> { val netBBPer100Hands = Stat.netBBPer100Hands(this.bbNet, this.estimatedHands) if (netBBPer100Hands != null) { - return stat.format(this.estimatedHands, currency = null) + return stat.textFormat(this.estimatedHands, currency = null) } else { return TextFormat(NULL_TEXT) } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt index 61bbe997..9d48fd6a 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt @@ -56,14 +56,18 @@ open class TournamentFeature : RealmObject(), NameManageable, StaticRowRepresent return rowRepresentation } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return when (row) { SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT - else -> return super.stringForRow(row) + else -> return super.charSequenceForRow(row, context, 0) } } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { return row.editingDescriptors(mapOf( "defaultValue" to this.name)) } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentName.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentName.kt index dadbfa2f..fb703d04 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentName.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentName.kt @@ -50,17 +50,21 @@ open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentabl } override fun adapterRows(): List? { - return TournamentName.rowRepresentation + return rowRepresentation } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return when (row) { SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT - else -> return super.stringForRow(row) + else -> return super.charSequenceForRow(row, context,0) } } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { return row.editingDescriptors(mapOf("defaultValue" to this.name)) } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt index 9e514fed..f33aad66 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt @@ -158,7 +158,7 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo } override fun formattedValue(stat: Stat): TextFormat { - return stat.format(this.amount, currency = this.bankroll?.utilCurrency) + return stat.textFormat(this.amount, currency = this.bankroll?.utilCurrency) } override fun legendValues( @@ -170,7 +170,7 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo ): LegendContent { val entryValue = this.formattedValue(stat) - val totalStatValue = stat.format(entry.y.toDouble(), currency = null) + val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null) val leftName = context.getString(R.string.amount) return DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue, leftName = leftName) } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt index 8a1c8f7c..7d1454ba 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt @@ -1,7 +1,6 @@ package net.pokeranalytics.android.model.realm import android.content.Context -import com.crashlytics.android.Crashlytics import io.realm.Realm import io.realm.RealmModel import io.realm.RealmObject @@ -119,10 +118,14 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab return rowRepresentation } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return when (row) { SimpleRow.NAME -> this.name - else -> return super.stringForRow(row) + else -> return super.charSequenceForRow(row, context, 0) } } @@ -133,7 +136,7 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab } } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { return row.editingDescriptors(mapOf("defaultValue" to this.name)) } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Action.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Action.kt new file mode 100644 index 00000000..16636257 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Action.kt @@ -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.compact(positions: LinkedHashSet, heroIndex: Int?): List { + val rows = mutableListOf() + 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, 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 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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt new file mode 100644 index 00000000..67a9bfdb --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt @@ -0,0 +1,248 @@ +package net.pokeranalytics.android.model.realm.handhistory + +import android.content.Context +import android.text.SpannableString +import android.text.TextUtils +import android.text.style.ForegroundColorSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.AppCompatTextView +import io.realm.RealmObject +import net.pokeranalytics.android.R +import net.pokeranalytics.android.exceptions.PAIllegalStateException +import net.pokeranalytics.android.model.handhistory.Street +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowViewType + +interface CardProperty + +/*** + * Returns a CharSequence containing each card representation + */ +fun List.formatted(context: Context) : CharSequence? { + var span: CharSequence? = null + this.forEach { + val formatted = it.formatted(context) + if (span == null) { + span = formatted + } + else { + span = TextUtils.concat(span, formatted) + } + } + return span +} + +open class Card : RealmObject() { + + companion object { + fun newInstance(value: Int? = null, suit: Suit? = null, index: Int = 0) : Card { + val card = Card() + value?.let { card.value = it } + suit?.let { card.suit = it} + card.index = index + return card + } + } + + /*** + * The Value of a Card + * Can represent all values from Deuce to Ace, or a blank + */ + class Value(var value: Int?) : CardProperty, RowRepresentable { + + companion object { + + val undefined: Value = Value(null) + + val values: List by lazy { + val v = mutableListOf(Value(null)) // null for x + (2..14).forEach { v.add(Value(it)) } + v + } + + fun format(value: Int?) : String { + return when(value) { + in 2..9 -> "$value" + 10 -> "T" + 11 -> "J" + 12 -> "Q" + 13 -> "K" + 14 -> "A" + else -> "X" + } + } + } + + override fun getDisplayName(context: Context): String { + return this.formatted + } + + val formatted: String + get() { return format(this.value) } + + override val viewType: Int = RowViewType.TITLE_GRID.ordinal + + } + + enum class Suit(val value: String) : CardProperty, RowRepresentable { + UNDEFINED("x"), + SPADES("♠︎"), + HEART("♥︎"), + DIAMOND("♦︎"), + CLOVER("♣︎"); + + companion object { + + val displaySuits: List by lazy { + listOf(SPADES, HEART, DIAMOND, CLOVER, UNDEFINED) + } + + fun format(suit: Suit?) : String { + return suit?.value ?: UNDEFINED.value + } + + fun color(suit: Suit?) : Int { + return suit?.color ?: R.color.white + } + + fun valueOf(suit: Int): Suit { + return when (suit) { + 0 -> SPADES + 1 -> DIAMOND + 2 -> CLOVER + 3 -> HEART + else -> throw PAIllegalStateException("unknow value: $suit") + } + } + } + + val color: Int + get() { + return when (this) { + UNDEFINED -> R.color.grey + SPADES -> R.color.black + HEART -> R.color.red + DIAMOND -> R.color.blue + CLOVER -> R.color.clover + } + } + + override fun getDisplayName(context: Context): String { + return this.value + } + + override val viewType: Int = RowViewType.TITLE_GRID.ordinal + + val evaluatorSuit: Int? + get() { + return when (this) { + SPADES -> net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card.SPADES + CLOVER -> net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card.CLUBS + DIAMOND -> net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card.DIAMONDS + HEART -> net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card.HEARTS + else -> null + } + } + } + + /*** + * The card value: 2..A + * can be undefined + */ + var value: Int? = null + + /*** + * Returns the formatted value of the card + */ + val formattedValue: String + get() { return Value.format(this.value) } + + /*** + * The card suit identifier: heart, spades... + */ + var suitIdentifier: Int? = null + + /*** + * Returns the suit of the card + */ + var suit: Suit? + get() { + return this.suitIdentifier?.let { Suit.values()[it] } + } + set(value) { + this.suitIdentifier = value?.ordinal + } + + /*** + * The card index in a list + */ + var index: Int = 0 + + /*** + * Returns the street where the card belongs based on its [index] + */ + val street: Street + get() { + return when (this.index) { + in 0..2 -> Street.FLOP + 3 -> Street.TURN + 4 -> Street.RIVER + else -> throw PAIllegalStateException("Card doesn't belong to any street") + } + } + + /*** + * Formats the Card into a Spannable String with the appropriate color for the card suit + * [lineBreak] adds "\n" between the value and the suit + */ + fun formatted(context: Context, lineBreak: Boolean = false) : SpannableString { + + val suit = Suit.format(this.suit) + val spannable = if (lineBreak) { + SpannableString(this.formattedValue + "\n" + suit) + } else { + SpannableString(this.formattedValue + suit) + } + + val suitStart = spannable.indexOf(suit) + spannable.setSpan( + ForegroundColorSpan(context.getColor(Suit.color(this.suit))), + suitStart, + spannable.length, + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE + ) +// Timber.d("card format = $spannable") + return spannable + } + + /*** + * Returns a view showing the card [value] and [suit] + */ + fun view(context: Context, layoutInflater: LayoutInflater, root: ViewGroup, blank: Boolean = false): View { + val textView = layoutInflater.inflate(R.layout.view_card, root, false) as AppCompatTextView + if (blank) { + textView.text = "\n" + } else { + textView.text = this.formatted(context, true) + } + return textView + } + + fun toEvaluatorCard():net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card? { + val rank = this.value + val suit = this.suit?.evaluatorSuit + return if (rank != null && suit != null) { + net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card(rank - 2, suit) + } else { + null + } + } + + val isWildCard: Boolean + get() { + return this.value == null || this.suit == null || this.suit == Suit.UNDEFINED + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt new file mode 100644 index 00000000..b8864066 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt @@ -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 { + + @PrimaryKey + override var id = UUID.randomUUID().toString() + + @Ignore + override val realmObjectClass: Class = 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 = RealmList() + + /*** + * The board + */ + var board: RealmList = RealmList() + + /*** + * The players actions + */ + var actions: RealmList = RealmList() + + /*** + * A list of players initial data: user, position, hand, stack + */ + var playerSetups: RealmList = 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 { + 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 + 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 + 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 { + val positions = Position.positionsPerPlayers(this.numberOfPlayers) + val copy = positions.clone() as LinkedHashSet + 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(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, 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 { + + val views = mutableListOf() + val layoutInflater = LayoutInflater.from(context) + + // Create all the possible cards list: hero, opponents, board + val cardSets = mutableListOf>() + + // 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 = 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): List { + + var runningPotAmount = 0.0 + val pots = mutableListOf() + 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() + + // 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() + 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): Collection { + + val wonPots = hashMapOf() + + 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): List { + + // 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() + results.forEachIndexed { index, i -> + if (i == best && i != Int.MAX_VALUE) { + winners.add(activePositions[index]) + } + } + winners + } ?: run { + listOf() // 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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/PlayerSetup.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/PlayerSetup.kt new file mode 100644 index 00000000..62b1bc16 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/PlayerSetup.kt @@ -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 = RealmList() + +} diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/WonPot.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/WonPot.kt new file mode 100644 index 00000000..f4494d59 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/WonPot.kt @@ -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 + +} diff --git a/app/src/main/java/net/pokeranalytics/android/model/utils/FavoriteSessionFinder.kt b/app/src/main/java/net/pokeranalytics/android/model/utils/FavoriteSessionFinder.kt index b33cae35..e0353f3d 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/utils/FavoriteSessionFinder.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/utils/FavoriteSessionFinder.kt @@ -15,7 +15,7 @@ private fun Session.parameterRepresentation(context: Context): String { var representation = "" this.significantFields().forEach { - representation += this.stringForRow(it, context) + representation += this.charSequenceForRow(it, context) } return representation diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/DataListActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/DataListActivity.kt deleted file mode 100644 index 65a18b4b..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/DataListActivity.kt +++ /dev/null @@ -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) - } - -} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/FilterDetailsActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/FilterDetailsActivity.kt deleted file mode 100644 index 4995c2ca..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/FilterDetailsActivity.kt +++ /dev/null @@ -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) - } - -} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/FiltersActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/FiltersActivity.kt deleted file mode 100644 index 23e9cbd0..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/FiltersActivity.kt +++ /dev/null @@ -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) - - } - -} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/FiltersListActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/FiltersListActivity.kt deleted file mode 100644 index 1dc842c7..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/FiltersListActivity.kt +++ /dev/null @@ -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) - } - -} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt index 47da2a92..4e28a4b9 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt @@ -53,6 +53,7 @@ class HomeActivity : BaseActivity() { override fun onResume() { super.onResume() AppGuard.requestPurchasesUpdate() + this.homePagerAdapter?.activityResumed() } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/BaseActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/BaseActivity.kt index 9407d1fa..32d01d4d 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/BaseActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/BaseActivity.kt @@ -9,6 +9,7 @@ import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment import com.crashlytics.android.Crashlytics import com.google.android.libraries.places.api.model.PlaceLikelihood import io.realm.Realm @@ -101,6 +102,12 @@ abstract class BaseActivity : AppCompatActivity() { return super.onOptionsItemSelected(item) } + fun showFragment(fragment: Fragment, containerId: Int) { + val fragmentTransaction = supportFragmentManager.beginTransaction() + fragmentTransaction.replace(containerId, fragment) + fragmentTransaction.commit() + } + /** * Return the realm instance */ diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt index 8ea9aa87..e91359dd 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt @@ -4,12 +4,15 @@ enum class RequestCode(var value: Int) { DEFAULT(1), FEED_MENU(100), FEED_TRANSACTION_DETAILS(101), + PLAYER_SELECTION(200), BANKROLL_DETAILS(700), BANKROLL_CREATE(701), BANKROLL_EDIT(702), - NEW_SESSION(800), - NEW_TRANSACTION(801), - NEW_REPORT(802), + NEW_DATA(800), + NEW_SESSION(801), + NEW_TRANSACTION(802), + NEW_REPORT(803), + NEW_HAND_HISTORY(804), IMPORT(900), SUBSCRIPTION(901), CURRENCY(902), diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt index 4408de4a..f5c5ba4b 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt @@ -4,7 +4,6 @@ import android.Manifest import android.app.Activity import android.content.Intent import android.content.pm.PackageManager -import android.graphics.Bitmap import android.provider.MediaStore import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat @@ -62,7 +61,7 @@ open class MediaActivity : BaseActivity() { } } } else if (data?.clipData != null) { - data?.clipData?.let { clipData -> + data.clipData?.let { clipData -> try { GlobalScope.launch(Dispatchers.Main) { @@ -87,7 +86,7 @@ open class MediaActivity : BaseActivity() { } } } else if (data?.data != null) { - data?.data?.let { uri -> + data.data?.let { uri -> try { GlobalScope.launch(Dispatchers.Main) { @@ -216,42 +215,42 @@ open class MediaActivity : BaseActivity() { } } - /** - * Ask for the acmera permission - */ - private fun askForCameraPermission() { - // Here, thisActivity is the current activity - if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), - PERMISSION_REQUEST_CAMERA) - } - } - - /** - * Ask for camera and storage permission - */ - private fun askForCameraAndStoragePermissions() { - - val permissions = ArrayList() - if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - permissions.add(Manifest.permission.CAMERA) - } - - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) - } - - if (permissions.size > 0) { - ActivityCompat.requestPermissions(this, permissions.toArray(arrayOfNulls(permissions.size)), PERMISSION_REQUEST_CAMERA) - } - } - - /** - * Called when a bitmap is return - * - * @param bitmap the bitmap returned - */ - open fun getBitmapImage(file: File?, bitmap: Bitmap?) {} +// /** +// * Ask for the acmera permission +// */ +// private fun askForCameraPermission() { +// // Here, thisActivity is the current activity +// if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { +// ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), +// PERMISSION_REQUEST_CAMERA) +// } +// } + +// /** +// * Ask for camera and storage permission +// */ +// private fun askForCameraAndStoragePermissions() { +// +// val permissions = ArrayList() +// if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { +// permissions.add(Manifest.permission.CAMERA) +// } +// +// if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { +// permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) +// } +// +// if (permissions.size > 0) { +// ActivityCompat.requestPermissions(this, permissions.toArray(arrayOfNulls(permissions.size)), PERMISSION_REQUEST_CAMERA) +// } +// } +// +// /** +// * Called when a bitmap is return +// * +// * @param bitmap the bitmap returned +// */ +// open fun getBitmapImage(file: File?, bitmap: Bitmap?) {} /** diff --git a/app/src/main/java/net/pokeranalytics/android/ui/adapter/ComparisonChartPagerAdapter.kt b/app/src/main/java/net/pokeranalytics/android/ui/adapter/ComparisonChartPagerAdapter.kt index 961d8554..ff9bc2a1 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/adapter/ComparisonChartPagerAdapter.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/adapter/ComparisonChartPagerAdapter.kt @@ -6,8 +6,8 @@ import android.view.ViewGroup import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter import net.pokeranalytics.android.R -import net.pokeranalytics.android.ui.fragment.CalendarFragment -import net.pokeranalytics.android.ui.fragment.FeedFragment +import net.pokeranalytics.android.ui.modules.calendar.CalendarFragment +import net.pokeranalytics.android.ui.modules.feed.FeedFragment import net.pokeranalytics.android.ui.fragment.GraphFragment import net.pokeranalytics.android.ui.fragment.components.BaseFragment import java.lang.ref.WeakReference @@ -15,9 +15,9 @@ import java.lang.ref.WeakReference /** * Comparison Chart Pager Adapter */ -class ComparisonChartPagerAdapter(val context: Context, fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager) { +class ComparisonChartPagerAdapter(val context: Context, fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - var weakReferences = SparseArray>() + private var weakReferences = SparseArray>() override fun getItem(position: Int): BaseFragment { return when (position) { @@ -59,11 +59,11 @@ class ComparisonChartPagerAdapter(val context: Context, fragmentManager: Fragmen /** * Return the fragment at the position key */ - fun getFragment(key: Int): BaseFragment? { - if (weakReferences.get(key) != null) { - return weakReferences.get(key).get() - } - return null - } +// fun getFragment(key: Int): BaseFragment? { +// if (weakReferences.get(key) != null) { +// return weakReferences.get(key).get() +// } +// return null +// } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/adapter/FilterSectionAdapter.kt b/app/src/main/java/net/pokeranalytics/android/ui/adapter/FilterSectionAdapter.kt new file mode 100644 index 00000000..f8ad8cb4 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/adapter/FilterSectionAdapter.kt @@ -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): RealmQuery { + 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): RealmQuery + +// val clazz: Class +} + +/** + * 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 +// var distinctTransactionsHeaders: RealmResults +) : + RecyclerView.Adapter(), RecyclerAdapter { + + private var headersPositions = HashMap() + private lateinit var sortedHeaders: SortedMap + + private var realmEntities: RealmResults + private var realmHeaders: RealmResults + + var filter: Filter? = null +// +// companion object { +// +// inline fun build(delegate: RowRepresentableDelegate?, dataSource: RowRepresentableDataSource, descriptor: EntityDescriptor, realmQuery: RealmQuery) : FilterSectionAdapter { +// 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(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) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/adapter/HomePagerAdapter.kt b/app/src/main/java/net/pokeranalytics/android/ui/adapter/HomePagerAdapter.kt index b4acc402..8017dedf 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/adapter/HomePagerAdapter.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/adapter/HomePagerAdapter.kt @@ -4,16 +4,21 @@ import android.util.SparseArray import android.view.ViewGroup import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter -import net.pokeranalytics.android.ui.fragment.* +import net.pokeranalytics.android.ui.modules.calendar.CalendarFragment +import net.pokeranalytics.android.ui.fragment.ReportsFragment +import net.pokeranalytics.android.ui.fragment.SettingsFragment +import net.pokeranalytics.android.ui.fragment.StatisticsFragment import net.pokeranalytics.android.ui.fragment.components.BaseFragment +import net.pokeranalytics.android.ui.modules.feed.FeedFragment import java.lang.ref.WeakReference /** * Home Adapter */ -class HomePagerAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager) { +class HomePagerAdapter(fragmentManager: FragmentManager) : + FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - var weakReferences = SparseArray>() + private var weakReferences = SparseArray>() override fun getItem(position: Int): BaseFragment { return when (position) { @@ -32,12 +37,12 @@ class HomePagerAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAda override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { super.destroyItem(container, position, `object`) - weakReferences.remove(position) + this.weakReferences.remove(position) } override fun instantiateItem(container: ViewGroup, position: Int): Any { val fragment = super.instantiateItem(container, position) as BaseFragment - weakReferences.put(position, WeakReference(fragment)) + this.weakReferences.put(position, WeakReference(fragment)) return fragment } @@ -52,14 +57,21 @@ class HomePagerAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAda } } - /** - * Return the fragment at the position key - */ - fun getFragment(key: Int): BaseFragment? { - if (weakReferences.get(key) != null) { - return weakReferences.get(key).get() + fun activityResumed() { + val ref = this.weakReferences.get(0) + ref?.get()?.let { + (it as FeedFragment).activityResumed() } - return null } +// /** +// * Return the fragment at the position key +// */ +// fun getFragment(key: Int): BaseFragment? { +// if (this.weakReferences.get(key) != null) { +// return this.weakReferences.get(key).get() +// } +// return null +// } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/adapter/RecyclerAdapter.kt b/app/src/main/java/net/pokeranalytics/android/ui/adapter/RecyclerAdapter.kt new file mode 100644 index 00000000..b4250788 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/adapter/RecyclerAdapter.kt @@ -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) { + } +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/adapter/ReportPagerAdapter.kt b/app/src/main/java/net/pokeranalytics/android/ui/adapter/ReportPagerAdapter.kt index c6c12033..38527de1 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/adapter/ReportPagerAdapter.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/adapter/ReportPagerAdapter.kt @@ -14,17 +14,13 @@ import net.pokeranalytics.android.ui.viewmodel.ReportHolder * Home Adapter */ class ReportPagerAdapter(private val context: Context, - val fragmentManager: FragmentManager, - private val reportHolder: ReportHolder) : FragmentPagerAdapter(fragmentManager) { + fragmentManager: FragmentManager, + private val reportHolder: ReportHolder) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { override fun getItem(position: Int): BaseFragment { return when (position) { - 0 -> { - GraphFragment.newInstance(style = GraphFragment.Style.BAR) - } - 1 -> { - GraphFragment.newInstance(style = GraphFragment.Style.MULTILINE) - } + 0 -> GraphFragment.newInstance(style = GraphFragment.Style.BAR) + 1 -> GraphFragment.newInstance(style = GraphFragment.Style.MULTILINE) 2 -> { val report = this.reportHolder.report ComposableTableReportFragment.newInstance(report) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableAdapter.kt b/app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableAdapter.kt index 00eb4d9c..e6913ee0 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableAdapter.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableAdapter.kt @@ -4,28 +4,19 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import net.pokeranalytics.android.ui.view.BindableHolder import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType -interface RowRepresentableDelegate { - fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean = false) {} - fun onRowValueChanged(value: Any?, row: RowRepresentable) {} - fun onRowDeleted(row: RowRepresentable) {} - fun onRowLongClick(itemView: View, row: RowRepresentable, position: Int) {} - -} - /** * An adapter capable of displaying a list of RowRepresentables * @param dataSource the datasource providing rows * @param delegate the delegate, notified of UI actions */ class RowRepresentableAdapter( - var dataSource: RowRepresentableDataSource, - var delegate: RowRepresentableDelegate? = null + override var dataSource: RowRepresentableDataSource, + override var delegate: RowRepresentableDelegate? = null ) : - RecyclerView.Adapter() { + RecyclerView.Adapter(), RecyclerAdapter { override fun getItemViewType(position: Int): Int { return this.dataSource.viewTypeForPosition(position) @@ -42,7 +33,7 @@ class RowRepresentableAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { this.dataSource.rowRepresentableForPosition(position)?.let { - (holder as BindableHolder).bind(position, it, this) + (holder as BindableHolder).onBind(position, it, this) } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableDataSource.kt b/app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableDataSource.kt index 51021a62..7d203a60 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableDataSource.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableDataSource.kt @@ -1,10 +1,12 @@ package net.pokeranalytics.android.ui.adapter import android.content.Context +import net.pokeranalytics.android.R import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.util.TextFormat +import kotlin.reflect.KClass /** * Base Interface to provide the RowRepresentable to the adapter @@ -92,11 +94,8 @@ class UnmanagedRowRepresentableException(message: String) : Exception(message) { /** * An interface used to provide RowRepresentableAdapter content and value in the form of rows * - * Also returns the appropriate value for any RowRepresentable in the form of : - * - string - * - booleans - * - actionIcon - * to display the appropriate numericValues in graphical filterConditions, such as labels, textfields, switchs... + * Returns values for any RowRepresentable in various forms to display + * the appropriate numericValues in graphical filterConditions, such as labels, textfields, switchs... */ interface DisplayableDataSource { @@ -118,17 +117,17 @@ interface DisplayableDataSource { return -1 } - /** - * Returns a localized string for a specific row + /*** + * Returns a CharSequence representation of the [row] */ - fun stringForRow(row: RowRepresentable, context: Context): String { - return stringForRow(row) + fun charSequenceForRow(row: RowRepresentable, context: Context): CharSequence { + return charSequenceForRow(row, context, 0) } - /** - * Returns a string for a specific row + /*** + * Returns a CharSequence representation of the [row] and [tag] */ - fun stringForRow(row: RowRepresentable): String { + fun charSequenceForRow(row: RowRepresentable, context: Context, tag: Int = 0): CharSequence { return "" } @@ -146,10 +145,17 @@ interface DisplayableDataSource { return 0 } + /*** + * Returns an arrowIcon + */ + fun arrowIcon(position: Int, row: RowRepresentable): Int? { + return R.drawable.ic_arrow_right + } + /** - * Returns whether the row, or its component, shoudl be enabled + * Returns whether the row, or its component, should be enabled */ - fun isEnabled(row: RowRepresentable): Boolean { + fun isEnabled(row: RowRepresentable, tag: Int): Boolean { return true } @@ -159,18 +165,43 @@ interface DisplayableDataSource { fun isSelectable(row: RowRepresentable): Boolean { return true } + + /*** + * Returns a list of content for the [row], using a specific [clazz] + */ + fun contentForRow(row: RowRepresentable, context: Context, clazz: KClass) : List { + return listOf() + } + + /*** + * Returns if the selection of the [row] + [tag] is focusable + */ + fun isFocusable(position: Int, row: RowRepresentable, tag: Int): Boolean { + return true + } + + fun backgroundColor(position: Int, row: RowRepresentable): Int? { + return null + } + + fun textColor(position: Int, row: RowRepresentable): Int? { + return null + } + } /** * An interface providing a way to describe how the edition of a [RowRepresentable] will be displayed */ interface EditableDataSource { + /** * A list of [RowRepresentableEditDescriptor] object specifying the way the edition will be handled */ - fun editDescriptors(row: RowRepresentable): ArrayList? { + fun editDescriptors(row: RowRepresentable): List? { return null } + } @@ -181,7 +212,7 @@ interface SelectableDataSource { /** * A boolean to indicate if the row is selected or not */ - fun isSelected(row: RowRepresentable): Boolean { + fun isSelected(position: Int, row: RowRepresentable, tag: Int): Boolean { return false } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt b/app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt index d59709fd..823f5bbb 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt @@ -5,17 +5,22 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color import android.net.Uri import android.util.TypedValue import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.ImageView import android.widget.LinearLayout import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.SearchView -import androidx.browser.customtabs.CustomTabsIntent import androidx.core.content.ContextCompat import androidx.core.content.FileProvider +import androidx.core.graphics.drawable.toBitmap import androidx.core.view.isVisible import androidx.fragment.app.Fragment import net.pokeranalytics.android.BuildConfig @@ -25,6 +30,7 @@ import net.pokeranalytics.android.util.DeviceUtils import net.pokeranalytics.android.util.TextFormat import net.pokeranalytics.android.util.URL import net.pokeranalytics.android.util.billing.AppGuard +import java.io.ByteArrayOutputStream import java.io.File @@ -96,10 +102,8 @@ fun BaseActivity.openContactMail(subjectStringRes: Int, filePath: String? = null // Open custom tab fun Context.openUrl(url: String) { - val builder: CustomTabsIntent.Builder = CustomTabsIntent.Builder() - builder.setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary)) - val customTabsIntent = builder.build() - customTabsIntent.launchUrl(this, Uri.parse(url)) + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + ContextCompat.startActivity(this, browserIntent, null) } // Display Alert Dialog @@ -171,4 +175,41 @@ fun SearchView.removeMargins() { layoutParams?.leftMargin = 0 layoutParams?.rightMargin = 0 searchEditFrame?.layoutParams = layoutParams -} \ No newline at end of file +} + +fun View.toByteArray() : ByteArray { + return this.convertToBitmap().toByteArray() +} + +fun View.convertToBitmap(): Bitmap { + val b = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(b) + val background = this.background + this.background?.let { + background.draw(canvas) + } ?: run { + canvas.drawColor(Color.WHITE) + } + this.draw(canvas) + return b +} + +fun ImageView.toByteArray() : ByteArray { + return this.drawable.toBitmap().toByteArray() +} + +fun Bitmap.toByteArray() : ByteArray { + val baos = ByteArrayOutputStream() + this.compress(Bitmap.CompressFormat.PNG, 100, baos) + return baos.toByteArray() +} + +fun Context.hideKeyboard(v: View) { + val imm = v.context.getSystemService(InputMethodManager::class.java) + imm?.hideSoftInputFromWindow(v.windowToken, 0) +} + +//fun Context.showKeyboard(view: View) { +// val imm = getSystemService(InputMethodManager::class.java) +// imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) +//} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ComparisonChartFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ComparisonChartFragment.kt index 9c78ac5d..8c69ee6f 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ComparisonChartFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ComparisonChartFragment.kt @@ -4,7 +4,7 @@ import android.os.Bundle import android.view.* import kotlinx.android.synthetic.main.fragment_comparison_chart.* import net.pokeranalytics.android.R -import net.pokeranalytics.android.ui.activity.BankrollActivity +import net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity import net.pokeranalytics.android.ui.activity.SettingsActivity import net.pokeranalytics.android.ui.adapter.ComparisonChartPagerAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate @@ -52,15 +52,15 @@ class ComparisonChartFragment : BaseFragment(), StaticRowRepresentableDataSource initUI() } - override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { - menu?.clear() - inflater?.inflate(R.menu.toolbar_comparison_chart, menu) + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + menu.clear() + inflater.inflate(R.menu.toolbar_comparison_chart, menu) this.comparisonChartMenu = menu super.onCreateOptionsMenu(menu, inflater) } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item!!.itemId) { + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { R.id.settings -> openChangeStatistics() } return true @@ -71,8 +71,8 @@ class ComparisonChartFragment : BaseFragment(), StaticRowRepresentableDataSource return rowRepresentation } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - super.onRowSelected(position, row, fromAction) + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { + super.onRowSelected(position, row, tag) when(row) { MoreTabRow.BANKROLL -> BankrollActivity.newInstance(requireContext()) MoreTabRow.SETTINGS -> SettingsActivity.newInstance(requireContext()) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt index 7c573980..a0f6d88e 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt @@ -90,12 +90,16 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return (row as CurrencyRow).currencyCodeAndSymbol } // RowRepresentableDelegate - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { val intent = Intent() intent.putExtra(INTENT_CURRENCY_CODE, (row as CurrencyRow).currency.currencyCode) this.activity?.setResult(Activity.RESULT_OK, intent) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/DataListFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/DataListFragment.kt deleted file mode 100644 index 722b9dac..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/DataListFragment.kt +++ /dev/null @@ -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 - - private lateinit var dataType: LiveData - private lateinit var items: RealmResults - 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 { - return realm.sorted(this.identifiableClass, editableOnly = true, filterableTypeUniqueIdentifier = dataType.subType) - } - - override fun deletableItems() : List { - 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() - } - -} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FiltersListFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/FiltersListFragment.kt deleted file mode 100644 index 8c4abc81..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FiltersListFragment.kt +++ /dev/null @@ -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 = Filter::class.java - private var dataType: LiveData = LiveData.FILTER - private lateinit var items: RealmResults - - /** - * 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 - } - - 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? { - 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? { - 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) - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/MoreFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/MoreFragment.kt index 2075c201..868de79f 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/MoreFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/MoreFragment.kt @@ -7,7 +7,7 @@ import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.fragment_more.* import net.pokeranalytics.android.R -import net.pokeranalytics.android.ui.activity.BankrollActivity +import net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity import net.pokeranalytics.android.ui.activity.SettingsActivity import net.pokeranalytics.android.ui.activity.Top10Activity import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter @@ -59,8 +59,8 @@ class MoreFragment : BaseFragment(), StaticRowRepresentableDataSource, RowRepres return rowRepresentation } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - super.onRowSelected(position, row, fromAction) + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { + super.onRowSelected(position, row, tag) when(row) { MoreTabRow.BANKROLL -> BankrollActivity.newInstance(requireContext()) MoreTabRow.TOP_10 -> Top10Activity.newInstance(requireContext()) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportCreationFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportCreationFragment.kt index 0584020b..7bc5c016 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportCreationFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportCreationFragment.kt @@ -14,14 +14,14 @@ import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.CustomFieldCriteria import net.pokeranalytics.android.model.realm.CustomField import net.pokeranalytics.android.model.realm.Filter -import net.pokeranalytics.android.ui.activity.FiltersActivity +import net.pokeranalytics.android.ui.modules.filter.FiltersActivity import net.pokeranalytics.android.ui.activity.ReportCreationActivity import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.fragment.components.RealmFragment -import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode -import net.pokeranalytics.android.ui.interfaces.FilterableType +import net.pokeranalytics.android.ui.modules.filter.FilterActivityRequestCode +import net.pokeranalytics.android.ui.modules.filter.FilterableType import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable @@ -60,16 +60,16 @@ class ReportCreationFragment : RealmFragment(), RowRepresentableDataSource, RowR } } - override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) - menu?.clear() - inflater?.inflate(R.menu.toolbar_report_creation, menu) - menu?.findItem(R.id.add)?.isVisible = false + menu.clear() + inflater.inflate(R.menu.toolbar_report_creation, menu) + menu.findItem(R.id.add)?.isVisible = false reportCreationMenu = menu } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { R.id.add -> { if (this.assistant.step == Assistant.Step.FILTER) { FiltersActivity.newInstanceForResult( @@ -182,13 +182,17 @@ class ReportCreationFragment : RealmFragment(), RowRepresentableDataSource, RowR } } - override fun isSelected(row: RowRepresentable): Boolean { + override fun isSelected( + position: Int, + row: RowRepresentable, + tag: Int + ): Boolean { return this.assistant.isSelected(row) } // RowRepresentableDelegate - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { val newStep = this.assistant.onRowSelected(position - 1) if (newStep) { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt index a13ddcc7..122f6234 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt @@ -22,7 +22,7 @@ import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.combined import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.realm.ReportSetup -import net.pokeranalytics.android.ui.activity.DataListActivity +import net.pokeranalytics.android.ui.modules.datalist.DataListActivity import net.pokeranalytics.android.ui.activity.ReportCreationActivity import net.pokeranalytics.android.ui.activity.components.ReportActivity import net.pokeranalytics.android.ui.activity.components.RequestCode @@ -143,8 +143,8 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc return this.adapterRows } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - super.onRowSelected(position, row, fromAction) + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { + super.onRowSelected(position, row, tag) when (row) { is ReportRow -> { @@ -152,7 +152,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc launchComputation(row.criteria, reportName) } is ReportSetup -> { - launchReportWithOptions(row.options, row.name) + launchReportWithOptions(row.options(getRealm()), row.name) } } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt index 69efbde1..40fe71f2 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt @@ -2,6 +2,7 @@ package net.pokeranalytics.android.ui.fragment import android.app.Activity import android.content.ActivityNotFoundException +import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle @@ -28,12 +29,11 @@ import net.pokeranalytics.android.ui.extensions.openContactMail import net.pokeranalytics.android.ui.extensions.openPlayStorePage import net.pokeranalytics.android.ui.extensions.openUrl import net.pokeranalytics.android.ui.fragment.components.RealmFragment +import net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity +import net.pokeranalytics.android.ui.modules.datalist.DataListActivity import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.rowrepresentable.SettingRow -import net.pokeranalytics.android.util.FileUtils -import net.pokeranalytics.android.util.Preferences -import net.pokeranalytics.android.util.URL -import net.pokeranalytics.android.util.UserDefaults +import net.pokeranalytics.android.util.* import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.IAPProducts import net.pokeranalytics.android.util.csv.ProductCSVDescriptors @@ -112,7 +112,11 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep return rowRepresentation } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return when (row) { SettingRow.SUBSCRIPTION -> AppGuard.subscriptionStatus(requireContext()) SettingRow.VERSION -> BuildConfig.VERSION_NAME + if (BuildConfig.DEBUG) " (${BuildConfig.VERSION_CODE}) DEBUG" else "" @@ -121,7 +125,14 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep } } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun boolForRow(row: RowRepresentable): Boolean { + return when (row) { + SettingRow.STOP_NOTIFICATION -> Preferences.showStopNotifications(requireContext()) + else -> false + } + } + + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { when (row) { SettingRow.BANKROLL_REPORT -> BankrollActivity.newInstance(requireContext()) SettingRow.TOP_10 -> Top10Activity.newInstance(requireContext()) @@ -159,6 +170,18 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep } } + override fun onRowValueChanged(value: Any?, row: RowRepresentable) { + when (row) { + SettingRow.STOP_NOTIFICATION -> { + val show = value as Boolean + if (!show) { + StopNotificationManager.stopAllStopNotifications(requireContext()) + } + Preferences.setShowStopNotifications(show, requireContext()) + } + else -> {} + } + } /** * Init UI diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt index 5f5c3b69..e9682c7e 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt @@ -22,11 +22,11 @@ import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.realm.ComputableResult import net.pokeranalytics.android.model.realm.Filter -import net.pokeranalytics.android.ui.activity.FiltersActivity +import net.pokeranalytics.android.ui.modules.filter.FiltersActivity import net.pokeranalytics.android.ui.fragment.components.FilterableFragment import net.pokeranalytics.android.ui.fragment.report.ComposableTableReportFragment -import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode -import net.pokeranalytics.android.ui.interfaces.FilterableType +import net.pokeranalytics.android.ui.modules.filter.FilterActivityRequestCode +import net.pokeranalytics.android.ui.modules.filter.FilterableType import timber.log.Timber import java.util.* import kotlin.coroutines.CoroutineContext diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt index 381888c4..566004d2 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt @@ -102,14 +102,17 @@ class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, Purchas val ssb = SpannableStringBuilder(upgradeString) val indexOfLastSpace = upgradeString.lastIndexOf(" ") - val end = upgradeString.chars().count().toInt() - val lightTypeFace = ResourcesCompat.getFont(requireContext(), R.font.roboto_light) - val boldTypeFace = ResourcesCompat.getFont(requireContext(), R.font.roboto_bold) + if (indexOfLastSpace >= 0) { - if (lightTypeFace != null && boldTypeFace != null) { - ssb.setSpan(TypefaceSpan(lightTypeFace), 0, indexOfLastSpace, Spanned.SPAN_EXCLUSIVE_INCLUSIVE) - ssb.setSpan(TypefaceSpan(boldTypeFace), indexOfLastSpace, end, Spanned.SPAN_EXCLUSIVE_INCLUSIVE) + val end = upgradeString.chars().count().toInt() + val lightTypeFace = ResourcesCompat.getFont(requireContext(), R.font.roboto_light) + val boldTypeFace = ResourcesCompat.getFont(requireContext(), R.font.roboto_bold) + + if (lightTypeFace != null && boldTypeFace != null) { + ssb.setSpan(TypefaceSpan(lightTypeFace), 0, indexOfLastSpace, Spanned.SPAN_EXCLUSIVE_INCLUSIVE) + ssb.setSpan(TypefaceSpan(boldTypeFace), indexOfLastSpace, end, Spanned.SPAN_EXCLUSIVE_INCLUSIVE) + } } this.title.text = ssb @@ -156,7 +159,7 @@ class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, Purchas * A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in * sequence. */ - private inner class ScreenSlidePagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { + private inner class ScreenSlidePagerAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { private var fragments: HashMap> = HashMap() diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt index bc57f0d1..c9479d89 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt @@ -17,8 +17,10 @@ abstract class BaseFragment : Fragment() { enum class BundleKey(val value: String) { STYLE("style"), PRIMARY_KEY("primary_key"), + SECONDARY_KEY("secondary_key"), DATA_TYPE("data_type"), - SHOW_MESSAGE("show_message") + SHOW_MESSAGE("show_message"), + ATTACHED("attached") } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/DeletableItemFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/DeletableItemFragment.kt index 9136dac1..a527b94c 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/DeletableItemFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/DeletableItemFragment.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.launch import net.pokeranalytics.android.R import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.interfaces.Deletable -import net.pokeranalytics.android.ui.activity.DataListActivity +import net.pokeranalytics.android.ui.modules.datalist.DataListActivity import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter /** diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/FilterableFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/FilterableFragment.kt index 2ef8ee2e..fad2f7b5 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/FilterableFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/FilterableFragment.kt @@ -14,9 +14,9 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import net.pokeranalytics.android.R import net.pokeranalytics.android.model.realm.Filter -import net.pokeranalytics.android.ui.interfaces.FilterHandler -import net.pokeranalytics.android.ui.interfaces.FilterHandler.Companion.INTENT_FILTER_UPDATE_FILTER_UI -import net.pokeranalytics.android.ui.interfaces.FilterableType +import net.pokeranalytics.android.ui.modules.filter.FilterHandler +import net.pokeranalytics.android.ui.modules.filter.FilterHandler.Companion.INTENT_FILTER_UPDATE_FILTER_UI +import net.pokeranalytics.android.ui.modules.filter.FilterableType import net.pokeranalytics.android.util.Preferences @@ -26,7 +26,8 @@ import net.pokeranalytics.android.util.Preferences * - Listen for INTENT_FILTER_UPDATE_FILTER_UI * - */ -open class FilterableFragment : RealmFragment(), FilterHandler { +open class FilterableFragment : RealmFragment(), + FilterHandler { override var currentFilterable: FilterableType = FilterableType.ALL set(value) { @@ -69,7 +70,7 @@ open class FilterableFragment : RealmFragment(), FilterHandler { return super.onCreateView(inflater, container, savedInstanceState) } - override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) view?.findViewById(R.id.toolbar)?.let { toolbar -> toolbar.menu.removeItem(R.id.menu_item_filter) @@ -79,8 +80,8 @@ open class FilterableFragment : RealmFragment(), FilterHandler { } } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { R.id.menu_item_filter -> { manageFilters(this) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/LoaderDialogFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/LoaderDialogFragment.kt index 82de63d6..9f64cda2 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/LoaderDialogFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/LoaderDialogFragment.kt @@ -46,8 +46,7 @@ class LoaderDialogFragment: DialogFragment() { override fun onStart() { super.onStart() - val window = dialog.window - window?.setBackgroundDrawableResource(android.R.color.transparent) + dialog?.window?.setBackgroundDrawableResource(android.R.color.transparent) } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetDoubleEditTextFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetDoubleEditTextFragment.kt index cc039a2a..6d9ebb0e 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetDoubleEditTextFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetDoubleEditTextFragment.kt @@ -56,9 +56,9 @@ class BottomSheetDoubleEditTextFragment : BottomSheetFragment() { // values[0] = (data[0].defaultValue ?: "").toString() // values[1] = (data[1].defaultValue ?: "").toString() - data[0].hint?.let { editText.hint = getString(it) } + data[0].hintResId?.let { editText.hint = getString(it) } editText.inputType = data[0].inputType ?: InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES - data[1].hint?.let { editText2.hint = getString(it) } + data[1].hintResId?.let { editText2.hint = getString(it) } editText2.inputType = data[1].inputType ?: InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES if (this.viewModel.valueAsPlaceholder) { @@ -96,10 +96,9 @@ class BottomSheetDoubleEditTextFragment : BottomSheetFragment() { } } - editText2.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { - this.viewModel.onRowValueChanged() + this.onRowValueChanged() // this.delegate.onRowValueChanged(values, row) dismiss() true diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetEditTextFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetEditTextFragment.kt index 7e1312bf..3cbe9d61 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetEditTextFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetEditTextFragment.kt @@ -36,7 +36,7 @@ class BottomSheetEditTextFragment : BottomSheetFragment() { LayoutInflater.from(requireContext()).inflate(R.layout.bottom_sheet_edit_text, view?.bottomSheetContainer, true) - data[0].hint?.let { editText.hint = getString(it) } + data[0].hintResId?.let { editText.hint = getString(it) } editText.inputType = data[0].inputType ?: InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES editText.addTextChangedListener { this.viewModel.stringValue = it?.toString() @@ -52,7 +52,7 @@ class BottomSheetEditTextFragment : BottomSheetFragment() { editText.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { - this.viewModel.onRowValueChanged() + this.onRowValueChanged() // delegate.onRowValueChanged(getValue(), row) dismiss() true diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetEditTextMultiLinesFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetEditTextMultiLinesFragment.kt index b3410d24..8d82e57f 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetEditTextMultiLinesFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetEditTextMultiLinesFragment.kt @@ -26,15 +26,15 @@ class BottomSheetEditTextMultiLinesFragment : BottomSheetFragment() { * Init UI */ private fun initUI() { - val data = getDescriptors()?:throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor not found") + val data = getDescriptors() ?: throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor not found") if (data.size != 1) { throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor inconsistency") } LayoutInflater.from(requireContext()).inflate(net.pokeranalytics.android.R.layout.bottom_sheet_edit_text_multi_lines, view?.bottomSheetContainer, true) - data[0].hint?.let { editText.hint = getString(it) } - editText.inputType = data[0].inputType ?: InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + data[0].hintResId?.let { editText.hint = getString(it) } + editText.inputType = data[0].inputType ?: InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES editText.addTextChangedListener { this.viewModel.stringValue = it?.toString() } data[0].defaultValue?.let { editText.setText(it.toString()) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetFragment.kt index 5eaf2b6f..a241eafc 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetFragment.kt @@ -17,8 +17,9 @@ import kotlinx.android.synthetic.main.fragment_bottom_sheet.* import net.pokeranalytics.android.R import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.LiveData -import net.pokeranalytics.android.ui.activity.EditableDataActivity +import net.pokeranalytics.android.ui.modules.data.EditableDataActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity +import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor @@ -29,13 +30,13 @@ import java.util.* class BottomSheetConfig(var row: RowRepresentable, var delegate: RowRepresentableDelegate, - var rowRepresentableEditDescriptors: ArrayList?, + var rowRepresentableEditDescriptors: List?, var isClearable: Boolean? = true, var currentCurrency: Currency? = null, var isDeletable: Boolean? = false, - var valueHasPlaceholder: Boolean? = null) { -} - + var valueHasPlaceholder: Boolean? = null, + var alternativeLabels: Boolean = false +) open class BottomSheetFragment : BottomSheetDialogFragment() { @@ -46,6 +47,10 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { ViewModelProviders.of(this).get(BottomSheetViewModel::class.java) } + private var delegate: RowRepresentableDelegate? = null + + protected lateinit var dataAdapter: RowRepresentableAdapter + companion object { private var config: BottomSheetConfig? = null @@ -53,18 +58,19 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { const val REQUEST_CODE_ADD_NEW_OBJECT = 100 fun create( - fragmentManager: FragmentManager?, + fragmentManager: FragmentManager, row: RowRepresentable, delegate: RowRepresentableDelegate, - rowRepresentableEditDescriptors: ArrayList?, + rowRepresentableEditDescriptors: List?, isClearable: Boolean? = true, currentCurrency: Currency? = null, isDeletable: Boolean? = false, - valueHasPlaceholder: Boolean? = null + valueHasPlaceholder: Boolean? = null, + alternativeLabels: Boolean = false ): BottomSheetFragment { val bottomSheetFragment = newInstance(row.bottomSheetType) bottomSheetFragment.show(fragmentManager, "bottomSheet") - this.config = BottomSheetConfig(row, delegate, rowRepresentableEditDescriptors, isClearable, currentCurrency, isDeletable, valueHasPlaceholder) + this.config = BottomSheetConfig(row, delegate, rowRepresentableEditDescriptors, isClearable, currentCurrency, isDeletable, valueHasPlaceholder, alternativeLabels) return bottomSheetFragment } @@ -108,12 +114,14 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { private fun configure(configuration: BottomSheetConfig) { this.viewModel.row = configuration.row - this.viewModel.delegate = configuration.delegate this.viewModel.rowRepresentableEditDescriptors = configuration.rowRepresentableEditDescriptors this.viewModel.isClearable = configuration.isClearable ?: true this.viewModel.currentCurrency = configuration.currentCurrency this.viewModel.isDeletable = configuration.isDeletable ?: false this.viewModel.valueAsPlaceholder = configuration.valueHasPlaceholder ?: false + this.viewModel.alternativeLabels = configuration.alternativeLabels + + this.delegate = configuration.delegate } @@ -128,16 +136,16 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { val pokerAnalyticsActivity = activity as BaseActivity val liveDataType = LiveData.values()[dataType] this.viewModel.addedData = liveDataType.getData(pokerAnalyticsActivity.getRealm(), primaryKey) - this.viewModel.onRowValueChanged() + this.onRowValueChanged() // this.delegate.onRowValueChanged(proxyItem, this.row) dismiss() } } @SuppressLint("RestrictedApi") - override fun setupDialog(dialog: Dialog?, style: Int) { + override fun setupDialog(dialog: Dialog, style: Int) { super.setupDialog(dialog, style) - dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) } /** @@ -157,14 +165,14 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { // Menu bottomSheetToolbar.menu.findItem(R.id.actionClear).setOnMenuItemClickListener { - this.viewModel.onClear() + this.onClear() // delegate.onRowValueChanged(null, row) dismiss() true } bottomSheetToolbar.menu.findItem(R.id.actionDelete).setOnMenuItemClickListener { - this.viewModel.onRowDeleted() + this.onRowDeleted() // delegate.onRowDeleted(row) dismiss() true @@ -190,7 +198,7 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { true } bottomSheetToolbar.menu.findItem(R.id.actionCheck).setOnMenuItemClickListener { - this.viewModel.onRowValueChanged() + this.onRowValueChanged() // this.delegate.onRowValueChanged(getValue(), row) dismiss() true @@ -204,7 +212,7 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { /** * Return the data list */ - fun getDescriptors(): ArrayList? { + fun getDescriptors(): List? { return this.viewModel.rowRepresentableEditDescriptors } @@ -212,4 +220,44 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { return this.viewModel.getValue() } + private fun onClear() { + this.delegate?.onRowValueChanged(null, this.viewModel.row) + } + + fun onRowValueChanged() { + + val row = this.viewModel.row + + // if some data has been added + this.viewModel.addedData?.let { + this.delegate?.onRowValueChanged(it, row) + return + } + + val value = this.viewModel.changedValue() + this.delegate?.onRowValueChanged(value, row) + } + + private fun onRowDeleted() { + this.delegate?.onRowDeleted(this.viewModel.row) + } + + fun onRowSelected(row: RowRepresentable) { + this.viewModel.rowSelected(row) + this.refreshRow(row) + } + + fun onRowSelected(position: Int) { + val value = this.viewModel.rowSelected(position) + this.delegate?.onRowValueChanged(value, this.viewModel.row) + } + + fun notifyDataSetChanged() { + this.dataAdapter.notifyDataSetChanged() + } + + fun refreshRow(row: RowRepresentable) { + this.dataAdapter.refreshRow(row) + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListFragment.kt index 2526d20e..6ffbeaf3 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListFragment.kt @@ -28,7 +28,7 @@ open class BottomSheetListFragment : BottomSheetFragment(), LiveRowRepresentable override fun onResume() { super.onResume() - this.viewModel.notifyDataSetChanged() + this.notifyDataSetChanged() } override fun rowRepresentableForPosition(position: Int): RowRepresentable? { @@ -49,9 +49,9 @@ open class BottomSheetListFragment : BottomSheetFragment(), LiveRowRepresentable return RowViewType.BOTTOM_SHEET_DATA.ordinal } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { - this.viewModel.onRowSelected(position) + this.onRowSelected(position) dismiss() // this.viewModel.realmData?.let { // val selectedData = it[position] @@ -61,7 +61,7 @@ open class BottomSheetListFragment : BottomSheetFragment(), LiveRowRepresentable // dismiss() // } // } - super.onRowSelected(position, row, fromAction) +// super.onRowSelected(position, row, tag) } /** @@ -86,9 +86,9 @@ open class BottomSheetListFragment : BottomSheetFragment(), LiveRowRepresentable val viewManager = LinearLayoutManager(requireContext()) val dataAdapter = RowRepresentableAdapter(this, this) - this.viewModel.dataAdapter = dataAdapter + this.dataAdapter = dataAdapter - reyclerView.apply { + recyclerView.apply { setHasFixedSize(true) layoutManager = viewManager adapter = dataAdapter diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListGameFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListGameFragment.kt index 4b025bbe..42b69915 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListGameFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListGameFragment.kt @@ -25,12 +25,12 @@ class BottomSheetListGameFragment : BottomSheetListFragment() { initUI() } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { this.viewModel.realmData?.let { val selectedData = it[position] selectedData?.let { data -> this.viewModel.someValues[1] = data - this.viewModel.onRowValueChanged() + this.onRowValueChanged() // this.delegate.onRowValueChanged(values, this.row) dismiss() } @@ -69,7 +69,7 @@ class BottomSheetListGameFragment : BottomSheetListFragment() { val viewManager2 = LinearLayoutManager(requireContext()) val dataAdapter = RowRepresentableAdapter(this, this) - this.viewModel.dataAdapter = dataAdapter + this.dataAdapter = dataAdapter recyclerView2.apply { setHasFixedSize(true) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetMultiSelectionFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetMultiSelectionFragment.kt index 06574fba..49d6b4d7 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetMultiSelectionFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetMultiSelectionFragment.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Intent import io.realm.RealmModel import net.pokeranalytics.android.model.LiveData -import net.pokeranalytics.android.ui.activity.EditableDataActivity +import net.pokeranalytics.android.ui.modules.data.EditableDataActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType @@ -26,16 +26,20 @@ open class BottomSheetMultiSelectionFragment : BottomSheetListFragment() { val liveDataType = LiveData.values()[dataType] val proxyItem: RealmModel? = liveDataType.getData(pokerAnalyticsActivity.getRealm(), primaryKey) this.viewModel.selectedRows.add(proxyItem as RowRepresentable) - this.viewModel.refreshRow(proxyItem as RowRepresentable) + this.refreshRow(proxyItem as RowRepresentable) // dataAdapter.refreshRow(proxyItem as RowRepresentable) } } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - this.viewModel.onRowSelected(row) + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { + this.onRowSelected(row) } - override fun isSelected(row: RowRepresentable): Boolean { + override fun isSelected( + position: Int, + row: RowRepresentable, + tag: Int + ): Boolean { return this.viewModel.isSelected(row) // return selectedRows.contains(row) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetNumericTextFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetNumericTextFragment.kt index e61e1552..86b50ff6 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetNumericTextFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetNumericTextFragment.kt @@ -36,7 +36,7 @@ class BottomSheetNumericTextFragment : BottomSheetFragment() { LayoutInflater.from(requireContext()).inflate(R.layout.bottom_sheet_edit_text, view?.bottomSheetContainer, true) - data[0].hint?.let { editText.hint = getString(it) } + data[0].hintResId?.let { editText.hint = getString(it) } editText.inputType = data[0].inputType ?: InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES editText.addTextChangedListener { @@ -68,7 +68,7 @@ class BottomSheetNumericTextFragment : BottomSheetFragment() { editText.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { - this.viewModel.onRowValueChanged() + this.onRowValueChanged() // delegate.onRowValueChanged(getValue(), row) dismiss() true diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStaticListFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStaticListFragment.kt index f85dc2c2..7df5b425 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStaticListFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStaticListFragment.kt @@ -25,7 +25,7 @@ class BottomSheetStaticListFragment : BottomSheetFragment(), StaticRowRepresenta override fun onResume() { super.onResume() - this.viewModel.notifyDataSetChanged() + this.notifyDataSetChanged() // dataAdapter.notifyDataSetChanged() } @@ -33,12 +33,12 @@ class BottomSheetStaticListFragment : BottomSheetFragment(), StaticRowRepresenta return this.viewModel.staticRows } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { this.viewModel.selectedRows.add(row) - this.viewModel.onRowValueChanged() + this.onRowValueChanged() // this.delegate.onRowValueChanged(row, this.row) dismiss() - super.onRowSelected(position, row, fromAction) +// super.onRowSelected(position, row, tag) } /** @@ -49,9 +49,9 @@ class BottomSheetStaticListFragment : BottomSheetFragment(), StaticRowRepresenta val viewManager = LinearLayoutManager(requireContext()) val dataAdapter = RowRepresentableAdapter(this, this) - this.viewModel.dataAdapter = dataAdapter + this.dataAdapter = dataAdapter - reyclerView.apply { + recyclerView.apply { setHasFixedSize(true) layoutManager = viewManager adapter = dataAdapter diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetSumFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetSumFragment.kt index 66d2423e..fd904c40 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetSumFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetSumFragment.kt @@ -65,7 +65,7 @@ class BottomSheetSumFragment : BottomSheetFragment() { button1.setOnClickListener { // val newValue = this.viewModel.currentDefaultValue + defaultValue1 this.viewModel.doubleValue = this.viewModel.currentDefaultValue + defaultValue1 - this.viewModel.onRowValueChanged() + this.onRowValueChanged() // this.delegate.onRowValueChanged(currentDefaultValue + defaultValue1, row) dismiss() } @@ -82,13 +82,13 @@ class BottomSheetSumFragment : BottomSheetFragment() { button2.visibility = if (defaultValue2 > 0) View.VISIBLE else View.GONE button2.setOnClickListener { this.viewModel.doubleValue = this.viewModel.currentDefaultValue + defaultValue2 - this.viewModel.onRowValueChanged() + this.onRowValueChanged() // this.delegate.onRowValueChanged(currentDefaultValue + defaultValue2, row) dismiss() } // First edit text - data[3].hint?.let { editText.hint = getString(it) } + data[3].hintResId?.let { editText.hint = getString(it) } editText.inputType = data[3].inputType ?: InputType.TYPE_CLASS_TEXT editText.addTextChangedListener { val valueToAdd = try { @@ -101,7 +101,7 @@ class BottomSheetSumFragment : BottomSheetFragment() { } // Second edit text - data[4].hint?.let { editText2.hint = getString(it) } + data[4].hintResId?.let { editText2.hint = getString(it) } editText2.inputType = data[4].inputType ?: InputType.TYPE_CLASS_TEXT editText2.addTextChangedListener { this.viewModel.doubleValue = try { @@ -113,7 +113,7 @@ class BottomSheetSumFragment : BottomSheetFragment() { editText2.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { - this.viewModel.onRowValueChanged() + this.onRowValueChanged() // this.delegate.onRowValueChanged(value, row) dismiss() true diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetTableSizeGridFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetTableSizeGridFragment.kt index d58f839d..d8083d73 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetTableSizeGridFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetTableSizeGridFragment.kt @@ -6,6 +6,7 @@ import android.view.View import androidx.recyclerview.widget.GridLayoutManager import kotlinx.android.synthetic.main.bottom_sheet_grid.* import kotlinx.android.synthetic.main.fragment_bottom_sheet.view.* +import net.pokeranalytics.android.R import net.pokeranalytics.android.model.TableSize import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate @@ -25,7 +26,7 @@ class BottomSheetTableSizeGridFragment : BottomSheetFragment(), StaticRowReprese override fun onResume() { super.onResume() - this.viewModel.notifyDataSetChanged() + this.notifyDataSetChanged() // dataAdapter.notifyDataSetChanged() } @@ -34,17 +35,17 @@ class BottomSheetTableSizeGridFragment : BottomSheetFragment(), StaticRowReprese */ private fun initUI() { LayoutInflater.from(requireContext()) - .inflate(net.pokeranalytics.android.R.layout.bottom_sheet_grid, view?.bottomSheetContainer, true) + .inflate(R.layout.bottom_sheet_grid, view?.bottomSheetContainer, true) val viewManager = GridLayoutManager(requireContext(), 3) val dataAdapter = RowRepresentableAdapter(this, this) - this.viewModel.dataAdapter = dataAdapter + this.dataAdapter = dataAdapter val spanCount = 3 val spacing = 2.px val includeEdge = false - reyclerView.apply { + recyclerView.apply { setHasFixedSize(true) layoutManager = viewManager adapter = dataAdapter @@ -53,20 +54,25 @@ class BottomSheetTableSizeGridFragment : BottomSheetFragment(), StaticRowReprese } override fun adapterRows(): List? { - return TableSize.all + return TableSize.all(this.viewModel.alternativeLabels) } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { this.viewModel.defaultSize = (row as TableSize).numberOfPlayer - this.viewModel.onRowValueChanged() -// this.delegate.onRowValueChanged((row as TableSize).numberOfPlayer, this.row) + this.onRowValueChanged() dismiss() } - override fun stringForRow(row: RowRepresentable): String { - this.context?.let { - return row.localizedTitle(it) - } - return "UNKNOWN CONTEXT FOR ROW $row" - } +// override fun charSequenceForRow(row: RowRepresentable, context: Context, tag: Int): String { +// +// if (this.viewModel.alternativeLabels) { +// return (row as TableSize).numberOfPlayer.toString() +// } +// +// this.context?.let { +// return row.localizedTitle(it) +// } +// return "UNKNOWN CONTEXT FOR ROW $row" +// } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt index fb87d866..14dbc5c6 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt @@ -13,7 +13,7 @@ import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.CustomFieldCriteria import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.realm.ReportSetup -import net.pokeranalytics.android.ui.fragment.data.DataManagerFragment +import net.pokeranalytics.android.ui.modules.data.DataManagerFragment import net.pokeranalytics.android.ui.viewmodel.ReportViewModel import net.pokeranalytics.android.ui.viewmodel.ViewModelHolder import net.pokeranalytics.android.util.extensions.findById @@ -37,10 +37,10 @@ abstract class AbstractReportFragment : DataManagerFragment() { private fun initData() { - this.viewModel.dataType = LiveData.REPORT_SETUP.ordinal - this.viewModel.primaryKey = this.selectedReport.options.reportSetupId + this.model.dataType = LiveData.REPORT_SETUP.ordinal + this.model.primaryKey = this.selectedReport.options.reportSetupId - this.viewModel.loadItemWithRealm(getRealm()) + this.model.loadItemWithRealm(getRealm()) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -48,7 +48,7 @@ abstract class AbstractReportFragment : DataManagerFragment() { initData() - this.deleteButtonShouldAppear = (this.viewModel.primaryKey != null) + this.deleteButtonShouldAppear = (this.model.primaryKey != null) this.saveButtonShouldAppear = this.selectedReport.options.userGenerated setDisplayHomeAsUpEnabled(true) @@ -71,7 +71,7 @@ abstract class AbstractReportFragment : DataManagerFragment() { view.findViewById(net.pokeranalytics.android.R.id.reportName) nameEditText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES - this.viewModel.primaryKey?.let { id -> + this.model.primaryKey?.let { id -> getRealm().findById(id)?.let { reportSetup -> nameEditText.hint = reportSetup.name } @@ -110,10 +110,10 @@ abstract class AbstractReportFragment : DataManagerFragment() { private fun saveReport(name: String) { this.reportViewModel.title = name - val rs = this.viewModel.item as ReportSetup + val rs = this.model.item as ReportSetup getRealm().executeTransaction { realm -> - val firstSave = (this.viewModel.primaryKey == null) + val firstSave = (this.model.primaryKey == null) if (firstSave) { val options = this.selectedReport.options rs.name = name @@ -139,7 +139,7 @@ abstract class AbstractReportFragment : DataManagerFragment() { } - this.viewModel.primaryKey = rs.id + this.model.primaryKey = rs.id this.deleteButtonShouldAppear = true setToolbarTitle(this.reportViewModel.title) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt index f550272f..3ad11ff0 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt @@ -88,7 +88,7 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab if (row is StatRow) { context?.let { _ -> row.computedStat?.let { - dc.textFormat = it.format() + dc.textFormat = it.textFormat } } } @@ -98,7 +98,7 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab override fun statFormatForRow(row: RowRepresentable): TextFormat { if (row is StatRow) { context?.let { _ -> - row.computedStat?.let { return it.format() } + row.computedStat?.let { return it.textFormat } } } return TextFormat(NULL_TEXT) @@ -168,7 +168,7 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab // RowRepresentableDelegate - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { if (this.hasComputationInProgress) { return diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt index 90004c77..3e3d90c8 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt @@ -70,21 +70,21 @@ class ProgressReportFragment : AbstractReportFragment() { val fragmentManager = parentActivity?.supportFragmentManager val fragmentTransaction = fragmentManager?.beginTransaction() - graphFragment = GraphFragment.newInstance(GraphFragment.Style.LINE) - fragmentTransaction?.add(R.id.graphContainer, graphFragment) + this.graphFragment = GraphFragment.newInstance(GraphFragment.Style.LINE) + fragmentTransaction?.add(R.id.graphContainer, this.graphFragment) fragmentTransaction?.commit() this.stat.aggregationTypes.firstOrNull()?.let { - reports[it] = this.selectedReport + this.reports[it] = this.selectedReport } this.stat.aggregationTypes.firstOrNull()?.let { aggregationType -> - reports[aggregationType]?.let { report -> + this.reports[aggregationType]?.let { report -> setGraphData(report, aggregationType) } } - val aggregationTypes = stat.aggregationTypes + val aggregationTypes = this.stat.aggregationTypes aggregationTypes.forEachIndexed { index, type -> val chip = Chip(requireContext()) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/graph/GraphUnderlyingEntry.kt b/app/src/main/java/net/pokeranalytics/android/ui/graph/GraphUnderlyingEntry.kt index 18c37842..d4c03d17 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/graph/GraphUnderlyingEntry.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/graph/GraphUnderlyingEntry.kt @@ -22,7 +22,7 @@ interface GraphUnderlyingEntry { ): LegendContent { val leftName = stat.localizedTitle(context) - val totalStatValue = stat.format(entry.y.toDouble(), currency = null) + val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null) return if (stat.legendHideRightValue) { DefaultLegendValues(this.entryTitle(context), totalStatValue, leftName = leftName) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/helpers/SwipeToDeleteCallback.kt b/app/src/main/java/net/pokeranalytics/android/ui/helpers/SwipeToDeleteCallback.kt index 510242f6..7dacff9c 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/helpers/SwipeToDeleteCallback.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/helpers/SwipeToDeleteCallback.kt @@ -6,12 +6,12 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import net.pokeranalytics.android.R -import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter + /** * Swipe to delete callback */ -class SwipeToDeleteCallback(var adapter: RowRepresentableAdapter, var onDelete: ((position: Int) -> Unit)? = null) : +class SwipeToDeleteCallback(private var onDelete: ((position: Int) -> Unit)? = null) : ItemTouchHelper.SimpleCallback(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.START or ItemTouchHelper.END) { override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { @@ -27,8 +27,8 @@ class SwipeToDeleteCallback(var adapter: RowRepresentableAdapter, var onDelete: c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean ) { - val foregroundView = viewHolder.itemView.findViewById(net.pokeranalytics.android.R.id.foreground) - val backgroundView = viewHolder.itemView.findViewById(net.pokeranalytics.android.R.id.background) + val foregroundView = viewHolder.itemView.findViewById(R.id.foreground) + val backgroundView = viewHolder.itemView.findViewById(R.id.background) foregroundView?.let { getDefaultUIUtil().onDraw(c, recyclerView, foregroundView, dX, dY, actionState, isCurrentlyActive) backgroundView?.findViewById(R.id.leftIcon)?.isVisible = dX > 0 @@ -36,9 +36,8 @@ class SwipeToDeleteCallback(var adapter: RowRepresentableAdapter, var onDelete: } } - override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { - val foregroundView = viewHolder.itemView.findViewById(net.pokeranalytics.android.R.id.foreground) + val foregroundView = viewHolder.itemView.findViewById(R.id.foreground) foregroundView?.let { getDefaultUIUtil().clearView(foregroundView) } @@ -46,7 +45,7 @@ class SwipeToDeleteCallback(var adapter: RowRepresentableAdapter, var onDelete: override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { viewHolder?.let { - val foregroundView = viewHolder.itemView.findViewById(net.pokeranalytics.android.R.id.foreground) + val foregroundView = viewHolder.itemView.findViewById(R.id.foreground) foregroundView?.let { getDefaultUIUtil().onSelected(foregroundView) } @@ -58,11 +57,29 @@ class SwipeToDeleteCallback(var adapter: RowRepresentableAdapter, var onDelete: actionState: Int, isCurrentlyActive: Boolean ) { viewHolder?.let { - val foregroundView = viewHolder.itemView.findViewById(net.pokeranalytics.android.R.id.foreground) + val foregroundView = viewHolder.itemView.findViewById(R.id.foreground) foregroundView?.let { getDefaultUIUtil().onDrawOver(c, recyclerView, foregroundView, dX, dY, actionState, isCurrentlyActive) } } } +// override fun getMovementFlags( +// recyclerView: RecyclerView, +// viewHolder: RecyclerView.ViewHolder +// ): Int { +// // Set movement flags based on the layout manager +// // Set movement flags based on the layout manager +// return if (viewHolder is HandHistoryAdapter.RowPlayerSetupHolder) { +// val dragFlags = +// ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT +// val swipeFlags = 0 +// ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags) +// } else { +// val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN +// val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END +// ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags) +// } +// } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollActivity.kt similarity index 96% rename from app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollActivity.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollActivity.kt index 834eba85..cbd06531 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollActivity.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.activity +package net.pokeranalytics.android.ui.modules.bankroll import android.content.Context import android.content.Intent diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollDetailsActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollDetailsActivity.kt similarity index 88% rename from app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollDetailsActivity.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollDetailsActivity.kt index abfa2640..d3345a62 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollDetailsActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollDetailsActivity.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.activity +package net.pokeranalytics.android.ui.modules.bankroll import android.content.Intent import android.os.Bundle @@ -6,7 +6,6 @@ import androidx.fragment.app.Fragment import net.pokeranalytics.android.R import net.pokeranalytics.android.calculus.bankroll.BankrollReport import net.pokeranalytics.android.ui.activity.components.BaseActivity -import net.pokeranalytics.android.ui.fragment.BankrollDetailsFragment class BankrollDetailsActivity : BaseActivity() { @@ -18,7 +17,7 @@ class BankrollDetailsActivity : BaseActivity() { * Default constructor */ fun newInstanceForResult(fragment: Fragment, bankrollReport: BankrollReport, requestCode: Int) { - this.bankrollReport = bankrollReport + Companion.bankrollReport = bankrollReport val intent = Intent(fragment.requireContext(), BankrollDetailsActivity::class.java) fragment.startActivityForResult(intent, requestCode) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollDetailsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollDetailsFragment.kt similarity index 88% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollDetailsFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollDetailsFragment.kt index bdae834e..95a28fa6 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollDetailsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollDetailsFragment.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.fragment +package net.pokeranalytics.android.ui.modules.bankroll import android.app.Activity.RESULT_OK import android.content.Intent @@ -14,8 +14,8 @@ import net.pokeranalytics.android.calculus.bankroll.BankrollReportManager import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.realm.Bankroll -import net.pokeranalytics.android.ui.activity.DataListActivity -import net.pokeranalytics.android.ui.activity.EditableDataActivity +import net.pokeranalytics.android.ui.modules.datalist.DataListActivity +import net.pokeranalytics.android.ui.modules.data.EditableDataActivity import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate @@ -32,7 +32,8 @@ class BankrollDetailsFragment : RealmFragment(), StaticRowRepresentableDataSourc companion object { fun newInstance(bankrollReport: BankrollReport): BankrollDetailsFragment { - val fragment = BankrollDetailsFragment() + val fragment = + BankrollDetailsFragment() fragment.bankrollId = bankrollReport.setup.bankrollId return fragment } @@ -72,9 +73,9 @@ class BankrollDetailsFragment : RealmFragment(), StaticRowRepresentableDataSourc } } - override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { - menu?.clear() - inflater?.inflate(R.menu.toolbar_comparison_chart, menu) // TODO R.menu.toolbar_comparison_chart? + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + menu.clear() + inflater.inflate(R.menu.toolbar_comparison_chart, menu) // TODO R.menu.toolbar_comparison_chart? this.bankrollDetailsMenu = menu updateMenuUI() super.onCreateOptionsMenu(menu, inflater) @@ -134,21 +135,21 @@ class BankrollDetailsFragment : RealmFragment(), StaticRowRepresentableDataSourc CustomizableRowRepresentable( RowViewType.TITLE_VALUE, resId = R.string.bankroll, - computedStat = totalComputedStat + valueTextFormat = totalComputedStat.textFormat ) ) rows.add( CustomizableRowRepresentable( RowViewType.TITLE_VALUE, resId = R.string.net_result, - computedStat = netComputedStat + valueTextFormat = netComputedStat.textFormat ) ) rows.add( CustomizableRowRepresentable( RowViewType.TITLE_VALUE, resId = R.string.net_banked, - computedStat = netBankedComputedStat + valueTextFormat = netBankedComputedStat.textFormat ) ) @@ -162,7 +163,7 @@ class BankrollDetailsFragment : RealmFragment(), StaticRowRepresentableDataSourc CustomizableRowRepresentable( RowViewType.TITLE_VALUE, title = typeName, - computedStat = computedStat + valueTextFormat = computedStat.textFormat ) ) } @@ -193,12 +194,12 @@ class BankrollDetailsFragment : RealmFragment(), StaticRowRepresentableDataSourc // RowRepresentableDelegate - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { R.id.settings -> editBankroll() } return true diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollFragment.kt similarity index 94% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollFragment.kt index 60214e3a..58f8b22e 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollFragment.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.fragment +package net.pokeranalytics.android.ui.modules.bankroll import android.app.Activity import android.content.Intent @@ -19,9 +19,8 @@ import net.pokeranalytics.android.calculus.bankroll.BankrollReportManager import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.realm.Bankroll -import net.pokeranalytics.android.ui.activity.BankrollDetailsActivity -import net.pokeranalytics.android.ui.activity.DataListActivity -import net.pokeranalytics.android.ui.activity.EditableDataActivity +import net.pokeranalytics.android.ui.modules.datalist.DataListActivity +import net.pokeranalytics.android.ui.modules.data.EditableDataActivity import net.pokeranalytics.android.ui.activity.GraphActivity import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter @@ -51,7 +50,8 @@ class BankrollFragment : DeletableItemFragment(), StaticRowRepresentableDataSour * Create new instance */ fun newInstance(): BankrollFragment { - val fragment = BankrollFragment() + val fragment = + BankrollFragment() val bundle = Bundle() fragment.arguments = bundle return fragment @@ -175,7 +175,7 @@ class BankrollFragment : DeletableItemFragment(), StaticRowRepresentableDataSour return rows } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { when (row) { is BankrollGraphRow -> { val lineDataSet = row.dataSet as LineDataSet diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/CalendarDetailsActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsActivity.kt similarity index 74% rename from app/src/main/java/net/pokeranalytics/android/ui/activity/CalendarDetailsActivity.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsActivity.kt index 96022765..029b493d 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/CalendarDetailsActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsActivity.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.activity +package net.pokeranalytics.android.ui.modules.calendar import android.content.Context import android.content.Intent @@ -8,7 +8,6 @@ import net.pokeranalytics.android.R import net.pokeranalytics.android.calculus.ComputedResults import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.ui.activity.components.BaseActivity -import net.pokeranalytics.android.ui.viewmodel.CalendarDetailsViewModel class CalendarDetailsActivity : BaseActivity() { @@ -27,9 +26,9 @@ class CalendarDetailsActivity : BaseActivity() { * Default constructor */ fun newInstance(context: Context, computedResults: ComputedResults, sessionTypeCondition: QueryCondition?, title: String?) { - this.computedResults = computedResults - this.sessionTypeCondition = sessionTypeCondition - this.detailsTitle = title + Companion.computedResults = computedResults + Companion.sessionTypeCondition = sessionTypeCondition + detailsTitle = title val intent = Intent(context, CalendarDetailsActivity::class.java) context.startActivity(intent) } @@ -47,9 +46,12 @@ class CalendarDetailsActivity : BaseActivity() { */ private fun initData() { - this.viewModel.computedResults = computedResults - this.viewModel.sessionTypeCondition = sessionTypeCondition - this.viewModel.detailsTitle = detailsTitle + this.viewModel.computedResults = + computedResults + this.viewModel.sessionTypeCondition = + sessionTypeCondition + this.viewModel.detailsTitle = + detailsTitle } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarDetailsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt similarity index 96% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarDetailsFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt index c1ff6b85..769d3db3 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarDetailsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.fragment +package net.pokeranalytics.android.ui.modules.calendar import android.content.Context import android.content.Intent @@ -26,13 +26,13 @@ import net.pokeranalytics.android.ui.activity.GraphActivity import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource +import net.pokeranalytics.android.ui.fragment.GraphFragment import net.pokeranalytics.android.ui.fragment.components.BaseFragment import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rowrepresentable.GraphRow import net.pokeranalytics.android.ui.view.rowrepresentable.StatDoubleRow -import net.pokeranalytics.android.ui.viewmodel.CalendarDetailsViewModel import timber.log.Timber import java.util.* import kotlin.collections.ArrayList @@ -134,7 +134,7 @@ class CalendarDetailsFragment : BaseFragment(), StaticRowRepresentableDataSource return rowRepresentables } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { when (row) { is GraphRow -> { val report = row.report @@ -146,10 +146,12 @@ class CalendarDetailsFragment : BaseFragment(), StaticRowRepresentableDataSource val dataSet = row.dataSet when (dataSet) { is LineDataSet-> { - GraphActivity.newInstance(requireContext(), listOf(dataSet), null, GraphFragment.Style.LINE, title) + GraphActivity.newInstance(requireContext(), listOf(dataSet), null, + GraphFragment.Style.LINE, title) } is BarDataSet -> { - GraphActivity.newInstance(requireContext(), null, listOf(dataSet), GraphFragment.Style.BAR, title) + GraphActivity.newInstance(requireContext(), null, listOf(dataSet), + GraphFragment.Style.BAR, title) } } } @@ -172,7 +174,6 @@ class CalendarDetailsFragment : BaseFragment(), StaticRowRepresentableDataSource val startDate = Date() - val realm = Realm.getDefaultInstance() val query = Query().merge(computedResults.group.query) query.remove(QueryCondition.IsCash) @@ -188,7 +189,9 @@ class CalendarDetailsFragment : BaseFragment(), StaticRowRepresentableDataSource stats = requiredStats, query = query ) + val realm = Realm.getDefaultInstance() val report = Calculator.computeStats(realm, options) + realm.close() Timber.d("Report take: ${System.currentTimeMillis() - startDate.time}ms") diff --git a/app/src/main/java/net/pokeranalytics/android/ui/viewmodel/CalendarDetailsViewModel.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsViewModel.kt similarity index 89% rename from app/src/main/java/net/pokeranalytics/android/ui/viewmodel/CalendarDetailsViewModel.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsViewModel.kt index 4f449b01..d34b78a2 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/viewmodel/CalendarDetailsViewModel.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsViewModel.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.viewmodel +package net.pokeranalytics.android.ui.modules.calendar import androidx.lifecycle.ViewModel import net.pokeranalytics.android.calculus.ComputedResults diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt similarity index 96% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt index ca1eb5a7..ce8d968c 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.fragment +package net.pokeranalytics.android.ui.modules.calendar import android.os.Bundle import android.view.LayoutInflater @@ -22,7 +22,6 @@ import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.combined import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.realm.ComputableResult -import net.pokeranalytics.android.ui.activity.CalendarDetailsActivity import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource @@ -52,7 +51,8 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable * Create new instance */ fun newInstance(): CalendarFragment { - val fragment = CalendarFragment() + val fragment = + CalendarFragment() val bundle = Bundle() fragment.arguments = bundle return fragment @@ -71,7 +71,8 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable private var datesForRows: HashMap = HashMap() private var sessionTypeCondition: QueryCondition? = null - private var currentTimeFilter: TimeFilter = TimeFilter.MONTH + private var currentTimeFilter: TimeFilter = + TimeFilter.MONTH private var currentStat = Stat.NET_RESULT // Life Cycle @@ -96,7 +97,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable return rows } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { when (currentTimeFilter) { TimeFilter.MONTH -> { @@ -201,7 +202,8 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable // Manage time queryWith filterTimeMonth.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { - currentTimeFilter = TimeFilter.MONTH + currentTimeFilter = + TimeFilter.MONTH filterTimeYear.isChecked = false displayData() } else if (currentTimeFilter == TimeFilter.MONTH) { @@ -211,7 +213,8 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable } filterTimeYear.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { - currentTimeFilter = TimeFilter.YEAR + currentTimeFilter = + TimeFilter.YEAR filterTimeMonth.isChecked = false displayData() } else if (currentTimeFilter == TimeFilter.YEAR) { @@ -374,7 +377,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable val row = CustomizableRowRepresentable( customViewType = RowViewType.TITLE_VALUE_ARROW, title = date.getDateMonth(), - computedStat = computedStat, + valueTextFormat = computedStat.textFormat, isSelectable = true ) @@ -391,7 +394,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable val row = CustomizableRowRepresentable( customViewType = RowViewType.TITLE_VALUE_ARROW, title = date.getDateYear(), - computedStat = computedStat, + valueTextFormat = computedStat.textFormat, isSelectable = true ) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/BankrollDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/BankrollDataFragment.kt similarity index 75% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/data/BankrollDataFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/data/BankrollDataFragment.kt index 6ebb912c..32f746fa 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/BankrollDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/BankrollDataFragment.kt @@ -1,13 +1,13 @@ -package net.pokeranalytics.android.ui.fragment.data +package net.pokeranalytics.android.ui.modules.data import android.app.Activity.RESULT_OK +import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View import net.pokeranalytics.android.R -import net.pokeranalytics.android.api.CurrencyConverterApi +import net.pokeranalytics.android.api.FreeConverterApi import net.pokeranalytics.android.model.realm.Bankroll -import net.pokeranalytics.android.model.retrofit.CurrencyConverterValue import net.pokeranalytics.android.ui.activity.CurrenciesActivity import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource @@ -23,12 +23,9 @@ import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.UserDefaults import net.pokeranalytics.android.util.extensions.toCurrency import net.pokeranalytics.android.util.extensions.toRate -import retrofit2.Call -import retrofit2.Response import java.util.* - /** * Custom EditableDataFragment to manage the Bankroll data */ @@ -37,7 +34,7 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS // Return the item as a Bankroll object private val bankroll: Bankroll get() { - return this.viewModel.item as Bankroll + return this.model.item as Bankroll } private lateinit var defaultCurrency: Currency @@ -83,7 +80,11 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS return rows } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return when (row) { SimpleRow.NAME -> if (bankroll.name.isNotEmpty()) bankroll.name else NULL_TEXT BankrollRow.CURRENCY -> { @@ -104,7 +105,7 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS val rate = this.bankroll.currency?.rate ?: 1.0 rate.toRate() } - else -> super.stringForRow(row) + else -> super.charSequenceForRow(row, context, 0) } } @@ -116,7 +117,7 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS } } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { return when (row) { SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.bankroll.name)) BankrollRow.INITIAL_VALUE -> { @@ -130,13 +131,13 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS } } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { when (row) { BankrollRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@BankrollDataFragment, RequestCode.CURRENCY.value ) BankrollRow.REFRESH_RATE -> refreshRate() - else -> super.onRowSelected(position, row, fromAction) + else -> super.onRowSelected(position, row, tag) } } @@ -210,31 +211,44 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS if (System.currentTimeMillis() - lastRefreshRateCall < 10 * 1000 || isRefreshingRate) { return } - lastRefreshRateCall = System.currentTimeMillis() + this.lastRefreshRateCall = System.currentTimeMillis() val currenciesConverterValue = "${bankroll.currency?.code}_${defaultCurrency.currencyCode}" - val call = CurrencyConverterApi.getApi(requireContext())?.convert(currenciesConverterValue) - call?.enqueue(object : retrofit2.Callback> { - - override fun onResponse(call: Call>, response: Response>) { - response.body()?.let { - it[currenciesConverterValue]?.value?.let { rate -> - onRowValueChanged(rate, BankrollRow.RATE) - } - } - - isRefreshingRate = false - rowRepresentableAdapter.refreshRow(BankrollRow.REFRESH_RATE) - } - override fun onFailure(call: Call>, t: Throwable) { - isRefreshingRate = false - rowRepresentableAdapter.refreshRow(BankrollRow.REFRESH_RATE) - } - }) + FreeConverterApi.currencyRate(currenciesConverterValue, requireContext()) { rate -> + onRowValueChanged(rate, BankrollRow.RATE) + isRefreshingRate = false + rowRepresentableAdapter.refreshRow(BankrollRow.REFRESH_RATE) + } - isRefreshingRate = true - rowRepresentableAdapter.refreshRow(BankrollRow.REFRESH_RATE) +// val call = CurrencyConverterApi.getApi(requireContext())?.convert(currenciesConverterValue) +// call?.enqueue(object : retrofit2.Callback> { +// +// override fun onResponse(call: Call>, response: Response>) { +// response.body()?.let { +// it[currenciesConverterValue]?.value?.let { rate -> +// Timber.d("rate found = $rate") +// onRowValueChanged(rate, BankrollRow.RATE) +// } ?: run { +// Timber.d("no rate for $currenciesConverterValue") +// } +// } ?: run { +// Timber.d("onResponse> no body in ${response}") +// } +// +// isRefreshingRate = false +// rowRepresentableAdapter.refreshRow(BankrollRow.REFRESH_RATE) +// } +// +// override fun onFailure(call: Call>, t: Throwable) { +// Timber.d("api call failed: ${t.message}") +// isRefreshingRate = false +// rowRepresentableAdapter.refreshRow(BankrollRow.REFRESH_RATE) +// } +// }) + + this.isRefreshingRate = true + this.rowRepresentableAdapter.refreshRow(BankrollRow.REFRESH_RATE) } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/CustomFieldDataFragment.kt similarity index 93% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/data/CustomFieldDataFragment.kt index df9be671..334e97eb 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/CustomFieldDataFragment.kt @@ -1,5 +1,6 @@ -package net.pokeranalytics.android.ui.fragment.data +package net.pokeranalytics.android.ui.modules.data +import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -33,7 +34,7 @@ class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDa // Return the item as a Custom TypedCSVField object private val customField: CustomField get() { - return this.viewModel.item as CustomField + return this.model.item as CustomField } private val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() { @@ -120,10 +121,14 @@ class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDa return customField.adapterRows() } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return when (row) { SimpleRow.NAME -> if (customField.name.isNotEmpty()) customField.name else NULL_TEXT - else -> super.stringForRow(row) + else -> super.charSequenceForRow(row, context, 0) } } @@ -142,7 +147,7 @@ class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDa } } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { return when (row) { SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.customField.name)) is CustomFieldEntry -> row.editingDescriptors(mapOf("defaultValue" to row.value)) @@ -150,13 +155,13 @@ class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDa } } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { when (row) { is CustomFieldEntry -> { val data = customField.editDescriptors(row) - BottomSheetFragment.create(fragmentManager, row, this, data, isClearable = false, isDeletable = true) + BottomSheetFragment.create(requireFragmentManager(), row, this, data, isClearable = false, isDeletable = true) } - else -> super.onRowSelected(position, row, fromAction) + else -> super.onRowSelected(position, row, tag) } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/DataManagerFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/DataManagerFragment.kt similarity index 79% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/data/DataManagerFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/data/DataManagerFragment.kt index ea95564e..a6b34ad4 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/DataManagerFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/DataManagerFragment.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.fragment.data +package net.pokeranalytics.android.ui.modules.data import android.app.Activity import android.content.Intent @@ -13,14 +13,13 @@ import net.pokeranalytics.android.R import net.pokeranalytics.android.exceptions.ConfigurationException import net.pokeranalytics.android.model.interfaces.Savable import net.pokeranalytics.android.model.interfaces.SaveValidityStatus -import net.pokeranalytics.android.ui.activity.DataListActivity -import net.pokeranalytics.android.ui.activity.EditableDataActivity import net.pokeranalytics.android.ui.fragment.components.RealmFragment +import net.pokeranalytics.android.ui.modules.datalist.DataListActivity import net.pokeranalytics.android.ui.viewmodel.DataManagerViewModel open class DataManagerFragment : RealmFragment() { - protected val viewModel: DataManagerViewModel by lazy { + protected val model: DataManagerViewModel by lazy { ViewModelProviders.of(this).get(DataManagerViewModel::class.java) } @@ -41,7 +40,6 @@ open class DataManagerFragment : RealmFragment() { this.updateMenuUI() } - private var editableMenu: Menu? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -49,24 +47,24 @@ open class DataManagerFragment : RealmFragment() { loadItem() } - override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { - menu?.clear() - inflater?.inflate(R.menu.toolbar_editable_data, menu) + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + menu.clear() + inflater.inflate(R.menu.toolbar_editable_data, menu) this.editableMenu = menu updateMenuUI() super.onCreateOptionsMenu(menu, inflater) } /** - * Update menu UI - */ + * Update menu UI + */ private fun updateMenuUI() { editableMenu?.findItem(R.id.delete)?.isVisible = this.deleteButtonShouldAppear editableMenu?.findItem(R.id.save)?.isVisible = this.saveButtonShouldAppear } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item!!.itemId) { + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { R.id.save -> saveData() R.id.delete -> deleteData() } @@ -79,7 +77,7 @@ open class DataManagerFragment : RealmFragment() { private fun loadItem() { // this.item = this.liveDataType.updateOrCreate(this.getRealm(), primaryKey) - this.deleteButtonShouldAppear = this.viewModel.primaryKey != null + this.deleteButtonShouldAppear = this.model.primaryKey != null } @@ -88,7 +86,7 @@ open class DataManagerFragment : RealmFragment() { */ protected open fun saveData() { - val savable = this.viewModel.item + val savable = this.model.item this.willSaveData() when (savable) { @@ -97,7 +95,7 @@ open class DataManagerFragment : RealmFragment() { when (status) { SaveValidityStatus.VALID -> { this.getRealm().executeTransaction { - val managedItem = it.copyToRealmOrUpdate(this.viewModel.item) + val managedItem = it.copyToRealmOrUpdate(this.model.item) if (managedItem is Savable) { val uniqueIdentifier = managedItem.id finishActivityWithResult(uniqueIdentifier) @@ -135,14 +133,14 @@ open class DataManagerFragment : RealmFragment() { val realm = this.getRealm() - if (this.viewModel.item.isValidForDelete(realm)) { + if (this.model.item.isValidForDelete(realm)) { val intent = Intent() - intent.putExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName, this.viewModel.item.id) + intent.putExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName, this.model.item.id) activity?.setResult(Activity.RESULT_OK, intent) activity?.finish() } else { - val status = this.viewModel.item.getDeleteStatus(requireContext(), realm) - val message = this.viewModel.item.getFailedDeleteMessage(status) + val status = this.model.item.getDeleteStatus(requireContext(), realm) + val message = this.model.item.getFailedDeleteMessage(status) val builder = AlertDialog.Builder(requireContext()) .setMessage(message) .setNegativeButton(R.string.ok, null) @@ -157,7 +155,7 @@ open class DataManagerFragment : RealmFragment() { */ private fun finishActivityWithResult(uniqueIdentifier: String) { val intent = Intent() - intent.putExtra(EditableDataActivity.IntentKey.DATA_TYPE.keyName, viewModel.dataType) + intent.putExtra(EditableDataActivity.IntentKey.DATA_TYPE.keyName, model.dataType) intent.putExtra(EditableDataActivity.IntentKey.PRIMARY_KEY.keyName, uniqueIdentifier) activity?.setResult(Activity.RESULT_OK, intent) activity?.finish() diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataActivity.kt similarity index 91% rename from app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataActivity.kt index c540be6b..405ba114 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataActivity.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.activity +package net.pokeranalytics.android.ui.modules.data import android.content.Context import android.content.Intent @@ -7,7 +7,6 @@ import androidx.fragment.app.Fragment import net.pokeranalytics.android.R import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.ui.activity.components.MediaActivity -import net.pokeranalytics.android.ui.fragment.data.* import java.io.File import java.util.* @@ -22,9 +21,9 @@ class EditableDataActivity : MediaActivity() { /** * Default constructor */ - fun newInstance(context: Context, dataType: Int, primaryKey: String? = null) { + fun newInstance(context: Context, dataType: LiveData, primaryKey: String? = null) { val intent = Intent(context, EditableDataActivity::class.java) - intent.putExtra(IntentKey.DATA_TYPE.keyName, dataType) + intent.putExtra(IntentKey.DATA_TYPE.keyName, dataType.ordinal) primaryKey?.let { intent.putExtra(IntentKey.PRIMARY_KEY.keyName, it) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/EditableDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataFragment.kt similarity index 72% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/data/EditableDataFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataFragment.kt index adc60700..4129c9e2 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/EditableDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataFragment.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.fragment.data +package net.pokeranalytics.android.ui.modules.data import android.os.Bundle import android.view.LayoutInflater @@ -11,6 +11,7 @@ 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.interfaces.Editable +import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate @@ -38,22 +39,15 @@ open class EditableDataFragment : DataManagerFragment(), RowRepresentableDelegat open fun initData() { - this.viewModel.dataType = this.arguments?.getInt(BundleKey.DATA_TYPE.value) - this.viewModel.primaryKey = this.arguments?.getString(BundleKey.PRIMARY_KEY.value) - viewModel.loadItemWithRealm(getRealm()) + this.model.dataType = this.arguments?.getInt(BundleKey.DATA_TYPE.value) + this.model.primaryKey = this.arguments?.getString(BundleKey.PRIMARY_KEY.value) - val dataSource = getDataSource() - this.rowRepresentableAdapter = RowRepresentableAdapter(getDataSource(), this) - //this.rowRepresentableAdapter.setHasStableIds(true) - this.recyclerView.adapter = rowRepresentableAdapter + this.model.loadItemWithRealm(getRealm()) - // When creating an object, open automatically the keyboard for the first row - if (!deleteButtonShouldAppear && shouldOpenKeyboard) { - val row = dataSource.adapterRows()?.firstOrNull() - if (row != null && this.viewModel.primaryKey == null) { - onRowSelected(0, row) - } - } + } + + open fun indexOfFirstRowToSelect(): Int { + return 0 } /** @@ -68,14 +62,14 @@ open class EditableDataFragment : DataManagerFragment(), RowRepresentableDelegat } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - BottomSheetFragment.create(fragmentManager, row, this, getDataSource().editDescriptors(row)) + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { + BottomSheetFragment.create(requireFragmentManager(), row, this, getDataSource().editDescriptors(row), valueHasPlaceholder = true) } override fun onRowValueChanged(value: Any?, row: RowRepresentable) { this.getRealm().executeTransaction { try { - (this.viewModel.item as Editable).updateValue(value, row) + (this.model.item as Editable).updateValue(value, row) } catch (e: Exception) { Crashlytics.log("Exception caught: row = $row, value=$value, class=${this.javaClass}") throw e @@ -92,12 +86,18 @@ open class EditableDataFragment : DataManagerFragment(), RowRepresentableDelegat setDisplayHomeAsUpEnabled(true) - val liveDataType = this.viewModel.liveDataType + val liveDataType = this.model.liveDataType + + val data: RealmModel? = liveDataType.getData(this.getRealm(), this.model.primaryKey) + data?.let { - val proxyItem: RealmModel? = - liveDataType.getData(this.getRealm(), this.viewModel.primaryKey) - proxyItem?.let { - this.appBar.toolbar.title = liveDataType.updateEntityLocalizedTitle(requireContext()) + val title = if (it is NameManageable) { + it.name + } else { + liveDataType.updateEntityLocalizedTitle(requireContext()) + } + + this.appBar.toolbar.title = title deleteButtonShouldAppear = true isUpdating = true } ?: run { @@ -109,13 +109,28 @@ open class EditableDataFragment : DataManagerFragment(), RowRepresentableDelegat setHasFixedSize(true) layoutManager = viewManager } + + val dataSource = getDataSource() + this.rowRepresentableAdapter = RowRepresentableAdapter(getDataSource(), this) + //this.rowRepresentableAdapter.setHasStableIds(true) + this.recyclerView.adapter = rowRepresentableAdapter + + // When creating an object, open automatically the keyboard for the first row + if (!deleteButtonShouldAppear && shouldOpenKeyboard) { + val index = indexOfFirstRowToSelect() + dataSource.adapterRows()?.let { rows -> + if (rows.size > index) { + onRowSelected(index, rows.get(index)) + } + } + } } /** * Return the data source */ open fun getDataSource(): RowRepresentableDataSource { - return this.viewModel.item as RowRepresentableDataSource + return this.model.item as RowRepresentableDataSource } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/LocationDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/LocationDataFragment.kt similarity index 92% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/data/LocationDataFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/data/LocationDataFragment.kt index 0bcb762a..ed8e641c 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/LocationDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/LocationDataFragment.kt @@ -1,5 +1,6 @@ -package net.pokeranalytics.android.ui.fragment.data +package net.pokeranalytics.android.ui.modules.data +import android.content.Context import android.os.Bundle import android.view.View import com.google.android.libraries.places.api.model.PlaceLikelihood @@ -23,7 +24,7 @@ class LocationDataFragment : EditableDataFragment(), StaticRowRepresentableDataS // Return the item as a Location object private val location: Location get() { - return this.viewModel.item as Location + return this.model.item as Location } // Loader boolean @@ -64,10 +65,14 @@ class LocationDataFragment : EditableDataFragment(), StaticRowRepresentableDataS return rows } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return when (row) { SimpleRow.NAME -> if (location.name.isNotEmpty())location.name else NULL_TEXT - else -> return super.stringForRow(row) + else -> return super.charSequenceForRow(row, context, 0) } } @@ -78,21 +83,21 @@ class LocationDataFragment : EditableDataFragment(), StaticRowRepresentableDataS } } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { return when (row) { SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.location.name)) else -> null } } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { // If we click on a location row, save the location (and finish activity) placesForRows[row]?.place?.let { place -> location.setPlace(place) saveData() return } - super.onRowSelected(position, row, fromAction) + super.onRowSelected(position, row, tag) } override fun onRowValueChanged(value: Any?, row: RowRepresentable) { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/PlayerDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt similarity index 91% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/data/PlayerDataFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt index 268804a2..8a8d082d 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/PlayerDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt @@ -1,6 +1,7 @@ -package net.pokeranalytics.android.ui.fragment.data +package net.pokeranalytics.android.ui.modules.data import android.app.Activity.RESULT_OK +import android.content.Context import android.content.Intent import android.graphics.Color import android.os.Bundle @@ -40,7 +41,7 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou private val player: Player get() { - return this.viewModel.item as Player + return this.model.item as Player } private var mediaActivity: MediaActivity? = null @@ -116,22 +117,26 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou } } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return when (row) { PlayerRow.NAME -> if (player.name.isNotEmpty()) player.name else NULL_TEXT PlayerRow.SUMMARY -> if (player.summary.isNotEmpty()) player.summary else NULL_TEXT - else -> super.stringForRow(row) + else -> super.charSequenceForRow(row, context, 0) } } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { when (row) { PlayerRow.IMAGE -> openPictureDialog() is Comment -> { val data = arrayListOf(RowRepresentableEditDescriptor(row.content)) - BottomSheetFragment.create(fragmentManager, row, this, data, isClearable = false, isDeletable = true) + BottomSheetFragment.create(requireFragmentManager(), row, this, data, isClearable = false, isDeletable = true) } - else -> super.onRowSelected(position, row, fromAction) + else -> super.onRowSelected(position, row, tag) } } @@ -207,7 +212,7 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou player.cleanupComments() } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { when (row) { PlayerRow.NAME -> return arrayListOf(RowRepresentableEditDescriptor(this.player.name, R.string.name)) PlayerRow.SUMMARY -> return arrayListOf(RowRepresentableEditDescriptor(this.player.summary, R.string.summary)) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/TransactionDataFragment.kt similarity index 80% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionDataFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/data/TransactionDataFragment.kt index 92b27851..8b860533 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/TransactionDataFragment.kt @@ -1,7 +1,7 @@ -package net.pokeranalytics.android.ui.fragment.data +package net.pokeranalytics.android.ui.modules.data -import android.os.Bundle -import android.view.View +import android.content.Context +import io.realm.kotlin.where import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay @@ -20,7 +20,6 @@ import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.extensions.round import net.pokeranalytics.android.util.extensions.shortDate import net.pokeranalytics.android.util.extensions.sorted -import java.util.* import kotlin.math.abs /** @@ -31,12 +30,23 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa // Return the item as a Transaction object private val transaction: Transaction get() { - return this.viewModel.item as Transaction + return this.model.item as Transaction } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - shouldOpenKeyboard = false + override fun initData() { + super.initData() + + // Initialize with the only bankroll if possible + if (this.transaction.bankroll == null) { + val bankrolls = getRealm().where().findAll() + if (bankrolls.size == 1) { + this.transaction.bankroll = bankrolls.first() + } + } + } + + override fun indexOfFirstRowToSelect(): Int { + return if (this.transaction.bankroll == null) 0 else 1 } override fun getDataSource(): RowRepresentableDataSource { @@ -47,18 +57,22 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa return this.transaction.adapterRows() } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return when (row) { TransactionRow.BANKROLL -> this.transaction.bankroll?.name ?: NULL_TEXT TransactionRow.TYPE -> this.transaction.type?.name ?: NULL_TEXT TransactionRow.AMOUNT -> if (this.transaction.amount != 0.0) abs(this.transaction.amount).round() else NULL_TEXT TransactionRow.COMMENT -> if (this.transaction.comment.isNotEmpty()) this.transaction.comment else NULL_TEXT TransactionRow.DATE -> this.transaction.date.shortDate() - else -> super.stringForRow(row) + else -> super.charSequenceForRow(row, context, 0) } } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { return when (row) { TransactionRow.BANKROLL -> row.editingDescriptors( mapOf( @@ -78,7 +92,7 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa } } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { when (row) { TransactionRow.DATE -> DateTimePickerManager.create( requireContext(), @@ -88,7 +102,7 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa onlyDate = true, isClearable = false ) - else -> super.onRowSelected(position, row, fromAction) + else -> super.onRowSelected(position, row, tag) } } @@ -103,7 +117,7 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa */ private fun selectNextRow(currentRow: RowRepresentable) { - if (this.viewModel.primaryKey == null) { + if (this.model.primaryKey == null) { // automatically change the row for new data GlobalScope.launch(Dispatchers.Main) { delay(200) when (currentRow) { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionTypeDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/TransactionTypeDataFragment.kt similarity index 80% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionTypeDataFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/data/TransactionTypeDataFragment.kt index 0e010122..2a316ebe 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionTypeDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/TransactionTypeDataFragment.kt @@ -1,5 +1,6 @@ -package net.pokeranalytics.android.ui.fragment.data +package net.pokeranalytics.android.ui.modules.data +import android.content.Context import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.model.realm.TransactionType import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource @@ -12,7 +13,7 @@ class TransactionTypeDataFragment : EditableDataFragment(), RowRepresentableData val transactionType: TransactionType get() { - return this.viewModel.item as TransactionType + return this.model.item as TransactionType } // RowRepresentableDataSource @@ -33,10 +34,14 @@ class TransactionTypeDataFragment : EditableDataFragment(), RowRepresentableData return this.transactionType.viewTypeForPosition(position) } - override fun stringForRow(row: RowRepresentable): String { + override fun charSequenceForRow( + row: RowRepresentable, + context: Context, + tag: Int + ): CharSequence { return when (row) { SimpleRow.NAME -> this.transactionType.name - else -> return super.stringForRow(row) + else -> return super.charSequenceForRow(row, context, 0) } } @@ -47,16 +52,19 @@ class TransactionTypeDataFragment : EditableDataFragment(), RowRepresentableData } } - override fun editDescriptors(row: RowRepresentable): ArrayList? { + override fun editDescriptors(row: RowRepresentable): List? { return row.editingDescriptors(mapOf("defaultValue" to this.transactionType.name)) } - override fun isEnabled(row: RowRepresentable): Boolean { + override fun isEnabled( + row: RowRepresentable, + tag: Int + ): Boolean { return when (row) { TransactionTypeRow.TRANSACTION_ADDITIVE -> { return this.transactionType.isValidForDelete(getRealm()) } - else -> super.isEnabled(row) + else -> super.isEnabled(row, 0) } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataListActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataListActivity.kt new file mode 100644 index 00000000..d2229f82 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataListActivity.kt @@ -0,0 +1,120 @@ +package net.pokeranalytics.android.ui.modules.datalist + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProviders +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.LiveData +import net.pokeranalytics.android.ui.activity.components.BaseActivity +import net.pokeranalytics.android.ui.activity.components.RequestCode +import net.pokeranalytics.android.ui.modules.filter.FilterActivityRequestCode + +class DataListActivity : BaseActivity() { + + val model: DataListViewModel by lazy { + ViewModelProviders.of(this).get(DataListViewModel::class.java) + } + + 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"), + DIALOG("dialog"), + ITEM_IDS("item_ids"), + DATA_SELECTION("selection") + } + + 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) + } + + fun newInstance(fragment: Fragment, dataType: LiveData, selection: Boolean, itemIds: Array? = null, showAddButton: Boolean = true) { + val context = fragment.requireContext() + fragment.startActivityForResult( + getIntent( + context, + dataType.ordinal, + showAddButton = showAddButton, + selection = selection, + itemIds = itemIds + ), RequestCode.PLAYER_SELECTION.ordinal) + } + + private fun getIntent(context: Context, + dataType: Int, + showAddButton: Boolean = true, + dialog: Boolean = false, + selection: Boolean = false, + itemIds: Array? = null): Intent { + + val intent = Intent(context, DataListActivity::class.java) + intent.putExtra(IntentKey.DATA_TYPE.keyName, dataType) + intent.putExtra(IntentKey.SHOW_ADD_BUTTON.keyName, showAddButton) + intent.putExtra(IntentKey.DIALOG.keyName, dialog) + intent.putExtra(IntentKey.DATA_SELECTION.keyName, selection) + intent.putExtra(IntentKey.ITEM_IDS.keyName, itemIds) + 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 dialog = intent.getBooleanExtra(IntentKey.DIALOG.keyName, true) + val selection = intent.getBooleanExtra(IntentKey.DATA_SELECTION.keyName, false) + val itemIds = intent.getStringArrayExtra(IntentKey.ITEM_IDS.keyName) + + this.model.dataType = LiveData.values()[dataType] + this.model.isSelectionInstance = selection + this.model.showAddButton = showAddButton + this.model.itemIds = itemIds + + val dataListFragment = DataListFragment() + + if (dialog) { + val dataSelectionDialogFragment = DataSelectionDialogFragment() + showFragment(dataSelectionDialogFragment, R.id.container) + dataSelectionDialogFragment.showFragment(dataListFragment, supportFragmentManager) + +// dataSelectionDialogFragment.show(supportFragmentManager, "alert") + } else { + showFragment(dataListFragment, R.id.container) + } + + + +// val fragment = dataListFragment as DataListFragment +// fragment.setData(dataType) +// fragment.updateUI(showAddButton) + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataListFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataListFragment.kt new file mode 100644 index 00000000..6d36e830 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataListFragment.kt @@ -0,0 +1,213 @@ +package net.pokeranalytics.android.ui.modules.datalist + +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.lifecycle.ViewModelProviders +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.modules.data.EditableDataActivity +import net.pokeranalytics.android.ui.modules.filter.FiltersActivity +import net.pokeranalytics.android.ui.activity.components.RequestCode +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.util.extensions.find +import net.pokeranalytics.android.util.extensions.sorted + + +open class DataListFragment : DeletableItemFragment(), RowRepresentableDelegate { + + val model: DataListViewModel by lazy { + ViewModelProviders.of(requireActivity()).get(DataListViewModel::class.java) + } + + companion object { + const val REQUEST_CODE_DETAILS = 1000 + } + + private var menu: Menu? = null + private var searchView: SearchView? = null + + open fun retrieveItems(realm: Realm): RealmResults { + return realm.sorted(this.model.identifiableClass, + editableOnly = true, + filterableTypeUniqueIdentifier = this.model.dataType.subType) + } + + override fun deletableItems() : List { + return this.model.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) + initData() + initUI() + } + + fun initData() { + + val itemIds = this.model.itemIds + val items = if (itemIds?.isNotEmpty() == true) { + getRealm().where(this.model.identifiableClass) + .`in`("id", itemIds) + .sort(this.model.dataType.sortFields, this.model.dataType.sortOrders) + .findAll() + } else { + this.retrieveItems(getRealm()) + } + this.model.setItemsList(items) + } + + /** + * Init UI + */ + private fun initUI() { + + val searchMenuItem = this.menu?.findItem(R.id.action_search) + searchMenuItem?.isVisible = this.model.isSearchable + + setToolbarTitle(this.model.dataType.pluralLocalizedTitle(requireContext())) + + setDisplayHomeAsUpEnabled(true) + + val viewManager = LinearLayoutManager(requireContext()) + this.dataListAdapter = RowRepresentableAdapter(this.model, this) + + val swipeToDelete = SwipeToDeleteCallback { position -> + val item = this.model.items[position] + if (item != null) { + val itemId = item.id + deleteItem(this.dataListAdapter, this.model.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.newInstanceForResult(this, + dataType = this.model.dataType, + primaryKey = null, + requestCode = RequestCode.NEW_DATA.value + ) + + } + + this.addButton.isVisible = this.model.showAddButton + + } + + 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.menu = menu + + val searchMenuItem = menu.findItem(R.id.action_search) + searchMenuItem.isVisible = this.model.isSearchable + + this.searchView = searchMenuItem.actionView as SearchView? + this.searchView?.removeMargins() + this.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 onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + when (requestCode) { + RequestCode.NEW_DATA.value -> { + if (this.model.isSelectionInstance && resultCode == Activity.RESULT_OK) { + data?.getStringExtra(EditableDataActivity.IntentKey.PRIMARY_KEY.keyName)?.let { id -> + finishActivityWithSelection(id) + } ?: throw PAIllegalStateException("identifier not found") + } + } + } + } + + private fun finishActivityWithSelection(identifier: String) { + val intent = Intent() + intent.putExtra(BundleKey.PRIMARY_KEY.value, identifier) + this.activity?.setResult(Activity.RESULT_OK, intent) + this.activity?.finish() + } + + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { + + if (this.model.isSelectionInstance) { // Ends the activity and go back to previous screen + val identifier = (row as Identifiable).id + finishActivityWithSelection(identifier) + } else { + + when (this.model.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 + this.model.dataType.openEditActivity(this, identifier, REQUEST_CODE_DETAILS) + } + } + } + + } + + /** + * Filter the items list with the given search content + */ + private fun filterItemsWithSearch(searchContent: String?) { + val items = getRealm().find(this.model.identifiableClass, searchContent) + this.model.setItemsList(items) + this.dataListAdapter.notifyDataSetChanged() + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataListViewModel.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataListViewModel.kt new file mode 100644 index 00000000..eaa8a6b8 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataListViewModel.kt @@ -0,0 +1,55 @@ +package net.pokeranalytics.android.ui.modules.datalist + +import androidx.lifecycle.ViewModel +import io.realm.RealmResults +import net.pokeranalytics.android.model.LiveData +import net.pokeranalytics.android.model.interfaces.Deletable +import net.pokeranalytics.android.ui.adapter.LiveRowRepresentableDataSource +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowViewType + +class DataListViewModel : ViewModel(), LiveRowRepresentableDataSource { + + /*** + * The managed data type + */ + lateinit var dataType: LiveData + + /*** + * Returns the selected item on row selection + */ + var isSelectionInstance: Boolean = false + + lateinit var items: RealmResults + + /*** + * The item ids to load + */ + var itemIds: Array? = null + + val isSearchable: Boolean + get() { return this.dataType.isSearchable } + + val identifiableClass: Class + get() { return this.dataType.relatedEntity } + + var showAddButton: Boolean = false + + 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 + } + + fun setItemsList(items: RealmResults) { + this.items = items + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataSelectionDialogFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataSelectionDialogFragment.kt new file mode 100644 index 00000000..cf5e8bd3 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataSelectionDialogFragment.kt @@ -0,0 +1,67 @@ +package net.pokeranalytics.android.ui.modules.datalist + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import net.pokeranalytics.android.R + + +class DataSelectionDialogFragment : DialogFragment() { + + companion object { + + fun newInstance(): DataSelectionDialogFragment { + val df = DataSelectionDialogFragment() + return df + } + + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_data_selection_dialog, container, false) + } + +// override fun onResume() { +// super.onResume() +// +// val params: ViewGroup.LayoutParams = dialog.window.attributes +// params.width = ViewGroup.LayoutParams.MATCH_PARENT +// params.height = ViewGroup.LayoutParams.MATCH_PARENT +// dialog.window.attributes = params as WindowManager.LayoutParams +// } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initDataListFragment() + } + + private fun initDataListFragment() { + + val dataListFragment = DataListFragment() + this.showFragment(dataListFragment, childFragmentManager) + +//// val dataType = this.arguments?.getInt(BundleKey.DATA_TYPE.value) ?: throw PAIllegalStateException("undefined datatype") +//// val itemIds = this.arguments?.getStringArray(BundleKey.ITEM_IDS.value) +//// dataListFragment.setData(dataType, itemIds) +//// +//// showFragment(dataListFragment) +//// +// } + } + + fun showFragment(fragment: Fragment, fragmentManager: FragmentManager) { + val fragmentTransaction = fragmentManager.beginTransaction() + fragmentTransaction.replace(R.id.frame_container, fragment) + fragmentTransaction.commit() + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FeedFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedFragment.kt similarity index 65% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/FeedFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedFragment.kt index 31ec10c2..e5d0370c 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FeedFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedFragment.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.fragment +package net.pokeranalytics.android.ui.modules.feed import android.app.Activity.RESULT_OK import android.content.Intent @@ -21,19 +21,24 @@ import net.pokeranalytics.android.model.interfaces.Editable import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Transaction -import net.pokeranalytics.android.ui.activity.* +import net.pokeranalytics.android.model.realm.handhistory.HandHistory +import net.pokeranalytics.android.ui.activity.BillingActivity import net.pokeranalytics.android.ui.activity.components.RequestCode -import net.pokeranalytics.android.ui.adapter.FeedSessionRowRepresentableAdapter -import net.pokeranalytics.android.ui.adapter.FeedTransactionRowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.fragment.components.FilterableFragment -import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode -import net.pokeranalytics.android.ui.interfaces.FilterableType +import net.pokeranalytics.android.ui.modules.data.EditableDataActivity +import net.pokeranalytics.android.ui.modules.datalist.DataListActivity +import net.pokeranalytics.android.ui.modules.filter.FilterActivityRequestCode +import net.pokeranalytics.android.ui.modules.filter.FilterableType +import net.pokeranalytics.android.ui.modules.filter.FiltersActivity +import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity +import net.pokeranalytics.android.ui.modules.session.SessionActivity import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.SmoothScrollLinearLayoutManager import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.extensions.count +import timber.log.Timber import java.text.SimpleDateFormat import java.util.* @@ -41,13 +46,15 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { private enum class Tab { SESSIONS, - TRANSACTIONS + TRANSACTIONS, + HAND_HISTORY } companion object { fun newInstance(): FeedFragment { - val fragment = FeedFragment() + val fragment = + FeedFragment() val bundle = Bundle() fragment.arguments = bundle return fragment @@ -57,12 +64,15 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { private var menuPosition: Int = 0 - private var currentTab = Tab.SESSIONS + private var currentTab = + Tab.SESSIONS - private lateinit var feedSessionAdapter: FeedSessionRowRepresentableAdapter - private lateinit var feedTransactionAdapter: FeedTransactionRowRepresentableAdapter + private lateinit var sessionAdapter: FeedSessionRowRepresentableAdapter + private lateinit var transactionAdapter: FeedTransactionRowRepresentableAdapter + private lateinit var handHistoryAdapter: FeedHandHistoryRowRepresentableAdapter private lateinit var realmTransactions: RealmResults + private lateinit var realmHandHistories: RealmResults private lateinit var betaLimitDate: Date private var newSessionCreated: Boolean = false @@ -70,33 +80,44 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { private var selectedTransaction: Transaction? = null private var selectedTransactionPosition: Int = -1 - override val observedEntities: List> = listOf(Session::class.java, Transaction::class.java) + override val observedEntities: List> = + listOf(Session::class.java, Transaction::class.java, HandHistory::class.java) - override fun entitiesChanged(clazz: Class, results: RealmResults) { + override fun entitiesChanged( + clazz: Class, + results: RealmResults + ) { super.entitiesChanged(clazz, results) when (clazz.kotlin) { Session::class -> { - this.feedSessionAdapter.refreshData() - this.feedSessionAdapter.notifyDataSetChanged() + this.sessionAdapter.refreshData() + this.sessionAdapter.notifyDataSetChanged() } Transaction::class -> { - this.feedTransactionAdapter.refreshData() - this.feedTransactionAdapter.notifyDataSetChanged() + this.transactionAdapter.refreshData() + this.transactionAdapter.notifyDataSetChanged() + } + HandHistory::class -> { + this.handHistoryAdapter.refreshData() + this.handHistoryAdapter.notifyDataSetChanged() } } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { super.onCreateView(inflater, container, savedInstanceState) return inflater.inflate(R.layout.fragment_feed, container, false) } - override fun onCreateContextMenu(menu: ContextMenu?, v: View?, menuInfo: ContextMenu.ContextMenuInfo?) { + override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { super.onCreateContextMenu(menu, v, menuInfo) - - if (v?.id == R.id.menuRecyclerView) { + if (v.id == R.id.menuRecyclerView) { activity?.menuInflater?.inflate(R.menu.menu_session, menu) } @@ -109,20 +130,17 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { itemView.showContextMenu() } - override fun onContextItemSelected(item: MenuItem?): Boolean { + override fun onContextItemSelected(item: MenuItem): Boolean { - when (item?.itemId) { + when (item.itemId) { R.id.duplicate -> { - - val sessionId = this.feedSessionAdapter.sessionIdForPosition(menuPosition) - if (sessionId != null) { - createNewSession(true, sessionId = sessionId, duplicate = true) + val session = this.sessionAdapter.sessionForPosition(menuPosition) + if (session != null) { + createNewSession(true, sessionId = session.id, duplicate = true) } else { throw PAIllegalStateException("Session not found for duplicate at position: $menuPosition") } } - else -> { - } } return true @@ -168,6 +186,7 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { 0 -> createNewSession(false) 1 -> createNewSession(true) 2 -> createNewTransaction() + 3 -> createNewHandHistory() } } else if (requestCode == RequestCode.FEED_TRANSACTION_DETAILS.value && resultCode == RESULT_OK && data != null) { if (data.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName) != null) { @@ -175,12 +194,17 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { } } else if (requestCode == FilterActivityRequestCode.CREATE_FILTER.ordinal && resultCode == RESULT_OK) { data?.let { - this.saveFilter(this.requireContext(), it.getStringExtra(FiltersActivity.IntentKey.FILTER_ID.keyName)) + this.saveFilter( + this.requireContext(), + it.getStringExtra(FiltersActivity.IntentKey.FILTER_ID.keyName) + ) } } else if (requestCode == RequestCode.NEW_TRANSACTION.value && resultCode == RESULT_OK) { this.selectTab(Tab.TRANSACTIONS) } else if (requestCode == RequestCode.NEW_SESSION.value && resultCode == RESULT_OK) { this.selectTab(Tab.SESSIONS) + } else if (requestCode == RequestCode.NEW_HAND_HISTORY.value && resultCode == RESULT_OK) { + this.selectTab(Tab.HAND_HISTORY) } } @@ -190,9 +214,12 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { realmTransactions.removeAllChangeListeners() } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { when (row) { - is Session -> SessionActivity.newInstance(requireContext(), sessionId = (row as Editable).id) + is Session -> SessionActivity.newInstance( + requireContext(), + sessionId = (row as Editable).id + ) is Transaction -> { selectedTransaction = row selectedTransactionPosition = position @@ -203,6 +230,9 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { RequestCode.FEED_TRANSACTION_DETAILS.value ) } + is HandHistory -> { + HandHistoryActivity.newInstance(this, row.id) + } } } @@ -211,11 +241,16 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { */ private fun initUI() { - this.feedSessionAdapter = FeedSessionRowRepresentableAdapter(getRealm(), this) + this.sessionAdapter = + FeedSessionRowRepresentableAdapter( + getRealm(), + this + ) registerForContextMenu(this.menuRecyclerView) - val messageToShow: Preferences.FeedMessage? = Preferences.feedMessageToShow(requireContext()) + val messageToShow: Preferences.FeedMessage? = + Preferences.feedMessageToShow(requireContext()) if (messageToShow != null) { messageBox.isVisible = true @@ -224,8 +259,8 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { messageToShow.actionResId?.let { messageBoxAction.text = requireContext().getString(it) messageToShow.action(requireContext())?.let { action -> - messageBoxAction.setOnClickListener { - action.invoke(it) + messageBoxAction.setOnClickListener { view -> + action.invoke(view) hideMessageBox(messageToShow) } } @@ -260,6 +295,9 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { Tab.TRANSACTIONS.ordinal -> { currentFilterable = FilterableType.TRANSACTION } + Tab.HAND_HISTORY.ordinal -> { + currentFilterable = FilterableType.HAND_HISTORY + } } tabChanged(tab.position) } @@ -304,10 +342,10 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { when (filter?.filterableType) { FilterableType.SESSION -> { - this.feedSessionAdapter.filter = filter + this.sessionAdapter.filter = filter } else -> { - this.feedSessionAdapter.filter = null + this.sessionAdapter.filter = null } } } @@ -322,22 +360,62 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { } // Transactions - this.realmTransactions = transactionFilter?.results() ?: run { getRealm().where().findAll() } + this.realmTransactions = + transactionFilter?.results() ?: run { getRealm().where().findAll() } this.realmTransactions = this.realmTransactions.sort("date", Sort.DESCENDING) var distinctDateTransactions = transactionFilter?.results("year", "month") ?: run { getRealm().where().distinct("year", "month").findAll() } distinctDateTransactions = distinctDateTransactions.sort("date", Sort.DESCENDING) - this.feedTransactionAdapter = - FeedTransactionRowRepresentableAdapter(this, realmTransactions, distinctDateTransactions) + this.transactionAdapter = + FeedTransactionRowRepresentableAdapter( + this, + realmTransactions, + distinctDateTransactions + ) + + } + + private fun loadHandHistories(filter: Filter? = null) { + val handHistoryFilter: Filter? = filter?.let { + if (it.filterableType == FilterableType.HAND_HISTORY) { + it + } else { + null + } + } + + // Transactions + this.realmHandHistories = + handHistoryFilter?.results() ?: run { getRealm().where().findAll() } + this.realmHandHistories = this.realmHandHistories.sort("date", Sort.DESCENDING) + + this.realmHandHistories.forEach { + Timber.d("date = ${it.date}, year=${it.year}, month = ${it.month}, day = ${it.dayOfMonth}") + } + + var hhDistinctDates = handHistoryFilter?.results("year", "month", "dayOfMonth") + ?: getRealm().where().distinct("year", "month", "dayOfMonth").findAll() + + hhDistinctDates = hhDistinctDates.sort("date", Sort.DESCENDING) + this.handHistoryAdapter = + FeedHandHistoryRowRepresentableAdapter( + this, + realmHandHistories, + hhDistinctDates + ) } /** * Create a new cash game */ - private fun createNewSession(isTournament: Boolean, sessionId: String? = null, duplicate: Boolean = false) { + private fun createNewSession( + isTournament: Boolean, + sessionId: String? = null, + duplicate: Boolean = false + ) { val sessionCount = getRealm().count(Session::class.java) if (!AppGuard.isProUser && sessionCount >= AppGuard.MAX_SESSIONS_BEFORE_REQUESTING_SUBSCRIPTION) { // && !BuildConfig.DEBUG @@ -374,7 +452,38 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { } } - EditableDataActivity.newInstanceForResult(this, LiveData.TRANSACTION, null, RequestCode.NEW_TRANSACTION.value) + EditableDataActivity.newInstanceForResult( + this, + LiveData.TRANSACTION, + null, + RequestCode.NEW_TRANSACTION.value + ) + } + + /*** + * Create a new hand history + */ + private fun createNewHandHistory() { + +// val intent = Intent(requireContext(), TestActivity::class.java) +// startActivity(intent) +// return + + AppGuard.endOfUse?.let { endDate -> + if (Date().after(endDate)) { + this.showEndOfUseMessage() + return + } + } + + // gets the first session of the adapter - the last created - to preconfigure the HH + if (this.sessionAdapter.itemCount > 0) { + this.sessionAdapter.sessionForPosition(0)?.let { session -> + HandHistoryActivity.newInstance(this, session, false) + } ?: throw PAIllegalStateException("Cannot happen") + } else { + HandHistoryActivity.newInstance(this) + } } /** @@ -399,7 +508,7 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { ).show() } - // Filter Handler +// Filter Handler override fun applyFilter() { super.applyFilter() @@ -407,6 +516,7 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { val filter: Filter? = this.currentFilter(this.requireContext(), getRealm()) this.loadSessions(filter) this.loadTransactions(filter) + this.loadHandHistories(filter) filter?.let { when (it.filterableType) { @@ -432,6 +542,7 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { super.removeFilter() this.loadSessions() this.loadTransactions() + this.loadHandHistories() this.setAdapter() } @@ -448,9 +559,14 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate { private fun setAdapter() { when (this.currentTab) { - Tab.SESSIONS -> menuRecyclerView.adapter = feedSessionAdapter - Tab.TRANSACTIONS -> menuRecyclerView.adapter = feedTransactionAdapter + Tab.SESSIONS -> menuRecyclerView.adapter = sessionAdapter + Tab.TRANSACTIONS -> menuRecyclerView.adapter = transactionAdapter + Tab.HAND_HISTORY -> menuRecyclerView.adapter = handHistoryAdapter } } + fun activityResumed() { + this.sessionAdapter.notifyDataSetChanged() // refreshes session durations + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedHandHistoryRowRepresentableAdapter.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedHandHistoryRowRepresentableAdapter.kt new file mode 100644 index 00000000..45e5fc30 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedHandHistoryRowRepresentableAdapter.kt @@ -0,0 +1,159 @@ +package net.pokeranalytics.android.ui.modules.feed + +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.RealmResults +import net.pokeranalytics.android.R +import net.pokeranalytics.android.exceptions.PAIllegalStateException +import net.pokeranalytics.android.model.realm.handhistory.HandHistory +import net.pokeranalytics.android.ui.adapter.BindableHolder +import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate +import net.pokeranalytics.android.ui.modules.handhistory.views.RowHandHistoryViewHolder +import net.pokeranalytics.android.ui.view.RowViewType +import net.pokeranalytics.android.util.NULL_TEXT +import net.pokeranalytics.android.util.extensions.longDate +import java.util.* +import kotlin.collections.HashMap + + +/** + * An adapter capable of displaying a list of RowRepresentables + * @param delegate the delegate, notified of UI actions + */ +class FeedHandHistoryRowRepresentableAdapter( + var delegate: RowRepresentableDelegate? = null, + private var realmHandHistories: RealmResults, + private var distinctHandHistoryHeaders: RealmResults +) : + RecyclerView.Adapter() { + + private var headersPositions = HashMap() + private lateinit var sortedHeaders: SortedMap + + init { + refreshData() + } + + /** + * Display a header + */ + inner class HeaderTitleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + BindableHolder { + fun bind(title: String) { + // Title + itemView.findViewById(R.id.title)?.let { + it.text = title + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return if (viewType == RowViewType.HAND_HISTORY.ordinal) { + val layout = LayoutInflater.from(parent.context).inflate(R.layout.row_hand_history_view, parent, false) + RowHandHistoryViewHolder(layout) + } else { + val layout = LayoutInflater.from(parent.context).inflate(R.layout.row_header_title, parent, false) + HeaderTitleViewHolder(layout) + } + + } + + override fun getItemViewType(position: Int): Int { + return if (this.sortedHeaders.containsKey(position)) { + RowViewType.HEADER_TITLE.ordinal + } else { + RowViewType.HAND_HISTORY.ordinal + } + } + + override fun getItemCount(): Int { + return this.realmHandHistories.size + this.distinctHandHistoryHeaders.size + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is RowHandHistoryViewHolder) { + val hh = getHandHistoryForPosition(position) ?: throw PAIllegalStateException("Should not happen") + holder.bind(position, hh, this.delegate) + } else if (holder is HeaderTitleViewHolder) { + holder.bind(getHeaderForPosition(position)) + } + } + + /** + * Return the header + */ + private fun getHeaderForPosition(position: Int): String { + if (this.sortedHeaders.containsKey(position)) { + val realmHeaderPosition = this.sortedHeaders.keys.indexOf(position) + return this.distinctHandHistoryHeaders[realmHeaderPosition]?.date?.longDate() ?: "" + } + return NULL_TEXT + } + + /** + * Get real index + */ + private fun getHandHistoryForPosition(position: Int): HandHistory? { + + // Row position + var headersBefore = 0 + for (key in this.sortedHeaders.keys) { + if (position > key) { + headersBefore++ + } else { + break + } + } + + return this.realmHandHistories[position - headersBefore] + } + + /** + * Refresh headers positions + */ + fun refreshData() { + + this.headersPositions.clear() + + var previousYear = Int.MAX_VALUE + var previousMonth = Int.MAX_VALUE + var previousDay = Int.MAX_VALUE + + val calendar = Calendar.getInstance() + + // Add headers if the date doesn't exist yet + for ((index, handHistory) in this.realmHandHistories.withIndex()) { + calendar.time = handHistory.date + if (checkHeaderCondition(calendar, previousYear, previousMonth, previousDay)) { + this.headersPositions[index + this.headersPositions.size] = handHistory.date + previousYear = calendar.get(Calendar.YEAR) + previousMonth = calendar.get(Calendar.MONTH) + previousDay = calendar.get(Calendar.DAY_OF_MONTH) + } + } + + this.sortedHeaders = this.headersPositions.toSortedMap() + +// Timber.d("]]] this.sortedHeaders = ${this.sortedHeaders}") + + } + + /** + * Check if we need to add a header + */ + private fun checkHeaderCondition(currentCalendar: Calendar, previousYear: Int, previousMonth: Int, previousDay: Int): Boolean { + + val year = currentCalendar.get(Calendar.YEAR) + val month = currentCalendar.get(Calendar.MONTH) + val day = currentCalendar.get(Calendar.DAY_OF_MONTH) + + return (year == previousYear && month == previousMonth && day < previousDay) + || (year == previousYear && month < previousMonth) + || year < previousYear + + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/adapter/FeedSessionRowRepresentableAdapter.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedSessionRowRepresentableAdapter.kt similarity index 94% rename from app/src/main/java/net/pokeranalytics/android/ui/adapter/FeedSessionRowRepresentableAdapter.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedSessionRowRepresentableAdapter.kt index 14a76ced..90c7a99c 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/adapter/FeedSessionRowRepresentableAdapter.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedSessionRowRepresentableAdapter.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.adapter +package net.pokeranalytics.android.ui.modules.feed import android.content.Context import android.view.LayoutInflater @@ -15,7 +15,8 @@ import kotlinx.android.synthetic.main.row_feed_session.view.* import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Session -import net.pokeranalytics.android.ui.view.BindableHolder +import net.pokeranalytics.android.ui.adapter.BindableHolder +import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.util.extensions.getMonthAndYear import timber.log.Timber @@ -71,7 +72,8 @@ class FeedSessionRowRepresentableAdapter( /** * Display a session view */ - inner class RowSessionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { + inner class RowSessionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + BindableHolder { fun bind(position: Int, row: Session?, adapter: FeedSessionRowRepresentableAdapter) { @@ -92,7 +94,8 @@ class FeedSessionRowRepresentableAdapter( /** * Display a session view */ - inner class HeaderTitleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { + inner class HeaderTitleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + BindableHolder { fun bind(title: String) { // Title itemView.findViewById(net.pokeranalytics.android.R.id.title)?.let { @@ -148,8 +151,8 @@ class FeedSessionRowRepresentableAdapter( throw PAIllegalStateException("Any position should always have a header, position = $position") } - fun sessionIdForPosition(position: Int): String? { - return this.getSessionForPosition(position)?.id + fun sessionForPosition(position: Int): Session? { + return this.getSessionForPosition(position) } /** diff --git a/app/src/main/java/net/pokeranalytics/android/ui/adapter/FeedTransactionRowRepresentableAdapter.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedTransactionRowRepresentableAdapter.kt similarity index 94% rename from app/src/main/java/net/pokeranalytics/android/ui/adapter/FeedTransactionRowRepresentableAdapter.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedTransactionRowRepresentableAdapter.kt index e66001d4..3d8afe9f 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/adapter/FeedTransactionRowRepresentableAdapter.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedTransactionRowRepresentableAdapter.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.adapter +package net.pokeranalytics.android.ui.modules.feed import android.view.LayoutInflater import android.view.View @@ -9,7 +9,8 @@ import io.realm.RealmResults import kotlinx.android.synthetic.main.row_transaction.view.* import net.pokeranalytics.android.R import net.pokeranalytics.android.model.realm.Transaction -import net.pokeranalytics.android.ui.view.BindableHolder +import net.pokeranalytics.android.ui.adapter.BindableHolder +import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.extensions.getMonthAndYear @@ -39,7 +40,8 @@ class FeedTransactionRowRepresentableAdapter( /** * Display a transaction view */ - inner class RowTransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { + inner class RowTransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + BindableHolder { fun bind(position: Int, row: Transaction?, adapter: FeedTransactionRowRepresentableAdapter) { @@ -54,7 +56,8 @@ class FeedTransactionRowRepresentableAdapter( /** * Display a header */ - inner class HeaderTitleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { + inner class HeaderTitleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + BindableHolder { fun bind(title: String) { // Title itemView.findViewById(R.id.title)?.let { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/NewDataMenuActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/NewDataMenuActivity.kt similarity index 96% rename from app/src/main/java/net/pokeranalytics/android/ui/activity/NewDataMenuActivity.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/feed/NewDataMenuActivity.kt index 3daab915..fe32048d 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/NewDataMenuActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/NewDataMenuActivity.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.activity +package net.pokeranalytics.android.ui.modules.feed import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -79,6 +79,10 @@ class NewDataMenuActivity : BaseActivity() { finishWithResult(2) } + newHandHistory.setOnClickListener { + finishWithResult(3) + } + container.setOnClickListener { hideMenu() } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FilterDetailsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterDetailsFragment.kt similarity index 75% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/FilterDetailsFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterDetailsFragment.kt index 481557e5..2103f1c0 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FilterDetailsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterDetailsFragment.kt @@ -1,19 +1,16 @@ -package net.pokeranalytics.android.ui.fragment +package net.pokeranalytics.android.ui.modules.filter -import android.app.Activity.RESULT_OK import android.content.Context -import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.fragment_filter_details.* import kotlinx.android.synthetic.main.fragment_filter_details.view.* import net.pokeranalytics.android.R import net.pokeranalytics.android.model.filter.QueryCondition -import net.pokeranalytics.android.model.realm.Filter -import net.pokeranalytics.android.ui.activity.FilterDetailsActivity import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource @@ -22,7 +19,6 @@ import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheet import net.pokeranalytics.android.ui.helpers.DateTimePickerManager import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType -import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow import net.pokeranalytics.android.util.NULL_TEXT @@ -32,11 +28,13 @@ import kotlin.collections.ArrayList open class FilterDetailsFragment : RealmFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { - lateinit var rowRepresentableAdapter: RowRepresentableAdapter - private lateinit var primaryKey: String - private lateinit var filterCategoryRow: FilterCategoryRow + val model: FilterViewModel by lazy { + ViewModelProviders.of(requireActivity()).get(FilterViewModel::class.java) + } + + private lateinit var rowRepresentableAdapter: RowRepresentableAdapter - private var currentFilter: Filter? = null +// private var currentFilter: Filter? = null private var rows: ArrayList = ArrayList() private var rowsForFilterSubcategoryRow: HashMap> = HashMap() @@ -56,10 +54,71 @@ open class FilterDetailsFragment : RealmFragment(), StaticRowRepresentableDataSo override fun onBackPressed() { super.onBackPressed() saveData() +// requireFragmentManager().popBackStackImmediate("f1", 0) +// requireFragmentManager().popBackStackImmediate() + } + + + /** + * Init UI + */ + private fun initUI() { + + setDisplayHomeAsUpEnabled(true) + + this.appBar.toolbar.title = getString(R.string.filter) + + val viewManager = LinearLayoutManager(requireContext()) + + recyclerView.apply { + setHasFixedSize(true) + layoutManager = viewManager + } + } + + /** + * Init data + */ + private fun initData() { + +// this.arguments?.let { bundle -> +// +// bundle.getString(BundleKey.PRIMARY_KEY.value)?.let { filterId -> +// this.currentFilter = getRealm().findById(filterId) +// } +// +// val category = bundle.getInt(BundleKey.DATA_TYPE.value) +// this.filterCategoryRow = FilterCategoryRow.values()[category] +// +// } ?: throw PAIllegalStateException("Missing bundle") + + //currentFilter = Filter.getFilterBydId(getRealm(), primaryKey) +// currentFilter = FiltersFragment.currentFilter + + Timber.d(">> Filter = ${this.model.currentFilter}") + Timber.d("selectedRow = ${this.model.selectedCategoryRow}") + + val filterCategoryRow = this.model.filterCategoryRow + + this.appBar.toolbar.title = filterCategoryRow.localizedTitle(requireContext()) + + this.rows.clear() + this.rowsForFilterSubcategoryRow.clear() + this.rows.addAll(filterCategoryRow.filterElements) + + this.rows.forEach { element -> + if (element is QueryCondition && this.model.currentFilter?.contains(element) == true) { + this.model.currentFilter?.loadValueForElement(element) + this.selectedRows.add(element) + } + } + + this.rowRepresentableAdapter = RowRepresentableAdapter(this, this) + this.recyclerView.adapter = rowRepresentableAdapter } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - super.onRowSelected(position, row, fromAction) + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { + super.onRowSelected(position, row, tag) if (row.viewType == RowViewType.TITLE_CHECK.ordinal) { updateRowsSelection(row) @@ -83,7 +142,7 @@ open class FilterDetailsFragment : RealmFragment(), StaticRowRepresentableDataSo minutes = if (it % 60 > 0) (it % 60).toString() else null } val data = row.editingDescriptors(mapOf("hours" to hours, "minutes" to minutes)) - BottomSheetFragment.create(fragmentManager, row, this, data, true) + BottomSheetFragment.create(requireFragmentManager(), row, this, data, true) } is QueryCondition.ListOfValues<*> -> { var valueAsString: String? = null @@ -91,19 +150,23 @@ open class FilterDetailsFragment : RealmFragment(), StaticRowRepresentableDataSo valueAsString = row.listOfValues.firstOrNull()?.toString() } val data = row.editingDescriptors(mapOf("valueAsString" to valueAsString)) - BottomSheetFragment.create(fragmentManager, row, this, data, true) + BottomSheetFragment.create(requireFragmentManager(), row, this, data, true) } } } - override fun stringForRow(row: RowRepresentable, context: Context): String { + override fun charSequenceForRow(row: RowRepresentable, context: Context): CharSequence { return when (row) { is QueryCondition.ListOfValues<*> -> row.firstValue(context) - else -> super.stringForRow(row) + else -> super.charSequenceForRow(row, context, 0) } ?: NULL_TEXT } - override fun isSelected(row: RowRepresentable): Boolean { + override fun isSelected( + position: Int, + row: RowRepresentable, + tag: Int + ): Boolean { return selectedRows.contains(row) } @@ -163,48 +226,6 @@ open class FilterDetailsFragment : RealmFragment(), StaticRowRepresentableDataSo return if (rowViewType != -1) rowViewType else RowViewType.TITLE_CHECK.ordinal } - /** - * Init UI - */ - private fun initUI() { - - setDisplayHomeAsUpEnabled(true) - - this.appBar.toolbar.title = getString(R.string.filter) - - val viewManager = LinearLayoutManager(requireContext()) - - recyclerView.apply { - setHasFixedSize(true) - layoutManager = viewManager - } - } - - /** - * Init data - */ - private fun initData() { - - //currentFilter = Filter.getFilterBydId(getRealm(), primaryKey) - currentFilter = FiltersFragment.currentFilter - - this.appBar.toolbar.title = filterCategoryRow.localizedTitle(requireContext()) - - this.rows.clear() - this.rowsForFilterSubcategoryRow.clear() - this.rows.addAll(filterCategoryRow.filterElements) - - this.rows.forEach { element -> - if (element is QueryCondition && currentFilter?.contains(element) == true) { - currentFilter?.loadValueForElement(element) - this.selectedRows.add(element) - } - } - - this.rowRepresentableAdapter = RowRepresentableAdapter(this, this) - this.recyclerView.adapter = rowRepresentableAdapter - } - /** * Update rows selection */ @@ -230,47 +251,42 @@ open class FilterDetailsFragment : RealmFragment(), StaticRowRepresentableDataSo println("list of selected rows : $selectedRows") // Update UI - rowRepresentableAdapter.refreshRow(row) + this.rowRepresentableAdapter.refreshRow(row) } /** * Save data */ private fun saveData() { + + val currentFilter = this.model.currentFilter + //TODO: Save currentFilter details data Timber.d("Save data for queryWith: ${currentFilter?.id}") - selectedRows.forEach { + this.selectedRows.forEach { Timber.d("Selected rows: $it") } getRealm().executeTransaction { - currentFilter?.remove(filterCategoryRow) - currentFilter?.createOrUpdateFilterConditions(selectedRows) + currentFilter?.remove(this.model.filterCategoryRow) + currentFilter?.createOrUpdateFilterConditions(this.selectedRows) } currentFilter?.filterConditions?.forEach { Timber.d("Condition: $it") } - finishActivityWithResult(currentFilter?.id) +// finishActivityWithResult(currentFilter?.id) } /** * Finish the activity with a result */ - private fun finishActivityWithResult(uniqueIdentifier: String?) { - val intent = Intent() - intent.putExtra(FilterDetailsActivity.IntentKey.FILTER_ID.keyName, uniqueIdentifier) - activity?.setResult(RESULT_OK, intent) - activity?.finish() - } - - /** - * Set fragment data - */ - fun setData(primaryKey: String, filterCategory: Int) { - this.primaryKey = primaryKey - this.filterCategoryRow = FilterCategoryRow.values()[filterCategory] - } +// private fun finishActivityWithResult(uniqueIdentifier: String?) { +// val intent = Intent() +// intent.putExtra(FilterDetailsActivity.IntentKey.FILTER_ID.keyName, uniqueIdentifier) +// activity?.setResult(RESULT_OK, intent) +// activity?.finish() +// } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/interfaces/FilterHandler.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterHandler.kt similarity index 91% rename from app/src/main/java/net/pokeranalytics/android/ui/interfaces/FilterHandler.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterHandler.kt index 8e8c1167..e7d73757 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/interfaces/FilterHandler.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterHandler.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.interfaces +package net.pokeranalytics.android.ui.modules.filter import android.content.Context import android.content.Intent @@ -6,7 +6,6 @@ import androidx.fragment.app.Fragment import io.realm.Realm import io.realm.kotlin.where import net.pokeranalytics.android.model.realm.Filter -import net.pokeranalytics.android.ui.activity.FiltersActivity import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.enumerations.IntIdentifiable import net.pokeranalytics.android.util.enumerations.IntSearchable @@ -56,7 +55,8 @@ interface FilterHandler { // Send broadcast val intent = Intent() - intent.action = INTENT_FILTER_UPDATE_FILTER_UI + intent.action = + INTENT_FILTER_UPDATE_FILTER_UI context.sendBroadcast(intent) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterViewModel.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterViewModel.kt new file mode 100644 index 00000000..450c633a --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterViewModel.kt @@ -0,0 +1,76 @@ +package net.pokeranalytics.android.ui.modules.filter + +import android.content.Context +import androidx.lifecycle.ViewModel +import io.realm.Realm +import net.pokeranalytics.android.exceptions.PAIllegalStateException +import net.pokeranalytics.android.model.realm.Filter +import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow +import net.pokeranalytics.android.util.extensions.findById + +class FilterViewModel : ViewModel(), StaticRowRepresentableDataSource { + + var currentFilter: Filter? = null + + // Main + var filterableType: FilterableType? = null + + var filterCopy: Filter? = null + private var categoryRows: ArrayList = ArrayList() + var primaryKey: String? = null + var selectedCategoryRow: RowRepresentable? = null + var isUpdating = false + + // Details + val filterCategoryRow: FilterCategoryRow + get() { + return this.selectedCategoryRow as FilterCategoryRow + } + + fun init(realm: Realm) { + + if (this.currentFilter != null) { // can be called twice and we don't want that + return + } + + this.primaryKey?.let { + + val filter = realm.findById(it) ?: throw PAIllegalStateException("Can't find filter with id=$it") + this.currentFilter = realm.copyFromRealm(filter) + this.isUpdating = true + } ?: run { + this.filterableType?.uniqueIdentifier?.let { + this.currentFilter = Filter.newInstance(it) //realm.copyFromRealm(Filter.newInstanceForResult(realm, this.filterableType.ordinal)) + } + } + + // Create a copy if the user cancels the updates + this.currentFilter?.let { + if (it.isValid && it.isManaged) { + this.filterCopy = realm.copyFromRealm(it) + } + } + + this.categoryRows.clear() + + this.filterableType?.let { + this.categoryRows.addAll(FilterCategoryRow.values(it)) + } + + } + + // Data source + + override fun adapterRows(): List? { + return this.categoryRows + } + + override fun charSequenceForRow(row: RowRepresentable, context: Context, tag: Int): CharSequence { + // Return the number of selected filters for this category + val count = this.currentFilter?.countBy(row as FilterCategoryRow) ?: 0 + return if (count > 0) count.toString() else "" + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersActivity.kt new file mode 100644 index 00000000..28ec56ce --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersActivity.kt @@ -0,0 +1,122 @@ +package net.pokeranalytics.android.ui.modules.filter + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProviders +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.activity.components.BaseActivity +import net.pokeranalytics.android.ui.fragment.components.BaseFragment + +class FiltersActivity : BaseActivity() { + + val model: FilterViewModel by lazy { + ViewModelProviders.of(this).get(FilterViewModel::class.java) + } + + 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 + + private var currentFragment: BaseFragment? = null + + 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() { + this.currentFragment?.onBackPressed() + + if (supportFragmentManager.backStackEntryCount > 0) { + supportFragmentManager.popBackStack() + + val ff = supportFragmentManager.findFragmentByTag(Tag.CATEGORIES.identifier) as FiltersFragment + ff.refreshView() + this.currentFragment = ff + + } else { + this.finish() + } + + } + + /** + * Init UI + */ + private fun initUI() { + + 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) + + this.model.primaryKey = filterId + this.model.filterableType = FilterableType.valueByIdentifier(uniqueIdentifier) + + val filtersFragment = FiltersFragment() +// val bundle = Bundle() +// bundle.putString(BaseFragment.BundleKey.PRIMARY_KEY.value, filterId) +// bundle.putInt(BaseFragment.BundleKey.DATA_TYPE.value, uniqueIdentifier) +// fragment.arguments = bundle + +// fragment.setData(filterId, filterableType) + val fragmentTransaction = this.supportFragmentManager.beginTransaction() + fragmentTransaction.add(R.id.container, filtersFragment, Tag.CATEGORIES.identifier) +// fragmentTransaction.addToBackStack(null) + fragmentTransaction.commit() + filtersFragment.updateMostUsedFiltersVisibility(!hideMostUsedFilters) + + } + + fun showDetailsFragment() { + + val detailsFragment = FilterDetailsFragment() + + val fragmentTransaction = this.supportFragmentManager.beginTransaction() + fragmentTransaction.replace(R.id.container, detailsFragment) + fragmentTransaction.addToBackStack(null) + fragmentTransaction.commit() + + this.currentFragment = detailsFragment + + } + + enum class Tag(var identifier: String) { + CATEGORIES("categories"), + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FiltersFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersFragment.kt similarity index 61% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/FiltersFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersFragment.kt index c04d3e02..019b78f1 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FiltersFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersFragment.kt @@ -1,10 +1,11 @@ -package net.pokeranalytics.android.ui.fragment +package net.pokeranalytics.android.ui.modules.filter import android.app.Activity.RESULT_OK import android.content.Intent import android.os.Bundle import android.view.* import androidx.core.view.isVisible +import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.chip.Chip import kotlinx.android.synthetic.main.fragment_editable_data.appBar @@ -12,58 +13,35 @@ import kotlinx.android.synthetic.main.fragment_editable_data.recyclerView import kotlinx.android.synthetic.main.fragment_filters.* import kotlinx.android.synthetic.main.fragment_filters.view.toolbar import net.pokeranalytics.android.R -import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.realm.Filter -import net.pokeranalytics.android.ui.activity.FilterDetailsActivity -import net.pokeranalytics.android.ui.activity.FiltersActivity -import net.pokeranalytics.android.ui.activity.FiltersListActivity import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate -import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.extensions.px import net.pokeranalytics.android.ui.fragment.components.RealmFragment -import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode -import net.pokeranalytics.android.ui.interfaces.FilterableType import net.pokeranalytics.android.ui.view.RowRepresentable -import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow import net.pokeranalytics.android.util.Preferences -import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.sorted import timber.log.Timber -open class FiltersFragment : RealmFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { +open class FiltersFragment : RealmFragment(), RowRepresentableDelegate { + + val model: FilterViewModel by lazy { + ViewModelProviders.of(requireActivity()).get(FilterViewModel::class.java) + } companion object { const val REQUEST_CODE_FILTER_DETAILS = 100 const val MOST_USED_FILTERS_DISPLAYED = 6 - - var currentFilter: Filter? = null - } private lateinit var rowRepresentableAdapter: RowRepresentableAdapter - private var filterCopy: Filter? = null - - private var rows: ArrayList = ArrayList() private var filterMenu: Menu? = null - private var primaryKey: String? = null - private lateinit var filterableType: FilterableType - private var selectedRow: RowRepresentable? = null - private var isUpdating = false private var showMostUsedFiltersLayout = true - /** - * Set fragment data - */ - fun setData(primaryKey: String?, filterableType: FilterableType) { - this.primaryKey = primaryKey - this.filterableType = filterableType - } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { super.onCreateView(inflater, container, savedInstanceState) return inflater.inflate(R.layout.fragment_filters, container, false) @@ -80,19 +58,10 @@ open class FiltersFragment : RealmFragment(), StaticRowRepresentableDataSource, super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_CODE_FILTER_DETAILS && resultCode == RESULT_OK) { - - // Update object - /* - currentFilter?.id?.let { currentFilterId -> - Filter.getFilterBydId(getRealm(), currentFilterId)?.let { filter -> - currentFilter = filter - } - } - */ - - selectedRow?.let { + this.model.selectedCategoryRow?.let { rowRepresentableAdapter.refreshRow(it) } + } else if (requestCode == FilterActivityRequestCode.SELECT_FILTER.ordinal) { updateMostUsedFilters() @@ -104,54 +73,38 @@ open class FiltersFragment : RealmFragment(), StaticRowRepresentableDataSource, } } - override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { - menu?.clear() - inflater?.inflate(R.menu.toolbar_editable_data, menu) + fun refreshView() { + this.model.selectedCategoryRow?.let { + rowRepresentableAdapter.refreshRow(it) + } + + +// this.rowRepresentableAdapter.notifyDataSetChanged() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + menu.clear() + inflater.inflate(R.menu.toolbar_editable_data, menu) this.filterMenu = menu updateMenuUI() super.onCreateOptionsMenu(menu, inflater) } override fun onBackPressed() { - if (isUpdating) { + if (this.model.isUpdating) { cancelUpdates() } else { - activity?.finish() +// activity?.finish() } } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item!!.itemId) { + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { R.id.save -> validateUpdates() } return true } - override fun adapterRows(): List? { - return rows - } - - override fun stringForRow(row: RowRepresentable): String { - // Return the number of selected filters for this category - var selectedFilters = "" - if (row is FilterCategoryRow) { - currentFilter?.countBy(row)?.let { nbFilters -> - if (nbFilters > 0) { - selectedFilters = nbFilters.toString() - } - } - } - return selectedFilters - } - - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - super.onRowSelected(position, row, fromAction) - selectedRow = row - currentFilter?.id?.let { filterId -> - FilterDetailsActivity.newInstanceForResult(this, filterId, (row as FilterCategoryRow).ordinal, REQUEST_CODE_FILTER_DETAILS) - } - } - /** * Init UI */ @@ -168,8 +121,8 @@ open class FiltersFragment : RealmFragment(), StaticRowRepresentableDataSource, } moreFilters.setOnClickListener { - LiveData.FILTER.subType = filterableType.uniqueIdentifier - FiltersListActivity.newSelectInstance(this, LiveData.FILTER.ordinal, false) + LiveData.FILTER.subType = this.model.filterableType?.uniqueIdentifier + FiltersListActivity.newSelectInstance(this, false) } mostUsedFiltersLayout.isVisible = showMostUsedFiltersLayout @@ -180,30 +133,34 @@ open class FiltersFragment : RealmFragment(), StaticRowRepresentableDataSource, */ private fun initData() { - val realm = getRealm() +// this.arguments?.let { bundle -> +// this.model.primaryKey = bundle.getString(BundleKey.PRIMARY_KEY.value) +// val type = bundle.getInt(BundleKey.DATA_TYPE.value) +// this.model.filterableType = FilterableType.valueByIdentifier(type) +// } ?: throw PAIllegalStateException("Missing bundle") - primaryKey?.let { + this.model.init(getRealm()) - val filter = realm.findById(it) ?: throw PAIllegalStateException("Can't find filter with id=$it") - currentFilter = realm.copyFromRealm(filter) - isUpdating = true - } ?: run { - currentFilter = Filter.newInstance(this.filterableType.uniqueIdentifier) //realm.copyFromRealm(Filter.newInstanceForResult(realm, this.filterableType.ordinal)) - } + this.rowRepresentableAdapter = RowRepresentableAdapter(this.model, this) + this.recyclerView.adapter = rowRepresentableAdapter - // Create a copy if the user cancels the updates - currentFilter?.let { - if (it.isValid && it.isManaged) { - filterCopy = getRealm().copyFromRealm(it) - } - } + } - rows.clear() - rows.addAll(FilterCategoryRow.values(this.filterableType)) + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { + super.onRowSelected(position, row, tag) - this.rowRepresentableAdapter = RowRepresentableAdapter(this, this) - this.recyclerView.adapter = rowRepresentableAdapter + this.model.selectedCategoryRow = row + this.model.currentFilter?.id?.let { _ -> + + (activity as FiltersActivity).showDetailsFragment() + +// this.model.filterCategoryRow = row as FilterCategoryRow +// FilterDetailsActivity.newInstanceForResult( +// this, filterId, (row as FilterCategoryRow).ordinal, +// REQUEST_CODE_FILTER_DETAILS +// ) + } } /** @@ -212,10 +169,10 @@ open class FiltersFragment : RealmFragment(), StaticRowRepresentableDataSource, private fun updateMostUsedFilters() { var nbChips = 0 - val filters = getRealm().sorted(Filter::class.java, editableOnly = false, filterableTypeUniqueIdentifier = this.filterableType.uniqueIdentifier) + val filters = getRealm().sorted(Filter::class.java, editableOnly = false, filterableTypeUniqueIdentifier = this.model.filterableType?.uniqueIdentifier) val currentFilterId = Preferences.getActiveFilterId(requireContext()) - if (isUpdating || filters.isEmpty() || (filters.size == 1 && filters.first()?.id == currentFilterId)) { + if (this.model.isUpdating || filters.isEmpty() || (filters.size == 1 && filters.first()?.id == currentFilterId)) { mostUsedFiltersLayout.visibility = View.GONE return } @@ -263,7 +220,9 @@ open class FiltersFragment : RealmFragment(), StaticRowRepresentableDataSource, * Validate the updates of the queryWith */ private fun validateUpdates() { - getRealm().executeTransaction { realm -> + val currentFilter = this.model.currentFilter + + getRealm().executeTransaction { realm -> currentFilter?.let { it.name = it.query.getName(requireContext()) realm.copyToRealmOrUpdate(it) @@ -278,6 +237,9 @@ open class FiltersFragment : RealmFragment(), StaticRowRepresentableDataSource, * Cancel the latest updates of the queryWith */ private fun cancelUpdates() { + + val filterCopy = this.model.filterCopy + val filterId = filterCopy?.id ?: "" getRealm().executeTransaction { realm -> filterCopy?.let { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersListActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersListActivity.kt new file mode 100644 index 00000000..3ac7d569 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersListActivity.kt @@ -0,0 +1,81 @@ +package net.pokeranalytics.android.ui.modules.filter + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProviders +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.LiveData +import net.pokeranalytics.android.ui.activity.components.BaseActivity +import net.pokeranalytics.android.ui.modules.datalist.DataListViewModel + +class FiltersListActivity : BaseActivity() { + + val model: DataListViewModel by lazy { + ViewModelProviders.of(this).get(DataListViewModel::class.java) + } + + enum class IntentKey(val keyName: String) { + DATA_TYPE("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, showAddButton: Boolean = true) { + val context = fragment.requireContext() + fragment.startActivityForResult( + getIntent( + context, + showAddButton + ), FilterActivityRequestCode.SELECT_FILTER.ordinal) + } + + private fun getIntent(context: Context, showAddButton: Boolean = true): Intent { + val intent = Intent(context, FiltersListActivity::class.java) + intent.putExtra(IntentKey.SHOW_ADD_BUTTON.keyName, showAddButton) + return intent + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + initData() + + setContentView(R.layout.activity_filters_list) + + initUI() + } + + private fun initData() { + this.model.dataType = LiveData.FILTER + } + + /** + * Init UI + */ + private fun initUI() { + + val showAddButton = intent.getBooleanExtra(IntentKey.SHOW_ADD_BUTTON.keyName, true) + +// val fragment = filtersListFragment as FiltersListFragment + + this.model.showAddButton = showAddButton + +// fragment.setData(dataType) +// fragment.updateUI(showAddButton) + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersListFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersListFragment.kt new file mode 100644 index 00000000..45b9aaac --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersListFragment.kt @@ -0,0 +1,115 @@ +package net.pokeranalytics.android.ui.modules.filter + +import android.app.Activity +import android.content.Context +import android.content.Intent +import net.pokeranalytics.android.model.realm.Filter +import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetFragment +import net.pokeranalytics.android.ui.modules.filter.FilterHandler.Companion.INTENT_FILTER_UPDATE_FILTER_UI +import net.pokeranalytics.android.ui.modules.datalist.DataListFragment +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.util.Preferences + + +open class FiltersListFragment : DataListFragment() { + +// private var identifiableClass: Class = Filter::class.java +// private var dataType: LiveData = LiveData.FILTER +// private lateinit var items: RealmResults +// +// /** +// * 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 +// } +// +// 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? { +// 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): List? { +// 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, this.model.items, filterId) + if (filterId == Preferences.getActiveFilterId(requireContext())) { + Preferences.setActiveFilterId("", requireContext()) + updateFilterUIIfNecessary(requireContext(), "") + } + } + else -> super.onRowDeleted(row) + } + } + + override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { + when (row) { + is Filter -> { + if (tag == 1) { + val data = row.editingDescriptors(mapOf("defaultValue" to row.name)) + BottomSheetFragment.create(requireFragmentManager(), 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 -> { + super.onRowSelected(position, row, tag) +// val identifier = (row as Identifiable).id +// EditableDataActivity.newInstanceForResult(this, this.dataType, identifier, REQUEST_CODE_DETAILS) + } + } + } + + /** + * Update filter UI + */ + private 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) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryActivity.kt new file mode 100644 index 00000000..769416a1 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryActivity.kt @@ -0,0 +1,204 @@ +package net.pokeranalytics.android.ui.modules.handhistory + +import android.Manifest +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Bundle +import android.os.IBinder +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.realm.Session +import net.pokeranalytics.android.model.realm.handhistory.HandHistory +import net.pokeranalytics.android.ui.activity.components.BaseActivity +import net.pokeranalytics.android.ui.activity.components.RequestCode +import net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService +import net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayerFragment +import timber.log.Timber + + +class HandHistoryActivity : BaseActivity() { + + private var replayExportService: ReplayExportService? = null + + enum class IntentKey(val keyName: String) { + IDENTIFIER("identifier"), + SESSION_CONFIGURATION("session_id"), + ATTACHED("attached") + } + + private var fragment: Fragment? = null + + companion object { + + fun newInstance(fragment: Fragment, session: Session, attached: Boolean) { + val intent = Intent(fragment.requireContext(), HandHistoryActivity::class.java) + intent.putExtra(IntentKey.SESSION_CONFIGURATION.keyName, session.id) + intent.putExtra(IntentKey.ATTACHED.keyName, attached) + fragment.startActivityForResult(intent, RequestCode.NEW_HAND_HISTORY.value) + } + + fun newInstance(fragment: Fragment, id: String? = null) { + val intent = Intent(fragment.requireContext(), HandHistoryActivity::class.java) + id?.let { intent.putExtra(IntentKey.IDENTIFIER.keyName, it) } + fragment.startActivityForResult(intent, RequestCode.NEW_HAND_HISTORY.value) + } + + } + + /** Defines callbacks for service binding, passed to bindService() */ + private val connection = object : ServiceConnection { + + override fun onServiceDisconnected(name: ComponentName?) { + replayExportService = null + } + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val binder = service as ReplayExportService.LocalBinder + replayExportService = binder.getService() + } + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_hand_history) + + initData() + initUI() + } + + private fun initData() { + val intent = Intent(this, ReplayExportService::class.java) + bindService(intent, connection, Context.BIND_AUTO_CREATE) + } + + /** + * Init UI + */ + private fun initUI() { + + val handHistoryId = intent.getStringExtra(IntentKey.IDENTIFIER.keyName) + if (handHistoryId != null) { + showReplayer(handHistoryId) + } else { + showEditMode() + } + + } + + fun showEditMode(handHistoryId: String? = null) { + + val sessionId = intent.getStringExtra(IntentKey.SESSION_CONFIGURATION.keyName) + val attached = intent.getBooleanExtra(IntentKey.ATTACHED.keyName, false) + + val fragment = HandHistoryFragment.newInstance(handHistoryId, sessionId, attached) + showFragment(fragment, R.id.container) + this.fragment = fragment + } + + fun showReplayer(handHistoryId: String) { +// val sessionId = intent.getStringExtra(IntentKey.SESSION_CONFIGURATION.keyName) +// val attached = intent.getBooleanExtra(IntentKey.ATTACHED.keyName, false) + + val fragment = ReplayerFragment.newInstance(handHistoryId) + showFragment(fragment, R.id.container) + this.fragment = fragment + } + + override fun onBackPressed() { + + val shouldShowDataLossWarning = ((this.fragment as? HandHistoryFragment)?.isEditing == true) + + if (shouldShowDataLossWarning) { + + AlertDialog.Builder(this) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.your_attention) + .setMessage(R.string.data_loss_warning) + .setPositiveButton(R.string.yes) { _, _ -> finish() } + .setNegativeButton(R.string.no, null) + .show() + + } else { + super.onBackPressed() + } + + } + + private lateinit var handHistory: HandHistory + + /*** + * Shows a popup with the various export options + */ + fun exportHand(handHistory: HandHistory) { + + this.handHistory = handHistory + + val builder: android.app.AlertDialog.Builder = android.app.AlertDialog.Builder(this) + builder.setTitle(R.string.export) + builder.setItems( + arrayOf( + getString(R.string.text), + getString(R.string.video) +// "GIF" + ) + ) { _, index -> + // The 'which' argument contains the index position + // of the selected item + when (index) { + 0 -> this.textExport() + 1 -> this.videoExportAskForPermission() + 2 -> this.gifExport() + } + } + builder.create().show() + + } + + private fun videoExportAskForPermission() { + + Toast.makeText(this, R.string.video_export_started, Toast.LENGTH_LONG).show() + + askForPermission(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), RequestCode.PERMISSION_WRITE_EXTERNAL_STORAGE.value) { granted -> + if (granted) { + videoExport() + } + } + } + + private fun videoExport() { + + val handHistoryId = this.handHistory.id + this.replayExportService?.export(handHistoryId) ?: run { + Toast.makeText(this, "Export service not available. Please contact support", Toast.LENGTH_LONG).show() + } + + } + + private fun gifExport() { + + } + + private fun textExport() { + + val hhString = this.handHistory.localizedString(this) + Timber.d("hand = $hhString") + + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.type = "text/plain" + shareIntent.putExtra(Intent.EXTRA_TEXT, hhString) + + startActivity(Intent.createChooser(shareIntent, "Share hand history")) + + } + + override fun onDestroy() { + super.onDestroy() + unbindService(connection) + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryAdapter.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryAdapter.kt new file mode 100644 index 00000000..b48f3166 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryAdapter.kt @@ -0,0 +1,641 @@ +package net.pokeranalytics.android.ui.modules.handhistory + +import android.content.res.ColorStateList +import android.text.InputType +import android.view.* +import android.widget.Button +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isEmpty +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.chip.Chip +import kotlinx.android.synthetic.main.row_hand_action.view.* +import kotlinx.android.synthetic.main.row_hand_action_read.view.* +import kotlinx.android.synthetic.main.row_hand_cards.view.* +import kotlinx.android.synthetic.main.row_hand_player_summary.view.* +import kotlinx.android.synthetic.main.row_hhsettings_player_setup.view.* +import kotlinx.android.synthetic.main.row_hhsettings_player_setup.view.position_button +import kotlinx.android.synthetic.main.row_hhsettings_player_setup.view.ps_hand_layout +import kotlinx.android.synthetic.main.row_hhsettings_player_setup_read.view.* +import kotlinx.android.synthetic.main.row_hhsettings_straddle.view.* +import kotlinx.android.synthetic.main.row_recycler.view.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.exceptions.PAIllegalStateException +import net.pokeranalytics.android.model.handhistory.Position +import net.pokeranalytics.android.model.realm.Player +import net.pokeranalytics.android.model.realm.handhistory.Card +import net.pokeranalytics.android.ui.adapter.BindableHolder +import net.pokeranalytics.android.ui.adapter.RecyclerAdapter +import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource +import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate +import net.pokeranalytics.android.ui.extensions.px +import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType +import net.pokeranalytics.android.ui.modules.handhistory.model.* +import net.pokeranalytics.android.ui.modules.handhistory.views.PositionAdapter +import net.pokeranalytics.android.ui.view.PlayerImageView +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.holder.RowViewHolder +import net.pokeranalytics.android.ui.view.rowrepresentable.ViewIdentifier +import net.pokeranalytics.android.util.extensions.formatted + + +enum class HandRowType(var layoutRes: Int) : ViewIdentifier, RowRepresentable { + DEFAULT(R.layout.row_title), + HEADER(R.layout.row_header_value), + SETTINGS_HEADER(R.layout.row_title_icon_arrow), + ACTION(R.layout.row_hand_action), + PLAYER_SUMMARY(R.layout.row_hand_player_summary), + STREET(R.layout.row_hand_cards), + STRADDLE(R.layout.row_hhsettings_straddle), + COMMENT(R.layout.row_hhsettings_comments), + PLAYER_NUMBER(R.layout.row_title_value), + PLAYER_SETUP(R.layout.row_hhsettings_player_setup), + PLAYER_SETUP_READ(R.layout.row_hhsettings_player_setup_read), + ANTE(R.layout.row_title_value), + BIG_BLIND_ANTE(R.layout.row_title_switch), + BIG_BLIND_ANTE_READ(R.layout.row_title_value), + ACTION_READ(R.layout.row_hand_action_read), + HERO_POSITION(R.layout.row_recycler), + PLAYER_POSITION(R.layout.row_recycler) + ; + + override val viewType: Int = this.ordinal + + override val identifier: Int + get() { return this.ordinal } + + override val bottomSheetType: BottomSheetType + get() { + return when(this) { + PLAYER_NUMBER -> BottomSheetType.GRID + COMMENT -> BottomSheetType.EDIT_TEXT_MULTI_LINES + ANTE -> BottomSheetType.NUMERIC_TEXT + else -> BottomSheetType.NONE + } + } + + override val resId: Int? + get() { + return when(this) { + PLAYER_NUMBER -> R.string.number_of_players + COMMENT -> R.string.comment + ANTE -> R.string.ante + BIG_BLIND_ANTE, BIG_BLIND_ANTE_READ -> R.string.bb_ante_option + SETTINGS_HEADER -> R.string.settings + else -> null + } + } + + override val imageRes: Int? + get() { + return when(this) { + COMMENT -> R.drawable.picto_comment + SETTINGS_HEADER -> R.drawable.picto_gear + else -> null + } + } + + override val imageClickable: Boolean? + get() { return true } + +} + +class HandHistoryAdapter( + override var dataSource: RowRepresentableDataSource, + override var delegate: RowRepresentableDelegate? = null) : + RecyclerView.Adapter(), + RecyclerAdapter { + + override fun getItemViewType(position: Int): Int { + return this.dataSource.viewTypeForPosition(position) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val rowType: HandRowType = HandRowType.values()[viewType] + val layout = LayoutInflater.from(parent.context).inflate(rowType.layoutRes, parent, false) + return when (rowType) { + HandRowType.DEFAULT -> RowViewHolder(layout) + HandRowType.HEADER -> RowViewHolder(layout) + HandRowType.SETTINGS_HEADER -> RowViewHolder(layout) + HandRowType.ACTION -> RowActionHolder(layout) + HandRowType.STREET -> RowStreetHolder(layout) + HandRowType.PLAYER_SUMMARY -> RowPlayerSummaryHolder(layout) + HandRowType.STRADDLE -> RowStraddleHolder(layout) + HandRowType.PLAYER_SETUP -> RowPlayerSetupHolder(layout) + HandRowType.PLAYER_SETUP_READ -> RowReadOnlyPlayerSetupHolder(layout) + HandRowType.COMMENT -> RowViewHolder(layout) + HandRowType.ACTION_READ -> RowActionReadHolder(layout) + HandRowType.HERO_POSITION, HandRowType.PLAYER_POSITION -> RowPositionHolder(layout) + HandRowType.PLAYER_NUMBER, + HandRowType.ANTE, + HandRowType.BIG_BLIND_ANTE, HandRowType.BIG_BLIND_ANTE_READ -> RowViewHolder(layout) + } + } + + override fun getItemCount(): Int { + return this.dataSource.numberOfRows() + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + this.dataSource.rowRepresentableForPosition(position)?.let { rowRepresentable -> + (holder as BindableHolder).onBind(position, rowRepresentable, this) + } + } + + /** + * Refresh the row in the adapter + */ + fun refreshRow(row: RowRepresentable) { + val rows = this.dataSource.adapterRows() + val index = rows?.indexOf(row) ?: -1 + if (index >= 0) { + notifyItemChanged(index) + } + } + + abstract inner class RowHandHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { + + private var currentPosition = 0 + + fun color(isFocused: Boolean) : Int { + val color = if (isFocused) R.color.kaki_medium else R.color.kaki_darker + return itemView.context.getColor(color) + } + + fun editTextSelected(editText: EditText) { + + val row = dataSource.rowRepresentableForPosition(this.currentPosition) + ?: throw PAIllegalStateException("Row Representable not found at index: $currentPosition") + + val tag = editText.tag as Int + + delegate?.onRowSelected(this.currentPosition, row, tag) + + setViewBackground(view = editText, isEnabled = true, isFocused = true) + } + + override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { + this.currentPosition = position + } + + protected fun configureEditTexts(index: Int, position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { + this.configureEditTexts(index..index, position, row, adapter) + } + + private fun configureEditTexts(tagRange: IntRange, position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { + + tagRange.forEach { tag -> + + val editText = itemView.findViewWithTag(tag) ?: throw PAIllegalStateException("Edit Text not found for tag: $tag, class: $this") + + // hides soft input view + editText.setTextIsSelectable(true) + + // Enabled + val isEnabled = adapter.dataSource.isEnabled(row, tag) + editText.isEnabled = isEnabled + + // Text + val string = adapter.dataSource.charSequenceForRow(row, itemView.context, tag) + editText.setText(string) + + // Focus + val isFocused = adapter.dataSource.isSelected(position, row, tag) + toggleFocus(editText, isFocused) + editText.isFocusable = adapter.dataSource.isFocusable(position, row, tag) + editText.isFocusableInTouchMode = adapter.dataSource.isFocusable(position, row, tag) + + // Put cursor at the end + if (isFocused) { + editText.setSelection(editText.text.length) + } + + // Background + setViewBackground(editText, isEnabled, isFocused) + + } + + } + + private fun setViewBackground(view: View, isEnabled: Boolean, isFocused: Boolean) { + val background = when { + !isEnabled -> R.drawable.rounded_kaki_darkest_rect // disabled + isFocused -> R.drawable.rounded_kaki_medium_rect // focused + else -> R.drawable.rounded_kaki_darker_rect // default + } + view.background = itemView.context.getDrawable(background) + } + + protected fun configureTextView(tag: Int, row: RowRepresentable, adapter: RecyclerAdapter) { + val textView = itemView.findViewWithTag(tag) ?: throw PAIllegalStateException("TextView not found for tag: $tag, class: $this") + textView.text = adapter.dataSource.charSequenceForRow(row, itemView.context, tag) + } + + protected fun setClickListener(editText: EditText) { + + editText.isFocusableInTouchMode = true + + editText.setOnTouchListener { _, event -> + + if (event.action == MotionEvent.ACTION_UP) { + // Both are required, otherwise requestFocus() fails + editText.isFocusable = true + editText.isFocusableInTouchMode = true + + editText.requestFocus() + + editTextSelected(editText) + } + return@setOnTouchListener true + } + + } + + protected fun setOnClickListener(button: Button) { + button.setOnClickListener { + button.backgroundTintList = ColorStateList.valueOf(color(true)) + val row = dataSource.rowRepresentableForPosition(currentPosition) + ?: throw PAIllegalStateException("Row Representable not found at index: $currentPosition") + delegate?.onRowSelected(currentPosition, row, button.tag as Int) + } + } + + protected fun setOnItemClickListener(button: Button, colorChange: Boolean = true) { + button.setOnClickListener { + if (colorChange) { + button.backgroundTintList = ColorStateList.valueOf(color(true)) + } + val row = dataSource.rowRepresentableForPosition(currentPosition) + ?: throw PAIllegalStateException("Row Representable not found at index: $currentPosition") + + val tag = button.tag as? Int ?: throw PAIllegalStateException("Button has no tag, text: ${button.text}, ref: $button") + delegate?.onItemClick(currentPosition, row, tag) + } + } + + private fun toggleFocus(editText: EditText, focused: Boolean) { + if (focused) { + editText.requestFocus() + } else { + editText.clearFocus() + } + } + + open fun editTextForTag(tag: Int) : EditText { + return itemView.findViewWithTag(tag) + } + + protected fun configurePlayerImage(playerImageView: PlayerImageView, position: Int, row: RowRepresentable) { + + // Player + val listener = View.OnClickListener { + val tag = playerImageView.tag as? Int ?: throw PAIllegalStateException("playerImageView has no tag, position: $position, row: $row, ref: $playerImageView") + delegate?.onItemClick(position, row, tag) + } + playerImageView.setOnImageClickListener(listener) + + val size = PlayerImageView.Size.SMALL + + val players = dataSource.contentForRow(row, itemView.context, Player::class) + val isHero = dataSource.boolForRow(row) + if (players.isNotEmpty()) { + playerImageView.setPlayer(players.first(), size, isHero) + } else { + playerImageView.clear(size) + playerImageView.setStrokeColor(isHero) + } + + } + + protected fun configureCardsLayout(layout: LinearLayout, isBoard: Boolean) { + layout.removeAllViews() + + val inflater = LayoutInflater.from(itemView.context) + + val isEdited = (dataSource as HandHistoryViewModel).isEdited + + dataSource.rowRepresentableForPosition(this.currentPosition)?.let { row -> + + val focused = dataSource.isSelected(this.currentPosition, row, layout.tag as Int) + + // background + val drawableId = when { + focused -> R.drawable.rounded_highlighted_board_background + isBoard || isEdited -> R.drawable.rounded_board_background + else -> R.drawable.transparent + } + layout.background = itemView.resources.getDrawable(drawableId, null) + + // Cards + dataSource.contentForRow(row, itemView.context, Card::class).forEach { card -> + val cardView = card.view(itemView.context, inflater, layout) + layout.addView(cardView) + } + + if (layout.isEmpty() && isEdited) { // adds layout hint + val hintView = TextView(itemView.context) + val hintResId = if (isBoard) R.string.board else R.string.hand + hintView.text = itemView.context.getString(hintResId).capitalize() + layout.addView(hintView) + layout.gravity = Gravity.CENTER + } else { + layout.gravity = if (isBoard || !isEdited) Gravity.START or Gravity.CENTER_VERTICAL else Gravity.CENTER + } + + // Listener + layout.setOnClickListener { + delegate?.onRowSelected(this.currentPosition, row, layout.tag as Int) + setViewBackground(layout, true, true) + } + + } + + } + + } + + inner class RowStraddleHolder(itemView: View) : RowHandHolder(itemView) { + + override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { + super.onBind(position, row, adapter) + + val straddleRow = row as StraddleRowRepresentable + + adapter.dataSource.backgroundColor(position, row)?.let { color -> + itemView.findViewById(R.id.container)?.setBackgroundColor(itemView.context.getColor(color)) + } + + itemView.positionsChipGroup.removeAllViews() + straddleRow.positions.forEach { pos -> + + val chip = Chip(itemView.context) + chip.id = View.generateViewId() + chip.text = pos.shortValue + chip.chipStartPadding = 4f.px + chip.chipEndPadding = 4f.px + + chip.isChecked = straddleRow.selectedPositions.contains(pos) + chip.setOnClickListener { + if (chip.isChecked) { + val added = straddleRow.add(pos) + chip.isChecked = added + } else { + straddleRow.remove(pos) + } + adapter.delegate?.onRowValueChanged(straddleRow.selectedPositions, row) + } + itemView.positionsChipGroup.addView(chip) + + } + } + } + + inner class RowPositionHolder(itemView: View) : RowHandHolder(itemView) { + + private var positionAdapter: PositionAdapter = PositionAdapter() + private var positionViewManager: LinearLayoutManager = + LinearLayoutManager(itemView.context, RecyclerView.HORIZONTAL, false) + + init { + itemView.recycler.apply { + setHasFixedSize(true) + layoutManager = positionViewManager + adapter = positionAdapter + } + } + + override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { + super.onBind(position, row, adapter) + + this.positionAdapter.positions = adapter.dataSource.contentForRow(row, itemView.context, Position::class) + this.positionAdapter.setOnClickListener { pos -> + adapter.delegate?.onRowValueChanged(pos, row) + this.positionAdapter.notifyDataSetChanged() + } + + if (row == HandRowType.HERO_POSITION) { + val heroIndex = adapter.dataSource.intForRow(row) + positionAdapter.setSelectedPosition(heroIndex) + } + } + + } + + inner class RowActionReadHolder(itemView: View) : RowHandHolder(itemView) { + + init { + itemView.player_image_rhar.tag = ActionReadRow.Tag.PLAYER.ordinal + itemView.stackText.tag = ActionReadRow.Tag.STACK.ordinal + } + + override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { + + configurePlayerImage(itemView.player_image_rhar, position, row) + + val actionReadRow = row as ActionReadRow + itemView.playersText.text = actionReadRow.positions.joinToString(", ") { it.value } + + actionReadRow.action?.let { type -> + itemView.actionText.text = type.localizedTitle(itemView.context) + val background = itemView.context.getDrawable(type.background) + itemView.action_container.background = background + } + + itemView.amountText.text = actionReadRow.amount?.formatted + itemView.stackText.text = actionReadRow.stack?.formatted + } + + } + + /** + * Display a hand action + */ + inner class RowActionHolder(itemView: View) : RowHandHolder(itemView) { + + init { + itemView.player_image_rha.tag = ComputedAction.Tag.PLAYER.ordinal + itemView.actionButton.tag = ComputedAction.Tag.ACTION.ordinal + itemView.amountEditText.tag = ComputedAction.Tag.AMOUNT.ordinal + + // Action + setOnClickListener(itemView.actionButton) + + // Amount + itemView.findViewById(R.id.amountEditText)?.let { amountEditText -> + + amountEditText.inputType = InputType.TYPE_NUMBER_FLAG_DECIMAL + amountEditText.isFocusableInTouchMode = true + + amountEditText.setOnTouchListener { _, event -> + +// Timber.d("=== event.action = ${event.action}") + if (event.action == MotionEvent.ACTION_UP) { + // Both are required, otherwise requestFocus() fails + amountEditText.isFocusable = true + amountEditText.isFocusableInTouchMode = true + + amountEditText.requestFocus() + + editTextSelected(amountEditText) + } + return@setOnTouchListener true + } + + } + } + + override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { + super.onBind(position, row, adapter) + + val computedAction = row as ComputedAction + + configurePlayerImage(itemView.player_image_rha, position, row) + + // Position + itemView.findViewById