diff --git a/app/build.gradle b/app/build.gradle index e30f903c..4304c29c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,10 +4,11 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: 'realm-android' apply plugin: 'io.fabric' +apply plugin: 'com.google.gms.google-services' // Crashlytics repositories { maven { url 'https://maven.fabric.io/public' } - maven { url 'https://jitpack.io' } + maven { url 'https://jitpack.io' } // required for MPAndroidChart } android { @@ -28,12 +29,15 @@ android { applicationId "net.pokeranalytics.android" minSdkVersion 23 targetSdkVersion 28 - versionCode 27 - versionName "1.0" + versionCode 30 + versionName "2.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { + debug { + ext.enableCrashlytics = false + } release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' @@ -56,7 +60,9 @@ android { } configurations { - all*.exclude group: 'com.google.guava', module: 'listenablefuture' + release { + all*.exclude group: 'com.google.guava', module: 'listenablefuture' + } } } @@ -72,7 +78,7 @@ dependencies { // Android implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.core:core-ktx:1.1.0-alpha05' + implementation 'androidx.core:core-ktx:1.2.0-alpha01' implementation 'com.google.android.material:material:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' @@ -87,11 +93,16 @@ dependencies { // Places implementation 'com.google.android.libraries.places:places:1.1.0' + // Billing / Subscriptions + // WARNING FOR 2.0: https://developer.android.com/google/play/billing/billing_library_releases_notes + // Purchases MUST BE ACKNOWLEDGED + implementation 'com.android.billingclient:billing:1.2.2' + // Firebase - implementation 'com.google.firebase:firebase-core:16.0.8' + implementation 'com.google.firebase:firebase-core:16.0.9' // Crashlytics - implementation 'com.crashlytics.sdk.android:crashlytics:2.9.9' + implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' // Logs implementation 'com.jakewharton.timber:timber:4.7.1' @@ -99,6 +110,9 @@ dependencies { // MPAndroidChart implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' + // CSV Parser: https://mvnrepository.com/artifact/org.apache.commons/commons-csv + implementation 'org.apache.commons:commons-csv:1.6' + // Instrumented Tests androidTestImplementation 'androidx.test:core:1.1.0' androidTestImplementation 'androidx.test:runner:1.1.1' @@ -107,15 +121,8 @@ dependencies { // Test testImplementation 'junit:junit:4.12' - - // Optional -- Robolectric environment - //testImplementation 'androidx.test:core:1.1.0' - // Optional -- Mockito framework testImplementation 'com.android.support.test:runner:1.0.2' testImplementation 'com.android.support.test:rules:1.0.2' - //testImplementation 'androidx.test.espresso:espresso-core:3.1.0' - } -apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/app/src/androidTest/java/net/pokeranalytics/android/model/CriteriaTest.kt b/app/src/androidTest/java/net/pokeranalytics/android/model/CriteriaTest.kt index 1248af55..bd0bf7ee 100644 --- a/app/src/androidTest/java/net/pokeranalytics/android/model/CriteriaTest.kt +++ b/app/src/androidTest/java/net/pokeranalytics/android/model/CriteriaTest.kt @@ -1,14 +1,15 @@ package net.pokeranalytics.android.model +import android.content.Context import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.realm.Session import org.junit.Assert.assertEquals import org.junit.Test import java.util.* +import androidx.test.platform.app.InstrumentationRegistry class CriteriaTest : BaseFilterInstrumentedUnitTest() { - @Test fun getQueryConditions() { @@ -57,9 +58,12 @@ class CriteriaTest : BaseFilterInstrumentedUnitTest() { val criterias = listOf(Criteria.MonthsOfYear, Criteria.DaysOfWeek) val combined = criterias.combined() + val context = InstrumentationRegistry.getInstrumentation().context + combined.forEach { it.conditions.forEach {qc-> - println(qc.getDisplayName()) + + println(qc.getDisplayName(context)) } } } @@ -86,11 +90,12 @@ class CriteriaTest : BaseFilterInstrumentedUnitTest() { val lastValue = firstValue + 10 realm.commitTransaction() + val context = InstrumentationRegistry.getInstrumentation().context val allMonths = Criteria.AllMonthsUpToNow.queries allMonths.forEach { it.conditions.forEach { qc-> - println("<<<<< ${qc.getDisplayName()}") + println("<<<<< ${qc.getDisplayName(context)}") } } } diff --git a/app/src/androidTest/java/net/pokeranalytics/android/performanceTests/PerfsInstrumentedUnitTest.kt b/app/src/androidTest/java/net/pokeranalytics/android/performanceTests/PerfsInstrumentedUnitTest.kt index e37cdf53..e33fa7e7 100644 --- a/app/src/androidTest/java/net/pokeranalytics/android/performanceTests/PerfsInstrumentedUnitTest.kt +++ b/app/src/androidTest/java/net/pokeranalytics/android/performanceTests/PerfsInstrumentedUnitTest.kt @@ -58,7 +58,7 @@ class PerfsInstrumentedUnitTest : RealmInstrumentedUnitTest() { val group = ComputableGroup(Query(), stats) val options = Calculator.Options() - options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION) + options.stats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION) val results: ComputedResults = Calculator.compute(realm, group, options) Timber.d("*** ended in ${System.currentTimeMillis() - start} milliseconds") diff --git a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/BankrollInstrumentedUnitTest.kt b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/BankrollInstrumentedUnitTest.kt index c97752b1..a76308ed 100644 --- a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/BankrollInstrumentedUnitTest.kt +++ b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/BankrollInstrumentedUnitTest.kt @@ -54,12 +54,12 @@ class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() { val t1 = realm.createObject(Transaction::class.java, UUID.randomUUID().toString()) t1.amount = 100.0 t1.type = TransactionType.getByValue(TransactionType.Value.BONUS, realm) - br1.transactions.add(t1) + t1.bankroll = br1 val t2 = realm.createObject(Transaction::class.java, UUID.randomUUID().toString()) t2.amount = 500.0 t2.type = TransactionType.getByValue(TransactionType.Value.BONUS, realm) - br2.transactions.add(t2) + t2.bankroll = br2 val s1 = newSessionInstance(realm) s1.bankroll = br1 @@ -73,16 +73,16 @@ class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() { val br1 = realm.where(Bankroll::class.java).equalTo("name", "br1").findFirst() val brSetup1 = BankrollReportSetup(br1) - val report1 = BankrollCalculator.computeReport(brSetup1) + val report1 = BankrollCalculator.computeReport(realm, brSetup1) Assert.assertEquals(400.0, report1.total, EPSILON) val br2 = realm.where(Bankroll::class.java).equalTo("name", "br2").findFirst() val brSetup2 = BankrollReportSetup(br2) - val report2 = BankrollCalculator.computeReport(brSetup2) + val report2 = BankrollCalculator.computeReport(realm, brSetup2) Assert.assertEquals(2000.0, report2.total, EPSILON) val brSetupAll = BankrollReportSetup() - val reportAll = BankrollCalculator.computeReport(brSetupAll) + val reportAll = BankrollCalculator.computeReport(realm, brSetupAll) Assert.assertEquals(2400.0, reportAll.total, EPSILON) } @@ -108,7 +108,7 @@ class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() { val t1 = realm.createObject(Transaction::class.java, UUID.randomUUID().toString()) t1.amount = 100.0 t1.type = TransactionType.getByValue(TransactionType.Value.BONUS, realm) - br1?.transactions?.add(t1) + t1.bankroll = br1 val s1 = newSessionInstance(realm) s1.bankroll = br1 @@ -117,11 +117,11 @@ class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() { } val brSetup1 = BankrollReportSetup(br1) - val report1 = BankrollCalculator.computeReport(brSetup1) + val report1 = BankrollCalculator.computeReport(realm, brSetup1) Assert.assertEquals(400.0, report1.total, EPSILON) val brSetupAll = BankrollReportSetup() - val reportAll = BankrollCalculator.computeReport(brSetupAll) + val reportAll = BankrollCalculator.computeReport(realm, brSetupAll) Assert.assertEquals(4000.0, reportAll.total, EPSILON) } diff --git a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/DeleteInstrumentedUnitTest.kt b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/DeleteInstrumentedUnitTest.kt index 5df4ab9e..f7faed23 100644 --- a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/DeleteInstrumentedUnitTest.kt +++ b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/DeleteInstrumentedUnitTest.kt @@ -3,6 +3,7 @@ package net.pokeranalytics.android.unitTests import net.pokeranalytics.android.components.RealmInstrumentedUnitTest import net.pokeranalytics.android.model.realm.Bankroll import net.pokeranalytics.android.model.realm.Currency +import net.pokeranalytics.android.util.extensions.findById import org.junit.Assert import org.junit.Test @@ -40,7 +41,7 @@ class DeleteInstrumentedUnitTest : RealmInstrumentedUnitTest() { var isValidForDelete = br1.isValidForDelete(realm) Assert.assertEquals(false, isValidForDelete) - realm.where(Bankroll::class.java).equalTo("id", "1").findFirst()?.let { + realm.findById(Bankroll::class.java, "1")?.let { isValidForDelete = it.isValidForDelete(realm) Assert.assertEquals(false, isValidForDelete) diff --git a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt index 6e56b746..e741456b 100644 --- a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt +++ b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt @@ -76,7 +76,7 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() { val group = ComputableGroup(Query()) val options = Calculator.Options() - options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION, + options.stats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION, Stat.LONGEST_STREAKS, Stat.LOCATIONS_PLAYED, Stat.DAYS_PLAYED) val results: ComputedResults = Calculator.compute(realm, group, options) @@ -255,7 +255,7 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() { val group = ComputableGroup(Query(), stats) val options = Calculator.Options() - options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION) + options.stats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION) val results: ComputedResults = Calculator.compute(realm, group, options) val delta = 0.01 @@ -322,7 +322,7 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() { val group = ComputableGroup(Query(), stats) val options = Calculator.Options() - options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION) + options.stats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION) val results: ComputedResults = Calculator.compute(realm, group, options) val delta = 0.01 @@ -405,7 +405,7 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() { val group = ComputableGroup(Query(), stats) val options = Calculator.Options() - options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION) + options.stats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION) val results: ComputedResults = Calculator.compute(realm, group, options) val delta = 0.01 @@ -728,7 +728,7 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() { val group = ComputableGroup(Query(QueryCondition.IsCash)) val options = Calculator.Options() - options.displayedStats = listOf(Stat.HOURLY_DURATION) + options.stats = listOf(Stat.HOURLY_DURATION) val results: ComputedResults = Calculator.compute(realm, group, options) val delta = 0.01 @@ -773,7 +773,7 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() { val group = ComputableGroup(Query(QueryCondition.IsCash)) val options = Calculator.Options() - options.displayedStats = listOf(Stat.HOURLY_DURATION) + options.stats = listOf(Stat.HOURLY_DURATION) val results: ComputedResults = Calculator.compute(realm, group, options) val delta = 0.01 diff --git a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/BlindFilterInstrumentedTest.kt b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/BlindFilterInstrumentedTest.kt index f1a85431..6260a833 100644 --- a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/BlindFilterInstrumentedTest.kt +++ b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/BlindFilterInstrumentedTest.kt @@ -47,7 +47,7 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() { listOfValues = arrayListOf(s1.blinds!!) } - blind.filterSectionRow = FilterSectionRow.BLIND + blind.filterSectionRow = FilterSectionRow.Blind val filterElement = FilterCondition(filterElementRows = arrayListOf(blind)) filter.updateValueBy(filterElement) @@ -97,8 +97,8 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() { listOfValues = arrayListOf(s2.blinds!!) } - blind1.filterSectionRow = FilterSectionRow.BLIND - blind2.filterSectionRow = FilterSectionRow.BLIND + blind1.filterSectionRow = FilterSectionRow.Blind + blind2.filterSectionRow = FilterSectionRow.Blind val filterElements = FilterCondition(filterElementRows = arrayListOf(blind1, blind2)) filter.updateValueBy(filterElements) @@ -146,11 +146,10 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() { listOfValues = arrayListOf(s3.blinds!!) } - blind.filterSectionRow = FilterSectionRow.BLIND + blind.filterSectionRow = FilterSectionRow.Blind val filterElement = FilterCondition(filterElementRows = arrayListOf(blind)) filter.updateValueBy(filterElement) - println("<<<< ${filter.listOfValues}") val sessions = Filter.queryOn(realm, Query(filter)) Assert.assertEquals(1, sessions.size) @@ -197,8 +196,8 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() { listOfValues = arrayListOf(s2.blinds!!) } - blind1.filterSectionRow = FilterSectionRow.BLIND - blind2.filterSectionRow = FilterSectionRow.BLIND + blind1.filterSectionRow = FilterSectionRow.Blind + blind2.filterSectionRow = FilterSectionRow.Blind val filterElement = FilterCondition(filterElementRows = arrayListOf(blind1, blind2)) filter.updateValueBy(filterElement) diff --git a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/CustomFieldFilterInstrumentedUnitTest.kt b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/CustomFieldFilterInstrumentedUnitTest.kt new file mode 100644 index 00000000..f3880227 --- /dev/null +++ b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/CustomFieldFilterInstrumentedUnitTest.kt @@ -0,0 +1,85 @@ +package net.pokeranalytics.android.unitTests.filter + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest +import net.pokeranalytics.android.model.filter.Query +import net.pokeranalytics.android.model.filter.QueryCondition +import net.pokeranalytics.android.model.realm.CustomField +import net.pokeranalytics.android.model.realm.CustomFieldEntry +import net.pokeranalytics.android.model.realm.Filter +import net.pokeranalytics.android.model.realm.Session +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import java.util.* + +@RunWith(AndroidJUnit4::class) +class CustomFieldFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { + + @Test + fun testCustomFieldListFilter() { + + val realm = this.mockRealm + realm.beginTransaction() + + val cf1 = CustomField() + cf1.id = "1" + cf1.type = CustomField.Type.LIST.ordinal + + val cfe1 = CustomFieldEntry() + val cfe2 = CustomFieldEntry() + cfe1.value = "super" + cfe2.value = "nul" + + cf1.entries.add(cfe1) + cf1.entries.add(cfe2) + + val s1 = Session.testInstance(100.0, false, Date(), 1) + s1.customFieldEntries.add(cfe1) + val s2 = Session.testInstance(100.0, true, Date(), 1) + s2.customFieldEntries.add(cfe2) + realm.commitTransaction() + + val sessions = Filter.queryOn(realm, Query(QueryCondition.CustomFieldListQuery(cfe2))) + + Assert.assertEquals(1, sessions.size) + sessions[0]?.run { + Assert.assertEquals(s2.id, (this).id) + } + } + + @Test + fun testCustomFieldAmountFilter() { + + val realm = this.mockRealm + realm.beginTransaction() + + val cf1 = CustomField() + cf1.id = "1234" + cf1.type = CustomField.Type.AMOUNT.ordinal + + + val cfe1 = CustomFieldEntry() + cfe1.id = "999" + cf1.entries.add(cfe1) + cfe1.numericValue = 30.0 + + val cfe2 = CustomFieldEntry() + cfe2.id = "888" + cf1.entries.add(cfe2) + cfe2.numericValue = 100.0 + + val s1 = Session.testInstance(100.0, false, Date(), 1) + s1.customFieldEntries.add(cfe1) + val s2 = Session.testInstance(100.0, true, Date(), 1) + s2.customFieldEntries.add(cfe2) + realm.commitTransaction() + + val sessions = Filter.queryOn(realm, Query(QueryCondition.CustomFieldNumberQuery(cf1.id, 100.0))) + + Assert.assertEquals(1, sessions.size) + sessions[0]?.run { + Assert.assertEquals(s2.id, (this).id) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/DateFilterInstrumentedUnitTest.kt b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/DateFilterInstrumentedUnitTest.kt index a09ba699..d619df95 100644 --- a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/DateFilterInstrumentedUnitTest.kt +++ b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/DateFilterInstrumentedUnitTest.kt @@ -8,6 +8,7 @@ import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.FilterCondition import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow +import net.pokeranalytics.android.util.extensions.hourMinute import net.pokeranalytics.android.util.extensions.startOfDay import org.junit.Assert import org.junit.Test @@ -34,7 +35,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { cal.time = s1.startDate val filterElementRow = QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(cal.get(Calendar.DAY_OF_WEEK)) } - filterElementRow.filterSectionRow = FilterSectionRow.DYNAMIC_DATE + filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate val filterElement = FilterCondition(arrayListOf(filterElementRow)) filter.updateValueBy(filterElement) @@ -63,7 +64,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { cal.time = s1.startDate val filterElementRow = QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(cal.get(Calendar.MONTH)) } - filterElementRow.filterSectionRow = FilterSectionRow.DYNAMIC_DATE + filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate val filterElement = FilterCondition(arrayListOf(filterElementRow)) filter.updateValueBy(filterElement) @@ -91,7 +92,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.AnyYear() cal.time = s1.startDate val filterElementRow = QueryCondition.AnyYear().apply { listOfValues = arrayListOf(cal.get(Calendar.YEAR)) } - filterElementRow.filterSectionRow = FilterSectionRow.DYNAMIC_DATE + filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate val filterElement = FilterCondition(arrayListOf(filterElementRow)) filter.updateValueBy(filterElement) @@ -379,13 +380,13 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { Session.testInstance(100.0, false, cal.time, 1) - cal.add(Calendar.HOUR_OF_DAY, 2) // adds one hour + cal.add(Calendar.DAY_OF_YEAR, 2) // adds one hour val s2 = Session.testInstance(100.0, true, cal.time, 1) realm.commitTransaction() val filter = QueryCondition.StartedFromDate() val filterElementRow = QueryCondition.StartedFromDate().apply { singleValue = s2.startDate!!} - filterElementRow.filterSectionRow = FilterSectionRow.FIXED_DATE + filterElementRow.filterSectionRow = FilterSectionRow.FixedDate filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -406,14 +407,14 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { cal.time = Date() // sets calendar time/date val s1 = Session.testInstance(100.0, false, cal.time, 1) - cal.add(Calendar.HOUR_OF_DAY, 2) // adds one hour + cal.add(Calendar.DAY_OF_YEAR, 2) // adds one hour Session.testInstance(100.0, true, cal.time, 1) realm.commitTransaction() val filter = QueryCondition.StartedToDate() val filterElementRow = QueryCondition.StartedToDate().apply { singleValue = s1.startDate!! } - filterElementRow.filterSectionRow = FilterSectionRow.FIXED_DATE + filterElementRow.filterSectionRow = FilterSectionRow.FixedDate filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -435,14 +436,14 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { Session.testInstance(100.0, false, cal.time, 1) - cal.add(Calendar.HOUR_OF_DAY, 2) // adds one hour + cal.add(Calendar.DAY_OF_YEAR, 2) // adds one hour val s2 = Session.testInstance(100.0, true, cal.time, 1) realm.commitTransaction() val filter = QueryCondition.EndedFromDate() val filterElementRow = QueryCondition.EndedFromDate().apply { singleValue = s2.endDate() } - filterElementRow.filterSectionRow = FilterSectionRow.FIXED_DATE + filterElementRow.filterSectionRow = FilterSectionRow.FixedDate filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -463,7 +464,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { cal.time = Date() // sets calendar time/date val s1 = Session.testInstance(100.0, false, cal.time, 1) - cal.add(Calendar.HOUR_OF_DAY, 2) // adds one hour + cal.add(Calendar.DAY_OF_YEAR, 2) // adds one hour Session.testInstance(100.0, true, cal.time, 1) realm.commitTransaction() @@ -471,7 +472,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.EndedToDate() val filterElementRow = QueryCondition.EndedToDate().apply { singleValue = s1.endDate() } - filterElementRow.filterSectionRow = FilterSectionRow.FIXED_DATE + filterElementRow.filterSectionRow = FilterSectionRow.FixedDate filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -481,4 +482,39 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { Assert.assertEquals(s1.id, (this).id) } } + + @Test + fun testFomTimeToTime() { + + val realm = this.mockRealm + realm.beginTransaction() + + val cal = Calendar.getInstance() // creates calendar + cal.time = Date() // sets calendar time/date + cal.set(Calendar.HOUR_OF_DAY, 14) // adds one hour + + println("<<<<< s1 ${cal.hourMinute()}") + val s1 = Session.testInstance(100.0, false, cal.time, 1) + println("<<<<< s1 ${cal.hourMinute()}") + cal.add(Calendar.HOUR_OF_DAY, 2) // adds one hour + println("<<<<< s2 ${cal.hourMinute()}") + val s2 = Session.testInstance(100.0, true, cal.time, 1) + println("<<<<< s2 ${cal.hourMinute()}") + + cal.set(Calendar.HOUR_OF_DAY, 23) // adds one hour + println("<<<<< s3 ${cal.hourMinute()}") + val s3 = Session.testInstance(100.0, true, cal.time, 2) + println("<<<<< s3 ${cal.hourMinute()}") + + realm.commitTransaction() + + val filter = QueryCondition.StartedFromTime(s2.startDate!!) + val filter2 = QueryCondition.EndedToTime(s2.endDate!!) + val sessions = Filter.queryOn(realm, Query(filter, filter2)) + + Assert.assertEquals(1, sessions.size) + sessions[0]?.run { + Assert.assertEquals(s2.id, (this).id) + } + } } \ No newline at end of file diff --git a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/RealmFilterInstrumentedUnitTest.kt b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/RealmFilterInstrumentedUnitTest.kt index ac1c3186..f52f3e2e 100644 --- a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/RealmFilterInstrumentedUnitTest.kt +++ b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/RealmFilterInstrumentedUnitTest.kt @@ -24,7 +24,7 @@ class RealmFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { filter.name = "testSaveLoadCashFilter" val filterElement = QueryCondition.IsCash - filterElement.filterSectionRow = FilterSectionRow.CASH_TOURNAMENT + filterElement.filterSectionRow = FilterSectionRow.CashOrTournament filter.createOrUpdateFilterConditions(arrayListOf(filterElement)) val useCount = filter.countBy(FilterCategoryRow.GENERAL) diff --git a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/SessionFilterInstrumentedUnitTest.kt b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/SessionFilterInstrumentedUnitTest.kt index 0b1de7a0..3bb0073b 100644 --- a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/SessionFilterInstrumentedUnitTest.kt +++ b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/SessionFilterInstrumentedUnitTest.kt @@ -110,7 +110,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.AnyBankroll() val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) } - filterElementRow.filterSectionRow = FilterSectionRow.BANKROLL + filterElementRow.filterSectionRow = FilterSectionRow.Bankroll filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -141,10 +141,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.AnyBankroll() val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) } - filterElementRow.filterSectionRow = FilterSectionRow.BANKROLL + filterElementRow.filterSectionRow = FilterSectionRow.Bankroll val filterElementRow2 = QueryCondition.AnyBankroll().apply { setObject(b2) } - filterElementRow2.filterSectionRow = FilterSectionRow.BANKROLL + filterElementRow2.filterSectionRow = FilterSectionRow.Bankroll filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -197,9 +197,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { realm.commitTransaction() val filterElementRow = QueryCondition.AnyGame().apply { setObject(g2) } - filterElementRow.filterSectionRow = FilterSectionRow.GAME + filterElementRow.filterSectionRow = FilterSectionRow.Game val filterElementRow2 = QueryCondition.AnyGame().apply { setObject(g3) } - filterElementRow2.filterSectionRow = FilterSectionRow.GAME + filterElementRow2.filterSectionRow = FilterSectionRow.Game val filterCondition = FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)) val queryCondition = filterCondition.queryCondition val sessions = Filter.queryOn(realm, Query(queryCondition)) @@ -225,7 +225,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.AnyLocation() val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) } - filterElementRow.filterSectionRow = FilterSectionRow.LOCATION + filterElementRow.filterSectionRow = FilterSectionRow.Location filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -257,9 +257,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.AnyLocation() val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) } - filterElementRow.filterSectionRow = FilterSectionRow.LOCATION + filterElementRow.filterSectionRow = FilterSectionRow.Location val filterElementRow2 = QueryCondition.AnyLocation().apply { setObject(l3) } - filterElementRow2.filterSectionRow = FilterSectionRow.LOCATION + filterElementRow2.filterSectionRow = FilterSectionRow.Location filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2))) @@ -287,7 +287,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.AnyTournamentName() val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) } - filterElementRow.filterSectionRow = FilterSectionRow.TOURNAMENT_NAME + filterElementRow.filterSectionRow = FilterSectionRow.TournamentName filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -318,9 +318,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.AnyTournamentName() val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) } - filterElementRow.filterSectionRow = FilterSectionRow.TOURNAMENT_NAME + filterElementRow.filterSectionRow = FilterSectionRow.TournamentName val filterElementRow2 = QueryCondition.AnyTournamentName().apply { setObject(t2) } - filterElementRow.filterSectionRow = FilterSectionRow.TOURNAMENT_NAME + filterElementRow.filterSectionRow = FilterSectionRow.TournamentName filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -354,11 +354,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.AllTournamentFeature() val filterElementRow = QueryCondition.AllTournamentFeature().apply { setObject(t1) } - filterElementRow.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE + filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature val filterElementRow2 = QueryCondition.AllTournamentFeature().apply { setObject(t2) } - filterElementRow2.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE + filterElementRow2.filterSectionRow = FilterSectionRow.TournamentFeature val filterElementRow3 = QueryCondition.AllTournamentFeature().apply { setObject(t4) } - filterElementRow3.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE + filterElementRow3.filterSectionRow = FilterSectionRow.TournamentFeature filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2, filterElementRow3))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -389,13 +389,13 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.AnyTournamentFeature() val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t1) } - filterElementRow.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE + filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature val filterElementRow2 = QueryCondition.AnyTournamentFeature().apply { setObject(t2) } - filterElementRow2.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE + filterElementRow2.filterSectionRow = FilterSectionRow.TournamentFeature val filterElementRow3 = QueryCondition.AnyTournamentFeature().apply { setObject(t3) } - filterElementRow3.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE + filterElementRow3.filterSectionRow = FilterSectionRow.TournamentFeature val filterElementRow4 = QueryCondition.AnyTournamentFeature().apply { setObject(t4) } - filterElementRow4.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE + filterElementRow4.filterSectionRow = FilterSectionRow.TournamentFeature filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2, filterElementRow3, filterElementRow4))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -423,7 +423,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.AnyTournamentFeature() val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t2) } - filterElementRow.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE + filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -448,9 +448,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.AnyTableSize() val filterElementRow = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(2) } - filterElementRow.filterSectionRow = FilterSectionRow.TABLE_SIZE + filterElementRow.filterSectionRow = FilterSectionRow.TableSize val filterElementRow2 = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(4) } - filterElementRow.filterSectionRow = FilterSectionRow.TABLE_SIZE + filterElementRow.filterSectionRow = FilterSectionRow.TableSize filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -475,7 +475,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.NetAmountWon() val filterElementRow = QueryCondition.more().apply { listOfValues = arrayListOf(204.0) } - filterElementRow.filterSectionRow = FilterSectionRow.VALUE + filterElementRow.filterSectionRow = FilterSectionRow.Value filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow))) val sessions = Filter.queryOn(realm, Query(filterElementRow)) @@ -500,7 +500,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { val filter = QueryCondition.NetAmountWon() val filterElementRow = QueryCondition.less().apply { listOfValues = arrayListOf(540.0) } - filterElementRow.filterSectionRow = FilterSectionRow.VALUE + filterElementRow.filterSectionRow = FilterSectionRow.Value filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow))) val sessions = Filter.queryOn(realm, Query(filter)) @@ -524,13 +524,13 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { realm.commitTransaction() val filterMore = QueryCondition.NetAmountWon() - val filterElementRow = QueryCondition.more().apply { listOfValues = arrayListOf(200.0) } - filterElementRow.filterSectionRow = FilterSectionRow.VALUE + val filterElementRow = QueryCondition.more().apply { listOfValues = arrayListOf(199.0) } + filterElementRow.filterSectionRow = FilterSectionRow.Value filterMore.updateValueBy(FilterCondition(arrayListOf(filterElementRow))) val filterLess = QueryCondition.NetAmountWon() val filterElementRow2 = QueryCondition.less().apply { listOfValues = arrayListOf(400.0) } - filterElementRow2.filterSectionRow = FilterSectionRow.VALUE + filterElementRow2.filterSectionRow = FilterSectionRow.Value filterLess.updateValueBy(FilterCondition(arrayListOf(filterElementRow2))) val sessions = Filter.queryOn(realm, Query(filterMore, filterLess)) @@ -564,7 +564,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { s4.result!!.buyin = 200.0 realm.commitTransaction() - val filterMore = QueryCondition.NumberOfRebuy(QueryCondition.Operator.MORE, 5.0) + val filterMore = QueryCondition.NumberOfRebuy(QueryCondition.Operator.MORE, 4.0) val sessions = Filter.queryOn(realm, Query(filterMore)) Assert.assertEquals(2, sessions.size) diff --git a/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/TransactionFilterInstrumentedUnitTest.kt b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/TransactionFilterInstrumentedUnitTest.kt new file mode 100644 index 00000000..e0b5314c --- /dev/null +++ b/app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/TransactionFilterInstrumentedUnitTest.kt @@ -0,0 +1,57 @@ +package net.pokeranalytics.android.unitTests.filter + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import io.realm.RealmList +import io.realm.RealmResults +import net.pokeranalytics.android.R +import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest +import net.pokeranalytics.android.model.filter.Query +import net.pokeranalytics.android.model.filter.QueryCondition +import net.pokeranalytics.android.model.realm.* +import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import java.util.* + +@RunWith(AndroidJUnit4::class) +class TransactionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { + + @Test + fun testTransactionTypeFilter() { + val context = InstrumentationRegistry.getInstrumentation().context + + val realm = this.mockRealm + realm.beginTransaction() + TransactionType.Value.values().forEachIndexed { index, value -> + val type = TransactionType() + val name = "test" + type.name = name + type.additive = value.additive + type.kind = index + type.lock = true + realm.insertOrUpdate(type) + } + + val t1: Transaction = realm.createObject(Transaction::class.java, "1") + t1.type = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm) + val t2: Transaction = realm.createObject(Transaction::class.java, "2") + t2.type = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm) + + val b1 = realm.createObject(Bankroll::class.java, "1") + t1.bankroll = b1 + + val b2 = realm.createObject(Bankroll::class.java, "2") + t2.bankroll = b2 + + realm.commitTransaction() + + val transactions = Filter.queryOn(realm, Query(QueryCondition.AnyTransactionType(t1.type!!))) + + Assert.assertEquals(1, transactions.size) + transactions[0]?.run { + Assert.assertEquals(t1.type!!.id, (this).type!!.id) + } + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce174f66..c296be62 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,115 +1,168 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:tools="http://schemas.android.com/tools" + package="net.pokeranalytics.android"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt index 96374a08..35ea4536 100644 --- a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt +++ b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt @@ -16,26 +16,35 @@ import net.pokeranalytics.android.model.utils.Seed import net.pokeranalytics.android.util.FakeDataManager import net.pokeranalytics.android.util.PokerAnalyticsLogs import net.pokeranalytics.android.util.UserDefaults +import net.pokeranalytics.android.util.billing.AppGuard import timber.log.Timber - class PokerAnalyticsApplication : Application() { override fun onCreate() { super.onCreate() - UserDefaults.init(this) + UserDefaults.init(this) + + // AppGuard / Billing services + AppGuard.load(this.applicationContext) // Realm Realm.init(this) val realmConfiguration = RealmConfiguration.Builder() .name(Realm.DEFAULT_REALM_NAME) - .schemaVersion(4) + .schemaVersion(6) .migration(PokerAnalyticsMigration()) .initialData(Seed(this)) .build() Realm.setDefaultConfiguration(realmConfiguration) +// val realm = Realm.getDefaultInstance() +// realm.executeTransaction { +// realm.where(Session::class.java).findAll().deleteAllFromRealm() +// } +// realm.close() + // Set up Crashlytics, disabled for debug builds val crashlyticsKit = Crashlytics.Builder() .core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()) @@ -44,7 +53,6 @@ class PokerAnalyticsApplication : Application() { // Initialize Fabric with the debug-disabled crashlytics. Fabric.with(this, crashlyticsKit) - if (BuildConfig.DEBUG) { // Logs Timber.plant(PokerAnalyticsLogs()) @@ -55,7 +63,7 @@ class PokerAnalyticsApplication : Application() { // this.createFakeSessions() } - Patcher.patchBreaks(applicationContext) + Patcher.patchAll(this.applicationContext) } /** diff --git a/app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt b/app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt index 8d84d753..888a4a63 100644 --- a/app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt +++ b/app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt @@ -16,7 +16,7 @@ import retrofit2.http.Query import java.util.concurrent.TimeUnit /** - * Currency Converter API + * CurrencyCode Converter API */ interface CurrencyConverterApi { diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/AggregationType.kt b/app/src/main/java/net/pokeranalytics/android/calculus/AggregationType.kt new file mode 100644 index 00000000..d29e7049 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/calculus/AggregationType.kt @@ -0,0 +1,40 @@ +package net.pokeranalytics.android.calculus + +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.Criteria +import net.pokeranalytics.android.ui.graph.AxisFormatting + +enum class AggregationType { + SESSION, + MONTH, + YEAR, + DURATION; + + val resId: Int + get() { + return when (this) { + SESSION -> R.string.session + MONTH -> R.string.month + YEAR -> R.string.year + DURATION -> R.string.duration + } + } + + val axisFormatting: AxisFormatting + get() { + return when (this) { + DURATION -> AxisFormatting.X_DURATION + else -> AxisFormatting.DEFAULT + } + } + + val criterias: List + get() { + return when (this) { + MONTH -> listOf(Criteria.AllMonthsUpToNow) + YEAR -> listOf(Criteria.Years) + else -> listOf() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/Aggregator.kt b/app/src/main/java/net/pokeranalytics/android/calculus/Aggregator.kt deleted file mode 100644 index cf0bf178..00000000 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Aggregator.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.pokeranalytics.android.calculus - -class AggregationParameter { - var values: List? = null -} - -class Aggregator { - - var parameters: List> = listOf() - -} - 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 0bb9db94..2f1726cb 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt @@ -1,6 +1,8 @@ package net.pokeranalytics.android.calculus +import android.content.Context import io.realm.Realm +import net.pokeranalytics.android.R import net.pokeranalytics.android.calculus.Stat.* import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.combined @@ -8,59 +10,114 @@ import net.pokeranalytics.android.model.extensions.hourlyDuration import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.filter import net.pokeranalytics.android.model.realm.ComputableResult +import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.SessionSet +import net.pokeranalytics.android.ui.activity.ComparisonReportActivity +import net.pokeranalytics.android.ui.activity.ProgressReportActivity +import net.pokeranalytics.android.ui.activity.TableReportActivity +import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.util.extensions.startOfDay -import timber.log.Timber import java.util.* import kotlin.math.max import kotlin.math.min /** - * The class performing stats computation + * The class performing statIds computation */ class Calculator { /** - * The options used for calculations or display + * The options used for calculations and display */ class Options( - display: Display = Display.TABLE, - evolutionValues: EvolutionValues = EvolutionValues.NONE, - stats: List = listOf(), - aggregationType: AggregationType? = null + var display: Display = Display.TABLE, + progressValues: ProgressValues = ProgressValues.NONE, + var stats: List = listOf(), + var criterias: List = listOf(), + var query: Query = Query(), + var filterId: String? = null, + private var aggregationType: AggregationType? = null, + var userGenerated: Boolean = false, + var reportSetupId: String? = null ) { + constructor(display: Display = Display.TABLE, + progressValues: ProgressValues = ProgressValues.NONE, + stats: List = listOf(), + criterias: List = listOf(), + filter: Filter? = null, + aggregationType: AggregationType? = null, + userGenerated: Boolean = false, + reportSetupId: String? = null) : + this(display, progressValues, stats, criterias, filter?.query ?: Query(), filter?.id, aggregationType, userGenerated, reportSetupId) + /** - * The way the stats are going to be displayed + * Specifies whether progress values should be added and their kind */ - enum class Display { + var progressValues: ProgressValues = progressValues + get() { + if (field == ProgressValues.NONE && this.display == Display.PROGRESS) { + return ProgressValues.STANDARD + } + return field + } + + init { + this.aggregationType?.let { + this.criterias = it.criterias + } + } + + /** + * The way the computed stats are going to be displayed + */ + enum class Display : RowRepresentable { TABLE, - EVOLUTION, + PROGRESS, COMPARISON, MAP, - POLYNOMIAL + POLYNOMIAL; + + override val resId: Int? + get() { + return when (this) { + TABLE -> R.string.table + PROGRESS -> R.string.progress + COMPARISON -> R.string.comparison + MAP -> R.string.map + POLYNOMIAL -> null + } + } + + val activityClass: Class<*> + get() { + return when (this) { + TABLE -> TableReportActivity::class.java + PROGRESS -> ProgressReportActivity::class.java + COMPARISON -> ComparisonReportActivity::class.java + else -> throw IllegalStateException("undefined activity for report display") +// MAP -> R.string.map +// POLYNOMIAL -> null + + } + } } /** * The type of evolution numericValues */ - enum class EvolutionValues { + enum class ProgressValues { NONE, STANDARD, TIMED } - var display: Display = display - var evolutionValues: EvolutionValues = evolutionValues - var displayedStats: List = stats - var aggregationType: AggregationType? = aggregationType - /** * This function determines whether the standard deviation should be computed */ val computeStandardDeviation: Boolean get() { - this.displayedStats.forEach { + this.stats.forEach { if (it == STANDARD_DEVIATION_BB_PER_100_HANDS || it == STANDARD_DEVIATION || it == STANDARD_DEVIATION_HOURLY) { return true } @@ -68,32 +125,56 @@ class Calculator { return false } + /** + * Whether the longest streaks should be computed + */ val computeLongestStreak: Boolean - get() { - return this.displayedStats.contains(LONGEST_STREAKS) - } + get() { + return this.stats.contains(LONGEST_STREAKS) + } + /** + * Whether the values should be sorted + */ val shouldSortValues: Boolean get() { - return this.evolutionValues != EvolutionValues.NONE || this.computeLongestStreak + return this.progressValues != ProgressValues.NONE || this.computeLongestStreak } + /** + * Whether the number of locations played should be computed + */ val computeLocationsPlayed: Boolean get() { - return this.displayedStats.contains(LOCATIONS_PLAYED) + return this.stats.contains(LOCATIONS_PLAYED) } + /** + * Whether the number of days played should be computed + */ val computeDaysPlayed: Boolean get() { - return this.displayedStats.contains(DAYS_PLAYED) + return this.stats.contains(DAYS_PLAYED) } - + /** + * Whether progress values should be managed at the group level + */ val shouldManageMultiGroupProgressValues: Boolean get() { - if (this.aggregationType != null) { - return this.aggregationType == AggregationType.MONTH || this.aggregationType == AggregationType.YEAR + return if (this.aggregationType != null) { + this.aggregationType == AggregationType.MONTH || this.aggregationType == AggregationType.YEAR } else { - return false + false } } + /** + * Returns some default name + */ + fun getName(context: Context): String { + return when (this.stats.size) { + 1 -> this.stats.first().localizedTitle(context) + else -> this.query.getName(context) + } + } + } companion object { @@ -106,37 +187,36 @@ class Calculator { stats: List? = null ): Report { - val options = Options(evolutionValues = Options.EvolutionValues.STANDARD, aggregationType = aggregationType) - options.displayedStats = listOf(stat) + val options = Options( + display = Options.Display.PROGRESS, + progressValues = Options.ProgressValues.STANDARD, + stats = listOf(stat), + aggregationType = aggregationType + ) + if (aggregationType == AggregationType.DURATION) { - options.evolutionValues = Options.EvolutionValues.TIMED + options.progressValues = Options.ProgressValues.TIMED } stats?.let { - options.displayedStats = stats + options.stats = stats } return when (aggregationType) { AggregationType.SESSION, AggregationType.DURATION -> this.computeGroups(realm, listOf(group), options) AggregationType.MONTH, AggregationType.YEAR -> { - this.computeStatsWithCriterias(realm, aggregationType.criterias, group.query, options) + this.computeStats(realm, options) } } } - - fun computeStatsWithCriterias( - realm: Realm, - criterias: List = listOf(), - query: Query = Query(), - options: Options = Options() - ): Report { + fun computeStats(realm: Realm, options: Options = Options()): Report { val computableGroups: MutableList = mutableListOf() - criterias.combined().forEach { comparatorQuery -> + options.criterias.combined().forEach { comparatorQuery -> - comparatorQuery.merge(query) + comparatorQuery.merge(options.query) val group = ComputableGroup(comparatorQuery) computableGroups.add(group) @@ -144,7 +224,7 @@ class Calculator { } if (computableGroups.size == 0) { - val group = ComputableGroup(query) + val group = ComputableGroup(options.query) computableGroups.add(group) } @@ -152,18 +232,18 @@ class Calculator { } /** - * Computes all stats for list of Session sessionGroup + * Computes all statIds for list of Session sessionGroup */ fun computeGroups(realm: Realm, groups: List, options: Options = Options()): Report { val report = Report(options) groups.forEach { group -> - val s = Date() + // val s = Date() // Clean existing computables / sessionSets if group is reused group.cleanup() - // Computes actual sessionGroup stats + // Computes actual sessionGroup statIds val results: ComputedResults = this.compute(realm, group, options) // Computes the compared sessionGroup if existing @@ -181,9 +261,9 @@ class Calculator { results.finalize() // later treatment, such as evolution numericValues sorting report.addResults(results) - val e = Date() - val duration = (e.time - s.time) / 1000.0 - Timber.d(">>> group ${group.name} in $duration seconds") +// val e = Date() +// val duration = (e.time - s.time) / 1000.0 +// Timber.d(">>> group ${group.name} in $duration seconds") } @@ -191,14 +271,14 @@ class Calculator { } /** - * Computes stats for a SessionSet + * Computes statIds for a SessionSet */ fun compute(realm: Realm, computableGroup: ComputableGroup, options: Options = Options()): ComputedResults { val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues) val computables = computableGroup.computables(realm, options.shouldSortValues) - Timber.d(">>>> Start computing group ${computableGroup.name}, ${computables.size} computables") +// Timber.d(">>>> Start computing group ${computableGroup.name}, ${computables.size} computables") results.addStat(NUMBER_OF_GAMES, computables.size.toDouble()) val sum = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble() @@ -256,7 +336,7 @@ class Calculator { } val shouldIterateOverComputables = - (options.evolutionValues == Options.EvolutionValues.STANDARD || options.computeLongestStreak) + (options.progressValues == Options.ProgressValues.STANDARD || options.computeLongestStreak) // Computable Result if (shouldIterateOverComputables) { @@ -339,7 +419,7 @@ class Calculator { var gBBSum: Double? = null var maxDuration: Double? = null - if (computableGroup.conditions.size == 0) { // SessionSets are fine + if (computableGroup.conditions.isEmpty()) { // SessionSets are fine gHourlyDuration = sessionSets.sum(SessionSet.Field.NET_DURATION.identifier).toDouble() / 3600000 // (milliseconds to hours) gBBSum = sessionSets.sum(SessionSet.Field.BB_NET.identifier).toDouble() @@ -350,7 +430,7 @@ class Calculator { } val shouldIterateOverSets = computableGroup.conditions.isNotEmpty() || - options.evolutionValues != Options.EvolutionValues.NONE || + options.progressValues != Options.ProgressValues.NONE || options.computeDaysPlayed // Session Set @@ -381,8 +461,8 @@ class Calculator { tHourlyRateBB = tBBSum / tHourlyDuration daysSet.add(sessionSet.startDate.startOfDay()) - when (options.evolutionValues) { - Options.EvolutionValues.STANDARD -> { + when (options.progressValues) { + Options.ProgressValues.STANDARD -> { results.addEvolutionValue(tHourlyRate, stat = HOURLY_RATE, data = sessionSet) results.addEvolutionValue(tIndex.toDouble(), stat = NUMBER_OF_SETS, data = sessionSet) results.addEvolutionValue( @@ -403,7 +483,7 @@ class Calculator { } } - Options.EvolutionValues.TIMED -> { + Options.ProgressValues.TIMED -> { results.addEvolutionValue(tRatedNetSum, tHourlyDuration, NET_RESULT, sessionSet) results.addEvolutionValue(tHourlyRate, tHourlyDuration, HOURLY_RATE, sessionSet) results.addEvolutionValue( 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 cae316c2..f4b61d7f 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Report.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Report.kt @@ -17,6 +17,7 @@ import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry import net.pokeranalytics.android.ui.view.DefaultLegendValues import net.pokeranalytics.android.ui.view.LegendContent import net.pokeranalytics.android.util.ColorUtils +import net.pokeranalytics.android.util.TextFormat import kotlin.math.abs /** @@ -44,7 +45,7 @@ class Report(var options: Calculator.Options) { */ fun lineEntries(stat: Stat? = null, context: Context): LineDataSet { val entries = mutableListOf() - val statToUse = stat ?: options.displayedStats.firstOrNull() + val statToUse = stat ?: options.stats.firstOrNull() val statName = statToUse?.name ?: "" statToUse?.let { @@ -60,7 +61,7 @@ class Report(var options: Calculator.Options) { fun barEntries(stat: Stat? = null, context: Context): BarDataSet { val entries = mutableListOf() - val statToUse = stat ?: options.displayedStats.firstOrNull() + val statToUse = stat ?: options.stats.firstOrNull() statToUse?.let { this._results.forEachIndexed { index, results -> @@ -79,7 +80,7 @@ class Report(var options: Calculator.Options) { fun multiLineEntries(context: Context): List { val dataSets = mutableListOf() - options.displayedStats.forEach { stat -> + options.stats.forEach { stat -> this._results.forEachIndexed { index, result -> val ds = result.singleLineEntries(stat, context) ds.color = ColorUtils.almostRandomColor(index, context) @@ -95,26 +96,22 @@ class Report(var options: Calculator.Options) { /** * A sessionGroup of computable items identified by a name */ -class ComputableGroup(query: Query, stats: List? = null) { +class ComputableGroup(var query: Query, var stats: List? = null) { -// constructor(query: Query, stats: List? = null) : this(query.name, query.conditions) -// -// private constructor(name: String = "", conditions: List = listOf(), stats: List? = null) - - var query: Query = query + /** + * A subgroup used to compute stat variation + */ + var comparedGroup: ComputableGroup? = null /** - * The display name of the group + * The computed statIds of the comparable sessionGroup */ - var name: String = "" - get() { - return this.query.name - } + var comparedComputedResults: ComputedResults? = null /** * A list of _conditions to get */ - var conditions: List = listOf() + val conditions: List get() { return this.query.conditions } @@ -164,21 +161,6 @@ class ComputableGroup(query: Query, stats: List? = null) { return sets } - /** - * The list of stats to display - */ - var stats: List? = stats - - /** - * A subgroup used to compute stat variation - */ - var comparedGroup: ComputableGroup? = null - - /** - * The computed stats of the comparable sessionGroup - */ - var comparedComputedResults: ComputedResults? = null - fun cleanup() { this._computables = null this._sessionSets = null @@ -195,14 +177,14 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu GraphUnderlyingEntry { /** - * The session group used to computed the stats + * The session group used to computed the statIds */ var group: ComputableGroup = group - // The computed stats of the sessionGroup + // The computed statIds of the sessionGroup private var _computedStats: MutableMap = mutableMapOf() - // The map containing all evolution numericValues for all stats + // The map containing all evolution numericValues for all statIds private var _evolutionValues: MutableMap> = mutableMapOf() private var shouldManageMultiGroupProgressValues = shouldManageMultiGroupProgressValues @@ -221,10 +203,10 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu } else { Point(value, data = data.objectIdentifier) } - this._addEvolutionValue(point, stat = stat) + this.addEvolutionValue(point, stat = stat) } - private fun _addEvolutionValue(point: Point, stat: Stat) { + private fun addEvolutionValue(point: Point, stat: Stat) { val evolutionValues = this._evolutionValues[stat] if (evolutionValues != null) { evolutionValues.add(point) @@ -246,7 +228,7 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu } /** - * Adds a [computedStat] to the list of stats + * Adds a [computedStat] to the list of statIds * Also computes evolution values using the previously computed values */ private fun addComputedStat(computedStat: ComputedStat) { @@ -366,9 +348,7 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu } fun finalize() { - this.consolidateProgressStats() - } // MPAndroidChart @@ -388,7 +368,7 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data)) } } - return DataSetFactory.lineDataSetInstance(entries, this.group.name, context) + return DataSetFactory.lineDataSetInstance(entries, this.group.query.getName(context), context) } fun durationEntries(stat: Stat, context: Context): LineDataSet { @@ -451,7 +431,9 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu // Stat Entry - override val entryTitle: String = this.group.name + override fun entryTitle(context: Context): String { + return this.group.query.getName(context) + } override fun formattedValue(stat: Stat): TextFormat { this.computedStat(stat)?.let { @@ -475,12 +457,12 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu return when (stat) { Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> { val totalStatValue = stat.format(entry.y.toDouble(), currency = null) - DefaultLegendValues(this.entryTitle, totalStatValue) + DefaultLegendValues(this.entryTitle(context), totalStatValue) } else -> { val entryValue = this.formattedValue(stat) val countValue = this.computedStat(Stat.NUMBER_OF_GAMES)?.format() - DefaultLegendValues(this.entryTitle, entryValue, countValue) + DefaultLegendValues(this.entryTitle(context), entryValue, countValue) } } } @@ -489,12 +471,12 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu return when (stat) { Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> { val totalStatValue = stat.format(entry.y.toDouble(), currency = null) - DefaultLegendValues(this.entryTitle, totalStatValue) + DefaultLegendValues(this.entryTitle(context), totalStatValue) } else -> { val entryValue = this.formattedValue(stat) val totalStatValue = stat.format(entry.y.toDouble(), currency = null) - DefaultLegendValues(this.entryTitle, entryValue, totalStatValue) + 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 2d204891..d065af5f 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt @@ -3,12 +3,12 @@ package net.pokeranalytics.android.calculus import android.content.Context import net.pokeranalytics.android.R import net.pokeranalytics.android.exceptions.FormattingException -import net.pokeranalytics.android.model.Criteria -import net.pokeranalytics.android.model.interfaces.Timed -import net.pokeranalytics.android.ui.graph.AxisFormatting 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.TextFormat +import net.pokeranalytics.android.util.enumerations.IntIdentifiable +import net.pokeranalytics.android.util.enumerations.IntSearchable import net.pokeranalytics.android.util.extensions.formatted import net.pokeranalytics.android.util.extensions.formattedHourlyDuration import net.pokeranalytics.android.util.extensions.toCurrency @@ -19,80 +19,56 @@ class StatFormattingException(message: String) : Exception(message) { } -class ObjectIdentifier(var id: String, var clazz: Class) { - -} +/** + * An enum representing all the types of Session statistics + */ +enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable { + + NET_RESULT(1), + BB_NET_RESULT(2), + HOURLY_RATE(3), + AVERAGE(4), + NUMBER_OF_SETS(5), + NUMBER_OF_GAMES(6), + HOURLY_DURATION(7), + AVERAGE_HOURLY_DURATION(8), + NET_BB_PER_100_HANDS(9), + HOURLY_RATE_BB(10), + AVERAGE_NET_BB(11), + WIN_RATIO(12), + AVERAGE_BUYIN(13), + ROI(14), + STANDARD_DEVIATION(15), + STANDARD_DEVIATION_HOURLY(16), + STANDARD_DEVIATION_BB_PER_100_HANDS(17), + HANDS_PLAYED(18), + LOCATIONS_PLAYED(19), + LONGEST_STREAKS(20), + MAXIMUM_NETRESULT(21), + MINIMUM_NETRESULT(22), + MAXIMUM_DURATION(23), + DAYS_PLAYED(24), + WINNING_SESSION_COUNT(25), + BB_SESSION_COUNT(26), + TOTAL_BUYIN(27), + RISK_OF_RUIN(28), + ; -enum class AggregationType { - SESSION, - MONTH, - YEAR, - DURATION; + companion object : IntSearchable { - val resId: Int - get() { - return when (this) { - SESSION -> R.string.session - MONTH -> R.string.month - YEAR -> R.string.year - DURATION -> R.string.duration - } + override fun valuesInternal(): Array { + return values() } - val axisFormatting: AxisFormatting - get() { - return when (this) { - DURATION -> AxisFormatting.X_DURATION - else -> AxisFormatting.DEFAULT + val userSelectableList: List + get() { + return values().filter { it.canBeUserSelected } } - } - val criterias: List - get() { - return when (this) { - MONTH -> listOf(Criteria.AllMonthsUpToNow) - YEAR -> listOf(Criteria.Years) - else -> listOf() + val evolutionValuesList: List + get() { + return values().filter { it.hasProgressValues } } - } - -} - -/** - * An enum representing all the types of Session statistics - */ -enum class Stat : RowRepresentable { - - NET_RESULT, - BB_NET_RESULT, - HOURLY_RATE, - AVERAGE, - NUMBER_OF_SETS, - NUMBER_OF_GAMES, - HOURLY_DURATION, - AVERAGE_HOURLY_DURATION, - NET_BB_PER_100_HANDS, - HOURLY_RATE_BB, - AVERAGE_NET_BB, - WIN_RATIO, - AVERAGE_BUYIN, - ROI, - STANDARD_DEVIATION, - STANDARD_DEVIATION_HOURLY, - STANDARD_DEVIATION_BB_PER_100_HANDS, - HANDS_PLAYED, - LOCATIONS_PLAYED, - LONGEST_STREAKS, - MAXIMUM_NETRESULT, - MINIMUM_NETRESULT, - MAXIMUM_DURATION, - DAYS_PLAYED, - WINNING_SESSION_COUNT, - BB_SESSION_COUNT, - TOTAL_BUYIN, - ; - - companion object { fun returnOnInvestment(netResult: Double, buyin: Double): Double? { if (buyin == 0.0) { @@ -150,6 +126,7 @@ enum class Stat : RowRepresentable { MINIMUM_NETRESULT -> R.string.min_net_result MAXIMUM_DURATION -> R.string.longest_session DAYS_PLAYED -> R.string.days_played + TOTAL_BUYIN -> R.string.total_buyin else -> throw IllegalStateException("Stat ${this.name} name required but undefined") } } @@ -171,7 +148,7 @@ enum class Stat : RowRepresentable { return TextFormat(value.toCurrency(currency), color) } // Red/green numericValues - HOURLY_RATE_BB, AVERAGE_NET_BB, NET_BB_PER_100_HANDS -> { + 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) } @@ -182,7 +159,7 @@ enum class Stat : RowRepresentable { HOURLY_DURATION, AVERAGE_HOURLY_DURATION, MAXIMUM_DURATION -> { return TextFormat(value.formattedHourlyDuration()) } // red/green percentages - WIN_RATIO, ROI -> { + WIN_RATIO, ROI, RISK_OF_RUIN -> { val color = if (value * 100 >= this.threshold) R.color.green else R.color.red return TextFormat("${(value * 100).formatted()}%", color) } // white amountsr @@ -197,7 +174,7 @@ enum class Stat : RowRepresentable { } } - val threshold: Double + private val threshold: Double get() { return when (this) { WIN_RATIO -> 50.0 @@ -206,6 +183,9 @@ enum class Stat : RowRepresentable { } + /** + * Returns a label used to display the legend right value, typically a total or an average + */ fun cumulativeLabelResId(context: Context): String { val resId = when (this) { AVERAGE, AVERAGE_HOURLY_DURATION, NET_BB_PER_100_HANDS, @@ -226,6 +206,9 @@ enum class Stat : RowRepresentable { } } + /** + * Returns the different available aggregation type for each statistic + */ val aggregationTypes: List get() { return when (this) { @@ -240,7 +223,10 @@ enum class Stat : RowRepresentable { } } - val hasEvolutionGraph: Boolean + /** + * Returns if the stat has an evolution graph + */ + val hasProgressGraph: Boolean get() { return when (this) { HOURLY_DURATION, AVERAGE_HOURLY_DURATION, @@ -249,7 +235,10 @@ enum class Stat : RowRepresentable { } } - val significantIndividualValue: Boolean + /** + * Returns if the stat has a significant value to display in a progress graph + */ + val graphSignificantIndividualValue: Boolean get() { return when (this) { WIN_RATIO, NUMBER_OF_SETS, NUMBER_OF_GAMES, STANDARD_DEVIATION, HOURLY_DURATION -> false @@ -257,7 +246,10 @@ enum class Stat : RowRepresentable { } } - val shouldShowNumberOfSessions: Boolean + /** + * Returns if the stat graph should show the number of sessions + */ + val graphShouldShowNumberOfSessions: Boolean get() { return when (this) { NUMBER_OF_GAMES, NUMBER_OF_SETS -> false @@ -265,7 +257,7 @@ enum class Stat : RowRepresentable { } } - val showXAxisZero: Boolean + val graphShowsXAxisZero: Boolean get() { return when (this) { HOURLY_DURATION -> true @@ -273,7 +265,7 @@ enum class Stat : RowRepresentable { } } - val showYAxisZero: Boolean + val graphShowsYAxisZero: Boolean get() { return when (this) { HOURLY_DURATION -> true @@ -281,6 +273,25 @@ enum class Stat : RowRepresentable { } } + private val canBeUserSelected: Boolean + get() { + return when (this) { + WINNING_SESSION_COUNT, BB_SESSION_COUNT, RISK_OF_RUIN -> false + else -> true + } + } + + private val hasProgressValues: Boolean + get() { + return when (this) { + NET_RESULT, NET_BB_PER_100_HANDS, HOURLY_RATE_BB, + AVERAGE_HOURLY_DURATION, HOURLY_DURATION, + NUMBER_OF_SETS, ROI, AVERAGE_BUYIN, WIN_RATIO, + AVERAGE_NET_BB, NUMBER_OF_GAMES, AVERAGE -> true + else -> false + } + } + override val viewType: Int = RowViewType.TITLE_VALUE.ordinal } diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt b/app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt index 2563b660..2f889e24 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt @@ -11,12 +11,15 @@ class BankrollCalculator { companion object { - fun computeReport(setup: BankrollReportSetup) : BankrollReport { + fun computeReport(realm: Realm, setup: BankrollReportSetup) : BankrollReport { - val realm = Realm.getDefaultInstance() + //val realm = Realm.getDefaultInstance() val report = BankrollReport(setup) - val bankrolls: List = if (setup.bankroll != null) listOf(setup.bankroll) else realm.where(Bankroll::class.java).findAll() + + val bankrolls: List = + if (setup.bankroll != null) listOf(setup.bankroll) + else realm.where(Bankroll::class.java).findAll() var initialValue = 0.0 var transactionNet = 0.0 @@ -25,8 +28,14 @@ class BankrollCalculator { val rate = if (setup.virtualBankroll) bankroll.rate else 1.0 - initialValue += bankroll.initialValue * rate - transactionNet += bankroll.transactions.sumByDouble { it.amount } * rate + if (setup.shouldAddInitialValue) { + initialValue += bankroll.initialValue * rate + } + + bankroll.transactions?.let { transactions -> + val sum = transactions.sum("amount") + transactionNet += rate * sum.toDouble() + } } report.transactionsNet = transactionNet @@ -72,7 +81,7 @@ class BankrollCalculator { report.generateGraphPointsIfNecessary() - realm.close() + //realm.close() return report } diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt b/app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt index 313979f5..2b7e4724 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt @@ -9,16 +9,48 @@ import net.pokeranalytics.android.model.interfaces.DatedValue import net.pokeranalytics.android.model.realm.Bankroll import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.ui.graph.DataSetFactory +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowViewType import java.util.* import kotlin.collections.HashMap +//object BankrollReportManager { +// +// var mainReport: BankrollReport? = null +// var reports: MutableMap = mutableMapOf() +// +// fun udpateBankrolls(bankrolls: List) { +// this.invalidateMainReport() +// bankrolls.forEach { +// this.reports.remove(it.id) +// } +// } +// +// fun deleteBankrolls(bankrolls: List) { +// this.invalidateMainReport() +// bankrolls.forEach { +// this.reports.remove(it.id) +// } +// } +// +// private fun invalidateMainReport() { +// this.mainReport = null +// } +// +// private fun launchReports(bankrolls: List) { +// +// this.mainReport = BankrollCalculator.computeReport() +// +// +// } +// +//} -class BankrollReport(setup: BankrollReportSetup) { - - /** - * The setup used to compute the report - */ - var setup: BankrollReportSetup = setup +/** + * This class holds the results from the BankrollCalculator computations + * It has all the information required for the Bankroll various displays + */ +class BankrollReport(var setup: BankrollReportSetup) : RowRepresentable { /** * The value of the bankroll @@ -30,6 +62,10 @@ class BankrollReport(setup: BankrollReportSetup) { * The initial value of the bankroll, or of all bankrolls if virtual is computed */ var initial: Double = 0.0 + set(value) { + field = value + this.computeBankrollTotal() + } /** * The net result from poker computables @@ -49,7 +85,10 @@ class BankrollReport(setup: BankrollReportSetup) { this.computeBankrollTotal() } - fun computeBankrollTotal() { + /** + * Computes the bankroll total + */ + private fun computeBankrollTotal() { this.total = this.initial + this.netResult + this.transactionsNet } @@ -59,7 +98,7 @@ class BankrollReport(setup: BankrollReportSetup) { var depositTotal: Double = 0.0 set(value) { field = value - this.netBanked = this.depositTotal + this.withdrawalTotal + this.computeNetBanked() } /** @@ -68,9 +107,16 @@ class BankrollReport(setup: BankrollReportSetup) { var withdrawalTotal: Double = 0.0 set(value) { field = value - this.netBanked = this.depositTotal + this.withdrawalTotal + this.computeNetBanked() } + /** + * Computes the net banked amount + */ + private fun computeNetBanked() { + this.netBanked = -this.withdrawalTotal - this.depositTotal + } + /** * The difference between withdrawals and deposits */ @@ -82,20 +128,47 @@ class BankrollReport(setup: BankrollReportSetup) { */ var riskOfRuin: Double? = null + /** + * The list of transactions held by the bankroll + */ var transactions: List = mutableListOf() private set + /** + * A map containing TransactionBuckets by transaction types + */ var transactionBuckets: HashMap = HashMap() private set - var evolutionPoints: MutableList = mutableListOf() - var evolutionItems: MutableList = mutableListOf() - private set + /** + * The list of bankroll graph points + */ + private var evolutionPoints: MutableList = mutableListOf() + /** + * The list of dated items used for the graph + */ + private var evolutionItems: MutableList = mutableListOf() + + override val viewType: Int + get() { + return if (setup.bankroll == null) { + RowViewType.LEGEND_DEFAULT.ordinal + } else { + RowViewType.TITLE_VALUE_ARROW.ordinal + } + } + + /** + * Adds a list of dated items to the evolution items used to get the bankroll graph + */ fun addDatedItems(items: Collection) { this.evolutionItems.addAll(items) } + /** + * Adds a transaction to its type bucket + */ fun addTransaction(transaction: Transaction) { transaction.type?.let { type -> @@ -111,11 +184,14 @@ class BankrollReport(setup: BankrollReportSetup) { bucket.addTransaction(transaction) } ?: run { - throw Exception("Transaction has no type") + throw IllegalStateException("Transaction has no type") } } + /** + * Generates the graph points used for the virtual bankroll + */ fun generateGraphPointsIfNecessary() { if (!this.setup.virtualBankroll) { @@ -124,18 +200,23 @@ class BankrollReport(setup: BankrollReportSetup) { this.evolutionItems.sortBy { it.date } + var total = 0.0 this.evolutionItems.forEach { - val point = BRGraphPoint(it.amount, it.date, it) + total += it.amount + val point = BRGraphPoint(total, it.date, it) this.evolutionPoints.add(point) } } + /** + * Returns a data set used for the bankroll graph + */ fun lineDataSet(context: Context): LineDataSet { val entries = mutableListOf() - this.evolutionPoints.forEach { - val entry = Entry(it.date.time.toFloat(), it.value.toFloat(), it.data) + this.evolutionPoints.forEachIndexed { index, point -> + val entry = Entry(index.toFloat(), point.value.toFloat(), point.data) entries.add(entry) } return DataSetFactory.lineDataSetInstance(entries, "", context) @@ -149,11 +230,18 @@ class BankrollReport(setup: BankrollReportSetup) { */ class BankrollReportSetup(val bankroll: Bankroll? = null, val from: Date? = null, val to: Date? = null) { + /** + * Returns whether the setup concerns the virtual bankroll, + * i.e. the bankroll summing all concrete bankrolls + */ val virtualBankroll: Boolean get() { return this.bankroll == null } + /** + * the query used to get bankroll transactions + */ val query: Query get() { val query = Query() @@ -175,20 +263,46 @@ class BankrollReportSetup(val bankroll: Bankroll? = null, val from: Date? = null return query } + /** + * Returns whether or not the initial value should be added for the bankroll total + */ + val shouldAddInitialValue: Boolean + get() { + return this.from == null + } + } +/** + * A TransactionBucket holds a list of _transactions and computes its amount sum + */ class TransactionBucket(useRate: Boolean = false) { - var transactions: MutableList = mutableListOf() - private set + /** + * Whether the bankroll rate should be used + */ + private var useRate: Boolean = useRate + + /** + * A list of _transactions + */ + private var _transactions: MutableList = mutableListOf() + + val transactions: List + get() { + return this._transactions + } + + /** + * The sum of all _transactions + */ var total: Double = 0.0 private set - var useRate: Boolean = useRate - private set + fun addTransaction(transaction: Transaction) { - this.transactions.add(transaction) + this._transactions.add(transaction) var rate = 1.0 if (this.useRate) { rate = transaction.bankroll?.currency?.rate ?: 1.0 diff --git a/app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt b/app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt index 1eabf395..7716e745 100644 --- a/app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt +++ b/app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt @@ -8,6 +8,9 @@ class RowRepresentableEditDescriptorException(message: String) : Exception(messa class ConfigurationException(message: String) : Exception(message) +class EnumIdentifierNotFoundException(message: String) : Exception(message) +class MisconfiguredSavableEnumException(message: String) : Exception(message) + sealed class PokerAnalyticsException(message: String) : Exception(message) { object FilterElementUnknownName : PokerAnalyticsException(message = "No filterElement name was found to identify the queryCondition") object FilterElementUnknownSectionName: PokerAnalyticsException(message = "No filterElement section name was found to identify the queryCondition") @@ -21,4 +24,4 @@ sealed class PokerAnalyticsException(message: String) : Exception(message) { data class QueryValueMapMissingKeys(val missingKeys: List) : PokerAnalyticsException(message = "valueMap does not contain $missingKeys") data class UnknownQueryTypeForRow(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "no queryWith type for $filterElementRow") data class MissingFieldNameForQueryCondition(val name: String) : PokerAnalyticsException(message = "Missing fieldname for QueryCondition ${name}") -} \ No newline at end of file +} diff --git a/app/src/main/java/net/pokeranalytics/android/model/Criteria.kt b/app/src/main/java/net/pokeranalytics/android/model/Criteria.kt index 0a33cc1a..fa679d19 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/Criteria.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/Criteria.kt @@ -3,6 +3,7 @@ package net.pokeranalytics.android.model import io.realm.Realm import io.realm.Sort import io.realm.kotlin.where +import net.pokeranalytics.android.R import net.pokeranalytics.android.exceptions.PokerAnalyticsException import net.pokeranalytics.android.model.Criteria.Bankrolls.comparison import net.pokeranalytics.android.model.Criteria.Blinds.comparison @@ -14,229 +15,325 @@ import net.pokeranalytics.android.model.Criteria.TournamentFeatures.comparison import net.pokeranalytics.android.model.Criteria.TournamentFees.comparison import net.pokeranalytics.android.model.Criteria.TournamentNames.comparison import net.pokeranalytics.android.model.Criteria.TournamentTypes.comparison +import net.pokeranalytics.android.model.Criteria.TransactionTypes.comparison import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.realm.* +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.util.enumerations.IntIdentifiable +import net.pokeranalytics.android.util.enumerations.IntSearchable +import net.pokeranalytics.android.util.extensions.findById fun List.combined(): List { - val comparatorList = ArrayList>() - this.forEach { criteria -> - comparatorList.add(criteria.queries) - } - return getCombinations(comparatorList) + val comparatorList = ArrayList>() + this.forEach { criteria -> + comparatorList.add(criteria.queries) + } + return getCombinations(comparatorList) } fun getCombinations(queries: List>): List { - if (queries.size == 0) { return listOf() } + if (queries.isEmpty()) { + return listOf() + } - val mutableQueries = queries.toMutableList() - var combinations = mutableQueries.removeAt(0) + val mutableQueries = queries.toMutableList() + var combinations = mutableQueries.removeAt(0) - for (queryList in mutableQueries) { + for (queryList in mutableQueries) { - val newCombinations = mutableListOf() - combinations.forEach { combinedQuery -> - queryList.forEach { queryToAdd -> - val nq = Query().merge(combinedQuery).merge(queryToAdd) - newCombinations.add(nq) - } - } - combinations = newCombinations - } + val newCombinations = mutableListOf() + combinations.forEach { combinedQuery -> + queryList.forEach { queryToAdd -> + val nq = Query().merge(combinedQuery).merge(queryToAdd) + newCombinations.add(nq) + } + } + combinations = newCombinations + } - return combinations + return combinations } -//fun getCombinations(lists: List): List { -// var combinations: LinkedHashSet = LinkedHashSet() -// var newCombinations: LinkedHashSet -// -// var index = 0 -// -// // extract each of the integers in the first list -// // and add each to ints as a new list -// if (lists.isNotEmpty()) { -// for (i in lists[0]) { -// val newList = ArrayList() -// newList.add(i) -// combinations.add(newList) -// } -// index++ -// } -// while (index < lists.size) { -// val nextList = lists[index] -// newCombinations = LinkedHashSet() -// for (first in combinations) { -// for (second in nextList) { -// val newList = ArrayList() -// newList.addAll(first) -// newList.add(second) -// newCombinations.add(newList) -// } -// } -// combinations = newCombinations -// -// index++ -// } -// -// return combinations.toList() -//} - -sealed class Criteria { - abstract class RealmCriteria : Criteria() { - inline fun comparison(): List { - return compare, T>() - } - } - - abstract class SimpleCriteria(private val conditions:List): Criteria() { - fun comparison(): List { - return conditions.map { Query(it) } - } - } - - abstract class ListCriteria : Criteria() { - inline fun , reified S : Comparable> comparison(): List { - QueryCondition.distinct()?.let { - val values = it.mapNotNull { session -> - when (this) { - is Limits -> if (session.limit is S) { session.limit as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue - is TournamentTypes -> if (session.tournamentType is S) { session.tournamentType as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue - is TableSizes -> if (session.tableSize is S) { session.tableSize as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue - is TournamentFees -> if (session.tournamentEntryFee is S) { session.tournamentEntryFee as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue - is Blinds -> if (session.blinds is S) { session.blinds as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue - else -> null - } - }.distinct() - return compareList(values = values) +sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable { + + abstract class RealmCriteria(uniqueIdentifier: Int) : Criteria(uniqueIdentifier) { + inline fun comparison(): List { + if (this is ListCustomFields) { + val objects = mutableListOf() + val realm = Realm.getDefaultInstance() + realm.findById(CustomField::class.java, this.customFieldId)?.entries?.forEach { + objects.add(QueryCondition.CustomFieldListQuery(it)) + } + objects.sorted() + realm.close() + return objects.map { Query(it) } } - return listOf() - } - } - - - object Bankrolls: RealmCriteria() - object Games: RealmCriteria() - object TournamentNames: RealmCriteria() - object Locations: RealmCriteria() - object TournamentFeatures: RealmCriteria() - object Limits: ListCriteria() - object TableSizes: ListCriteria() - object TournamentTypes: ListCriteria() - object MonthsOfYear: SimpleCriteria(List(12) { index -> QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(index)} }) - object DaysOfWeek: SimpleCriteria(List(7) { index -> QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(index + 1) } }) - object SessionTypes: SimpleCriteria(listOf(QueryCondition.IsCash, QueryCondition.IsTournament)) - object BankrollTypes: SimpleCriteria(listOf(QueryCondition.IsLive, QueryCondition.IsOnline)) - object DayPeriods: SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd)) - object Years: ListCriteria() - object AllMonthsUpToNow: ListCriteria() - object Blinds: ListCriteria() - object TournamentFees: ListCriteria() - object Cash: SimpleCriteria(listOf(QueryCondition.IsCash)) - object Tournament: SimpleCriteria(listOf(QueryCondition.IsTournament)) - - val queries: List - get() { - return when (this) { - is AllMonthsUpToNow -> { - val realm = Realm.getDefaultInstance() - val firstSession= realm.where().sort("startDate", Sort.ASCENDING).findFirst() - val lastSession = realm.where().sort("startDate", Sort.DESCENDING).findFirst() - realm.close() - - val years: ArrayList = arrayListOf() - - val firstYear = firstSession?.year ?: return years - val firstMonth = firstSession.month ?: return years - val lastYear = lastSession?.year ?: return years - val lastMonth = lastSession.month ?: return years - - for (year in firstYear..lastYear) { - val currentYear = QueryCondition.AnyYear(year) - for (month in 0..11) { - - if (year == firstYear && month < firstMonth) { - continue - } - if (year == lastYear && month > lastMonth) { - continue - } - - val currentMonth = QueryCondition.AnyMonthOfYear(month) - val query = Query(currentYear, currentMonth) - years.add(query) - } + return compare, T>() + } + } + + abstract class SimpleCriteria(private val conditions: List, uniqueIdentifier: Int) : Criteria(uniqueIdentifier) { + fun comparison(): List { + return conditions.map { Query(it) } + } + } + + abstract class ListCriteria(uniqueIdentifier: Int) : Criteria(uniqueIdentifier) { + inline fun , reified S : Comparable> comparison(): List { + + if (this is ValueCustomFields) { + val realm = Realm.getDefaultInstance() + val distincts = realm.where().equalTo("customFields.id", this.customFieldId).findAll().sort("numericValue", Sort.ASCENDING) + realm.close() + + val objects = mutableListOf() + distincts.mapNotNull { + it.numericValue + }.distinct().forEach {value -> + val condition: QueryCondition.CustomFieldNumberQuery = when (this.customFieldType(realm)) { + CustomField.Type.AMOUNT.uniqueIdentifier -> QueryCondition.CustomFieldAmountQuery() + CustomField.Type.NUMBER.uniqueIdentifier -> QueryCondition.CustomFieldNumberQuery() + else -> throw PokerAnalyticsException.QueryValueMapUnexpectedValue + }.apply { + this.customFieldId = this@ListCriteria.customFieldId + listOfValues = arrayListOf(value) } - years - } - else -> { - return this.queryConditions + objects.add(condition) } + objects.sorted() + return objects.map { Query(it) } } - } - - val queryConditions: List - get() { - return when (this) { - is Bankrolls -> comparison() - is Games -> comparison() - is TournamentFeatures -> comparison() - is TournamentNames -> comparison() - is Locations -> comparison() - is SimpleCriteria -> comparison() - is Limits -> comparison() - is TournamentTypes -> comparison() - is TableSizes -> comparison() - is TournamentFees -> comparison() - is Years -> { - val years = arrayListOf() - val realm = Realm.getDefaultInstance() - val lastSession = realm.where().sort("startDate", Sort.DESCENDING).findFirst() - val yearNow = lastSession?.year ?: return years - - realm.where().sort("year", Sort.ASCENDING).findFirst()?.year?.let { - for (index in 0..(yearNow - it)) { - val yearCondition = QueryCondition.AnyYear().apply { - listOfValues = arrayListOf(it + index) - } - years.add(Query(yearCondition)) - } + + + QueryCondition.distinct()?.let { + val values = it.mapNotNull { session -> + when (this) { + is Limits -> if (session.limit is S) { + session.limit as S + } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue + is TournamentTypes -> if (session.tournamentType is S) { + session.tournamentType as S + } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue + is TableSizes -> if (session.tableSize is S) { + session.tableSize as S + } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue + is TournamentFees -> if (session.tournamentEntryFee is S) { + session.tournamentEntryFee as S + } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue + is Blinds -> if (session.blinds is S) { + session.blinds as S + } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue + else -> null + } + }.distinct() + return compareList(values = values) + } + return listOf() + } + } + + + object Bankrolls : RealmCriteria(1) + object Games : RealmCriteria(2) + object TournamentNames : RealmCriteria(3) + object Locations : RealmCriteria(4) + object TournamentFeatures : RealmCriteria(5) + object TransactionTypes : RealmCriteria(6) + object Limits : ListCriteria(7) + object TableSizes : ListCriteria(8) + object TournamentTypes : ListCriteria(9) + object MonthsOfYear : SimpleCriteria(List(12) { index -> + QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(index) } + }, 10) + + object DaysOfWeek : SimpleCriteria(List(7) { index -> + QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(index + 1) } + }, 11) + + object SessionTypes : SimpleCriteria(listOf(QueryCondition.IsCash, QueryCondition.IsTournament), 12) + object BankrollTypes : SimpleCriteria(listOf(QueryCondition.IsLive, QueryCondition.IsOnline), 13) + object DayPeriods : SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd), 14) + object Years : ListCriteria(15) + object AllMonthsUpToNow : ListCriteria(16) + object Blinds : ListCriteria(17) + object TournamentFees : ListCriteria(18) + object Cash : SimpleCriteria(listOf(QueryCondition.IsCash), 19) + object Tournament : SimpleCriteria(listOf(QueryCondition.IsTournament), 20) + data class ListCustomFields(override var customFieldId: String) : RealmCriteria(21), CustomFieldCriteria + data class ValueCustomFields(override var customFieldId: String) : ListCriteria(22), CustomFieldCriteria + + val queries: List + get() { + return when (this) { + is AllMonthsUpToNow -> { + val realm = Realm.getDefaultInstance() + val firstSession = realm.where().sort("startDate", Sort.ASCENDING).findFirst() + val lastSession = realm.where().sort("startDate", Sort.DESCENDING).findFirst() + realm.close() + + val years: ArrayList = arrayListOf() + + val firstYear = firstSession?.year ?: return years + val firstMonth = firstSession.month ?: return years + val lastYear = lastSession?.year ?: return years + val lastMonth = lastSession.month ?: return years + + for (year in firstYear..lastYear) { + val currentYear = QueryCondition.AnyYear(year) + for (month in 0..11) { + + if (year == firstYear && month < firstMonth) { + continue + } + if (year == lastYear && month > lastMonth) { + continue + } + + val currentMonth = QueryCondition.AnyMonthOfYear(month) + val query = Query(currentMonth, currentYear) + years.add(query) + } + } + years + } + else -> { + return this.queryConditions + } + } + } + + val queryConditions: List + get() { + return when (this) { + is Bankrolls -> comparison() + is Games -> comparison() + is TournamentFeatures -> comparison() + is TournamentNames -> comparison() + is Locations -> comparison() + is TransactionTypes -> comparison() + is SimpleCriteria -> comparison() + is Limits -> comparison() + is TournamentTypes -> comparison() + is TableSizes -> comparison() + is TournamentFees -> comparison() + is Years -> { + val years = arrayListOf() + val realm = Realm.getDefaultInstance() + val lastSession = realm.where().sort("startDate", Sort.DESCENDING).findFirst() + val yearNow = lastSession?.year ?: return years + + realm.where().sort("year", Sort.ASCENDING).findFirst()?.year?.let { + for (index in 0..(yearNow - it)) { + val yearCondition = QueryCondition.AnyYear().apply { + listOfValues = arrayListOf(it + index) + } + years.add(Query(yearCondition)) + } + } + realm.close() + years + } + is Blinds -> comparison() + is ListCustomFields -> comparison() + is ValueCustomFields -> { + val realm = Realm.getDefaultInstance() + val queries = when (this.customFieldType(realm)) { + CustomField.Type.AMOUNT.uniqueIdentifier -> comparison() + CustomField.Type.NUMBER.uniqueIdentifier -> comparison() + else -> throw PokerAnalyticsException.QueryTypeUnhandled } - realm.close() - years - } - is Blinds -> comparison() - else -> throw PokerAnalyticsException.QueryTypeUnhandled - } - } - - companion object { - inline fun , reified T : NameManageable> compare(): List { - val objects = mutableListOf() - val realm = Realm.getDefaultInstance() - realm.where().findAll().forEach { - val condition = (QueryCondition.getInstance() as S).apply { - setObject(it) + realm.close() + queries } - objects.add(condition) - } - objects.sorted() - realm.close() - return objects.map { Query(it) } - } - - inline fun < reified S : QueryCondition.ListOfValues, T:Any > compareList(values:List): List { - val objects = mutableListOf() - values.forEach { - val condition =(S::class.java.newInstance()).apply { - listOfValues = arrayListOf(it) - } - objects.add(condition) - } - objects.sorted() - return objects.map { Query(it) } - } - } + else -> throw PokerAnalyticsException.QueryTypeUnhandled + } + } + + override val resId: Int? + get() { + return when (this) { + Bankrolls -> R.string.bankroll + Games -> R.string.game + TournamentNames -> R.string.tournament_name + Locations -> R.string.location + TournamentFeatures -> R.string.tournament_feature + Limits -> R.string.limit + TableSizes -> R.string.table_size + TournamentTypes -> R.string.tournament_type + MonthsOfYear -> R.string.month_of_the_year + DaysOfWeek -> R.string.day_of_the_week + SessionTypes -> R.string.cash_or_tournament + BankrollTypes -> R.string.live_or_online + DayPeriods -> R.string.weekdays_or_weekend + Years -> R.string.year + AllMonthsUpToNow -> R.string.month + Blinds -> R.string.blind + TournamentFees -> R.string.entry_fees +// is ListCustomFields -> this.customField.resId +// is ValueCustomFields -> this.customField.resId + else -> null + } + } + + companion object : IntSearchable { + + inline fun , reified T : NameManageable> compare(): List { + val objects = mutableListOf() + val realm = Realm.getDefaultInstance() + realm.where().findAll().forEach { + val condition = (QueryCondition.getInstance() as S).apply { + setObject(it) + } + objects.add(condition) + } + objects.sorted() + realm.close() + return objects.map { Query(it) } + } + + inline fun , T : Any> compareList(values: List): List { + val objects = mutableListOf() + values.forEach { + val condition = (S::class.java.newInstance()).apply { + listOfValues = arrayListOf(it) + } + objects.add(condition) + } + objects.sorted() + return objects.map { Query(it) } + } + + // SavableEnum + override fun valuesInternal(): Array { + return all.toTypedArray() + } + + val all: List + get() { + return listOf( + Bankrolls, Games, TournamentNames, Locations, + TournamentFeatures, Limits, TableSizes, TournamentTypes, + MonthsOfYear, DaysOfWeek, SessionTypes, + BankrollTypes, DayPeriods, Years, + AllMonthsUpToNow, Blinds, TournamentFees + ) + } + } + } + +interface CustomFieldCriteria { + var customFieldId: String + + fun customField(realm: Realm) : CustomField { + return realm.findById(this.customFieldId) ?: throw IllegalStateException("Custom field not found") + } + + fun customFieldType(realm: Realm): Int { + return this.customField(realm).type + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/Limit.kt b/app/src/main/java/net/pokeranalytics/android/model/Limit.kt index d99031d8..ca04d3e4 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/Limit.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/Limit.kt @@ -1,5 +1,6 @@ package net.pokeranalytics.android.model +import android.content.Context import net.pokeranalytics.android.ui.view.RowRepresentable enum class Limit : RowRepresentable { @@ -9,6 +10,21 @@ enum class Limit : RowRepresentable { SPREAD, MIXED; + companion object { + + fun getInstance(value: String) : Limit? { + return when (value) { + "No Limit" -> NO + "Pot Limit" -> POT + "Fixed Limit", "Limit" -> FIXED + "Mixed Limit" -> MIXED + "Spread Limit" -> SPREAD + else -> null + } + } + + } + val shortName: String get() { return when (this) { @@ -32,7 +48,8 @@ enum class Limit : RowRepresentable { } } - override fun getDisplayName(): String { + override fun getDisplayName(context: Context): String { return this.longName } + } \ 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 b59df116..ba7658ff 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/LiveData.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/LiveData.kt @@ -2,15 +2,11 @@ package net.pokeranalytics.android.model import android.content.Context import io.realm.Realm -import io.realm.RealmObject -import io.realm.RealmResults -import io.realm.Sort -import io.realm.kotlin.where import net.pokeranalytics.android.R -import net.pokeranalytics.android.model.interfaces.CountableUsage import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.ui.view.Localizable +import net.pokeranalytics.android.util.extensions.findById /** * An enum managing the business objects related to a realm results @@ -22,76 +18,14 @@ enum class LiveData : Localizable { TOURNAMENT_NAME, TOURNAMENT_FEATURE, TRANSACTION, - TRANSACTION_TYPE; + TRANSACTION_TYPE, + FILTER, + CUSTOM_FIELD, + REPORT_SETUP; - fun items(realm: Realm, fieldName: String? = null, sortOrder: Sort? = null): RealmResults<*> { - var results = realm.where(this.relatedEntity).findAll().sort(fieldName ?: this.sortingFieldName, sortOrder ?: this.sorting) + var subType:Int? = null - if (results.size > 0) { - if (results.first() is CountableUsage) { - this.setUseCount(realm, results) - return results.sort("useCount", Sort.DESCENDING) - } - } - return results - } - - fun setUseCount(realm: Realm, realmResults: RealmResults<*>) { - realm.executeTransaction { - realmResults.forEach { countableUsage -> - - when (this) { - TOURNAMENT_FEATURE -> { - (countableUsage as CountableUsage).useCount = it.where().contains( - "tournamentFeatures.id", - countableUsage.id - ).count().toInt() - } - else -> { - (countableUsage as CountableUsage).useCount = it.where().equalTo( - "${relatedEntity.simpleName.decapitalize()}.id", - countableUsage.id - ).count().toInt() - } - } - - } - } - } - - /** - * Return a copy of a RealmResults - */ - fun itemsArray(realm: Realm, fieldName: String? = null, sortOrder: Sort? = null): ArrayList<*> { - val results: ArrayList = ArrayList() - results.addAll( - realm.copyFromRealm( - realm.where(this.relatedEntity).findAll().sort( - fieldName ?: this.sortingFieldName, sortOrder ?: this.sorting - ) - ) - ) - - return results - } - - private val sortingFieldName: String - get() { - return when (this) { - TRANSACTION -> "date" - else -> "name" - } - } - - private val sorting: Sort - get() { - return when (this) { - TRANSACTION -> Sort.DESCENDING - else -> Sort.ASCENDING - } - } - - private val relatedEntity: Class + val relatedEntity: Class get() { return when (this) { BANKROLL -> Bankroll::class.java @@ -101,15 +35,14 @@ enum class LiveData : Localizable { TOURNAMENT_FEATURE -> TournamentFeature::class.java TRANSACTION -> Transaction::class.java TRANSACTION_TYPE -> TransactionType::class.java + FILTER -> Filter::class.java + CUSTOM_FIELD -> CustomField::class.java + REPORT_SETUP -> ReportSetup::class.java } } - fun deleteData(realm: Realm, data: Deletable) { - realm.where(this.relatedEntity).equalTo("id", data.id).findAll().deleteAllFromRealm() - } - - fun updateOrCreate(realm: Realm, primaryKey: String?): RealmObject { - val proxyItem: RealmObject? = this.getData(realm, primaryKey) + fun updateOrCreate(realm: Realm, primaryKey: String?): Deletable { + val proxyItem: Deletable? = this.getData(realm, primaryKey) proxyItem?.let { return realm.copyFromRealm(it) } ?: run { @@ -117,14 +50,14 @@ enum class LiveData : Localizable { } } - fun newEntity(): RealmObject { + private fun newEntity(): Deletable { return this.relatedEntity.newInstance() } - fun getData(realm: Realm, primaryKey: String?): RealmObject? { - var proxyItem: RealmObject? = null + fun getData(realm: Realm, primaryKey: String?): Deletable? { + var proxyItem: Deletable? = null primaryKey?.let { - val t = realm.where(this.relatedEntity).equalTo("id", it).findFirst() + val t = realm.findById(this.relatedEntity, it) t?.let { proxyItem = t } @@ -140,16 +73,67 @@ enum class LiveData : Localizable { LOCATION -> R.string.location TOURNAMENT_NAME -> R.string.tournament_name TOURNAMENT_FEATURE -> R.string.tournament_feature + TRANSACTION -> R.string.operation + TRANSACTION_TYPE -> R.string.operation_type + FILTER -> R.string.filter + CUSTOM_FIELD -> R.string.custom_field + REPORT_SETUP -> R.string.custom + } + } + + val pluralResId: Int + get() { + return when (this) { + BANKROLL -> R.string.bankrolls + GAME -> R.string.games + LOCATION -> R.string.locations + TOURNAMENT_NAME -> R.string.tournament_names + TOURNAMENT_FEATURE -> R.string.tournament_features TRANSACTION -> R.string.operations TRANSACTION_TYPE -> R.string.operation_types + FILTER -> R.string.filters + CUSTOM_FIELD -> R.string.custom_fields + REPORT_SETUP -> R.string.custom } } - /** - * Return the new entity title + private val newResId: Int + get() { + return when (this) { + BANKROLL -> R.string.new_bankroll + GAME -> R.string.new_variant + LOCATION -> R.string.new_location + TOURNAMENT_NAME -> R.string.new_tournament_name + TOURNAMENT_FEATURE -> R.string.new_tournament_feature + TRANSACTION -> R.string.new_operation + TRANSACTION_TYPE -> R.string.new_operation_type + FILTER -> R.string.new_filter + CUSTOM_FIELD -> R.string.new_custom_field + REPORT_SETUP -> R.string.new_report + } + } + + + + /** + * Return the new entity titleResId */ fun newEntityLocalizedTitle(context: Context): String { - return "${context.getString(R.string.new_str)} ${this.localizedTitle(context)}" + return context.getString(this.newResId) + } + + /** + * Return the update entity titleResId + */ + fun updateEntityLocalizedTitle(context: Context): String { + return context.getString(R.string.update_entity, this.localizedTitle(context).toLowerCase()) + } + + /** + * Return the update entity titleResId + */ + fun pluralLocalizedTitle(context: Context): String { + return context.getString(this.pluralResId, context) } } 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 c1bba39f..2b3c69b4 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/TableSize.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/TableSize.kt @@ -6,21 +6,32 @@ import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITLE_GRID.ordinal) : RowRepresentable { + companion object { - val all : List - get() { - return Array(9, init = - { index -> TableSize(index + 2) }).toList() - } + + val all: List + get() { + return Array(9, init = + { index -> TableSize(index + 2) }).toList() + } + + fun valueForLabel(label: String) : Int? { + return when (label) { + "Full Ring", "Full-Ring" -> 10 + "Short-Handed", "Short Handed" -> 6 + "Heads-Up", "Heads Up" -> 2 + else -> null + } + } } - override fun getDisplayName(): String { - return if (this.numberOfPlayer == 2) { - return "HU" - } else { - "${this.numberOfPlayer}-max" - } - } + override fun getDisplayName(context: Context): String { + return if (this.numberOfPlayer == 2) { + return "HU" + } else { + "${this.numberOfPlayer}-max" + } + } override val resId: Int? get() { 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 64a22295..c17b647a 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt @@ -1,5 +1,6 @@ package net.pokeranalytics.android.model +import android.content.Context import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType @@ -13,6 +14,14 @@ enum class TournamentType : RowRepresentable { get() { return TournamentType.values() as List } + + fun getValueForLabel(label: String) : TournamentType? { + return when (label) { + "Single-Table" -> SNG + "Multi-Table" -> MTT + else -> null + } + } } override val resId: Int? @@ -23,7 +32,7 @@ enum class TournamentType : RowRepresentable { } } - override fun getDisplayName(): String { + override fun getDisplayName(context: Context): String { return when (this) { MTT -> "MTT" SNG -> "SNG" diff --git a/app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt b/app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt index eefa27fb..0eb40936 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt @@ -64,15 +64,20 @@ class FilterHelper { Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition) Result::class.java -> Result.fieldNameForQueryType(queryCondition) else -> { - throw UnmanagedFilterField("Filterable type fields are not defined for class ${T::class}") + null +// throw UnmanagedFilterField("Filterable type fields are not defined for class ${T::class}") } } + return fieldName + +/* fieldName?.let { return fieldName } ?: run { throw PokerAnalyticsException.MissingFieldNameForQueryCondition(queryCondition.name) } +*/ } diff --git a/app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt b/app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt index ffcfdd62..ef2f0b2b 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt @@ -1,6 +1,10 @@ package net.pokeranalytics.android.model.filter +import android.content.Context import io.realm.RealmQuery +import io.realm.kotlin.where +import net.pokeranalytics.android.R +import net.pokeranalytics.android.util.NULL_TEXT fun List.mapFirstCondition() : List { return this.map { it.conditions.first() } @@ -38,17 +42,51 @@ class Query { this._conditions.addAll(queryConditions) } - val name: String - get() { - return this._conditions.joinToString(" : ") { it.getDisplayName() } + val defaultName: String + get() { + return when (this._conditions.size) { + 0 -> NULL_TEXT + else -> this._conditions.joinToString("") { it.id.joinToString("") } + } + } + + fun getName(context: Context): String { + return when (this._conditions.size) { + 0 -> context.getString(R.string.all_sessions) // @todo should be dependant of the underlying type, ie. Session, Transaction... + else -> this._conditions.joinToString(" : ") { it.getDisplayName(context) } } + } inline fun queryWith(query: RealmQuery): RealmQuery { var realmQuery = query - this.conditions.forEach { - realmQuery = it.queryWith(realmQuery) + + 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) + } } - return realmQuery + + //println("<<<<<< ${realmQuery}") + val queryLast = this.conditions.filter { + it is QueryCondition.Last + }.firstOrNull() + queryLast?.let {qc -> + (qc as QueryCondition.Last).singleValue?.let { + return realmQuery.limit(it.toLong()) + } + } + return realmQuery } fun merge(query: Query) : Query { 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 9b9ec76b..9a13f72a 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 @@ -1,10 +1,12 @@ package net.pokeranalytics.android.model.filter +import android.content.Context import io.realm.Realm import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort import io.realm.kotlin.where +import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.R import net.pokeranalytics.android.exceptions.PokerAnalyticsException import net.pokeranalytics.android.model.Limit @@ -19,10 +21,9 @@ import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.UserDefaults -import net.pokeranalytics.android.util.extensions.endOfDay -import net.pokeranalytics.android.util.extensions.startOfDay -import net.pokeranalytics.android.util.extensions.toCurrency +import net.pokeranalytics.android.util.extensions.* import java.text.DateFormatSymbols +import java.text.NumberFormat import java.util.* import kotlin.collections.ArrayList @@ -33,581 +34,947 @@ import kotlin.collections.ArrayList sealed class QueryCondition : FilterElementRow { - companion object { - inline fun < reified T:QueryCondition> more():T { return T::class.java.newInstance().apply { this.operator = Operator.MORE } } - inline fun < reified T:QueryCondition> less():T { return T::class.java.newInstance().apply { this.operator = Operator.LESS } } - inline fun < reified T:QueryCondition> moreOrLess():ArrayList { return arrayListOf(more(), less()) } - - fun valueOf(name:String) : T { - val kClass = Class.forName("${QueryCondition::class.qualifiedName}$$name").kotlin - val instance = kClass.objectInstance ?: kClass.java.newInstance() - return instance as T - } - - inline fun getInstance(): QueryCondition { - return when (T::class.java) { - Bankroll::class.java -> AnyBankroll() - Game::class.java -> AnyGame() - Location::class.java -> AnyLocation() - TournamentName::class.java -> AnyTournamentName() - TournamentFeature::class.java -> AnyTournamentFeature() - else -> throw PokerAnalyticsException.QueryTypeUnhandled - } - } - - inline fun < reified T: Filterable, reified S: QueryCondition, reified U:Comparable>distinct(): RealmResults? { - FilterHelper.fieldNameForQueryType(S::class.java)?.let { - val realm = Realm.getDefaultInstance() - - val distincts = when (T::class) { - String::class, Int::class -> realm.where().distinct(it).findAll().sort(it, Sort.ASCENDING) - else -> realm.where().isNotNull(it).findAll().sort(it, Sort.ASCENDING) - } - - realm.close() - return distincts - } - return null - } - } - - enum class Operator { - ANY, - ALL, - MORE, - LESS, - EQUALS, - BETWEEN, - BETWEEN_RIGHT_EXCLUSIVE, - BETWEEN_LEFT_EXCLUSIVE, - ; - } - - val baseId = this::class.simpleName ?: throw PokerAnalyticsException.FilterElementUnknownName - - val id: List get() { - when (this.operator) { - Operator.MORE, Operator.LESS -> return listOf("$baseId+${this.operator.name}") - } - - return when (this) { - is SingleValue<*> -> listOf(baseId) - is ListOfValues<*> -> { - if (listOfValues.isEmpty()) { return listOf(baseId) } - this.listOfValues.map{ "$baseId+$it" } - } - else -> listOf(baseId) - } - } - - open var operator: Operator = Operator.ANY - - abstract class ListOfValues: QueryCondition(), Comparable> where T:Comparable { - - abstract var listOfValues: ArrayList - abstract fun labelForValue(value:T): String - - override fun getDisplayName(): String { - return when (listOfValues.size) { - 0 -> return NULL_TEXT - 1,2 -> listOfValues.map { labelForValue(it) }.joinToString(", ") - else -> "${listOfValues.size} $baseId" - } - } - - override fun compareTo(other: ListOfValues): Int { - return listOfValues.sorted().first().compareTo(other.listOfValues.sorted().first()) - } - } - - abstract class SingleValue: ListOfValues() where T:Comparable { - override var listOfValues = ArrayList() - abstract var singleValue : T - } - - abstract class ListOfDouble: ListOfValues() { - open var sign: Int = 1 - - override var listOfValues = arrayListOf(0.0) - override fun updateValueBy(filterCondition: FilterCondition) { - super.updateValueBy(filterCondition) - listOfValues = filterCondition.getValues() - } - override fun labelForValue(value: Double): String { - return value.toCurrency(UserDefaults.currency) - } - } - - abstract class ListOfInt: ListOfValues() { - override var listOfValues = arrayListOf(0) - override fun updateValueBy(filterCondition: FilterCondition) { - super.updateValueBy(filterCondition) - listOfValues = filterCondition.getValues() - } - override fun labelForValue(value: Int): String { - return value.toString() - } - } - - abstract class ListOfString: ListOfValues() { - override var listOfValues = ArrayList() - override fun labelForValue(value: String): String { return value } - override fun updateValueBy(filterCondition: FilterCondition) { - super.updateValueBy(filterCondition) - listOfValues = filterCondition.getValues() - } - } - - abstract class SingleDate: SingleValue() { - override fun labelForValue(value: Date): String { - return value.toString() - } - - override var singleValue: Date - get() { return listOfValues.firstOrNull() ?: Date() } - set(value) { listOfValues.add(value) } - - override fun updateValueBy(filterCondition: FilterCondition) { - super.updateValueBy(filterCondition) - singleValue = filterCondition.getValue() - } - } - - abstract class SingleInt: SingleValue() { - override fun labelForValue(value: Int): String { - return value.toString() - } - override var singleValue: Int - get() { return listOfValues.firstOrNull() ?: 0 } - set(value) { listOfValues.add(value) } - - override fun updateValueBy(filterCondition: FilterCondition) { - super.updateValueBy(filterCondition) - singleValue = filterCondition.getValue() - } - } - - override fun getDisplayName(): String { return baseId } - - override var filterSectionRow: FilterSectionRow = FilterSectionRow.CASH_TOURNAMENT - - abstract class QueryDataCondition < T: NameManageable > : ListOfString() { - fun setObject(dataObject: T) { - this.listOfValues.removeAll(this.listOfValues) - this.listOfValues.add(dataObject.id) - } - - abstract val entity : Class - - override fun getDisplayName(): String { - val realm = Realm.getDefaultInstance() - val completeLabel = when (listOfValues.size) { - 0 -> return NULL_TEXT - 1,2 -> { - return listOfValues.map { labelForValue(realm, it) }.joinToString(", ") - } - else -> "${listOfValues.size} $baseId" - } - realm.close() - return completeLabel - } - - private fun labelForValue(realm:Realm, value:String): String { - val query = realm.where(entity) - return query.equalTo("id", value).findFirst()?.name ?: NULL_TEXT - } - } - - - interface DateTime { - val showTime: Boolean - } - - abstract class DateQuery: SingleDate(), DateTime { - override val showTime: Boolean = false - - } - - abstract class TimeQuery: DateQuery() { - override val showTime: Boolean = true - } - - object IsLive : QueryCondition() { - override fun getDisplayName(): String { return "Live" } - } - - object IsCash : QueryCondition() { - override fun getDisplayName(): String { return "Cash" } - } - - object IsOnline : QueryCondition() { - override fun getDisplayName(): String { return "Online" } - } - - object IsTournament : QueryCondition() { - override fun getDisplayName(): String { return "Tournament" } - } - - class AnyBankroll(): QueryDataCondition() { - override var entity: Class = Bankroll::class.java - constructor(bankroll: Bankroll): this() { - this.setObject(bankroll) - } - } - class AnyGame(): QueryDataCondition() { - override val entity: Class = Game::class.java - constructor(game: Game): this() { - this.setObject(game) - } - } - - class AnyTournamentName(): QueryDataCondition() { - override val entity: Class = TournamentName::class.java - constructor(tournamentName: TournamentName): this() { - this.setObject(tournamentName) - } - } - - class AnyTournamentFeature(): QueryDataCondition() { - override val entity: Class = TournamentFeature::class.java - constructor(tournamentFeature: TournamentFeature): this() { - this.setObject(tournamentFeature) - } - } - - class AllTournamentFeature(): QueryDataCondition() { - override var operator = Operator.ALL - override val entity: Class = TournamentFeature::class.java - constructor(tournamentFeature: TournamentFeature): this() { - this.setObject(tournamentFeature) - } - } - - class AnyLocation(): QueryDataCondition() { - override val entity: Class = Location::class.java - constructor(location: Location): this() { - this.setObject(location) - } - } - - class AnyLimit: ListOfInt() { - override fun labelForValue(value: Int): String { - return Limit.values()[value].getDisplayName() - } - } - - class AnyTableSize: ListOfInt() { - override fun labelForValue(value: Int): String { - return TableSize(value).getDisplayName() - } - } - - class AnyTournamentType: ListOfInt() { - override fun labelForValue(value: Int): String { - return TournamentType.values()[value].getDisplayName() - } - } - - class AnyBlind: ListOfString() - - class LastGame: SingleInt() - class LastSession: SingleInt() - - class NumberOfTable: ListOfInt() - - class NumberOfRebuy(): ListOfDouble() { - constructor(operator: Operator, numberOfRebuy: Double) : this() { - this.operator = operator - this.listOfValues = arrayListOf(numberOfRebuy) - } - } - - open class TournamentFinalPosition(): ListOfInt() { - constructor(operator: Operator, finalPosition: Int) : this() { - this.operator = operator - this.listOfValues = arrayListOf(finalPosition) - } - } - - open class NetAmount: ListOfDouble() - class NetAmountWon: NetAmount() - class NetAmountLost: NetAmount() { override var sign: Int = -1 } - - class TournamentNumberOfPlayer: ListOfInt() - - class StartedFromDate: DateQuery() { override var operator = Operator.MORE } - class StartedToDate: DateQuery() { override var operator = Operator.LESS } - class EndedFromDate: DateQuery() { override var operator = Operator.MORE } - class EndedToDate: DateQuery() { override var operator = Operator.LESS } - - class AnyDayOfWeek: ListOfInt() { - override fun labelForValue(value: Int): String { - return DateFormatSymbols.getInstance(Locale.getDefault()).weekdays[value] - } - } - - class AnyMonthOfYear(): ListOfInt() { - override fun labelForValue(value: Int): String { - return DateFormatSymbols.getInstance(Locale.getDefault()).months[value] - } - - constructor(month:Int) : this() { - listOfValues = arrayListOf(month) - } - } - - class AnyYear(): ListOfInt() { - override fun labelForValue(value: Int): String { - return "$value" - } - - constructor(year:Int) : this() { - listOfValues = arrayListOf(year) - } - } - - object IsWeekDay: QueryCondition() - object IsWeekEnd: QueryCondition() - object IsToday: QueryCondition() - object WasYesterday: QueryCondition() - object WasTodayAndYesterday: QueryCondition() - object DuringThisWeek: QueryCondition() - object DuringThisMonth: QueryCondition() - object DuringThisYear: QueryCondition() - - class TournamentFee: ListOfDouble() { - override fun labelForValue(value: Double): String { - return value.toCurrency(UserDefaults.currency) - } - } - - class PastDay: SingleInt() { - override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal - } - - class Duration: SingleInt() { - var minutes:Int - get() { return singleValue } - set(value) { singleValue = value } - - override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal - override val bottomSheetType: BottomSheetType = BottomSheetType.DOUBLE_EDIT_TEXT - } - - class StartedFromTime: TimeQuery() { - override var operator = Operator.MORE - init { - this.singleValue = Date().startOfDay() - } - } - - class EndedToTime: TimeQuery() { - override var operator = Operator.LESS - init { - this.singleValue = Date().endOfDay() - } - } - - /** - * main method of the enum - * providing a base RealmQuery [realmQuery], the method is able to attached the corresponding query and returns the newly formed [RealmQuery] - */ - inline fun queryWith(realmQuery: RealmQuery): RealmQuery { - val fieldName = FilterHelper.fieldNameForQueryType(this::class.java) - fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown - - when (this) { - //is Between -> realmQuery.between(fieldName, leftValue, rightValue) - //is BetweenLeftExclusive -> realmQuery.greaterThan(fieldName, leftValue).and().lessThanOrEqualTo(fieldName, rightValue) - //is BetweenRightExclusive -> realmQuery.greaterThanOrEqualTo(fieldName, leftValue).and().lessThan(fieldName, rightValue) - IsLive, IsOnline -> return realmQuery.equalTo(fieldName, this == IsLive) - IsCash -> return realmQuery.equalTo(fieldName, Session.Type.CASH_GAME.ordinal) - IsTournament -> return realmQuery.equalTo(fieldName, Session.Type.TOURNAMENT.ordinal) - IsWeekEnd, IsWeekDay -> { - var query = realmQuery - if (this == IsWeekDay) { - query = realmQuery.not() - } - return query.`in`(fieldName, arrayOf(Calendar.SATURDAY, Calendar.SUNDAY)) - } - IsToday -> { - val startDate = Date() - return realmQuery.between(fieldName, startDate.startOfDay(), startDate.endOfDay()) - } - WasTodayAndYesterday-> { - val startDate = Date() - val calendar = Calendar.getInstance() - calendar.time = startDate - calendar.add(Calendar.HOUR_OF_DAY, -24) - return realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay()) - } - WasYesterday -> { - val calendar = Calendar.getInstance() - calendar.time = Date() - calendar.add(Calendar.HOUR_OF_DAY, -24) - return realmQuery.between(fieldName, calendar.time.startOfDay(), calendar.time.endOfDay()) - } - DuringThisWeek -> { - val startDate = Date() - val calendar = Calendar.getInstance() - calendar.time = startDate - calendar.set(Calendar.DAY_OF_WEEK_IN_MONTH, Calendar.SUNDAY) - return realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay()) - } - DuringThisMonth -> { - val startDate = Date() - val calendar = Calendar.getInstance() - calendar.time = startDate - calendar.set(Calendar.DAY_OF_MONTH, 1) - return realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay()) - } - DuringThisYear -> { - val startDate = Date() - val calendar = Calendar.getInstance() - calendar.time = startDate - calendar.set(Calendar.DAY_OF_YEAR, 1) - return realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay()) - } - } - - return when (operator) { - Operator.EQUALS -> { - when (this) { - is SingleDate -> realmQuery.equalTo(fieldName, singleValue) - is SingleInt -> realmQuery.equalTo(fieldName, singleValue) - is ListOfInt -> realmQuery.equalTo(fieldName, listOfValues.first()) - is ListOfDouble -> realmQuery.equalTo(fieldName, listOfValues.first() * sign) - is ListOfString -> realmQuery.equalTo(fieldName, listOfValues.first()) - else -> realmQuery - } - } - Operator.MORE -> { - when (this) { - is SingleDate -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue) - is SingleInt -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue) - is ListOfInt -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first()) - is ListOfDouble -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first() * sign) - else -> realmQuery - } - } - Operator.LESS -> { - when (this) { - is SingleDate -> realmQuery.lessThanOrEqualTo(fieldName, singleValue) - is SingleInt -> realmQuery.lessThanOrEqualTo(fieldName, singleValue) - is ListOfInt -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first()) - is ListOfDouble -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first() * sign) - else -> realmQuery - } - } - Operator.ALL -> { - when (this) { - is ListOfInt -> { - listOfValues.forEach { realmQuery.equalTo(fieldName, it) } - realmQuery - } - is ListOfDouble -> { - listOfValues.forEach { realmQuery.equalTo(fieldName, it * sign) } - realmQuery - } - is ListOfString -> { - listOfValues.forEach { realmQuery.equalTo(fieldName, it) } - realmQuery - } - else -> realmQuery - } - } - Operator.ANY -> { - when (this) { - is ListOfInt -> realmQuery.`in`(fieldName, listOfValues.toTypedArray()) - is ListOfDouble -> realmQuery.`in`(fieldName, listOfValues.toTypedArray()) - is ListOfString -> realmQuery.`in`(fieldName, listOfValues.toTypedArray()) - else -> realmQuery - } - } - else -> realmQuery - } - } - - open fun updateValueBy(filterCondition: FilterCondition) { - filterCondition.operator?.let { - this.operator = Operator.values()[it] - } - } - - override val viewType: Int - get() { - return when (this) { - is PastDay -> RowViewType.TITLE_VALUE_CHECK.ordinal - is LastGame -> RowViewType.TITLE_VALUE_CHECK.ordinal - is LastSession -> RowViewType.TITLE_VALUE_CHECK.ordinal - else -> { - when (this.operator) { - Operator.MORE -> RowViewType.TITLE_VALUE_CHECK.ordinal - Operator.LESS -> RowViewType.TITLE_VALUE_CHECK.ordinal - else -> RowViewType.TITLE_CHECK.ordinal - } - } - } - } - - override val bottomSheetType: BottomSheetType - get() { - return when (this) { - is PastDay -> BottomSheetType.EDIT_TEXT - is LastGame -> BottomSheetType.EDIT_TEXT - is LastSession -> BottomSheetType.EDIT_TEXT - else -> { - when (this.operator) { - Operator.MORE -> BottomSheetType.EDIT_TEXT - Operator.LESS -> BottomSheetType.EDIT_TEXT - else -> BottomSheetType.NONE - } - } + companion object { + inline fun more(): T { + return T::class.java.newInstance().apply { this.operator = Operator.MORE } + } + + inline fun less(): T { + return T::class.java.newInstance().apply { this.operator = Operator.LESS } + } + + inline fun moreOrLess(): ArrayList { + return arrayListOf(more(), less()) + } + + fun valueOf(name: String): T { + val kClass = Class.forName("${QueryCondition::class.qualifiedName}$$name").kotlin + val instance = kClass.objectInstance ?: kClass.java.newInstance() + return instance as T + } + + inline fun getInstance(): QueryCondition { + return when (T::class.java) { + Bankroll::class.java -> AnyBankroll() + Game::class.java -> AnyGame() + Location::class.java -> AnyLocation() + TransactionType::class.java -> AnyTransactionType() + TournamentName::class.java -> AnyTournamentName() + TournamentFeature::class.java -> AllTournamentFeature() + else -> throw PokerAnalyticsException.QueryTypeUnhandled + } + } + + inline fun > distinct(): RealmResults? { + FilterHelper.fieldNameForQueryType(S::class.java)?.let { + val realm = Realm.getDefaultInstance() + + val distincts = when (T::class) { + String::class, Int::class -> realm.where().distinct(it).findAll().sort(it, Sort.ASCENDING) + else -> realm.where().isNotNull(it).findAll().sort(it, Sort.ASCENDING) + } + + realm.close() + return distincts + } + return null + } + } + + enum class Operator { + ANY, + ALL, + MORE, + LESS, + EQUALS, + TRUE, + ; + } + + val baseId = this::class.simpleName ?: throw PokerAnalyticsException.FilterElementUnknownName + + val groupId: String + get() { + when (this.operator) { + Operator.MORE, Operator.LESS -> return "${this.operator.name.toLowerCase().capitalize()}$baseId" + } + return baseId + } + + val id: List + get() { + when (this.operator) { + Operator.MORE, Operator.LESS -> return listOf("$baseId+${this.operator.name}") + } + + return when (this) { + is SingleValue<*> -> listOf(baseId) + is ListOfValues<*> -> { + if (listOfValues.isEmpty()) { + return listOf(baseId) + } + this.listOfValues.map { "$baseId+$it" } + } + else -> listOf(baseId) + } + } + + abstract var operator: Operator + + abstract class ListOfValues : QueryCondition(), Comparable> where T : Comparable { + + abstract var listOfValues: ArrayList + abstract fun labelForValue(value: T, context: Context): String + + open fun entityName(context: Context): String { + return getDisplayName(context) + } + + override fun getDisplayName(context: Context): String { + val prefix = this.resId?.let { + context.getString(it) + " " + } ?: "" + + return when (listOfValues.size) { + 0 -> return NULL_TEXT + 1, 2 -> prefix + listOfValues.map { labelForValue(it, context) }.joinToString(", ") + else -> "${listOfValues.size} $prefix ${entityName(context)}" + } + } + + override fun compareTo(other: ListOfValues): Int { + return listOfValues.sorted().first().compareTo(other.listOfValues.sorted().first()) + } + + fun firstValue(context: Context): String? { + return this.listOfValues.firstOrNull()?.let { this.labelForValue(it, context) } + } + } + + abstract class SingleValue : ListOfValues() where T : Comparable { + override var listOfValues = ArrayList() + abstract var singleValue: T? + } + + abstract class ListOfDouble : ListOfValues() { + open var sign: Int = 1 + override var operator: Operator = Operator.ANY + override var listOfValues: ArrayList = arrayListOf() + override fun updateValueBy(filterCondition: FilterCondition) { + super.updateValueBy(filterCondition) + listOfValues = filterCondition.getValues() + } + + override fun labelForValue(value: Double, context: Context): String { + return value.toCurrency(UserDefaults.currency) + } + } + + abstract class ListOfInt : ListOfValues() { + override var operator: Operator = Operator.ANY + override var listOfValues: ArrayList = arrayListOf() + override fun updateValueBy(filterCondition: FilterCondition) { + super.updateValueBy(filterCondition) + listOfValues = filterCondition.getValues() + } + + override fun labelForValue(value: Int, context: Context): String { + return value.toString() + } + } + + abstract class ListOfString : ListOfValues() { + override var operator: Operator = Operator.ANY + override var listOfValues = ArrayList() + override fun labelForValue(value: String, context: Context): String { + return value + } + + override fun updateValueBy(filterCondition: FilterCondition) { + super.updateValueBy(filterCondition) + listOfValues = filterCondition.getValues() + } + } + + abstract class SingleDate : SingleValue() { + override fun labelForValue(value: Date, context: Context): String { + return value.shortDate() + } + + override var listOfValues = ArrayList() + + override var singleValue: Date? + get() { + return listOfValues.firstOrNull() + } + set(value) { + listOfValues.removeAll(this.listOfValues) + value?.let { listOfValues.add(it) } + } + + override fun updateValueBy(filterCondition: FilterCondition) { + super.updateValueBy(filterCondition) + singleValue = filterCondition.getValue() + } + } + + abstract class SingleInt : SingleValue() { + override fun labelForValue(value: Int, context: Context): String { + return value.toString() + } + + override var singleValue: Int? + get() { + return listOfValues.firstOrNull() + } + set(value) { + listOfValues.removeAll(this.listOfValues) + value?.let { listOfValues.add(it) } + } + + override fun updateValueBy(filterCondition: FilterCondition) { + super.updateValueBy(filterCondition) + singleValue = filterCondition.getValue() + } + } + + override fun getDisplayName(context: Context): String { + this.resId?.let { + return context.getString(it) + } + return baseId + } + + override var filterSectionRow: FilterSectionRow = FilterSectionRow.CashOrTournament + + abstract class QueryDataCondition : ListOfString() { + fun setObject(dataObject: T) { + this.listOfValues.removeAll(this.listOfValues) + this.listOfValues.add(dataObject.id) + } + + abstract val entity: Class + + override fun getDisplayName(context: Context): String { + val realm = Realm.getDefaultInstance() + val entityName = entityName(realm) + val completeLabel = when (listOfValues.size) { + 0 -> NULL_TEXT + 1, 2 -> { + listOfValues.map { labelForValue(realm, it) }.joinToString(", ") + } + else -> "${listOfValues.size} $entityName" + } + realm.close() + return completeLabel + } + + open fun entityName(realm: Realm): String { + return baseId + } + + private fun labelForValue(realm: Realm, value: String): String { + val query = realm.where(entity) + return query.equalTo("id", value).findFirst()?.name ?: NULL_TEXT + } + } + + + interface DateTime { + val showTime: Boolean + } + + abstract class DateQuery : SingleDate(), DateTime { + override val showTime: Boolean = false + + override fun labelForValue(value: Date, context: Context): String { + return singleValue?.let { + if (showTime) { + it.shortTime() + } else { + it.shortDate() + } + } ?: NULL_TEXT + } + } + + abstract class TimeQuery : DateQuery() { + override val showTime: Boolean = true + } + + abstract class TrueQueryCondition : QueryCondition() { + override var operator: Operator = Operator.TRUE + } + + object IsLive : TrueQueryCondition() + + object IsCash : TrueQueryCondition() + + object IsOnline : TrueQueryCondition() + + object IsTournament : TrueQueryCondition() + + class AnyBankroll() : QueryDataCondition() { + override var entity: Class = Bankroll::class.java + + constructor(bankroll: Bankroll) : this() { + this.setObject(bankroll) + } + } + + class AnyGame() : QueryDataCondition() { + override val entity: Class = Game::class.java + + constructor(game: Game) : this() { + this.setObject(game) + } + } + + class AnyTournamentName() : QueryDataCondition() { + override val entity: Class = TournamentName::class.java + + constructor(tournamentName: TournamentName) : this() { + this.setObject(tournamentName) + } + } + + class AnyTournamentFeature() : QueryDataCondition() { + override val entity: Class = TournamentFeature::class.java + + constructor(tournamentFeature: TournamentFeature) : this() { + this.setObject(tournamentFeature) + } + } + + class AllTournamentFeature() : QueryDataCondition() { + override var operator = Operator.ALL + override val entity: Class = TournamentFeature::class.java + + constructor(tournamentFeature: TournamentFeature) : this() { + this.setObject(tournamentFeature) + } + } + + class AnyLocation() : QueryDataCondition() { + override val entity: Class = Location::class.java + + constructor(location: Location) : this() { + this.setObject(location) + } + } + + class AnyTransactionType() : QueryDataCondition() { + override val entity: Class = TransactionType::class.java + + constructor(transactionType: TransactionType) : this() { + this.setObject(transactionType) + } + } + + class AnyLimit : ListOfInt() { + override fun labelForValue(value: Int, context: Context): String { + return Limit.values()[value].getDisplayName(context) + } + } + + class AnyTableSize : ListOfInt() { + override fun labelForValue(value: Int, context: Context): String { + return TableSize(value).getDisplayName(context) + } + } + + class AnyTournamentType : ListOfInt() { + override fun labelForValue(value: Int, context: Context): String { + return TournamentType.values()[value].getDisplayName(context) + } + } + + class AnyBlind : ListOfString() + + object Last : SingleInt() { + override var operator = Operator.EQUALS + override fun getDisplayName(context: Context): String { + //TODO update string "last %i" + return "${context.getString(R.string.last_i_records)} $singleValue" + } + } + + class NumberOfTable : ListOfInt() { + override fun labelForValue(value: Int, context: Context): String { + return value.toString() + " " + context.getString(R.string.tables) + } + + override fun entityName(context: Context): String { + return "" + } + } + + class NumberOfRebuy() : ListOfDouble() { + constructor(operator: Operator, numberOfRebuy: Double) : this() { + this.operator = operator + this.listOfValues = arrayListOf(numberOfRebuy) + } + + override fun labelForValue(value: Double, context: Context): String { + return value.toString() + } + } + + class TournamentFinalPosition() : ListOfInt() { + constructor(operator: Operator, finalPosition: Int) : this() { + this.operator = operator + this.listOfValues = arrayListOf(finalPosition) + } + + override fun labelForValue(value: Int, context: Context): String { + val suffix = when (value%10) { + 1 -> context.getString(R.string.ordinal_suffix_first) + 2 -> context.getString(R.string.ordinal_suffix_second) + 3 -> context.getString(R.string.ordinal_suffix_third) + else -> context.getString(R.string.ordinal_suffix_default) } - } - - override val resId: Int? - get() { - return when (this) { - is IsCash -> R.string.cash_game - is IsTournament -> R.string.tournament - is IsToday -> R.string.today - is WasYesterday -> R.string.yesterday - is WasTodayAndYesterday -> R.string.yesterday_and_today - is DuringThisWeek -> R.string.current_week - is DuringThisMonth -> R.string.current_month - is DuringThisYear -> R.string.current_year - is StartedFromTime, is StartedFromDate -> R.string.from - is EndedToDate, is EndedToTime-> R.string.to - is IsLive -> R.string.live - is IsOnline -> R.string.online - is IsWeekDay -> R.string.week_days - is IsWeekEnd -> R.string.weekend - is PastDay -> R.string.period_in_days - is LastGame -> R.string.last_records - is LastSession -> R.string.last_sessions - is NetAmountWon -> { - when (this.operator) { - Operator.MORE -> R.string.won_amount_more_than - Operator.LESS -> R.string.won_amount_less_than - else -> null - } - } - is NetAmountLost -> { - when (this.operator) { - Operator.MORE -> R.string.lost_amount_more_than - Operator.LESS -> R.string.lost_amount_less_than - else -> null - } - } - else -> { - when (this.operator) { - Operator.MORE -> R.string.more_than - Operator.LESS -> R.string.less_than - else -> null - } - } - } - } + return "$value$suffix "+context.getString(R.string.position) + } + + override fun entityName(context: Context): String { + return "" + } + } + + open class NetAmount : ListOfDouble() + + class NetAmountWon : NetAmount() + class NetAmountLost : NetAmount() { + override var sign: Int = -1 + } + + class TournamentNumberOfPlayer : ListOfInt() { + override fun labelForValue(value: Int, context: Context): String { + return value.toString() + " " + context.getString(R.string.number_of_players) + } + + override fun entityName(context: Context): String { + return "" + } + } + + class StartedFromDate : DateQuery() { + override var operator = Operator.MORE + } + + class StartedToDate : DateQuery() { + override var operator = Operator.LESS + } + + class EndedFromDate : DateQuery() { + override var operator = Operator.MORE + } + + class EndedToDate : DateQuery() { + override var operator = Operator.LESS + } + + class AnyDayOfWeek : ListOfInt() { + override fun labelForValue(value: Int, context: Context): String { + return DateFormatSymbols.getInstance(Locale.getDefault()).weekdays[value].capitalize() + } + } + + class AnyMonthOfYear() : ListOfInt() { + override fun labelForValue(value: Int, context: Context): String { + return DateFormatSymbols.getInstance(Locale.getDefault()).months[value].capitalize() + } + + constructor(month: Int) : this() { + listOfValues = arrayListOf(month) + } + } + + class AnyYear() : ListOfInt() { + override fun labelForValue(value: Int, context: Context): String { + return "$value" + } + + constructor(year: Int) : this() { + listOfValues = arrayListOf(year) + } + } + + object IsWeekDay : TrueQueryCondition() + object IsWeekEnd : TrueQueryCondition() + object IsToday : TrueQueryCondition() + object WasYesterday : TrueQueryCondition() + object WasTodayAndYesterday : TrueQueryCondition() + object DuringThisWeek : TrueQueryCondition() + object DuringThisMonth : TrueQueryCondition() + object DuringThisYear : TrueQueryCondition() + + class TournamentFee : ListOfDouble() { + override fun labelForValue(value: Double, context: Context): String { + return value.toCurrency(UserDefaults.currency) + } + } + + class PastDay : SingleInt() { + override var operator = Operator.EQUALS + override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal + + override fun labelForValue(value: Int, context: Context): String { + return value.toString() + } + + override fun entityName(context: Context): String { + return this.resId?.let { + " " + context.getString(it) + } ?: "" + } + } + + class Duration : SingleInt() { + override var operator = Operator.EQUALS + var minutes: Int? + get() { + return singleValue + } + set(value) { + singleValue = value + } + + val netDuration: Long? + get() { + minutes?.let { + return (it * 60 * 1000).toLong() + } + return null + } + + override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal + override val bottomSheetType: BottomSheetType = BottomSheetType.DOUBLE_EDIT_TEXT + + override fun labelForValue(value: Int, context: Context): String { + return value.toMinutes(context) + } + } + + class StartedFromTime() : TimeQuery() { + override var operator = Operator.MORE + + constructor(date: Date) : this() { + singleValue = date + } + + + } + + class EndedToTime() : TimeQuery() { + override var operator = Operator.LESS + + constructor(date: Date) : this() { + singleValue = date + } + + } + + interface CustomFieldRelated { + var customFieldId: String + + fun customFieldName(realm: Realm): String { + val query = realm.where(CustomField::class.java) + val name = query.equalTo("id", customFieldId).findFirst()?.name + return name?.let { + "$it " + } ?: run { "" } + } + } + + class CustomFieldQuery() : QueryDataCondition() { + override var entity: Class = CustomField::class.java + + constructor(customField: CustomField) : this() { + this.setObject(customField) + } + } + + open class CustomFieldNumberQuery() : ListOfDouble(), CustomFieldRelated { + override var customFieldId: String = "" + override var operator: Operator = Operator.EQUALS + + constructor(customFieldId: String, value: Double) : this() { + this.listOfValues = arrayListOf(value) + this.customFieldId = customFieldId + } + + override fun getDisplayName(context: Context): String { + val realm = Realm.getDefaultInstance() + val name = customFieldName(realm) + val prefix = this.resId?.let { + context.getString(it) + " " + } ?: "" + + val completeLabel = when (listOfValues.size) { + 0 -> return NULL_TEXT + 1, 2 -> { + return name + prefix + listOfValues.map { labelForValue(it, context) }.joinToString(", ") + } + else -> "${listOfValues.size} $prefix $name" + } + realm.close() + return completeLabel + } + + override fun labelForValue(value: Double, context: Context): String { + return NumberFormat.getInstance().format(value) + } + + override fun updateValueBy(filterCondition: FilterCondition) { + super.updateValueBy(filterCondition) + listOfValues = filterCondition.getValues() + customFieldId = filterCondition.stringValue ?: throw PokerAnalyticsException.QueryValueMapUnexpectedValue + } + } + + class CustomFieldAmountQuery : CustomFieldNumberQuery() { + override fun labelForValue(value: Double, context: Context): String { + return value.toCurrency(UserDefaults.currency) + } + } + + class CustomFieldListQuery() : QueryDataCondition(), CustomFieldRelated { + override var entity: Class = CustomFieldEntry::class.java + override var customFieldId: String = "" + + constructor(customFieldEntry: CustomFieldEntry) : this() { + this.setObject(customFieldEntry) + this.customFieldId = customFieldEntry.customFields?.firstOrNull()?.id + ?: throw PokerAnalyticsException.QueryValueMapUnexpectedValue + } + + override fun entityName(realm: Realm): String { + return customFieldName(realm) + } + + override fun updateValueBy(filterCondition: FilterCondition) { + super.updateValueBy(filterCondition) + listOfValues = filterCondition.getValues() + customFieldId = filterCondition.stringValue ?: throw PokerAnalyticsException.QueryValueMapUnexpectedValue + } + } + + /** + * main method of the enum + * providing a base RealmQuery [realmQuery], the method is able to attached the corresponding query and returns the newly formed [RealmQuery] + */ + inline fun queryWith( + realmQuery: RealmQuery, + otherQueryCondition: QueryCondition? = null + ): RealmQuery { + val fieldName = FilterHelper.fieldNameForQueryType(this::class.java) + if (BuildConfig.DEBUG) { + fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown + } + fieldName ?: return realmQuery + + when (this) { + //is Between -> realmQuery.between(fieldName, leftValue, rightValue) + //is BetweenLeftExclusive -> realmQuery.greaterThan(fieldName, leftValue).and().lessThanOrEqualTo(fieldName, rightValue) + //is BetweenRightExclusive -> realmQuery.greaterThanOrEqualTo(fieldName, leftValue).and().lessThan(fieldName, rightValue) + is IsLive, is IsOnline -> return realmQuery.equalTo(fieldName, this == IsLive) + is IsCash -> return realmQuery.equalTo(fieldName, Session.Type.CASH_GAME.ordinal) + is IsTournament -> return realmQuery.equalTo(fieldName, Session.Type.TOURNAMENT.ordinal) + is IsWeekEnd, is IsWeekDay -> { + var query = realmQuery + if (this == IsWeekDay) { + query = realmQuery.not() + } + return query.`in`(fieldName, arrayOf(Calendar.SATURDAY, Calendar.SUNDAY)) + } + is IsToday -> { + val startDate = Date() + return realmQuery.greaterThanOrEqualTo(fieldName, startDate.startOfDay()).and() + .lessThanOrEqualTo(fieldName, startDate.endOfDay()) + } + is WasTodayAndYesterday -> { + val startDate = Date() + val calendar = Calendar.getInstance() + calendar.time = startDate + calendar.add(Calendar.HOUR_OF_DAY, -24) + return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and() + .lessThanOrEqualTo(fieldName, startDate.endOfDay()) + } + is WasYesterday -> { + val calendar = Calendar.getInstance() + calendar.time = Date() + calendar.add(Calendar.HOUR_OF_DAY, -24) + return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and() + .lessThanOrEqualTo(fieldName, calendar.time.endOfDay()) + } + is PastDay -> { + singleValue?.let { + val startDate = Date() + val calendar = Calendar.getInstance() + calendar.time = startDate + calendar.add(Calendar.DAY_OF_YEAR, -it) + return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and() + .lessThanOrEqualTo(fieldName, startDate.endOfDay()) + } + return realmQuery + } + is DuringThisWeek -> { + val startDate = Date() + val calendar = Calendar.getInstance() + calendar.time = startDate + calendar.set(Calendar.DAY_OF_WEEK_IN_MONTH, Calendar.SUNDAY) + return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and() + .lessThanOrEqualTo(fieldName, startDate.endOfDay()) + } + is DuringThisMonth -> { + val startDate = Date() + val calendar = Calendar.getInstance() + calendar.time = startDate + calendar.set(Calendar.DAY_OF_MONTH, 1) + return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and() + .lessThanOrEqualTo(fieldName, startDate.endOfDay()) + } + is DuringThisYear -> { + val startDate = Date() + val calendar = Calendar.getInstance() + calendar.time = startDate + calendar.set(Calendar.DAY_OF_YEAR, 1) + return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and() + .lessThanOrEqualTo(fieldName, startDate.endOfDay()) + } + is StartedFromTime -> { + val calendar = Calendar.getInstance() + singleValue?.let { + calendar.time = it + realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute()) + if (otherQueryCondition is EndedToTime) { + otherQueryCondition.singleValue?.let { endTime -> + calendar.time = endTime + realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute()) + } + } + } + return realmQuery + } + is EndedToTime -> { + val calendar = Calendar.getInstance() + singleValue?.let { + calendar.time = singleValue + realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute()) + if (otherQueryCondition is StartedFromTime) { + otherQueryCondition.singleValue?.let { startTime -> + calendar.time = startTime + realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute()) + } + } + } + return realmQuery + } + } + + if (this is CustomFieldRelated) { + FilterHelper.fieldNameForQueryType(CustomFieldQuery::class.java)?.let { + realmQuery.equalTo(it, customFieldId) + } + } + + if (this is ListOfValues<*>) { + if (this.listOfValues.isEmpty()) { + if (BuildConfig.DEBUG) { + throw PokerAnalyticsException.FilterElementExpectedValueMissing + } + return realmQuery + } + } + + return when (operator) { + Operator.EQUALS -> { + when (this) { + is SingleDate -> realmQuery.equalTo( + fieldName, + singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing + ) + is SingleInt -> realmQuery.equalTo( + fieldName, + singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing + ) + is ListOfInt -> realmQuery.equalTo(fieldName, listOfValues.first()) + is ListOfDouble -> realmQuery.equalTo(fieldName, listOfValues.first() * sign) + is ListOfString -> realmQuery.equalTo(fieldName, listOfValues.first()) + else -> realmQuery + } + } + Operator.MORE -> { + when (this) { + is SingleDate -> realmQuery.greaterThanOrEqualTo( + fieldName, + singleValue?.startOfDay() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing + ) + is Duration -> realmQuery.greaterThan( + fieldName, + netDuration ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing + ) + is TournamentFinalPosition -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first()) + is TournamentNumberOfPlayer -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first()) + is SingleInt -> realmQuery.greaterThan( + fieldName, + singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing + ) + is ListOfInt -> realmQuery.greaterThan(fieldName, listOfValues.first()) + is NetAmountLost -> realmQuery.lessThan(fieldName, listOfValues.first() * -1) + is ListOfDouble -> realmQuery.greaterThan(fieldName, listOfValues.first() * sign) + else -> realmQuery + } + } + Operator.LESS -> { + when (this) { + is SingleDate -> realmQuery.lessThanOrEqualTo( + fieldName, + singleValue?.endOfDay() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing + ) + is Duration -> realmQuery.lessThan( + fieldName, + netDuration ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing + ) + is TournamentFinalPosition -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first()) + is TournamentNumberOfPlayer -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first()) + is SingleInt -> realmQuery.lessThan( + fieldName, + singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing + ) + is ListOfInt -> realmQuery.lessThan(fieldName, listOfValues.first()) + is NetAmountLost -> { + realmQuery.greaterThan(fieldName, listOfValues.first() * -1) + realmQuery.lessThan(fieldName, 0.0) + } + is NetAmountWon -> { + realmQuery.lessThan(fieldName, listOfValues.first()) + realmQuery.greaterThan(fieldName, 0.0) + } + is ListOfDouble -> realmQuery.lessThan(fieldName, listOfValues.first() * sign) + else -> realmQuery + } + } + Operator.ALL -> { + when (this) { + is ListOfInt -> { + listOfValues.forEach { realmQuery.equalTo(fieldName, it) } + realmQuery + } + is ListOfDouble -> { + listOfValues.forEach { realmQuery.equalTo(fieldName, it * sign) } + realmQuery + } + is ListOfString -> { + listOfValues.forEach { realmQuery.equalTo(fieldName, it) } + realmQuery + } + else -> realmQuery + } + } + Operator.ANY -> { + when (this) { + is ListOfInt -> realmQuery.`in`(fieldName, listOfValues.toTypedArray()) + is ListOfDouble -> realmQuery.`in`(fieldName, listOfValues.toTypedArray()) + is ListOfString -> realmQuery.`in`(fieldName, listOfValues.toTypedArray()) + else -> realmQuery + } + } + else -> realmQuery + } + } + + open fun updateValueBy(filterCondition: FilterCondition) { + filterCondition.operator?.let { + this.operator = Operator.values()[it] + } + } + + override val viewType: Int + get() { + return when (this) { + is PastDay -> RowViewType.TITLE_VALUE_CHECK.ordinal + else -> { + when (this.operator) { + Operator.MORE -> RowViewType.TITLE_VALUE_CHECK.ordinal + Operator.LESS -> RowViewType.TITLE_VALUE_CHECK.ordinal + else -> RowViewType.TITLE_CHECK.ordinal + } + } + } + } + + override val bottomSheetType: BottomSheetType + get() { + return when (this) { + is PastDay -> BottomSheetType.EDIT_TEXT + else -> { + when (this.operator) { + Operator.MORE -> BottomSheetType.EDIT_TEXT + Operator.LESS -> BottomSheetType.EDIT_TEXT + else -> BottomSheetType.NONE + } + } + } + } + + override val resId: Int? + get() { + return when (this) { + is IsCash -> R.string.cash_game + is IsTournament -> R.string.tournament + is IsToday -> R.string.today + is WasYesterday -> R.string.yesterday + is WasTodayAndYesterday -> R.string.yesterday_and_today + is DuringThisWeek -> R.string.current_week + is DuringThisMonth -> R.string.current_month + is DuringThisYear -> R.string.current_year + is StartedFromDate -> R.string.from + is StartedFromTime -> R.string.from_time + is EndedToDate -> R.string.to + is EndedToTime -> R.string.to_time + is IsLive -> R.string.live + is IsOnline -> R.string.online + is IsWeekDay -> R.string.week_days + is IsWeekEnd -> R.string.weekend + is PastDay -> R.string.period_in_days + is TournamentNumberOfPlayer -> { + when (this.operator) { + Operator.MORE -> R.string.minimum + Operator.LESS -> R.string.maximum + else -> null + } + } + is NetAmountWon -> { + when (this.operator) { + Operator.MORE -> R.string.won_amount_more_than + Operator.LESS -> R.string.won_amount_less_than + else -> null + } + } + is NetAmountLost -> { + when (this.operator) { + Operator.MORE -> R.string.lost_amount_more_than + Operator.LESS -> R.string.lost_amount_less_than + else -> null + } + } + is TournamentFinalPosition -> { + when (this.operator) { + Operator.MORE -> R.string.minimum + Operator.LESS -> R.string.maximum + else -> null + } + } + else -> { + when (this.operator) { + Operator.MORE -> R.string.more_than + Operator.LESS -> R.string.less_than + else -> null + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/interfaces/Manageable.kt b/app/src/main/java/net/pokeranalytics/android/model/interfaces/Manageable.kt index 6ea3ecca..74097255 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/interfaces/Manageable.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/interfaces/Manageable.kt @@ -1,5 +1,6 @@ package net.pokeranalytics.android.model.interfaces +import android.content.Context import io.realm.Realm import io.realm.RealmModel import net.pokeranalytics.android.R @@ -12,12 +13,19 @@ enum class SaveValidityStatus { DATA_INVALID; } +enum class DeleteValidityStatus { + VALID, + INVALID, + SESSIONS_LINKED, + TRANSACTIONS_LINKED; +} + /** - * An interface to grouped object which are managed by the database + * An interface to group object which are managed by the database */ interface Manageable : Savable, Deletable, Editable -interface NameManageable: Manageable { +interface NameManageable : Manageable { var name: String override fun isValidForSave(): Boolean { @@ -25,26 +33,26 @@ interface NameManageable: Manageable { } override fun alreadyExists(realm: Realm): Boolean { - return realm.where(this::class.java).equalTo("name", this.name).and().notEqualTo("id", this.id).findAll().isNotEmpty() + return realm.where(this::class.java).equalTo("name", this.name).and().notEqualTo("id", this.id).findAll().isNotEmpty() } override fun getFailedSaveMessage(status: SaveValidityStatus): Int { - throw ModelException("${this::class.java} getFailedSaveMessage for $status not handled") + throw ModelException("${this::class.java} getFailedSaveMessage for $status not handled") } - override fun getFailedDeleteMessage(): Int { - return R.string.relationship_error - } + override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int { + return R.string.relationship_error + } } /** - * An interface associate a unique identifier to an object + * An interface associate a unique uniqueIdentifier to an object */ interface Identifiable : RealmModel { /** - * A unique identifier getter + * A unique uniqueIdentifier getter */ var id: String } @@ -87,7 +95,7 @@ interface Savable : Identifiable { /** * A method to get the reason why the object can't be saved */ - fun getFailedSaveMessage(status:SaveValidityStatus): Int + fun getFailedSaveMessage(status: SaveValidityStatus): Int } @@ -101,8 +109,22 @@ interface Deletable : Identifiable { */ fun isValidForDelete(realm: Realm): Boolean + + fun getDeleteStatus(context: Context, realm: Realm): DeleteValidityStatus { + if (!isValidForDelete(realm)) { + return DeleteValidityStatus.INVALID + } + return DeleteValidityStatus.VALID + } + /** * A method to get the reason why the object can't be deleted */ - fun getFailedDeleteMessage(): Int + fun getFailedDeleteMessage(status: DeleteValidityStatus): Int + + /** + * A method to override if we need to delete linked objects or other stuff + */ + fun deleteDependencies() {} + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/interfaces/Timed.kt b/app/src/main/java/net/pokeranalytics/android/model/interfaces/Timed.kt index aa2741dd..1123f298 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/interfaces/Timed.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/interfaces/Timed.kt @@ -1,7 +1,7 @@ package net.pokeranalytics.android.model.interfaces -import net.pokeranalytics.android.calculus.ObjectIdentifier import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry +import net.pokeranalytics.android.ui.graph.ObjectIdentifier import java.util.* interface Timed : GraphUnderlyingEntry, Identifiable { diff --git a/app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt b/app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt index bb4550a8..d87eb77e 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt @@ -11,7 +11,18 @@ class Patcher { companion object { - fun patchBreaks(context: Context) { + fun patchAll(context: Context) { + + Preferences.executeOnce(Preferences.Keys.PATCH_BREAK, context) { + patchBreaks() + } + Preferences.executeOnce(Preferences.Keys.PATCH_TRANSACTION_TYPES_NAMES, context) { + patchDefaultTransactionTypes(context) + } + + } + + private fun patchBreaks() { val realm = Realm.getDefaultInstance() val sets = realm.where(SessionSet::class.java).findAll() @@ -44,7 +55,20 @@ class Patcher { } - } + private fun patchDefaultTransactionTypes(context: Context) { + val realm = Realm.getDefaultInstance() + realm.executeTransaction { + val tts = realm.where(TransactionType::class.java).findAll() + tts.forEach { tt -> + tt.kind?.let { kind -> + val value = TransactionType.Value.values()[kind] + tt.name = value.localizedTitle(context) + } + } + } + realm.close() + } + } } \ 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 44755ecc..eccf538f 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 @@ -3,61 +3,51 @@ package net.pokeranalytics.android.model.migrations import io.realm.DynamicRealm import io.realm.RealmMigration import timber.log.Timber -import java.util.* -import io.realm.RealmObjectSchema - - class PokerAnalyticsMigration : RealmMigration { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { - // DynamicRealm exposes an editable schema - val schema = realm.schema - - var currentVersion = oldVersion.toInt() - Timber.d("*** migrate from $oldVersion to $newVersion") - - // Migrate to version 1 - if (currentVersion == 0) { - Timber.d("*** Running migration 1") - - schema.get("Filter")?.let { - it.addField("entityType", Int::class.java).setNullable("entityType", true) - } - schema.get("FilterElement")?.let { - it.setNullable("filterName", true) - it.setNullable("sectionName", true) - } - schema.get("FilterElementBlind")?.let { - it.renameField("code", "currencyCode") - } - currentVersion++ - } - - // Migrate to version 2 - if (currentVersion == 1) { - Timber.d("*** Running migration ${currentVersion + 1}") - schema.rename("FilterElement", "FilterCondition") - schema.get("Filter")?.let { - it.renameField("filterElements", "filterConditions") - } - schema.get("SessionSet")?.let { - it.addField("id", String::class.java).setRequired("id", true) - it.addPrimaryKey("id") - } - currentVersion++ - } - - // Migrate to version 2 - if (currentVersion == 2) { - Timber.d("*** Running migration ${currentVersion + 1}") - schema.rename("Report", "ReportSetup") - - schema.get("Filter")?.let { - it.removeField("entityType") + // DynamicRealm exposes an editable schema + val schema = realm.schema + + var currentVersion = oldVersion.toInt() + Timber.d("*** migrate from $oldVersion to $newVersion") + + // Migrate to version 1 + if (currentVersion == 0) { + Timber.d("*** Running migration 1") + + schema.get("Filter")?.addField("entityType", Int::class.java)?.setNullable("entityType", true) + schema.get("FilterElement")?.let { + it.setNullable("filterName", true) + it.setNullable("sectionName", true) } + schema.get("FilterElementBlind")?.renameField("code", "currencyCode") + currentVersion++ + } + + // Migrate to version 2 + if (currentVersion == 1) { + Timber.d("*** Running migration ${currentVersion + 1}") + schema.rename("FilterElement", "FilterCondition") + + schema.get("Filter")?.renameField("filterElements", "filterConditions") + + schema.get("SessionSet")?.let { + it.addField("id", String::class.java).setRequired("id", true) + it.addPrimaryKey("id") + } + currentVersion++ + } + + // Migrate to version 3 + if (currentVersion == 2) { + Timber.d("*** Running migration ${currentVersion + 1}") + schema.rename("Report", "ReportSetup") + + schema.get("Filter")?.removeField("entityType") schema.get("Session")?.let { it.addField("blinds", String::class.java).transform { @@ -74,33 +64,87 @@ class PokerAnalyticsMigration : RealmMigration { it.addRealmListField("intValues", Integer::class.java) it.addField("doubleValue", Double::class.java).setNullable("doubleValue", true) it.addRealmListField("doubleValues", Double::class.java) - if(it.isRequired("doubleValues")) { + if (it.isRequired("doubleValues")) { it.setRequired("doubleValues", false) } it.addField("stringValue", String::class.java) } - schema.get("ComputableResult")?.let { - it.removeField("sessionSet") - } + schema.get("ComputableResult")?.removeField("sessionSet") - schema.get("Bankroll")?.let { - it.addField("initialValue", Double::class.java) - } + schema.get("Bankroll")?.addField("initialValue", Double::class.java) currentVersion++ - } + } - // Migrate to version 3 + // Migrate to version 4 if (currentVersion == 3) { Timber.d("*** Running migration ${currentVersion + 1}") - schema.get("Result")?.let { - it.addField("numberOfRebuy", Double::class.java).setNullable("numberOfRebuy", true) + schema.get("Result")?.addField("numberOfRebuy", Double::class.java)?.setNullable("numberOfRebuy", true) + currentVersion++ + } + + // Migrate to version 5 + if (currentVersion == 4) { + Timber.d("*** Running migration ${currentVersion + 1}") + schema.get("Bankroll")?.removeField("transactions") + currentVersion++ + } + + // Migrate to version 6 + if (currentVersion == 5) { + Timber.d("*** Running migration ${currentVersion + 1}") + schema.get("Transaction")?.let { + it.addField("dayOfWeek", Integer::class.java) + it.addField("month", Integer::class.java) + it.addField("year", Integer::class.java) + it.addField("dayOfMonth", Integer::class.java) + } + + val cfEntry = schema.create("CustomFieldEntry")?.let { + it.addField("id", String::class.java).setRequired("id", true) + it.addPrimaryKey("id") + it.addField("value", String::class.java).setNullable("value", false) + it.addField("order", Integer::class.java).setNullable("order", false) +// it.addRealmObjectField("customField", it).setNullable("customField", false) + it.addField("numericValue", Double::class.java).setNullable("numericValue", true) + } + + cfEntry?.let { customFieldEntrySchema -> + schema.get("CustomField")?.let { + it.addField("type", Integer::class.java).setNullable("type", false) + it.addField("duplicateValue", Boolean::class.java) + it.addField("sortCondition", Integer::class.java).setRequired("sortCondition", true) + it.addRealmListField("entries", customFieldEntrySchema) + } + + schema.get("Session")?.let { + it.addField("startDateHourMinuteComponent", Double::class.java) + .setNullable("startDateHourMinuteComponent", true) + it.addField("endDateHourMinuteComponent", Double::class.java) + .setNullable("endDateHourMinuteComponent", true) + it.addRealmListField("customFieldEntries", customFieldEntrySchema) + } + } + + schema.get("ReportSetup")?.let { + it.addRealmListField("statIds", Int::class.java).setNullable("statIds", true) + it.addRealmListField("criteriaCustomFieldIds", String::class.java).setNullable("criteriaCustomFieldIds", true) + it.addRealmListField("criteriaIds", Int::class.java).setNullable("criteriaIds", true) + it.removeField("filters") + schema.get("Filter")?.let { filterSchema -> + it.addRealmObjectField("filter", filterSchema) + } + } + + schema.get("Filter")?.addField("filterableTypeUniqueIdentifier", Integer::class.java) + schema.get("Filter")?.addField("useCount", Int::class.java) + schema.get("Filter")?.removeField("usageCount") currentVersion++ } - } + } override fun equals(other: Any?): Boolean { return other is RealmMigration diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt index cd577725..8f5574a2 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt @@ -1,11 +1,14 @@ package net.pokeranalytics.android.model.realm +import android.content.Context import io.realm.Realm -import io.realm.RealmList import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey import io.realm.kotlin.where import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.ui.view.RowRepresentable @@ -13,66 +16,106 @@ import net.pokeranalytics.android.ui.view.rowrepresentable.BankrollRow import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow import java.util.* -open class Bankroll() : RealmObject(), NameManageable, RowRepresentable { - - @PrimaryKey - override var id = UUID.randomUUID().toString() - - override var name: String = "" - - // Indicates whether the bankroll is live or online - var live: Boolean = true - - // The list of transactions of the bankroll - var transactions: RealmList = RealmList() - - // The currency of the bankroll - var currency: Currency? = null - - // The initial value of the bankroll - var initialValue: Double = 0.0 - - val rate: Double - get() { - return this.currency?.rate ?: 1.0 - } - - override fun getDisplayName(): String { - return this.name - } - - override fun updateValue(value: Any?, row: RowRepresentable) { - when (row) { - SimpleRow.NAME -> this.name = value as String? ?: "" - BankrollRow.LIVE -> { - this.live = if (value is Boolean) !value else false - } - BankrollRow.INITIAL_VALUE -> { - this.initialValue = value as Double? ?: 0.0 - } - BankrollRow.CURRENCY -> { - //TODO handle a use default currency option - this.currency?.code = value as String? - } - BankrollRow.RATE -> { - this.currency?.rate = value as Double? - } - } - } - - override fun isValidForDelete(realm: Realm): Boolean { +open class Bankroll : RealmObject(), NameManageable, RowRepresentable { + + @PrimaryKey + override var id = UUID.randomUUID().toString() + + override var name: String = "" + + // Indicates whether the bankroll is live or online + var live: Boolean = true + + /** + * The list of transactions of the bankroll + */ + @LinkingObjects("bankroll") + val transactions: RealmResults? = null + + // The currency of the bankroll + var currency: Currency? = null + + // The initial value of the bankroll + var initialValue: Double = 0.0 + + val rate: Double + get() { + return this.currency?.rate ?: 1.0 + } + + override fun getDisplayName(context: Context): String { + return this.name + } + + override fun updateValue(value: Any?, row: RowRepresentable) { + when (row) { + SimpleRow.NAME -> this.name = value as String? ?: "" + BankrollRow.LIVE -> { + this.live = if (value is Boolean) !value else false + } + BankrollRow.INITIAL_VALUE -> { + this.initialValue = value as Double? ?: 0.0 + } + BankrollRow.CURRENCY -> { + //TODO handle a use default currency option + this.currency?.code = value as String? + } + BankrollRow.RATE -> { + this.currency?.rate = value as Double? + } + } + } + + override fun isValidForDelete(realm: Realm): Boolean { return realm.where().equalTo("bankroll.id", id).findAll().isEmpty() + && realm.where().equalTo("bankroll.id", id).findAll().isEmpty() + } + + override fun getDeleteStatus(context: Context, realm: Realm): DeleteValidityStatus { + return if (!realm.where().equalTo("bankroll.id", id).findAll().isEmpty()) { + DeleteValidityStatus.SESSIONS_LINKED + } else if (!realm.where().equalTo("bankroll.id", id).findAll().isEmpty()) { + DeleteValidityStatus.TRANSACTIONS_LINKED + } else { + DeleteValidityStatus.VALID + } + } + + override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int { + return when (status) { + DeleteValidityStatus.SESSIONS_LINKED -> R.string.bankroll_relationship_error + DeleteValidityStatus.TRANSACTIONS_LINKED -> R.string.bankroll_relationship_error_transactions + else -> super.getFailedDeleteMessage(status) + } + } + + override fun getFailedSaveMessage(status: SaveValidityStatus): Int { + return when (status) { + SaveValidityStatus.DATA_INVALID -> R.string.empty_name_for_br_error + SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_bankroll_name_error + else -> super.getFailedSaveMessage(status) + } + } + + companion object { + + fun getOrCreate(realm: Realm, name: String, live: Boolean = true, currencyCode: String? = null, currencyRate: Double? = null) : Bankroll { + + val br = realm.where().equalTo("name", name).findFirst() + return if (br != null) { + br + } else { + val bankroll = Bankroll() + bankroll.name = name + bankroll.live = live + val currency = Currency() + currency.code = currencyCode + currency.rate = currencyRate + bankroll.currency = currency + realm.copyToRealm(bankroll) + } + } + } - override fun getFailedDeleteMessage(): Int { - return R.string.bankroll_relationship_error - } - - override fun getFailedSaveMessage(status: SaveValidityStatus): Int { - return when (status) { - SaveValidityStatus.DATA_INVALID -> R.string.empty_name_for_br_error - SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_bankroll_name_error - else -> super.getFailedSaveMessage(status) - } - } } \ No newline at end of file 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 32361079..9acf817e 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 @@ -1,19 +1,313 @@ package net.pokeranalytics.android.model.realm +import android.content.Context +import android.text.InputType +import io.realm.Realm +import io.realm.RealmList import io.realm.RealmObject +import io.realm.annotations.Ignore import io.realm.annotations.PrimaryKey +import io.realm.kotlin.where +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.Criteria +import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus +import net.pokeranalytics.android.model.interfaces.NameManageable +import net.pokeranalytics.android.model.interfaces.SaveValidityStatus +import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource +import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType +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.ui.view.rowrepresentable.CustomFieldRow +import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable +import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow +import net.pokeranalytics.android.util.enumerations.IntIdentifiable import java.util.* +import kotlin.collections.ArrayList -open class CustomField : RealmObject() { +open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable { - @PrimaryKey - var id = UUID.randomUUID().toString() + /** + * The custom field type: a list of items, a number or an amont + */ + enum class Type(override var uniqueIdentifier: Int, var resId: Int, var isEnabled: Boolean = true) : + IntIdentifiable { + LIST(0, R.string.enum_custom_field_type), + NUMBER(1, R.string.number), + AMOUNT(2, R.string.amount) + } - // The name of the currency field - var name: String = "" + /** + * The sorting used for the list, either custom, or alphabetically asc/desc + */ + enum class Sort(override var uniqueIdentifier: Int) : IntIdentifiable { + DEFAULT(0), + ASCENDING(1), + DESCENDING(2) + } - // @todo + @PrimaryKey + override var id = UUID.randomUUID().toString() + /** + * The name of the custom field + */ + override var name: String = "" + + // The type of the custom fields, mapped with the CustomField.Type enum + var type: Int = Type.LIST.uniqueIdentifier + set(value) { + if (field == Type.LIST.uniqueIdentifier && value != Type.LIST.uniqueIdentifier) { + this.removeListEntries() + } + field = value + + this.updateRowRepresentation() + } + + /** + * Indicates whether the custom field value should be copied when a session is duplicated + */ + var duplicateValue: Boolean = false + + /** + * The list of entries for the LIST type + */ + var entries: RealmList = RealmList() + + /** + * The sorting of the entries, mapped with the CustomField.Sort enum + */ + var sortCondition: Int = Sort.DEFAULT.uniqueIdentifier + set(value) { + field = value + sortEntries() + updateRowRepresentation() + } + + @Ignore + private var entriesToDelete: ArrayList = ArrayList() + + @Ignore + override var viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal + + @Ignore + private var rowRepresentation: List = mutableListOf() + + + //helper + + val isListType: Boolean + get() { + return this.type == Type.LIST.uniqueIdentifier + } + + val isAmountType: Boolean + get() { + return this.type == Type.AMOUNT.uniqueIdentifier + } + + override fun localizedTitle(context: Context): String { + return this.name + } + + override fun getDisplayName(context: Context): String { + return this.name + } + + override fun adapterRows(): List? { + return rowRepresentation + } + + override fun updateValue(value: Any?, row: RowRepresentable) { + when (row) { + SimpleRow.NAME -> this.name = value as String? ?: "" + CustomFieldRow.TYPE -> this.type = (value as Type?)?.uniqueIdentifier ?: Type.LIST.uniqueIdentifier + CustomFieldRow.COPY_ON_DUPLICATE -> this.duplicateValue = value as Boolean? ?: false + } + } + + override fun isValidForSave(): Boolean { + return super.isValidForSave() + } + + override fun getFailedSaveMessage(status: SaveValidityStatus): Int { + return when (status) { + SaveValidityStatus.DATA_INVALID -> R.string.cf_empty_field_error + SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_cf_error + else -> super.getFailedSaveMessage(status) + } + } + + override fun alreadyExists(realm: Realm): Boolean { + return realm.where(this::class.java).equalTo("name", this.name).and().notEqualTo("id", this.id).findAll() + .isNotEmpty() + } + + override fun isValidForDelete(realm: Realm): Boolean { + val sessions = realm.where().contains("customFieldEntries.customField.id", id).findAll() + return sessions.isEmpty() + } + + override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int { + //TODO: + return R.string.cf_entry_delete_popup_message + } + + override val bottomSheetType: BottomSheetType + get() { + return when (type) { + Type.LIST.uniqueIdentifier -> BottomSheetType.LIST_STATIC + else -> BottomSheetType.NUMERIC_TEXT + } + } + + override fun deleteDependencies() { + if (isValid) { + val entries = realm.where().equalTo("customField.id", id).findAll() + entries.deleteAllFromRealm() + } + } + + override fun editDescriptors(row: RowRepresentable): ArrayList? { + return when (row) { + is CustomFieldEntry -> row.editingDescriptors( + mapOf( + "defaultValue" to row.value + ) + ) + else -> null + } + } + + override fun editingDescriptors(map: Map): ArrayList? { + return when (type) { + Type.LIST.uniqueIdentifier -> { + val defaultValue: Any? by map + val data: RealmList? by map + arrayListOf( + RowRepresentableEditDescriptor(defaultValue, staticData = data) + ) + } + else -> { + val defaultValue: Double? by map + arrayListOf( + RowRepresentableEditDescriptor( + defaultValue, inputType = InputType.TYPE_CLASS_NUMBER + or InputType.TYPE_NUMBER_FLAG_DECIMAL + or InputType.TYPE_NUMBER_FLAG_SIGNED + ) + ) + } + } + } + + /** + * Update the row representation + */ + private fun updatedRowRepresentationForCurrentState(): List { + val rows = ArrayList() + rows.add(SimpleRow.NAME) + rows.add(CustomFieldRow.TYPE) + + if (type == Type.LIST.uniqueIdentifier && entries.size >= 0) { + if (entries.isNotEmpty()) { + rows.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, R.string.items_list)) + sortEntries() + entries.forEach { customFieldEntry -> + customFieldEntry.isMovable = sortCondition == Sort.DEFAULT.uniqueIdentifier + } + rows.addAll(entries) + } + } + + return rows + } + + /** + * Sort the entries element + */ + private fun sortEntries() { + when (sortCondition) { + Sort.ASCENDING.uniqueIdentifier -> entries.sortBy { it.value } + Sort.DESCENDING.uniqueIdentifier -> entries.sortByDescending { it.value } + } + entries.forEachIndexed { index, customFieldEntry -> + customFieldEntry.order = index + } + } + + fun updateRowRepresentation() { + this.rowRepresentation = this.updatedRowRepresentationForCurrentState() + } + + /** + * Add an entry + */ + fun addEntry(): CustomFieldEntry { + val entry = CustomFieldEntry() + this.entries.add(entry) + sortEntries() + updateRowRepresentation() + return entry + } + + /** + * Delete an entry + */ + fun deleteEntry(entry: CustomFieldEntry) { + entries.remove(entry) + entriesToDelete.add(entry) + sortEntries() + updateRowRepresentation() + } + + private fun removeListEntries() { + + this.entriesToDelete.addAll(entries) + this.entries.clear() + + if (realm != null) { + realm.executeTransaction { + this.entriesToDelete.forEach { + if (it.isManaged) { + it.deleteFromRealm() + } + } + } + } + } + + /** + * Clean the entries if the type is not a list & remove the deleted entries from realm + */ +// fun cleanEntries(realm: Realm) { +// realm.executeTransaction { +// +// if (!isListType) { +// entriesToDelete.addAll(entries) +// entries.clear() +// } +// +// // @TODO +// entriesToDelete.forEach { +// Timber.d("Delete entry: V=${it.value} N=${it.numericValue} / ID=${it.id}") +// realm.where().equalTo("id", it.id).findFirst()?.deleteFromRealm() +// } +// entriesToDelete.clear() +// } +// } + + /** + * Returns a comparison criteria based on this custom field + */ + val criteria: Criteria + get() { + return when (this.type) { + CustomField.Type.LIST.uniqueIdentifier -> Criteria.ListCustomFields(this.id) + else -> Criteria.ValueCustomFields(this.id) + } + } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/CustomFieldEntry.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/CustomFieldEntry.kt new file mode 100644 index 00000000..f6703266 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/CustomFieldEntry.kt @@ -0,0 +1,148 @@ +package net.pokeranalytics.android.model.realm + +import android.content.Context +import android.text.InputType +import io.realm.Realm +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.Ignore +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey +import io.realm.kotlin.where +import net.pokeranalytics.android.R +import net.pokeranalytics.android.exceptions.ModelException +import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus +import net.pokeranalytics.android.model.interfaces.NameManageable +import net.pokeranalytics.android.model.interfaces.SaveValidityStatus +import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType +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.NULL_TEXT +import net.pokeranalytics.android.util.extensions.toCurrency +import java.text.NumberFormat +import java.util.* +import java.util.Currency + + +open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable { + + @PrimaryKey + override var id = UUID.randomUUID().toString() + + /** + * The order in the list + */ + var order: Int = 0 + + /** + * The inverse relationship with CustomField + */ + @LinkingObjects("entries") + val customFields: RealmResults? = null + + val customField: CustomField? + get() { + return this.customFields?.first() + } + + /** + * The string value of the entry + */ + var value: String = "" + + /** + * The numeric value of the entry + */ + var numericValue: Double? = null + + @Ignore + override var name: String = value + get() { return value } + + @Ignore + var isMovable: Boolean = false + + @Ignore + override val viewType: Int = RowViewType.TITLE_VALUE_ACTION.ordinal + + override val imageRes: Int? + get() { + return if (isMovable) R.drawable.ic_reorder else null + } + + override val imageTint: Int? + get() { + return R.color.kaki + } + + @Ignore + override val bottomSheetType: BottomSheetType = BottomSheetType.EDIT_TEXT + + override fun localizedTitle(context: Context): String { + return context.getString(R.string.value) + } + + override fun getDisplayName(context: Context): String { + return if (value.isNotEmpty()) value else NULL_TEXT + } + + override fun editingDescriptors(map: Map): ArrayList? { + val defaultValue: Any? by map + return arrayListOf( + RowRepresentableEditDescriptor(defaultValue, R.string.value, InputType.TYPE_CLASS_TEXT) + ) + } + + override fun isValidForSave(): Boolean { + return true + } + + override fun alreadyExists(realm: Realm): Boolean { + return realm.where(this::class.java).notEqualTo("id", this.id).findAll().isNotEmpty() + } + + override fun getFailedSaveMessage(status: SaveValidityStatus): Int { + throw ModelException("${this::class.java} getFailedSaveMessage for $status not handled") + } + + override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int { + return R.string.cf_entry_delete_popup_message + } + + override fun deleteDependencies() { + if (isValid) { + val entries = realm.where().equalTo("customField.id", id).findAll() + entries.deleteAllFromRealm() + } + } + + override fun updateValue(value: Any?, row: RowRepresentable) { + this.value = value as String? ?: "" + } + + override fun isValidForDelete(realm: Realm): Boolean { + if (realm.where().contains("customFieldEntries.id", id).findAll().isNotEmpty()) { + return false + } + return true + } + + /** + * Return the amount + */ + fun getFormattedValue(currency: Currency? = null): String { + return when (customField?.type) { + CustomField.Type.AMOUNT.uniqueIdentifier -> { + numericValue?.toCurrency(currency) ?: run { NULL_TEXT } + } + CustomField.Type.NUMBER.uniqueIdentifier -> { + NumberFormat.getInstance().format(this.numericValue) + } + else -> { + value + } + } + } + +} \ No newline at end of file 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 d83b2e1e..3f1e481b 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 @@ -1,13 +1,21 @@ package net.pokeranalytics.android.model.realm +import android.content.Context import io.realm.* import io.realm.annotations.PrimaryKey import io.realm.kotlin.where +import net.pokeranalytics.android.R import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.QueryCondition +import net.pokeranalytics.android.model.interfaces.CountableUsage +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.ui.interfaces.FilterableType +import net.pokeranalytics.android.ui.view.ImageDecorator +import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow -import timber.log.Timber import java.util.* /** @@ -15,14 +23,16 @@ import java.util.* * It contains a list of [FilterCondition] describing the complete query to launch * The [Filter] is working closely with a [Filterable] interface providing the entity we want the query being launched on */ -open class Filter : RealmObject() { +open class Filter : RealmObject(), RowRepresentable, Identifiable, Deletable, CountableUsage, ImageDecorator { companion object { // Create a new instance - fun newInstance(realm: Realm): Filter { + fun newInstance(filterableTypeUniqueIdentifier:Int): Filter { val filter = Filter() - return realm.copyToRealm(filter) + filter.filterableTypeUniqueIdentifier = filterableTypeUniqueIdentifier + return filter + //return realm.copyToRealm(filter) } // Get a queryWith by its id @@ -31,43 +41,54 @@ open class Filter : RealmObject() { } inline fun queryOn(realm: Realm, query: Query, sortField: String? = null): RealmResults { - var realmQuery = realm.where() - query.conditions.forEach { - realmQuery = it.queryWith(realmQuery) - } - sortField?.let { - realmQuery.sort(it) - } - Timber.d(">>> Filter query: ${realmQuery.description}") - return realmQuery.findAll() - } + val realmQuery = realm.where() + sortField?.let { + return query.queryWith(realmQuery).sort(it).findAll() + } ?: run { + return query.queryWith(realmQuery).findAll() + } + } } @PrimaryKey - var id = UUID.randomUUID().toString() + override var id = UUID.randomUUID().toString() // the queryWith name var name: String = "" + get() { + if (field.isEmpty()) { + return this.query.defaultName + } + return field + } - // the number of use of the queryWith, - // for MutableRealmInteger, see https://realm.io/docs/java/latest/#counters - val usageCount: MutableRealmInteger = MutableRealmInteger.valueOf(0) + override var useCount: Int = 0 var filterConditions: RealmList = RealmList() private set + private var filterableTypeUniqueIdentifier: Int? = null + + val filterableType: FilterableType + get() { + this.filterableTypeUniqueIdentifier?.let { + return FilterableType.valueByIdentifier(it) + } + return FilterableType.ALL + } + fun createOrUpdateFilterConditions(filterConditionRows: ArrayList) { println("list of querys saving: ${filterConditionRows.map { it.id }}") println("list of querys previous: ${this.filterConditions.map { it.queryCondition.id }}") filterConditionRows .map { - it.filterSectionRow + it.groupId } .distinct() - .forEach { filterName-> + .forEach { groupId-> filterConditionRows .filter { - it.filterSectionRow == filterName + it.groupId == groupId } .apply { @@ -76,7 +97,7 @@ open class Filter : RealmObject() { casted.addAll(this) val newFilterCondition = FilterCondition(casted) val previousCondition = filterConditions.filter { - it.filterName == newFilterCondition.filterName + it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator } filterConditions.removeAll(previousCondition) filterConditions.add(newFilterCondition) @@ -118,15 +139,40 @@ open class Filter : RealmObject() { } } - inline fun results(): RealmResults { - var realmQuery = realm.where() - this.filterConditions.map { - it.queryCondition - }.forEach { - realmQuery = it.queryWith(realmQuery) - } + inline fun results(firstField: String? = null, secondField: String? = null): RealmResults { + + val realmQuery = realm.where() + + if (firstField != null && secondField != null) { + return this.query.queryWith(realmQuery).distinct(firstField, secondField).findAll() + } + + if (firstField != null) { + return this.query.queryWith(realmQuery).distinct(firstField).findAll() + } - return realmQuery.findAll() + return this.query.queryWith(realmQuery).findAll() + } + + val query: Query + get() { + val query = Query() + this.filterConditions.forEach { + query.add(it.queryCondition) + } + return query + } + + override fun getDisplayName(context: Context): String { + if (name.isNotEmpty()) return name + return this.query.getName(context) + } + + override fun isValidForDelete(realm: Realm): Boolean { + return true } + override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int { + return R.string.relationship_error + } } 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 d0ca264d..46c4fc5b 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 @@ -18,9 +18,12 @@ open class FilterCondition() : RealmObject() { val row = filterElementRows.first() this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName this.operator = row.operator.ordinal + if (row is QueryCondition.CustomFieldRelated) { + this.stringValue = row.customFieldId + } when (row) { - is QueryCondition.SingleInt -> this.setValue(row.singleValue) - is QueryCondition.SingleDate -> this.setValue(row.singleValue) + is QueryCondition.SingleInt -> this.setValue(row.singleValue?:throw PokerAnalyticsException.FilterElementExpectedValueMissing) + is QueryCondition.SingleDate -> this.setValue(row.singleValue?:throw PokerAnalyticsException.FilterElementExpectedValueMissing) is QueryCondition.ListOfDouble -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfDouble).listOfValues }) is QueryCondition.ListOfInt -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfInt).listOfValues }) is QueryCondition.ListOfString -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfString).listOfValues }) @@ -46,7 +49,6 @@ open class FilterCondition() : RealmObject() { var operator: Int? = null inline fun getValues(): ArrayList < T > { - println("<<<< r $stringValues") return when (T::class) { Int::class -> ArrayList().apply { intValues?.map { add(it as T) } } Double::class -> ArrayList().apply { doubleValues?.map { add(it as T) } } 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 bfb54b51..2334baa0 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 @@ -1,5 +1,6 @@ package net.pokeranalytics.android.model.realm +import android.content.Context import io.realm.Realm import io.realm.RealmObject import io.realm.annotations.PrimaryKey @@ -47,7 +48,7 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc return this.name } - override fun getDisplayName(): String { + override fun getDisplayName(context: Context): String { return this.name } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Location.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Location.kt index acbf0c63..df4136e4 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Location.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Location.kt @@ -1,13 +1,14 @@ package net.pokeranalytics.android.model.realm +import android.content.Context import com.google.android.libraries.places.api.model.Place import io.realm.Realm import io.realm.RealmObject import io.realm.annotations.PrimaryKey import io.realm.kotlin.where import net.pokeranalytics.android.R -import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.NameManageable +import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow import java.util.* @@ -30,7 +31,7 @@ open class Location : RealmObject(), NameManageable, RowRepresentable { // the latitude of the location var latitude: Double? = null - override fun getDisplayName(): String { + override fun getDisplayName(context: Context): String { return this.name } 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 9014a1b9..2db6f02e 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 @@ -1,33 +1,98 @@ package net.pokeranalytics.android.model.realm +import android.content.Context +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.calculus.Calculator +import net.pokeranalytics.android.calculus.Stat +import net.pokeranalytics.android.model.Criteria +import net.pokeranalytics.android.model.interfaces.Deletable +import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowViewType +import net.pokeranalytics.android.util.extensions.findById import java.util.* -enum class ReportDisplay { - TABLE, - GRAPH, - MAP -} -open class ReportSetup : RealmObject() { +open class ReportSetup : RealmObject(), RowRepresentable, Deletable { @PrimaryKey - var id = UUID.randomUUID().toString() + override var id = UUID.randomUUID().toString() // The name of the report var name: String = "" // The type of display of the report - var display: Int = ReportDisplay.TABLE.ordinal + var display: Int = Calculator.Options.Display.TABLE.ordinal + + /** + * A list of statIds to compute + * Must contain at least 1 + */ + var statIds: RealmList = RealmList() + + /** + * An optional list of criteriaIds to compare statIds + */ + var criteriaIds: RealmList = RealmList() + + /** + * An optional list of custom fields ids to be compared + */ + var criteriaCustomFieldIds: RealmList = RealmList() + + /** + * An optional filter to narrow the results + */ + var filter: Filter? = null + + // RowRepresentable + override fun getDisplayName(context: Context): String { + return this.name + } + + @Ignore + override val viewType: Int = RowViewType.TITLE_ARROW.ordinal + + /** + * Returns the Options based on the ReportSetup parameters + */ + val options: Calculator.Options + get() { + + 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() + allCriteria.addAll(criteria) + allCriteria.addAll(cfCriteria) + + return Calculator.Options( + display = Calculator.Options.Display.values()[this.display], + stats = stats, + criterias = allCriteria, + filter = this.filter, + userGenerated = true, + reportSetupId = this.id + ) + } - // @todo define the configuration options + // Deletable -// var criteria: List = listOf() -// var stats: List = listOf() + override fun isValidForDelete(realm: Realm): Boolean { + return true + } - // The filters associated with the report - var filters: RealmList = RealmList() + override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt index 3f0bc919..644d0de3 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt @@ -106,7 +106,7 @@ open class Result : RealmObject(), Filterable { val transactionsSum = transactions.sumByDouble { it.amount } - val isLive = this.session?.bankroll?.live ?: true + val isLive = this.session?.isLive ?: true if (isLive) { val buyin = this.buyin ?: 0.0 val cashOut = this.cashout ?: 0.0 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 1cca7337..088ac609 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 @@ -12,10 +12,11 @@ import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey import io.realm.kotlin.where import net.pokeranalytics.android.R -import net.pokeranalytics.android.calculus.* +import net.pokeranalytics.android.calculus.ComputedStat +import net.pokeranalytics.android.calculus.Stat +import net.pokeranalytics.android.calculus.StatFormattingException import net.pokeranalytics.android.exceptions.ModelException import net.pokeranalytics.android.model.Limit -import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.TableSize import net.pokeranalytics.android.model.TournamentType import net.pokeranalytics.android.model.extensions.SessionState @@ -28,11 +29,13 @@ import net.pokeranalytics.android.model.utils.SessionSetManager import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.UnmanagedRowRepresentableException import net.pokeranalytics.android.ui.fragment.GraphFragment +import net.pokeranalytics.android.ui.graph.ObjectIdentifier import net.pokeranalytics.android.ui.view.* import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRow import net.pokeranalytics.android.ui.view.rowrepresentable.SessionRow import net.pokeranalytics.android.util.NULL_TEXT +import net.pokeranalytics.android.util.TextFormat import net.pokeranalytics.android.util.UserDefaults import net.pokeranalytics.android.util.extensions.* import java.text.DateFormat @@ -47,7 +50,20 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat enum class Type { CASH_GAME, - TOURNAMENT + TOURNAMENT; + + companion object { + + fun getValueFromString(string: String): Type? { + return when (string) { + "Cash", "Cash Game" -> CASH_GAME + "Tournament" -> TOURNAMENT + else -> null + } + } + + } + } companion object { @@ -76,19 +92,24 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat AnyTableSize::class.java -> "tableSize" AnyTournamentType::class.java -> "tournamentType" AnyBlind::class.java -> "blinds" - NumberOfTable::class.java -> "numberOfTable" + NumberOfTable::class.java -> "numberOfTables" NetAmountWon::class.java, NetAmountLost::class.java -> "computableResults.ratedNet" NumberOfRebuy::class.java -> "result.numberOfRebuy" - TournamentNumberOfPlayer::class.java -> "result.tournamentNumberOfPlayers" + TournamentNumberOfPlayer::class.java -> "tournamentNumberOfPlayers" TournamentFinalPosition::class.java -> "result.tournamentFinalPosition" TournamentFee::class.java -> "tournamentEntryFee" - StartedFromDate::class.java, StartedToDate::class.java -> "startDate" - EndedFromDate::class.java, EndedToDate::class.java -> "endDate" + StartedFromDate::class.java, StartedToDate::class.java, EndedFromDate::class.java, EndedToDate::class.java -> "startDate" AnyDayOfWeek::class.java, IsWeekEnd::class.java, IsWeekDay::class.java -> "dayOfWeek" AnyMonthOfYear::class.java -> "month" AnyYear::class.java -> "year" - IsToday::class.java, WasYesterday::class.java, WasTodayAndYesterday::class.java, DuringThisYear::class.java, DuringThisMonth::class.java, DuringThisWeek::class.java -> "startDate" - else -> null + PastDay::class.java, IsToday::class.java, WasYesterday::class.java, WasTodayAndYesterday::class.java, DuringThisYear::class.java, DuringThisMonth::class.java, DuringThisWeek::class.java -> "startDate" + StartedFromTime::class.java -> "startDateHourMinuteComponent" + EndedToTime::class.java -> "endDateHourMinuteComponent" + Duration::class.java -> "netDuration" + CustomFieldListQuery::class.java -> "customFieldEntries.id" + CustomFieldAmountQuery::class.java, CustomFieldNumberQuery::class.java -> "customFieldEntries.numericValue" + CustomFieldQuery::class.java -> "customFieldEntries.customFields.id" + else -> null } } } @@ -118,12 +139,39 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat override var year: Int? = null override var dayOfMonth: Int? = null + private var startDateHourMinuteComponent: Double? = null + get() { + if (field == null && startDate != null) { + val cal = Calendar.getInstance() + cal.time = startDate + field = cal.hourMinute() + } + return field + } + + private var endDateHourMinuteComponent: Double? = null + get() { + if (field == null && endDate != null) { + val cal = Calendar.getInstance() + cal.time = endDate + field = cal.hourMinute() + } + return field + } + /** * The start date of the session */ var startDate: Date? = null set(value) { field = value + if (field == null) { + startDateHourMinuteComponent = null + } else { + val cal = Calendar.getInstance() + cal.time = field + startDateHourMinuteComponent = cal.hourMinute() + } this.updateTimeParameter(field) this.computeNetDuration() @@ -142,6 +190,14 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat var endDate: Date? = null set(value) { field = value + if (field == null) { + endDateHourMinuteComponent = null + } else { + val cal = Calendar.getInstance() + cal.time = field + endDateHourMinuteComponent = cal.hourMinute() + } + this.computeNetDuration() this.dateChanged() this.defineDefaultTournamentBuyinIfNecessary() @@ -224,7 +280,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat field = value this.computeStats() formatBlinds() - this.result?.computeNumberOfRebuy() + this.result?.computeNumberOfRebuy() } var blinds: String? = null @@ -234,10 +290,10 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat // The entry fee of the tournament var tournamentEntryFee: Double? = null - set(value) { - field = value - this.result?.computeNumberOfRebuy() - } + set(value) { + field = value + this.result?.computeNumberOfRebuy() + } // The total number of players who participated in the tournament var tournamentNumberOfPlayers: Int? = null @@ -251,6 +307,9 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat // The features of the tournament, like Knockout, Shootout, Turbo... var tournamentFeatures: RealmList = RealmList() + // The custom fields values + var customFieldEntries: RealmList = RealmList() + fun bankrollHasBeenUpdated() { formatBlinds() } @@ -344,7 +403,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat } /** - * Pre-compute various stats + * Pre-compute various statIds */ fun computeStats() { @@ -373,8 +432,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat val numberOfHandsPerHour: Double get() { val tableSize = this.tableSize ?: 9 // 9 is the default table size if null - val isLive = this.bankroll?.live ?: true - val playerHandsPerHour = if (isLive) LIVE_PLAYER_HANDS_PER_HOUR else ONLINE_PLAYER_HANDS_PER_HOUR + val playerHandsPerHour = if (this.isLive) LIVE_PLAYER_HANDS_PER_HOUR else ONLINE_PLAYER_HANDS_PER_HOUR return playerHandsPerHour / tableSize.toDouble() } @@ -391,6 +449,24 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat return this.bbNet / this.hourlyDuration } + val isLive: Boolean + get() { + this.bankroll?.let { + return it.live + } + return true // default should be true + } + + val hasBuyin: Boolean + get() { + return this.result?.buyin != null + } + + val hasNetResult: Boolean + get() { + return this.result?.netResult != null + } + // Manageable override fun isValidForSave(): Boolean { @@ -512,7 +588,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat } /** - * Return the game title + * Return the game titleResId * Example: NL Holdem */ fun getFormattedGame(): String { @@ -532,7 +608,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat return blinds ?: NULL_TEXT } - fun formatBlinds() { + fun formatBlinds() { blinds = null if (cgBigBlind == null) return cgBigBlind?.let { bb -> @@ -545,12 +621,13 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat /** * Delete the object from realm - * @TODO: Cascade delete? */ fun delete() { - realm.executeTransaction { - cleanup() - deleteFromRealm() + if (isValid) { + realm.executeTransaction { + cleanup() + deleteFromRealm() + } } } @@ -572,7 +649,17 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat @Ignore override val viewType: Int = RowViewType.ROW_SESSION.ordinal - override fun getDisplayName(): String { + // Override to surcharge custom field viewType + override fun viewTypeForPosition(position: Int): Int { + rowRepresentationForCurrentState[position].let { + if (it is CustomField) { + return RowViewType.TITLE_VALUE.ordinal + } + } + return super.viewTypeForPosition(position) + } + + override fun getDisplayName(context: Context): String { return "Session ${this.creationDate}" } @@ -637,6 +724,13 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat // Rows rows.addAll(SessionRow.getRows(this)) + + // Add custom fields + realm?.let { + rows.add(SeparatorRow()) + rows.addAll(it.sorted()) + } + return rows } @@ -670,10 +764,12 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat SessionRow.START_DATE -> this.startDate?.shortDateTime() ?: NULL_TEXT SessionRow.TABLE_SIZE -> this.tableSize?.let { TableSize(it).localizedTitle(context) } ?: NULL_TEXT SessionRow.TIPS -> result?.tips?.toCurrency(currency) ?: NULL_TEXT - SessionRow.TOURNAMENT_TYPE -> this.tournamentType?.let { - TournamentType.values()[it].localizedTitle(context) - } ?: run { - NULL_TEXT + SessionRow.TOURNAMENT_TYPE -> { + this.tournamentType?.let { + TournamentType.values()[it].localizedTitle(context) + } ?: run { + NULL_TEXT + } } SessionRow.TOURNAMENT_FEATURE -> { if (tournamentFeatures.size > 2) { @@ -689,7 +785,13 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat } } SessionRow.TOURNAMENT_NAME -> tournamentName?.name ?: NULL_TEXT - else -> throw UnmanagedRowRepresentableException("Unmanaged row = ${row.getDisplayName()}") + is CustomField -> { + customFieldEntries.find { it.customField?.id == row.id }?.let { customFieldEntry -> + return customFieldEntry.getFormattedValue(currency) + } + return NULL_TEXT + } + else -> throw UnmanagedRowRepresentableException("Unmanaged row = ${row}") } } @@ -707,32 +809,32 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat SessionRow.BANKROLL -> row.editingDescriptors( mapOf( "defaultValue" to this.bankroll, - "data" to LiveData.BANKROLL.items(realm) + "data" to realm.sorted() // LiveData.Bankroll.items(realm) ) ) SessionRow.GAME -> row.editingDescriptors( mapOf( "limit" to this.limit, "defaultValue" to this.game, - "data" to LiveData.GAME.items(realm) + "data" to realm.sorted() //LiveData.Game.items(realm) ) ) SessionRow.LOCATION -> row.editingDescriptors( mapOf( "defaultValue" to this.location, - "data" to LiveData.LOCATION.items(realm) + "data" to realm.sorted() // LiveData.Location.items(realm) ) ) SessionRow.TOURNAMENT_FEATURE -> row.editingDescriptors( mapOf( "defaultValue" to this.tournamentFeatures, - "data" to LiveData.TOURNAMENT_FEATURE.items(realm) + "data" to realm.sorted() //LiveData.TournamentFeature.items(realm) ) ) SessionRow.TOURNAMENT_NAME -> row.editingDescriptors( mapOf( "defaultValue" to this.tournamentName, - "data" to LiveData.TOURNAMENT_NAME.items(realm) + "data" to realm.sorted() //LiveData.TournamentName.items(realm) ) ) SessionRow.TOURNAMENT_TYPE -> row.editingDescriptors( @@ -796,6 +898,19 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat "tips" to result?.tips ) ) + is CustomField -> { + row.editingDescriptors( + when (row.type) { + CustomField.Type.LIST.uniqueIdentifier -> mapOf( + "defaultValue" to customFieldEntries.find { it.customField?.id == row.id }?.value, + "data" to row.entries + ) + else -> mapOf( + "defaultValue" to customFieldEntries.find { it.customField?.id == row.id }?.numericValue + ) + } + ) + } else -> null } } @@ -905,7 +1020,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat result = localResult } SessionRow.TOURNAMENT_NAME -> tournamentName = value as TournamentName? - SessionRow.TOURNAMENT_TYPE -> tournamentType = value as Int? + SessionRow.TOURNAMENT_TYPE -> tournamentType = (value as TournamentType?)?.ordinal SessionRow.TOURNAMENT_FEATURE -> { value?.let { @@ -915,6 +1030,28 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat tournamentFeatures.removeAll(this.tournamentFeatures) } } + + is CustomField -> { + customFieldEntries.filter { it.customField?.id == row.id }.let { + customFieldEntries.removeAll(it) + } + when (row.type) { + CustomField.Type.AMOUNT.uniqueIdentifier, + CustomField.Type.NUMBER.uniqueIdentifier -> { + if (value != null) { + val customFieldEntry = CustomFieldEntry() + customFieldEntry.numericValue = value as Double? + customFieldEntries.add(customFieldEntry) + row.entries.add(customFieldEntry) + } + } + CustomField.Type.LIST.uniqueIdentifier -> { + if (value != null && value is CustomFieldEntry) { + customFieldEntries.add(value) + } + } + } + } } } @@ -922,10 +1059,9 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat // Stat Entry - override val entryTitle: String - get() { - return DateFormat.getDateInstance(DateFormat.SHORT).format(this.startDate) - } + override fun entryTitle(context: Context): String { + return DateFormat.getDateInstance(DateFormat.SHORT).format(this.startDate) + } override fun formattedValue(stat: Stat): TextFormat { @@ -962,7 +1098,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat } } ?: run { - throw java.lang.IllegalStateException("Asking for stats on Session without Result") + throw java.lang.IllegalStateException("Asking for statIds on Session without Result") } } @@ -973,16 +1109,14 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat style: GraphFragment.Style, groupName: String, context: Context - ) : LegendContent { + ): LegendContent { when (style) { GraphFragment.Style.MULTILINE -> { val secondTitle = stat.localizedTitle(context) val entryValue = this.formattedValue(stat) - val dateValue = TextFormat(this.entryTitle) - - + val dateValue = TextFormat(this.entryTitle(context)) return MultilineLegendValues(groupName, secondTitle, entryValue, dateValue) } @@ -1001,7 +1135,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat } } - DefaultLegendValues(this.entryTitle, left, right) + DefaultLegendValues(this.entryTitle(context), left, right) } else -> { super.legendValues(stat, entry, style, groupName, context) 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 52b998c6..da9976b6 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 @@ -1,18 +1,18 @@ package net.pokeranalytics.android.model.realm +import android.content.Context import io.realm.Realm import io.realm.RealmObject import io.realm.RealmResults -import io.realm.annotations.Ignore import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey -import net.pokeranalytics.android.calculus.ObjectIdentifier import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.StatFormattingException -import net.pokeranalytics.android.calculus.TextFormat +import net.pokeranalytics.android.util.TextFormat import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.interfaces.Timed +import net.pokeranalytics.android.ui.graph.ObjectIdentifier import net.pokeranalytics.android.util.NULL_TEXT import java.text.DateFormat import java.util.* @@ -74,8 +74,10 @@ open class SessionSet() : RealmObject(), Timed, Filterable { var ratedNet: Double = 0.0 - @Ignore - val hourlyRate: Double = this.ratedNet / this.hourlyDuration + val hourlyRate: Double + get() { + return this.ratedNet / this.hourlyDuration + } var estimatedHands: Double = 0.0 @@ -113,10 +115,9 @@ open class SessionSet() : RealmObject(), Timed, Filterable { // Stat Base - override val entryTitle: String - get() { - return DateFormat.getDateInstance(DateFormat.SHORT).format(this.startDate) - } + override fun entryTitle(context: Context): String { + return DateFormat.getDateInstance(DateFormat.SHORT).format(this.startDate) + } override fun formattedValue(stat: Stat) : TextFormat { return when (stat) { diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrame.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrame.kt index f6970a25..f9fe2236 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrame.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrame.kt @@ -174,7 +174,7 @@ // } // // fun createSessionSet(owner: Session) { -// val set: SessionSet = SessionSet.newInstance(this.realm) +// val set: SessionSet = SessionSet.newInstanceForResult(this.realm) // set.timeFrame?.let { // it.startDate = this.startDate // it.endDate = this.endDate @@ -269,7 +269,7 @@ // sessionSets.deleteAllFromRealm() // // // Create a new sets -// val set: SessionSet = SessionSet.newInstance(this.realm) +// val set: SessionSet = SessionSet.newInstanceForResult(this.realm) // set.timeFrame?.let { // it.setDate(startDate, endDate) // } ?: run { 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 9ad58aad..f11f0e73 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 @@ -1,13 +1,14 @@ package net.pokeranalytics.android.model.realm +import android.content.Context import io.realm.Realm import io.realm.RealmObject import io.realm.annotations.PrimaryKey import io.realm.kotlin.where import net.pokeranalytics.android.R import net.pokeranalytics.android.model.interfaces.CountableUsage -import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.NameManageable +import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor @@ -38,7 +39,7 @@ open class TournamentFeature : RealmObject(), NameManageable, StaticRowRepresent // CountableUsage override var useCount: Int = 0 - override fun getDisplayName(): String { + override fun getDisplayName(context: Context): String { return 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 1e7f21b9..cad45445 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 @@ -1,12 +1,13 @@ package net.pokeranalytics.android.model.realm +import android.content.Context import io.realm.Realm import io.realm.RealmObject import io.realm.annotations.PrimaryKey import io.realm.kotlin.where import net.pokeranalytics.android.R -import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.NameManageable +import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor @@ -33,7 +34,7 @@ open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentabl // The name of the tournament override var name: String = "" - override fun getDisplayName(): String { + override fun getDisplayName(context: Context): String { return 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 481a9444..748b3e84 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 @@ -1,26 +1,36 @@ package net.pokeranalytics.android.model.realm +import android.content.Context +import com.github.mikephil.charting.data.Entry import io.realm.Realm import io.realm.RealmObject import io.realm.annotations.Ignore import io.realm.annotations.PrimaryKey import net.pokeranalytics.android.R +import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.QueryCondition -import net.pokeranalytics.android.model.interfaces.DatedValue -import net.pokeranalytics.android.model.interfaces.Manageable -import net.pokeranalytics.android.model.interfaces.SaveValidityStatus +import net.pokeranalytics.android.model.interfaces.* import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource +import net.pokeranalytics.android.ui.fragment.GraphFragment +import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry +import net.pokeranalytics.android.ui.view.DefaultLegendValues +import net.pokeranalytics.android.ui.view.LegendContent import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.rowrepresentable.TransactionRow +import net.pokeranalytics.android.util.TextFormat +import net.pokeranalytics.android.util.extensions.findById +import java.text.DateFormat import java.util.* import kotlin.collections.ArrayList -open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSource, RowRepresentable, Filterable, DatedValue { +open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSource, RowRepresentable, TimeFilterable, Filterable, DatedValue, + GraphUnderlyingEntry { companion object { + val rowRepresentation: List by lazy { val rows = ArrayList() rows.addAll(TransactionRow.values()) @@ -29,13 +39,23 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo fun fieldNameForQueryType(queryCondition: Class): String? { return when (queryCondition) { - QueryCondition.AnyBankroll::class.java -> "bankroll.id" - QueryCondition.StartedFromDate::class.java, QueryCondition.StartedToDate::class.java -> "date" + + QueryCondition.AnyBankroll::class.java -> "bankroll.id" + QueryCondition.AnyTransactionType::class.java -> "type.id" + QueryCondition.StartedFromDate::class.java, QueryCondition.StartedToDate::class.java, QueryCondition.EndedFromDate::class.java, QueryCondition.EndedToDate::class.java -> "date" + QueryCondition.AnyDayOfWeek::class.java, QueryCondition.IsWeekEnd::class.java, QueryCondition.IsWeekDay::class.java -> "dayOfWeek" + QueryCondition.AnyMonthOfYear::class.java -> "month" + QueryCondition.AnyYear::class.java -> "year" + QueryCondition.PastDay::class.java, QueryCondition.IsToday::class.java, QueryCondition.WasYesterday::class.java, QueryCondition.WasTodayAndYesterday::class.java, QueryCondition.DuringThisYear::class.java, QueryCondition.DuringThisMonth::class.java, QueryCondition.DuringThisWeek::class.java -> "date" else -> null } } } + init { + this.updateTimeParameter(Date()) + } + @PrimaryKey override var id = UUID.randomUUID().toString() @@ -47,6 +67,10 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo // The date of the transaction override var date: Date = Date() + set(value) { + field = value + this.updateTimeParameter(field) + } // The type of the transaction var type: TransactionType? = null @@ -54,6 +78,12 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo // A user comment var comment: String = "" + // Timed interface + override var dayOfWeek: Int? = null + override var month: Int? = null + override var year: Int? = null + override var dayOfMonth: Int? = null + @Ignore override val viewType: Int = RowViewType.ROW_TRANSACTION.ordinal @@ -75,10 +105,6 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo return bankroll != null && type != null && amount != 0.0 } - override fun alreadyExists(realm: Realm): Boolean { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - override fun getFailedSaveMessage(status: SaveValidityStatus): Int { return if (bankroll == null) { R.string.no_br_popup_message @@ -89,12 +115,47 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo } } + override fun alreadyExists(realm: Realm): Boolean { + return realm.findById(id) != null + } + override fun isValidForDelete(realm: Realm): Boolean { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + return true + } + + override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int { + return R.string.relationship_error + } + + override fun getSaveValidityStatus(realm: Realm): SaveValidityStatus { + if (bankroll == null || type == null || amount == 0.0) { + return SaveValidityStatus.DATA_INVALID + } + return SaveValidityStatus.VALID + } + + // GraphUnderlyingEntry + + override fun entryTitle(context: Context): String { + return DateFormat.getDateInstance(DateFormat.SHORT).format(this.date) + } + + override fun formattedValue(stat: Stat): TextFormat { + return stat.format(this.amount) } - override fun getFailedDeleteMessage(): Int { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + override fun legendValues( + stat: Stat, + entry: Entry, + style: GraphFragment.Style, + groupName: String, + context: Context + ): LegendContent { + + val entryValue = this.formattedValue(stat) + val totalStatValue = stat.format(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 0a55ef46..6c038aa3 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,10 +1,14 @@ package net.pokeranalytics.android.model.realm +import android.content.Context import io.realm.Realm import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource +import net.pokeranalytics.android.ui.view.Localizable import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow @@ -15,10 +19,19 @@ import kotlin.collections.ArrayList open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable { - enum class Value(val additive: Boolean) { + enum class Value(val additive: Boolean) : Localizable { WITHDRAWAL(false), DEPOSIT(true), - BONUS(true) + BONUS(true); + + override val resId: Int? + get() { + return when (this) { + WITHDRAWAL -> R.string.withdrawal + DEPOSIT -> R.string.deposit + BONUS -> R.string.bonus + } + } } companion object { @@ -42,10 +55,15 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab @PrimaryKey override var id = UUID.randomUUID().toString() - // The name of the transaction type + /** + * The name of the transaction type + */ override var name: String = "" - // Whether or not the amount is added, or subtracted to the bankroll total + /** + * Whether or not the amount is added, or subtracted to the bankroll total + * The value can only be changed if no transaction is linked to this type + */ var additive: Boolean = false // Whether or not the type can be deleted by the user @@ -54,7 +72,7 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab // The predefined kind, if necessary, like: Withdrawal, deposit, or tips var kind: Int? = null - override fun getDisplayName(): String { + override fun getDisplayName(context: Context): String { return this.name } @@ -69,6 +87,13 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab } } + override fun boolForRow(row: RowRepresentable): Boolean { + return when (row) { + TransactionTypeRow.TRANSACTION_ADDITIVE -> this.additive + else -> super.boolForRow(row) + } + } + override fun editDescriptors(row: RowRepresentable): ArrayList? { return row.editingDescriptors(mapOf("defaultValue" to this.name)) } @@ -76,15 +101,18 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab override fun updateValue(value: Any?, row: RowRepresentable) { when (row) { SimpleRow.NAME -> this.name = value as String? ?: "" + TransactionTypeRow.TRANSACTION_ADDITIVE -> this.additive = value as Boolean? ?: false } } override fun isValidForDelete(realm: Realm): Boolean { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + return realm.where(Transaction::class.java) + .equalTo("type.id", this.id).findAll().isEmpty() } - override fun getFailedDeleteMessage(): Int { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int { + return R.string.transaction_relationship_error } + } diff --git a/app/src/main/java/net/pokeranalytics/android/model/retrofit/ConvertResult.kt b/app/src/main/java/net/pokeranalytics/android/model/retrofit/ConvertResult.kt index 1520c711..82d94c5d 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/retrofit/ConvertResult.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/retrofit/ConvertResult.kt @@ -3,7 +3,7 @@ package net.pokeranalytics.android.model.retrofit import com.google.gson.annotations.SerializedName /** - * Currency Converter mapping class + * CurrencyCode Converter mapping class */ class CurrencyConverterValue { @SerializedName("val") 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 f4b3d9bc..57a8e17b 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 @@ -11,7 +11,7 @@ import net.pokeranalytics.android.ui.view.rowrepresentable.SessionRow * Returns all significant parameters concatenated in a String * Not suitable for display */ -fun Session.parameterRepresentation(context: Context): String { +private fun Session.parameterRepresentation(context: Context): String { var representation = "" this.significantFields().forEach { @@ -56,9 +56,8 @@ class FavoriteSessionFinder { /** * A counter convenience class */ - class Counter(session: Session) { + class Counter(var session: Session) { - var session: Session = session var counter: Int = 1 fun increment() { @@ -77,7 +76,7 @@ class FavoriteSessionFinder { fun copyParametersFromFavoriteSession(newSession: Session, location: Location?, context: Context) { val favoriteSession = - FavoriteSessionFinder.favoriteSession(newSession.type, location, newSession.realm, context) + favoriteSession(newSession.type, location, newSession.realm, context) favoriteSession?.let { fav -> diff --git a/app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt b/app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt index a0d2d371..bb2875a8 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt @@ -3,6 +3,7 @@ package net.pokeranalytics.android.model.utils import android.content.Context import io.realm.Realm import io.realm.kotlin.where +import net.pokeranalytics.android.R import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.Currency import net.pokeranalytics.android.util.UserDefaults @@ -19,7 +20,7 @@ class Seed(var context:Context) : Realm.Transaction { } private fun createDefaultTournamentFeatures(realm: Realm) { - context.resources.getStringArray(net.pokeranalytics.android.R.array.seed_tournament_features).forEach { + context.resources.getStringArray(R.array.seed_tournament_features).forEach { val tournamentFeature = TournamentFeature() tournamentFeature.id = UUID.randomUUID().toString() tournamentFeature.name = it @@ -29,7 +30,7 @@ class Seed(var context:Context) : Realm.Transaction { private fun createDefaultCurrencyAndBankroll(realm: Realm) { - // Currency + // CurrencyCode val localeCurrency = UserDefaults.getLocaleCurrency() val defaultCurrency = Currency() defaultCurrency.code = localeCurrency.currencyCode @@ -37,15 +38,15 @@ class Seed(var context:Context) : Realm.Transaction { // Bankroll val bankroll = Bankroll() - bankroll.name = context.resources.getString(net.pokeranalytics.android.R.string.live) + bankroll.name = context.resources.getString(R.string.live) bankroll.live = true bankroll.currency = realm.where().equalTo("code", localeCurrency.currencyCode).findFirst() realm.insertOrUpdate(bankroll) } private fun createDefaultGames(realm: Realm) { - val gamesName = context.resources.getStringArray(net.pokeranalytics.android.R.array.seed_games) - val gamesShortName = context.resources.getStringArray(net.pokeranalytics.android.R.array.seed_games_short_name) + val gamesName = context.resources.getStringArray(R.array.seed_games) + val gamesShortName = context.resources.getStringArray(R.array.seed_games_short_name) for ((index, name) in gamesName.withIndex()) { val game = Game() game.id = UUID.randomUUID().toString() @@ -58,6 +59,7 @@ class Seed(var context:Context) : Realm.Transaction { private fun createDefaultTransactionTypes(realm: Realm) { TransactionType.Value.values().forEachIndexed { index, value -> val type = TransactionType() + type.name = value.localizedTitle(context) type.additive = value.additive type.kind = index type.lock = true diff --git a/app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt b/app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt index c62b84c3..76cce72c 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt @@ -7,9 +7,7 @@ import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.SessionSet import kotlin.math.max -class CorruptSessionSetException(message: String) : Exception(message) { - -} +class CorruptSessionSetException(message: String) : Exception(message) /** * The manager is in charge of updating the abstract concept of timeline, @@ -169,7 +167,7 @@ class SessionSetManager { sessionSet.deleteFromRealm() sessions.forEach { - SessionSetManager.updateTimeline(it) + updateTimeline(it) } } } diff --git a/app/src/main/java/net/pokeranalytics/android/model/utils/SessionUtils.kt b/app/src/main/java/net/pokeranalytics/android/model/utils/SessionUtils.kt new file mode 100644 index 00000000..d8d93f7e --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/utils/SessionUtils.kt @@ -0,0 +1,21 @@ +package net.pokeranalytics.android.model.utils + +import io.realm.Realm +import net.pokeranalytics.android.model.realm.Session +import java.util.* + +class SessionUtils { + + companion object { + + /** + * Returns true if the provided parameters doesn't correspond to an existing session + */ + fun unicityCheck(realm: Realm, startDate: Date, endDate: Date, net: Double) : Boolean { + val sessions = realm.where(Session::class.java).equalTo("startDate", startDate).equalTo("endDate", endDate).equalTo("result.net", net).findAll() + return sessions.isEmpty() + } + + } + +} diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollActivity.kt index e1b6ea05..0f37c4d8 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollActivity.kt @@ -4,11 +4,19 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.fragment.app.Fragment +import io.realm.RealmResults import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.realm.Bankroll +import net.pokeranalytics.android.model.realm.ComputableResult +import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity class BankrollActivity : PokerAnalyticsActivity() { + private lateinit var computableResults: RealmResults + private lateinit var bankrolls: RealmResults + private lateinit var transactions: RealmResults + companion object { fun newInstance(context: Context) { val intent = Intent(context, BankrollActivity::class.java) @@ -28,6 +36,55 @@ class BankrollActivity : PokerAnalyticsActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_bankroll) + +// this.computableResults = getRealm().where(ComputableResult::class.java).findAll() // ComputableResult are existing only if sessions are ended +// this.computableResults.addChangeListener { t, changeSet -> +// +// val bankrolls = mutableSetOf() +// val indexes = mutableSetOf() +// indexes.addAll(changeSet.changes.toList()) +// indexes.addAll(changeSet.insertions.toList()) +// indexes.addAll(changeSet.deletions.toList()) +// indexes.forEach { index -> +// t[index]?.session?.bankroll?.let { br -> +// bankrolls.add(br) +// } +// } +// this.computeBankrollReports(bankrolls) +// } +// this.bankrolls = getRealm().where(Bankroll::class.java).findAll() // ComputableResult are existing only if sessions are ended +// this.bankrolls.addChangeListener { _, changeSet -> +// +// +// +// +// +// } +// this.transactions = getRealm().where(Transaction::class.java).findAll() // ComputableResult are existing only if sessions are ended +// this.transactions.addChangeListener { t, changeSet -> +// +// val bankrolls = mutableSetOf() +// val indexes = mutableSetOf() +// indexes.addAll(changeSet.changes.toList()) +// indexes.addAll(changeSet.insertions.toList()) +// indexes.addAll(changeSet.deletions.toList()) +// indexes.forEach { index -> +// if (t.isNotEmpty()) { +// t[index]?.bankroll?.let { br -> +// bankrolls.add(br) +// } +// } +// } +// this.computeBankrollReports(bankrolls) +// } + + } + + fun computeBankrollReports(bankrolls: Collection) { + + + + } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollDetailsActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollDetailsActivity.kt new file mode 100644 index 00000000..131192b3 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollDetailsActivity.kt @@ -0,0 +1,48 @@ +package net.pokeranalytics.android.ui.activity + +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.Fragment +import net.pokeranalytics.android.R +import net.pokeranalytics.android.calculus.bankroll.BankrollReport +import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +import net.pokeranalytics.android.ui.fragment.BankrollDetailsFragment + +class BankrollDetailsActivity : PokerAnalyticsActivity() { + + companion object { + + private var bankrollReport: BankrollReport? = null + + /** + * Default constructor + */ + fun newInstanceForResult(fragment: Fragment, bankrollReport: BankrollReport, requestCode: Int) { + this.bankrollReport = bankrollReport + val intent = Intent(fragment.requireContext(), BankrollDetailsActivity::class.java) + fragment.startActivityForResult(intent, requestCode) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_bankroll_details) + initUI() + } + + /** + * Init UI + */ + private fun initUI() { + + bankrollReport?.let { + val fragmentTransaction = supportFragmentManager.beginTransaction() + val reportDetailsFragment = BankrollDetailsFragment.newInstance(it) + fragmentTransaction.add(R.id.container, reportDetailsFragment) + fragmentTransaction.commit() + + bankrollReport = null + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt new file mode 100644 index 00000000..71aafcd9 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt @@ -0,0 +1,31 @@ +package net.pokeranalytics.android.ui.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +import net.pokeranalytics.android.util.billing.AppGuard + +class BillingActivity : PokerAnalyticsActivity() { + + companion object { + fun newInstance(context: Context) { + val intent = Intent(context, BillingActivity::class.java) + context.startActivity(intent) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_billing) + } + + override fun onResume() { + super.onResume() + + } + +} + + diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/ComparisonReportActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/ComparisonReportActivity.kt new file mode 100644 index 00000000..8dda891b --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/ComparisonReportActivity.kt @@ -0,0 +1,36 @@ +package net.pokeranalytics.android.ui.activity + +import android.os.Bundle +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.activity.components.ReportActivity +import net.pokeranalytics.android.ui.fragment.report.ComparisonReportFragment + + +class ComparisonReportActivity : ReportActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_report_details) + initUI() + } + + /** + * Init UI + */ + private fun initUI() { + + parameters?.let { + + val report = it.report + val title = it.title + + val fragmentTransaction = supportFragmentManager.beginTransaction() + val reportDetailsFragment = ComparisonReportFragment.newInstance(report, title) + fragmentTransaction.add(R.id.reportDetailsContainer, reportDetailsFragment) + fragmentTransaction.commit() + } + parameters = null + + } + +} 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 index bc712d81..846a901b 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/DataListActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/DataListActivity.kt @@ -3,23 +3,37 @@ 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.PokerAnalyticsActivity import net.pokeranalytics.android.ui.fragment.DataListFragment +import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode class DataListActivity : PokerAnalyticsActivity() { enum class IntentKey(val keyName: String) { DATA_TYPE("DATA_TYPE"), - ITEM_DELETED("ITEM_DELETED") + 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) - context.startActivity(intent) + intent.putExtra(IntentKey.SHOW_ADD_BUTTON.keyName, showAddButton) + return intent } } @@ -34,18 +48,11 @@ class DataListActivity : PokerAnalyticsActivity() { * 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) - } - - /** - * Init data - */ - private fun initData() { - - + fragment.updateUI(showAddButton) } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt index fc9e305b..dacbc126 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt @@ -7,12 +7,10 @@ import androidx.fragment.app.Fragment import net.pokeranalytics.android.R import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity -import net.pokeranalytics.android.ui.fragment.BankrollDataFragment -import net.pokeranalytics.android.ui.fragment.EditableDataFragment -import net.pokeranalytics.android.ui.fragment.LocationDataFragment -import net.pokeranalytics.android.ui.fragment.TransactionDataFragment +import net.pokeranalytics.android.ui.fragment.data.* class EditableDataActivity : PokerAnalyticsActivity() { + enum class IntentKey(val keyName: String) { DATA_TYPE("DATA_TYPE"), PRIMARY_KEY("PRIMARY_KEY"); @@ -34,9 +32,9 @@ class EditableDataActivity : PokerAnalyticsActivity() { /** * Create a new instance for result */ - fun newInstanceForResult(fragment: Fragment, dataType: Int, primaryKey: String? = null, requestCode: Int) { + fun newInstanceForResult(fragment: Fragment, dataType: LiveData, primaryKey: String? = null, requestCode: Int) { val intent = Intent(fragment.requireContext(), 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) } @@ -65,6 +63,8 @@ class EditableDataActivity : PokerAnalyticsActivity() { LiveData.BANKROLL.ordinal -> BankrollDataFragment() LiveData.LOCATION.ordinal -> LocationDataFragment() LiveData.TRANSACTION.ordinal -> TransactionDataFragment() + LiveData.CUSTOM_FIELD.ordinal -> CustomFieldDataFragment() + LiveData.TRANSACTION_TYPE.ordinal -> TransactionTypeDataFragment() else -> EditableDataFragment() } 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 index cdedb52f..dcd63a61 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/FilterDetailsActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/FilterDetailsActivity.kt @@ -5,6 +5,7 @@ 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.PokerAnalyticsActivity import net.pokeranalytics.android.ui.fragment.FilterDetailsFragment @@ -16,6 +17,7 @@ class FilterDetailsActivity : PokerAnalyticsActivity() { } companion object { + /** * Default constructor */ @@ -29,7 +31,8 @@ class FilterDetailsActivity : PokerAnalyticsActivity() { /** * Create a new instance for result */ - fun newInstanceForResult(fragment: Fragment, filterId: String, filterCategoryOrdinal: Int, requestCode: Int) { + 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) @@ -63,7 +66,6 @@ class FilterDetailsActivity : PokerAnalyticsActivity() { 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 index afd9f49f..a78e3435 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/FiltersActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/FiltersActivity.kt @@ -7,33 +7,37 @@ import androidx.fragment.app.Fragment import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity import net.pokeranalytics.android.ui.fragment.FiltersFragment +import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode +import net.pokeranalytics.android.ui.interfaces.FilterableType -class FiltersActivity : PokerAnalyticsActivity() { +class FiltersActivity : PokerAnalyticsActivity() { enum class IntentKey(val keyName: String) { - FILTER_ID("FILTER_ID"); + FILTER_ID("FILTER_ID"), + FILTERABLE_TYPE("FILTERABLE_TYPE"), + HIDE_MOST_USED_FILTERS("HIDE_MOST_USED_FILTERS"), + ; } private lateinit var fragment: FiltersFragment companion object { /** - * Default constructor + * Create a new instance for result */ - fun newInstance(context: Context, filterId: String? = null) { + 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) } - context.startActivity(intent) - } - - /** - * Create a new instance for result - */ - fun newInstanceForResult(fragment: Fragment, requestCode: Int) { - val intent = Intent(fragment.requireContext(), FiltersActivity::class.java) - fragment.startActivityForResult(intent, requestCode) + return intent } } @@ -55,11 +59,15 @@ class FiltersActivity : PokerAnalyticsActivity() { 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() fragmentTransaction.add(R.id.container, fragment) fragmentTransaction.commit() - fragment.setData(filterId) + fragment.setData(filterId, filterableType) + fragment.updateMostUsedFiltersVisibility(!hideMostUsedFilters) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/GraphActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/GraphActivity.kt new file mode 100644 index 00000000..1eae0563 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/GraphActivity.kt @@ -0,0 +1,70 @@ +package net.pokeranalytics.android.ui.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.github.mikephil.charting.data.BarDataSet +import com.github.mikephil.charting.data.LineDataSet +import kotlinx.android.synthetic.main.activity_graph.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +import net.pokeranalytics.android.ui.fragment.GraphFragment + + +class GraphActivity : PokerAnalyticsActivity() { + + companion object { + + private var lineDataSets: List? = null + private var barDataSets: List? = null + private var style: GraphFragment.Style? = GraphFragment.Style.LINE + private var activityTitle: String? = null + + /** + * Default constructor + */ + fun newInstance( + context: Context, lineDataSets: List? = null, barDataSets: List? = null, + style: GraphFragment.Style = GraphFragment.Style.LINE, title: String? = null + ) { + this.lineDataSets = lineDataSets + this.barDataSets = barDataSets + this.style = style + this.activityTitle = title + val intent = Intent(context, GraphActivity::class.java) + context.startActivity(intent) + } + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_graph) + initUI() + } + + /** + * Init UI + */ + private fun initUI() { + + activityTitle?.let { + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + title = activityTitle + activityTitle = null + } + + style?.let { + val fragmentTransaction = supportFragmentManager.beginTransaction() + val graphFragment = GraphFragment.newInstance(lineDataSets, barDataSets, it) + fragmentTransaction.add(R.id.container, graphFragment) + fragmentTransaction.commit() + } + + lineDataSets = null + barDataSets = null + style = null + } + +} 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 f1a31005..59cc7c79 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 @@ -3,20 +3,21 @@ package net.pokeranalytics.android.ui.activity import android.app.KeyguardManager import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Build import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import androidx.appcompat.app.AlertDialog import com.google.android.material.bottomnavigation.BottomNavigationView import io.realm.RealmResults import kotlinx.android.synthetic.main.activity_home.* import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.R import net.pokeranalytics.android.model.realm.Currency -import net.pokeranalytics.android.ui.activity.FiltersActivity import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +import net.pokeranalytics.android.ui.activity.components.RequestCode +import net.pokeranalytics.android.ui.activity.components.ResultCode import net.pokeranalytics.android.ui.adapter.HomePagerAdapter +import net.pokeranalytics.android.ui.extensions.showAlertDialog +import net.pokeranalytics.android.util.billing.AppGuard import timber.log.Timber @@ -30,24 +31,11 @@ class HomeActivity : PokerAnalyticsActivity() { } } - private var homeMenu: Menu? = null - private lateinit var currencies: RealmResults + private var homePagerAdapter: HomePagerAdapter? = null private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item -> when (item.itemId) { - //CLEAN - /* - R.id.navigation_history -> { - displayFragment(0)§ - } - R.id.navigation_stats -> { - displayFragment(1) - } - R.id.navigation_settings -> { - displayFragment(2) - } - */ R.id.navigation_history -> { displayFragment(0) } @@ -70,6 +58,11 @@ class HomeActivity : PokerAnalyticsActivity() { return@OnNavigationItemSelectedListener true } + override fun onResume() { + super.onResume() + AppGuard.requestPurchasesUpdate() + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -87,22 +80,41 @@ class HomeActivity : PokerAnalyticsActivity() { observeRealmObjects() initUI() checkFirstLaunch() + } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menu?.clear() - menuInflater.inflate(R.menu.toolbar_home, menu) - this.homeMenu = menu - //TODO: Change queryWith button visibility - homeMenu?.findItem(R.id.filter)?.isVisible = false - return super.onCreateOptionsMenu(menu) + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + + intent?.let { + + when (intent.action) { + "android.intent.action.VIEW" -> { // import + val data = it.data + if (data != null) { + this.requestImportConfirmation(data) + } else { + throw IllegalStateException("URI null on import") + } + } + else -> { + Timber.d("Intent ${intent.action} unmanaged") + } + } + } + } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { - R.id.filter -> manageFilters() + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + when (requestCode) { + RequestCode.IMPORT.value -> { + if (resultCode == ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) { + showAlertDialog(context = this, message = R.string.unknown_import_format_popup_message) + } + } } - return super.onOptionsItemSelected(item) } private fun observeRealmObjects() { @@ -112,14 +124,12 @@ class HomeActivity : PokerAnalyticsActivity() { // observe currency changes this.currencies = realm.where(Currency::class.java).findAll() this.currencies.addChangeListener { t, _ -> - - realm.beginTransaction() - t.forEach { - it.refreshRelatedRatedValues() + realm.executeTransaction { + t.forEach { + it.refreshRelatedRatedValues() + } } - realm.commitTransaction() } - } /** @@ -127,17 +137,13 @@ class HomeActivity : PokerAnalyticsActivity() { */ private fun initUI() { - setSupportActionBar(toolbar) - navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener) navigation.selectedItemId = R.id.navigation_history - val homePagerAdapter = HomePagerAdapter(supportFragmentManager) + homePagerAdapter = HomePagerAdapter(supportFragmentManager) viewPager.offscreenPageLimit = 5 viewPager.enablePaging = false viewPager.adapter = homePagerAdapter - - updateToolbar(0) } /** @@ -159,83 +165,16 @@ class HomeActivity : PokerAnalyticsActivity() { */ private fun displayFragment(index: Int) { viewPager.setCurrentItem(index, false) - updateToolbar(index) - } - - /** - * Update toolbar - */ - private fun updateToolbar(index: Int) { - when (index) { - //CLEAN - /* - 0 -> { - toolbar.title = getString(R.string.feed) - homeMenu?.findItem(R.id.queryWith)?.isVisible = false - } - 1 -> { - toolbar.title = getString(R.string.stats) - homeMenu?.findItem(R.id.queryWith)?.isVisible = false - } - 2 -> { - toolbar.title = getString(R.string.services) - homeMenu?.findItem(R.id.queryWith)?.isVisible = false - } - */ - - 0 -> { - toolbar.title = getString(R.string.feed) - homeMenu?.findItem(R.id.filter)?.isVisible = false - } - 1 -> { - toolbar.title = getString(R.string.stats) - homeMenu?.findItem(R.id.filter)?.isVisible = false - } - 2 -> { - toolbar.title = getString(R.string.calendar) - homeMenu?.findItem(R.id.filter)?.isVisible = false - } - 3 -> { - toolbar.title = getString(R.string.reports) - homeMenu?.findItem(R.id.filter)?.isVisible = false - } - 4 -> { - toolbar.title = getString(R.string.services) //getString(R.string.more) - homeMenu?.findItem(R.id.filter)?.isVisible = false - } - } } - /** - * Manage filters - */ - private fun manageFilters() { - - val filterSelected = false - - val choices = ArrayList() - choices.add(getString(R.string.new_str)) + // Import - if (filterSelected) { - choices.add(getString(R.string.modify_current_filter)) - choices.add(getString(R.string.load_from_db)) - choices.add(getString(R.string.remove_filter)) - } + private fun requestImportConfirmation(uri: Uri) { - val builder = AlertDialog.Builder(this) - builder.setTitle(R.string.filter_selection) - .setCancelable(true) - .setItems(choices.toTypedArray()) { _, which -> - Timber.d("Click on $which") - when (which) { - 0 -> FiltersActivity.newInstance(this@HomeActivity) - } - } - .setNegativeButton(R.string.cancel) { _, _ -> - Timber.d("Click on cancel") - } + showAlertDialog(context = this, title = R.string.import_confirmation, showCancelButton = true, positiveAction = { + ImportActivity.newInstanceForResult(this, uri) + }) - builder.show() } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt new file mode 100644 index 00000000..fd9fef8c --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt @@ -0,0 +1,86 @@ +package net.pokeranalytics.android.ui.activity + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.fragment.app.FragmentActivity +import io.realm.Realm +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +import net.pokeranalytics.android.ui.activity.components.RequestCode +import net.pokeranalytics.android.ui.fragment.ImportFragment +import timber.log.Timber + +class ImportActivity : PokerAnalyticsActivity() { + + private lateinit var fileURI: Uri + + enum class IntentKey(val keyName: String) { + URI("uri") + } + + companion object { + + /** + * Create a new instance for result + */ + fun newInstanceForResult(context: FragmentActivity, uri: Uri) { + context.startActivityForResult(getIntent(context, uri), RequestCode.IMPORT.value) + } + + private fun getIntent(context: Context, uri: Uri): Intent { + val intent = Intent(context, ImportActivity::class.java) + intent.putExtra(ImportActivity.IntentKey.URI.keyName, uri) + return intent + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + this.fileURI = intent.getParcelableExtra(ImportActivity.IntentKey.URI.keyName) + + setContentView(R.layout.activity_import) + initUI() + + } + + override fun onStop() { + super.onStop() + + // Updates the main thread instance with newly inserted data + val realm = Realm.getDefaultInstance() + realm.refresh() + realm.close() + } + + private fun initUI() { + + val fragmentTransaction = supportFragmentManager.beginTransaction() + val fragment = ImportFragment() + + val fis = contentResolver.openInputStream(fileURI) + Timber.d("Load fragment data with: $fis") + fis?.let { + fragment.setData(it) + } + + fragmentTransaction.add(R.id.container, fragment) + fragmentTransaction.commit() + + } + +// private fun requestPermission() { +// if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { +// ActivityCompat.requestPermissions( +// this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSION_REQUEST_ACCESS_FINE_LOCATION +// ) +// } +// } +// +// override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { +// super.onRequestPermissionsResult(requestCode, permissions, grantResults) +// } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/NewDataMenuActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/NewDataMenuActivity.kt new file mode 100644 index 00000000..ac7570b2 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/NewDataMenuActivity.kt @@ -0,0 +1,127 @@ +package net.pokeranalytics.android.ui.activity + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.view.ViewAnimationUtils +import kotlinx.android.synthetic.main.activity_new_data.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +import net.pokeranalytics.android.ui.extensions.px + + +class NewDataMenuActivity : PokerAnalyticsActivity() { + + enum class IntentKey(val keyName: String) { + CHOICE("CHOICE"), + } + + companion object { + fun newInstance(context: Context) { + val intent = Intent(context, NewDataMenuActivity::class.java) + context.startActivity(intent) + } + } + + private val fabSize = 48.px + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(net.pokeranalytics.android.R.layout.activity_new_data) + initUI() + } + + override fun onBackPressed() { + hideMenu() + } + + override fun onPause() { + super.onPause() + overridePendingTransition(0, 0) + } + + /** + * Init UI + */ + private fun initUI() { + + overridePendingTransition(0, 0) + + container.viewTreeObserver.addOnGlobalLayoutListener { + showMenu() + } + + newCashGame.setOnClickListener { + finishWithResult(0) + } + + newTournament.setOnClickListener { + finishWithResult(1) + } + + newTransaction.setOnClickListener { + finishWithResult(2) + } + + container.setOnClickListener { + hideMenu() + } + } + + /** + * Set the result and hide menu + */ + private fun finishWithResult(choice: Int) { + val intent = Intent() + intent.putExtra(IntentKey.CHOICE.keyName, choice) + setResult(RESULT_OK, intent) + GlobalScope.launch(Dispatchers.Main) { + delay(200) + hideMenu(true) + } + } + + /** + * Show menu + */ + private fun showMenu() { + + val cx = menuContainer.measuredWidth - fabSize / 2 + val cy = menuContainer.measuredHeight - fabSize / 2 + val finalRadius = Math.max(menuContainer.width, menuContainer.height) + val anim = ViewAnimationUtils.createCircularReveal(menuContainer, cx, cy, 0f, finalRadius.toFloat()) + anim.duration = 150 + + menuContainer.visibility = View.VISIBLE + anim.start() + } + + /** + * Hide menu + */ + private fun hideMenu(hideQuickly: Boolean = false) { + + val cx = menuContainer.measuredWidth - fabSize / 2 + val cy = menuContainer.measuredHeight - fabSize / 2 + val initialRadius = menuContainer.width + val anim = ViewAnimationUtils.createCircularReveal(menuContainer, cx, cy, initialRadius.toFloat(), 0f) + anim.duration = 150 + + anim.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + super.onAnimationEnd(animation) + menuContainer.visibility = View.INVISIBLE + finish() + } + }) + + anim.start() + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/ProgressReportActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/ProgressReportActivity.kt new file mode 100644 index 00000000..c6ce7ee1 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/ProgressReportActivity.kt @@ -0,0 +1,63 @@ +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.calculus.Report +import net.pokeranalytics.android.calculus.Stat +import net.pokeranalytics.android.ui.activity.components.ReportActivity +import net.pokeranalytics.android.ui.activity.components.ReportParameters +import net.pokeranalytics.android.ui.activity.components.RequestCode +import net.pokeranalytics.android.ui.fragment.report.ProgressReportFragment + +class ProgressReportActivity : ReportActivity() { + + companion object { + + // Unparcel fails when setting a custom Parcelable object on Entry so we use a static reference to passe objects + + /** + * Default constructor + */ + fun newInstance(context: Context, report: Report, title: String, stat: Stat? = null, displayAggregationChoices: Boolean = true) { + parameters = ReportParameters(report, title, stat, showAggregationChoices = displayAggregationChoices) + val intent = Intent(context, ProgressReportActivity::class.java) + context.startActivity(intent) + } + + fun newInstanceForResult(fragment: Fragment, report: Report, title: String, stat: Stat? = null, displayAggregationChoices: Boolean = true) { + parameters = ReportParameters(report, title, stat, showAggregationChoices = displayAggregationChoices) + val intent = Intent(fragment.context, ProgressReportActivity::class.java) + fragment.startActivityForResult(intent, RequestCode.DEFAULT.value) + } + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_progress_report) + initUI() + } + + /** + * Init UI + */ + private fun initUI() { + + val fragmentTransaction = supportFragmentManager.beginTransaction() + val statisticDetailsFragment = ProgressReportFragment() + fragmentTransaction.add(R.id.statisticDetailsContainer, statisticDetailsFragment) + fragmentTransaction.commit() + + parameters?.let { + val report = it.report + val stat = it.stat ?: report.options.stats.first() + statisticDetailsFragment.setData(report, stat, it.showAggregationChoices, it.title) + parameters = null + } + + } + +} diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/ReportCreationActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/ReportCreationActivity.kt new file mode 100644 index 00000000..064fdf36 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/ReportCreationActivity.kt @@ -0,0 +1,29 @@ +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.calculus.Calculator +import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +import net.pokeranalytics.android.ui.activity.components.RequestCode + +class ReportCreationActivity : PokerAnalyticsActivity() { + + companion object { + + var options: Calculator.Options? = null + + fun newInstanceForResult(fragment: Fragment, context: Context) { + val intent = Intent(context, ReportCreationActivity::class.java) + fragment.startActivityForResult(intent, RequestCode.NEW_REPORT.value) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_report_creation) + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/ReportDetailsActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/ReportDetailsActivity.kt deleted file mode 100644 index be1b5dd1..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/ReportDetailsActivity.kt +++ /dev/null @@ -1,55 +0,0 @@ -package net.pokeranalytics.android.ui.activity - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import net.pokeranalytics.android.R -import net.pokeranalytics.android.calculus.Report -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity -import net.pokeranalytics.android.ui.fragment.ReportDetailsFragment - - - -class ReportDetailsActivity : PokerAnalyticsActivity() { - - companion object { - - // Unparcel fails when setting a custom Parcelable object on Entry so we use a static reference to passe objects - private var report: Report? = null - private var reportTitle: String = "" - - /** - * Default constructor - */ - fun newInstance(context: Context, report: Report, reportTitle: String) { - //parameters = GraphParameters(stat, group, report) - this.report = report - this.reportTitle = reportTitle - val intent = Intent(context, ReportDetailsActivity::class.java) - context.startActivity(intent) - } - - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_report_details) - initUI() - } - - /** - * Init UI - */ - private fun initUI() { - - report?.let { - val fragmentTransaction = supportFragmentManager.beginTransaction() - val reportDetailsFragment = ReportDetailsFragment.newInstance(it, reportTitle) - fragmentTransaction.add(R.id.reportDetailsContainer, reportDetailsFragment) - fragmentTransaction.commit() - - report = null - } - } - -} diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/SessionActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/SessionActivity.kt index bb2fe108..e082fb49 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/SessionActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/SessionActivity.kt @@ -1,8 +1,10 @@ package net.pokeranalytics.android.ui.activity +import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.fragment.app.Fragment import kotlinx.android.synthetic.main.activity_session.* import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity @@ -16,6 +18,7 @@ class SessionActivity: PokerAnalyticsActivity() { } companion object { + fun newInstance(context: Context, isTournament: Boolean? = false, sessionId: String? = "") { val intent = Intent(context, SessionActivity::class.java) isTournament?.let { @@ -27,6 +30,17 @@ class SessionActivity: PokerAnalyticsActivity() { context.startActivity(intent) } + fun newInstanceforResult(fragment: Fragment, isTournament: Boolean? = false, sessionId: String? = "", requestCode: Int) { + val intent = Intent(fragment.requireContext(), SessionActivity::class.java) + isTournament?.let { + intent.putExtra(IntentKey.IS_TOURNAMENT.keyName, isTournament) + } + sessionId?.let { + intent.putExtra(IntentKey.SESSION_ID.keyName, sessionId) + } + + fragment.startActivityForResult(intent, requestCode) + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -37,6 +51,7 @@ class SessionActivity: PokerAnalyticsActivity() { } override fun onBackPressed() { + setResult(Activity.RESULT_OK) super.onBackPressed() (sessionFragment as SessionFragment).onBackPressed() } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/StatisticDetailsActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/StatisticDetailsActivity.kt deleted file mode 100644 index ec737828..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/StatisticDetailsActivity.kt +++ /dev/null @@ -1,59 +0,0 @@ -package net.pokeranalytics.android.ui.activity - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import net.pokeranalytics.android.R -import net.pokeranalytics.android.calculus.ComputableGroup -import net.pokeranalytics.android.calculus.Report -import net.pokeranalytics.android.calculus.Stat -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity -import net.pokeranalytics.android.ui.fragment.StatisticDetailsFragment - - -class StatisticsDetailsParameters(var stat: Stat, var computableGroup: ComputableGroup, var report: Report, var title: String? = null) - -class StatisticDetailsActivity : PokerAnalyticsActivity() { - - companion object { - - // Unparcel fails when setting a custom Parcelable object on Entry so we use a static reference to passe objects - private var parameters: StatisticsDetailsParameters? = null - private var displayAggregationChoices: Boolean = true - - /** - * Default constructor - */ - fun newInstance(context: Context, stat: Stat, group: ComputableGroup, report: Report, displayAggregationChoices: Boolean = true, title: String? = null) { - parameters = StatisticsDetailsParameters(stat, group, report, title) - this.displayAggregationChoices = displayAggregationChoices - val intent = Intent(context, StatisticDetailsActivity::class.java) - context.startActivity(intent) - } - - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_statistic_details) - initUI() - } - - /** - * Init UI - */ - private fun initUI() { - - val fragmentTransaction = supportFragmentManager.beginTransaction() - val statisticDetailsFragment = StatisticDetailsFragment() - fragmentTransaction.add(R.id.statisticDetailsContainer, statisticDetailsFragment) - fragmentTransaction.commit() - - parameters?.let { - statisticDetailsFragment.setData(it.stat, it.computableGroup, it.report, displayAggregationChoices, it.title) - parameters = null - } - - } - -} diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/TableReportActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/TableReportActivity.kt new file mode 100644 index 00000000..96bc4ddd --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/TableReportActivity.kt @@ -0,0 +1,35 @@ +package net.pokeranalytics.android.ui.activity + +import android.os.Bundle +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.activity.components.ReportActivity +import net.pokeranalytics.android.ui.fragment.report.TableReportFragment + +class TableReportActivity : ReportActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_table_report) + initUI() + } + + /** + * Init UI + */ + private fun initUI() { + + parameters?.let { + + val report = it.report + val title = it.title + + val fragmentTransaction = supportFragmentManager.beginTransaction() + val fragment = TableReportFragment.newInstance(report, title) + fragmentTransaction.add(R.id.reportDetailsContainer, fragment) + fragmentTransaction.commit() + } + parameters = null + + } + +} \ No newline at end of file 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 new file mode 100644 index 00000000..ed11d47f --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt @@ -0,0 +1,13 @@ +package net.pokeranalytics.android.ui.activity.components + +enum class RequestCode(var value: Int) { + DEFAULT(1), + NEW_SESSION(800), + NEW_TRANSACTION(801), + NEW_REPORT(802), + IMPORT(900) +} + +enum class ResultCode(var value: Int) { + IMPORT_UNRECOGNIZED_FORMAT(901) +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/PokerAnalyticsActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/PokerAnalyticsActivity.kt index 38bd6081..77b324f0 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/PokerAnalyticsActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/PokerAnalyticsActivity.kt @@ -1,12 +1,14 @@ package net.pokeranalytics.android.ui.activity.components -import android.Manifest import android.Manifest.permission.ACCESS_FINE_LOCATION import android.content.pm.PackageManager +import android.os.Bundle +import android.os.PersistableBundle import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import com.crashlytics.android.Crashlytics import com.google.android.libraries.places.api.model.PlaceLikelihood import io.realm.Realm import net.pokeranalytics.android.model.realm.Location @@ -18,18 +20,43 @@ open class PokerAnalyticsActivity : AppCompatActivity() { companion object { const val PERMISSION_REQUEST_ACCESS_FINE_LOCATION = 1000 + const val PERMISSION_REQUEST_READ_EXTERNAL_STORAGE = 1001 const val PLAY_SERVICES_RESOLUTION_REQUEST = 2000 } private var realm: Realm? = null private var permissionCallback: ((granted: Boolean) -> Unit)? = null + + // Lifecycle + + override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { + super.onCreate(savedInstanceState, persistentState) + Crashlytics.log("$this.localClassName onCreate") + } + + override fun onResume() { + super.onResume() + Crashlytics.log("$this.localClassName onResume") + } + + override fun onPause() { + super.onPause() + Crashlytics.log("$this.localClassName onPause") + } + + override fun onDestroy() { + super.onDestroy() + Crashlytics.log("$this.localClassName onDestroy") + this.realm?.close() + } + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { PERMISSION_REQUEST_ACCESS_FINE_LOCATION -> { - if (permissions.isNotEmpty() && permissions[0] == Manifest.permission.ACCESS_FINE_LOCATION + if (permissions.isNotEmpty() && permissions[0] == ACCESS_FINE_LOCATION && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED ) { permissionCallback?.invoke(true) @@ -53,11 +80,6 @@ open class PokerAnalyticsActivity : AppCompatActivity() { return super.onOptionsItemSelected(item) } - override fun onDestroy() { - super.onDestroy() - this.realm?.close() - } - /** * Return the realm instance */ diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/ReportActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/ReportActivity.kt new file mode 100644 index 00000000..e3c8e51c --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/ReportActivity.kt @@ -0,0 +1,37 @@ +package net.pokeranalytics.android.ui.activity.components + +import android.content.Context +import android.content.Intent +import androidx.fragment.app.Fragment +import net.pokeranalytics.android.calculus.Report +import net.pokeranalytics.android.calculus.Stat + +class ReportParameters(var report: Report, var title: String, var stat: Stat? = null, var showAggregationChoices: Boolean = true) + +abstract class ReportActivity : PokerAnalyticsActivity() { + + companion object { + + // Unparcel fails when setting a custom Parcelable object on Entry so we use a static reference to passe objects + var parameters: ReportParameters? = null + + /** + * Default constructor + */ + fun newInstance(context: Context, report: Report, reportTitle: String, stat: Stat? = null) { + val options = report.options + this.parameters = ReportParameters(report, reportTitle, stat) + val intent = Intent(context, options.display.activityClass) + context.startActivity(intent) + } + + fun newInstanceForResult(fragment: Fragment, report: Report, reportTitle: String, stat: Stat? = null) { + val options = report.options + this.parameters = ReportParameters(report, reportTitle, stat) + val intent = Intent(fragment.requireContext(), options.display.activityClass) + fragment.startActivityForResult(intent, RequestCode.DEFAULT.value) + } + + } + +} \ No newline at end of file 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 1457bfb5..71bb2b84 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 @@ -8,7 +8,7 @@ import androidx.fragment.app.FragmentStatePagerAdapter import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.fragment.CalendarFragment import net.pokeranalytics.android.ui.fragment.GraphFragment -import net.pokeranalytics.android.ui.fragment.HistoryFragment +import net.pokeranalytics.android.ui.fragment.FeedFragment import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment import java.lang.ref.WeakReference @@ -24,7 +24,7 @@ class ComparisonChartPagerAdapter(val context: Context, fragmentManager: Fragmen 0 -> GraphFragment() 1 -> GraphFragment() 2 -> CalendarFragment.newInstance() - else -> HistoryFragment.newInstance() + else -> FeedFragment.newInstance() } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/adapter/HistorySessionRowRepresentableAdapter.kt b/app/src/main/java/net/pokeranalytics/android/ui/adapter/FeedSessionRowRepresentableAdapter.kt similarity index 90% rename from app/src/main/java/net/pokeranalytics/android/ui/adapter/HistorySessionRowRepresentableAdapter.kt rename to app/src/main/java/net/pokeranalytics/android/ui/adapter/FeedSessionRowRepresentableAdapter.kt index 6e602693..405167e7 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/adapter/HistorySessionRowRepresentableAdapter.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/adapter/FeedSessionRowRepresentableAdapter.kt @@ -7,7 +7,7 @@ import android.view.ViewGroup import androidx.appcompat.widget.AppCompatTextView import androidx.recyclerview.widget.RecyclerView import io.realm.RealmResults -import kotlinx.android.synthetic.main.row_history_session.view.* +import kotlinx.android.synthetic.main.row_feed_session.view.* import net.pokeranalytics.android.R import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.ui.view.BindableHolder @@ -24,7 +24,7 @@ import kotlin.collections.HashMap * @param dataSource the datasource providing rows * @param delegate the delegate, notified of UI actions */ -class HistorySessionRowRepresentableAdapter( +class FeedSessionRowRepresentableAdapter( var delegate: RowRepresentableDelegate? = null, var realmResults: RealmResults, var pendingRealmResults: RealmResults, @@ -43,7 +43,7 @@ class HistorySessionRowRepresentableAdapter( * Display a session view */ inner class RowSessionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { - fun bind(position: Int, row: Session?, adapter: HistorySessionRowRepresentableAdapter) { + fun bind(position: Int, row: Session?, adapter: FeedSessionRowRepresentableAdapter) { itemView.sessionRow.setData(row as Session) val listener = View.OnClickListener { @@ -57,7 +57,7 @@ class HistorySessionRowRepresentableAdapter( * Display a session view */ inner class HeaderTitleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { - fun bind(position: Int, title: String, adapter: HistorySessionRowRepresentableAdapter) { + fun bind(title: String) { // Title itemView.findViewById(R.id.title)?.let { it.text = title @@ -66,17 +66,16 @@ class HistorySessionRowRepresentableAdapter( } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - if (viewType == RowViewType.ROW_SESSION.ordinal) { - val layout = LayoutInflater.from(parent.context).inflate(R.layout.row_history_session, parent, false) - return RowSessionViewHolder(layout) + return if (viewType == RowViewType.ROW_SESSION.ordinal) { + val layout = LayoutInflater.from(parent.context).inflate(R.layout.row_feed_session, parent, false) + RowSessionViewHolder(layout) } else { val layout = LayoutInflater.from(parent.context).inflate(R.layout.row_header_title, parent, false) - return HeaderTitleViewHolder(layout) + HeaderTitleViewHolder(layout) } } - override fun getItemViewType(position: Int): Int { if (sortedHeaders.containsKey(position)) { return RowViewType.HEADER_TITLE.ordinal @@ -93,7 +92,7 @@ class HistorySessionRowRepresentableAdapter( if (holder is RowSessionViewHolder) { holder.bind(position, getSessionForPosition(position), this) } else if (holder is HeaderTitleViewHolder) { - holder.bind(position, getHeaderForPosition(holder.itemView.context, position), this) + holder.bind(getHeaderForPosition(holder.itemView.context, position)) } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/adapter/FeedTransactionRowRepresentableAdapter.kt b/app/src/main/java/net/pokeranalytics/android/ui/adapter/FeedTransactionRowRepresentableAdapter.kt new file mode 100644 index 00000000..ea262236 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/adapter/FeedTransactionRowRepresentableAdapter.kt @@ -0,0 +1,163 @@ +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.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.view.RowViewType +import net.pokeranalytics.android.util.NULL_TEXT +import net.pokeranalytics.android.util.extensions.getMonthAndYear +import java.util.* +import kotlin.collections.HashMap + + +/** + * An adapter capable of displaying a list of RowRepresentables + * @param dataSource the datasource providing rows + * @param delegate the delegate, notified of UI actions + */ +class FeedTransactionRowRepresentableAdapter( + var delegate: RowRepresentableDelegate? = null, + var realmTransactions: RealmResults, + var distinctTransactionsHeaders: RealmResults +) : + RecyclerView.Adapter() { + + private var headersPositions = HashMap() + private lateinit var sortedHeaders: SortedMap + + init { + refreshData() + } + + /** + * Display a transaction view + */ + inner class RowTransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { + fun bind(position: Int, row: Transaction?, adapter: FeedTransactionRowRepresentableAdapter) { + + 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 { + return if (viewType == RowViewType.ROW_TRANSACTION.ordinal) { + val layout = LayoutInflater.from(parent.context).inflate(R.layout.row_transaction, parent, false) + RowTransactionViewHolder(layout) + } else { + val layout = LayoutInflater.from(parent.context).inflate(R.layout.row_header_title, parent, false) + HeaderTitleViewHolder(layout) + } + + } + + + override fun getItemViewType(position: Int): Int { + if (sortedHeaders.containsKey(position)) { + return RowViewType.HEADER_TITLE.ordinal + } else { + return RowViewType.ROW_TRANSACTION.ordinal + } + } + + override fun getItemCount(): Int { + return realmTransactions.size + distinctTransactionsHeaders.size + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is RowTransactionViewHolder) { + holder.bind(position, getTransactionForPosition(position), this) + } else if (holder is 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 distinctTransactionsHeaders[realmHeaderPosition]?.date?.getMonthAndYear() ?: "" + } + return NULL_TEXT + } + + /** + * Get real index + */ + private fun getTransactionForPosition(position: Int): Transaction? { + + // Row position + var headersBefore = 0 + for (key in sortedHeaders.keys) { + if (position > key) { + headersBefore++ + } else { + break + } + } + + return realmTransactions[position - headersBefore] + } + + /** + * Refresh headers positions + */ + fun refreshData() { + + headersPositions.clear() + + val start = System.currentTimeMillis() + + 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 realmTransactions.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 d9f0c784..1c7aeb98 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 @@ -17,12 +17,12 @@ class HomePagerAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAda override fun getItem(position: Int): PokerAnalyticsFragment { return when (position) { - 0 -> HistoryFragment.newInstance() + 0 -> FeedFragment.newInstance() 1 -> StatisticsFragment.newInstance() 2 -> CalendarFragment.newInstance() 3 -> ReportsFragment.newInstance() - 4 -> SettingsFragment.newInstance() // MoreFragment.newInstance() - else -> HistoryFragment.newInstance() + 4 -> MoreFragment.newInstance() + else -> FeedFragment.newInstance() } } @@ -43,7 +43,7 @@ class HomePagerAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAda override fun getItemPosition(obj: Any): Int { return when (obj) { - HistoryFragment::class.java -> 0 + FeedFragment::class.java -> 0 StatisticsFragment::class.java -> 1 CalendarFragment::class.java -> 2 ReportsFragment::class.java -> 3 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 9b17e9bc..4d20ee9d 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 @@ -9,7 +9,7 @@ import androidx.viewpager.widget.PagerAdapter import net.pokeranalytics.android.R import net.pokeranalytics.android.calculus.Report import net.pokeranalytics.android.ui.fragment.GraphFragment -import net.pokeranalytics.android.ui.fragment.TableReportFragment +import net.pokeranalytics.android.ui.fragment.report.ComposableTableReportFragment import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment import java.lang.ref.WeakReference @@ -31,7 +31,7 @@ class ReportPagerAdapter(val context: Context, val fragmentManager: FragmentMana GraphFragment.newInstance(lineDataSets = dataSetList, style = GraphFragment.Style.MULTILINE) } 2 -> { - TableReportFragment.newInstance(report) + ComposableTableReportFragment.newInstance(report) } else -> PokerAnalyticsFragment() } 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 49b51960..ad37ff4f 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 @@ -10,6 +10,7 @@ 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) {} } /** @@ -47,16 +48,20 @@ class RowRepresentableAdapter( */ fun refreshRow(row: RowRepresentable) { - if (row.viewType == RowViewType.TITLE_SWITCH.ordinal) { + if (row.viewType == RowViewType.TITLE_SWITCH.ordinal || + row.viewType == RowViewType.LIST.ordinal) { // Avoid to refresh the view because it will refresh itself - // Caution if we want to update the title for example + // Caution if we want to update the titleResId for example return } - val index = this.dataSource.indexForRow(row) + val rows = this.dataSource.adapterRows() + val index = rows?.indexOf(row) ?: -1 if (index >= 0) { notifyItemChanged(index) } + + //notifyDataSetChanged() } /** 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 63dab8ef..26dec55a 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,9 +1,9 @@ package net.pokeranalytics.android.ui.adapter import android.content.Context -import net.pokeranalytics.android.calculus.TextFormat import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor +import net.pokeranalytics.android.util.TextFormat /** * Base Interface to provide the RowRepresentable to the adapter @@ -30,8 +30,6 @@ interface RowRepresentableDataSource: EditableDataSource, DisplayableDataSource, */ fun viewTypeForPosition(position:Int): Int - //TODO should be removed - fun indexForRow(row: RowRepresentable): Int } /** @@ -61,12 +59,6 @@ interface StaticRowRepresentableDataSource: RowRepresentableDataSource { throw IllegalStateException("Need to implement Data Source") } - override fun indexForRow(row: RowRepresentable): Int { - this.adapterRows()?.let { - return it.indexOf(row) - } - throw IllegalStateException("Need to implement Data Source") - } } @@ -85,11 +77,11 @@ interface LiveRowRepresentableDataSource: RowRepresentableDataSource { * Custom class providing the value to display and how to display them */ class DisplayDescriptor( - var boolValue: Boolean? = null, - var stringValue: String? = null, - var textFormat: TextFormat? = null, - var actionIcon: Int? = null, - var context: Context? = null) { + var boolValue: Boolean? = null, + var stringValue: String? = null, + var textFormat: TextFormat? = null, + var actionIcon: Int? = null, + var context: Context? = null) { } class UnmanagedRowRepresentableException(message: String) : Exception(message) { @@ -118,6 +110,13 @@ interface DisplayableDataSource { return false } + /** + * Returns a int for a specific row + */ + fun intForRow(row: RowRepresentable): Int { + return -1 + } + /** * Returns a localized string for a specific row */ @@ -140,12 +139,25 @@ interface DisplayableDataSource { } /** - * Returns an action icon identifier for a specific row + * Returns an action icon uniqueIdentifier for a specific row */ fun actionIconForRow(row: RowRepresentable): Int? { return 0 } + /** + * Returns whether the row, or its component, shoudl be enabled + */ + fun isEnabled(row: RowRepresentable): Boolean { + return true + } + + /** + * Returns whether the row, or its component, shoudl be enabled + */ + fun isSelectable(row: RowRepresentable): Boolean { + return true + } } /** 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 10647782..525741b6 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,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.res.Resources import android.net.Uri +import android.util.TypedValue import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -15,18 +16,15 @@ import androidx.core.content.FileProvider import androidx.core.view.isVisible import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.R -import net.pokeranalytics.android.calculus.TextFormat import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment 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.File - - - - // Sizes val Int.dp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt() @@ -69,8 +67,9 @@ fun PokerAnalyticsActivity.openPlayStorePage() { } // Open email for "Contact us" -fun PokerAnalyticsActivity.openContactMail(subjectStringRes: Int, filePath: String?= null) { - val info = "v${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}), Android ${android.os.Build.VERSION.SDK_INT}, ${DeviceUtils.getDeviceName()}" +fun PokerAnalyticsActivity.openContactMail(subjectStringRes: Int, filePath: String? = null) { + val info = + "v${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}) - ${AppGuard.isProUser}, Android ${android.os.Build.VERSION.SDK_INT}, ${DeviceUtils.getDeviceName()}" val emailIntent = Intent(Intent.ACTION_SEND) @@ -104,6 +103,7 @@ fun PokerAnalyticsActivity.openUrl(url: String) { fun PokerAnalyticsActivity.showAlertDialog(title: Int? = null, message: Int? = null) { showAlertDialog(this, title, message) } + fun PokerAnalyticsFragment.showAlertDialog(title: Int? = null, message: Int? = null) { context?.let { showAlertDialog(it, title, message) @@ -113,7 +113,10 @@ fun PokerAnalyticsFragment.showAlertDialog(title: Int? = null, message: Int? = n /** * Create and show an alert dialog */ -fun showAlertDialog(context: Context, title: Int? = null, message: Int? = null) { +fun showAlertDialog( + context: Context, title: Int? = null, message: Int? = null, cancelButtonTitle: Int? = null, showCancelButton: Boolean = false, + positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null +) { val builder = AlertDialog.Builder(context) title?.let { builder.setTitle(title) @@ -121,7 +124,19 @@ fun showAlertDialog(context: Context, title: Int? = null, message: Int? = null) message?.let { builder.setMessage(message) } - builder.setPositiveButton(net.pokeranalytics.android.R.string.ok, null) + builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ -> + positiveAction?.invoke() + } + + if (cancelButtonTitle != null) { + builder.setNegativeButton(cancelButtonTitle) { _, _ -> + negativeAction?.invoke() + } + } else if (showCancelButton) { + builder.setNegativeButton(R.string.cancel) { _, _ -> + negativeAction?.invoke() + } + } builder.show() } @@ -140,4 +155,9 @@ fun View.showWithAnimation() { isVisible = true animate().cancel() animate().alpha(1f).start() +} + +fun View.addCircleRipple() = with(TypedValue()) { + context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, this, true) + setBackgroundResource(resourceId) } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollDetailsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollDetailsFragment.kt new file mode 100644 index 00000000..ab2175f2 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollDetailsFragment.kt @@ -0,0 +1,167 @@ +package net.pokeranalytics.android.ui.fragment + +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.os.Bundle +import android.view.* +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.android.synthetic.main.fragment_bankroll.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.calculus.ComputedStat +import net.pokeranalytics.android.calculus.Stat +import net.pokeranalytics.android.calculus.bankroll.BankrollReport +import net.pokeranalytics.android.model.LiveData +import net.pokeranalytics.android.ui.activity.DataListActivity +import net.pokeranalytics.android.ui.activity.EditableDataActivity +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.components.PokerAnalyticsFragment +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowViewType +import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable + +class BankrollDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { + + companion object { + + const val REQUEST_CODE_EDIT = 1000 + + /** + * Create new instance + */ + fun newInstance(bankrollReport: BankrollReport): BankrollDetailsFragment { + val fragment = BankrollDetailsFragment() + fragment.bankrollReport = bankrollReport + return fragment + } + } + + private lateinit var bankrollAdapter: RowRepresentableAdapter + private lateinit var bankrollReport: BankrollReport + + private var bankrollDetailsMenu: Menu? = null + private var rows: ArrayList = ArrayList() + + // Life Cycle + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_bankroll_details, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUI() + initData() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE_EDIT && resultCode == RESULT_OK) { + if (data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName) != null) { + activity?.setResult(RESULT_OK, data) + activity?.finish() + } else { + updateMenuUI() + } + } + } + + override fun adapterRows(): List? { + return rows + } + + override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + + } + + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + menu?.clear() + inflater?.inflate(R.menu.toolbar_comparison_chart, menu) + this.bankrollDetailsMenu = menu + updateMenuUI() + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item!!.itemId) { + R.id.settings -> editBankroll() + } + return true + } + + // Business + + /** + * Init data + */ + private fun initData() { + + rows.clear() + + rows.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, resId = R.string.global)) + + val totalComputedStat = ComputedStat(Stat.NET_RESULT, bankrollReport.total) + val netComputedStat = ComputedStat(Stat.NET_RESULT, bankrollReport.netResult) + val netBankedComputedStat = ComputedStat(Stat.NET_RESULT, bankrollReport.netBanked) + + rows.add(CustomizableRowRepresentable(RowViewType.TITLE_VALUE, resId = R.string.bankroll, computedStat = totalComputedStat)) + rows.add(CustomizableRowRepresentable(RowViewType.TITLE_VALUE, resId = R.string.net_result, computedStat = netComputedStat)) + rows.add(CustomizableRowRepresentable(RowViewType.TITLE_VALUE, resId = R.string.net_banked, computedStat = netBankedComputedStat)) + + if (bankrollReport.transactionBuckets.isNotEmpty()) { + rows.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, resId = R.string.operations)) + bankrollReport.transactionBuckets.keys.forEach { key -> + bankrollReport.transactionBuckets[key]?.let { transactionBucket -> + val typeName = transactionBucket.transactions.firstOrNull()?.type?.getDisplayName(requireContext()) + val computedStat = ComputedStat(Stat.NET_RESULT, transactionBucket.total) + rows.add(CustomizableRowRepresentable(RowViewType.TITLE_VALUE, title = typeName, computedStat = computedStat)) + } + } + } + + } + + /** + * Init UI + */ + private fun initUI() { + + setDisplayHomeAsUpEnabled(true) + + updateMenuUI() + + bankrollAdapter = RowRepresentableAdapter(this, this) + + val viewManager = LinearLayoutManager(requireContext()) + + recyclerView.apply { + setHasFixedSize(true) + layoutManager = viewManager + adapter = bankrollAdapter + } + } + + /** + * Update menu UI + */ + private fun updateMenuUI() { + + if (bankrollReport.setup.virtualBankroll) { + setToolbarTitle(getString(R.string.total)) + bankrollDetailsMenu?.findItem(R.id.settings)?.isVisible = false + } else { + setToolbarTitle(bankrollReport.setup.bankroll?.name) + bankrollDetailsMenu?.findItem(R.id.settings)?.isVisible = true + } + + } + + /** + * Open Bankroll edit activity + */ + private fun editBankroll() { + EditableDataActivity.newInstanceForResult(this, LiveData.BANKROLL, bankrollReport.setup.bankroll?.id, REQUEST_CODE_EDIT) + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollFragment.kt index b0c06521..58e954a9 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollFragment.kt @@ -1,72 +1,228 @@ package net.pokeranalytics.android.ui.fragment +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager +import com.github.mikephil.charting.data.LineDataSet +import io.realm.RealmObject +import io.realm.RealmResults import kotlinx.android.synthetic.main.fragment_bankroll.* -import kotlinx.android.synthetic.main.fragment_stats.recyclerView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import net.pokeranalytics.android.R -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity -import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment - -class BankrollFragment : PokerAnalyticsFragment() { +import net.pokeranalytics.android.calculus.ComputedStat +import net.pokeranalytics.android.calculus.Stat +import net.pokeranalytics.android.calculus.bankroll.BankrollCalculator +import net.pokeranalytics.android.calculus.bankroll.BankrollReport +import net.pokeranalytics.android.calculus.bankroll.BankrollReportSetup +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.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.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.components.DeletableItemFragment +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.util.extensions.sorted +import timber.log.Timber +import java.util.* +import kotlin.collections.ArrayList + +class BankrollFragment : DeletableItemFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { + + companion object { + + const val REQUEST_CODE_DETAILS = 100 + const val REQUEST_CODE_CREATE = 101 + + /** + * Create new instance + */ + fun newInstance(): BankrollFragment { + val fragment = BankrollFragment() + val bundle = Bundle() + fragment.arguments = bundle + return fragment + } + } - companion object { + private var rows: ArrayList = ArrayList() + private var bankrollReportForRow: HashMap = HashMap() + private var lastItemClickedPosition: Int = 0 + private var lastItemClickedId: String = "" + private var deletedRow: RowRepresentable? = null - /** - * Create new instance - */ - fun newInstance(): BankrollFragment { - val fragment = BankrollFragment() - val bundle = Bundle() - fragment.arguments = bundle - return fragment - } - } + private lateinit var bankrolls: RealmResults - private lateinit var parentActivity: PokerAnalyticsActivity + override fun deletableItems() : List { + return this.bankrolls + } // Life Cycle - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_bankroll, container, false) - } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_bankroll, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUI() + initData() + } + + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE_DETAILS && resultCode == Activity.RESULT_OK) { + val itemToDeleteId = data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName) + itemToDeleteId?.let { id -> + GlobalScope.launch(Dispatchers.Main) { + delay(300) + deleteItem(dataListAdapter, bankrolls, id) + } + } + + } else if (requestCode == REQUEST_CODE_CREATE && resultCode == Activity.RESULT_OK) { + //TODO: Refresh bankrolls + initData() + } + + } + + override fun adapterRows(): List? { + return rows + } + + override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + when (row) { + is GraphRow -> { + val lineDataSet = row.dataSet as LineDataSet + GraphActivity.newInstance(requireContext(), listOf(lineDataSet), title = getString(R.string.bankroll)) + } + else -> { + if (bankrollReportForRow.containsKey(row)) { + bankrollReportForRow[row]?.let { bankrollReport -> + lastItemClickedPosition = position + lastItemClickedId = (row as? Identifiable)?.id ?: "" + BankrollDetailsActivity.newInstanceForResult(this, bankrollReport, REQUEST_CODE_DETAILS) + } + } + } + } + } + + // Business + + /** + * Init data + */ + private fun initData() { + + val realm = getRealm() + this.bankrolls = realm.sorted() + + rows.clear() + bankrollReportForRow.clear() + + GlobalScope.launch { + + launch(Dispatchers.Main) { + + // TODO: Improve that + // We are in the main thread... + + val startDate = Date() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initData() - initUI() - } + // Graph + val globalBankrollReportSetup = BankrollReportSetup() + val globalBankrollReport = BankrollCalculator.computeReport(getRealm(), globalBankrollReportSetup) + rows.add(0, GraphRow(dataSet = globalBankrollReport.lineDataSet(requireContext()))) + rows.add(globalBankrollReport) + bankrollReportForRow[globalBankrollReport] = globalBankrollReport + // Bankrolls + rows.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, resId = R.string.bankrolls)) - // Business + Timber.d("initData: ${System.currentTimeMillis() - startDate.time}ms") - /** - * Init data - */ - private fun initData() { +// val bankrolls = LiveData.Bankroll.items(getRealm()) as RealmResults - } + bankrolls.forEach { bankroll -> + val bankrollReportSetup = BankrollReportSetup(bankroll) + val bankrollReport = BankrollCalculator.computeReport(getRealm(), bankrollReportSetup) + val computedStat = ComputedStat(Stat.NET_RESULT, bankrollReport.total) + val row = + CustomizableRowRepresentable(RowViewType.TITLE_VALUE_ARROW, title = bankroll.name, computedStat = computedStat, isSelectable = true) + row.id = bankroll.id + + rows.add(row) + bankrollReportForRow[row] = bankrollReport + } + + if (!isDetached) { + dataListAdapter.notifyDataSetChanged() + } + } + } + + } /** * Init UI */ private fun initUI() { - parentActivity = activity as PokerAnalyticsActivity + setDisplayHomeAsUpEnabled(true) - parentActivity.setSupportActionBar(toolbar) - parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true) - setHasOptionsMenu(true) + dataListAdapter = RowRepresentableAdapter(this, this) val viewManager = LinearLayoutManager(requireContext()) recyclerView.apply { setHasFixedSize(true) layoutManager = viewManager - //adapter = statsAdapter + adapter = dataListAdapter + } + + addButton.setOnClickListener { + EditableDataActivity.newInstanceForResult(this@BankrollFragment, dataType = LiveData.BANKROLL, primaryKey = null, requestCode = REQUEST_CODE_CREATE) + } + } + + override fun updateUIAfterDeletion(itemPosition: Int) { + lastItemClickedPosition = rows.indexOfFirst { if (it is Identifiable) it.id == lastItemClickedId else false } + deletedRow = rows.find { if (it is Identifiable) it.id == lastItemClickedId else false } + rows.removeAt(lastItemClickedPosition) + dataListAdapter.notifyItemRemoved(lastItemClickedPosition) + } + + override fun updateUIAfterUndoDeletion(newItem: RealmObject) { + + // TODO: Improve that + // We are recreating a Bankroll report because the last one is invalid => the bankroll of the setup has been deleted + + deletedRow?.let { row -> + val bankrollReportSetup = BankrollReportSetup(newItem as Bankroll) + val bankrollReport = BankrollCalculator.computeReport(getRealm(), bankrollReportSetup) + bankrollReportForRow[row] = bankrollReport + + rows.add(lastItemClickedPosition, row) + dataListAdapter.notifyItemInserted(lastItemClickedPosition) } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarDetailsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarDetailsFragment.kt index 7a7bd784..a58d034b 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarDetailsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarDetailsFragment.kt @@ -20,8 +20,7 @@ import net.pokeranalytics.android.calculus.ComputedResults import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.QueryCondition -import net.pokeranalytics.android.ui.activity.StatisticDetailsActivity -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +import net.pokeranalytics.android.ui.activity.ProgressReportActivity import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource @@ -44,7 +43,6 @@ class CalendarDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresentable } } - private lateinit var parentActivity: PokerAnalyticsActivity private lateinit var statsAdapter: RowRepresentableAdapter private var title: String? = "" @@ -80,14 +78,7 @@ class CalendarDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresentable */ private fun initUI() { - parentActivity = activity as PokerAnalyticsActivity - - // Avoid a bug during setting the title - toolbar.title = "" - - parentActivity.setSupportActionBar(toolbar) - parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true) - setHasOptionsMenu(true) + setDisplayHomeAsUpEnabled(true) var tabIndexToSelect = 0 sessionTypeCondition?.let { @@ -134,11 +125,7 @@ class CalendarDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresentable * Display data */ private fun displayData() { - - title?.let { - toolbar.title = it - } - + setToolbarTitle(title) } // StaticRowRepresentableDataSource @@ -150,8 +137,12 @@ class CalendarDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresentable override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { when (row) { is GraphRow -> { - row.report.results.firstOrNull()?.group?.let { computableGroup -> - StatisticDetailsActivity.newInstance(requireContext(), row.stat, computableGroup, row.report, false, row.title) + val report = row.report + val stat = row.stat + + if (report != null && stat != null) { + val title = row.title ?: stat.localizedTitle(requireContext()) + ProgressReportActivity.newInstance(requireContext(), report, title, stat, false) } } } @@ -196,25 +187,39 @@ class CalendarDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresentable // } val requiredStats: List = listOf(Stat.LOCATIONS_PLAYED, Stat.LONGEST_STREAKS, Stat.DAYS_PLAYED, Stat.STANDARD_DEVIATION_HOURLY) - val options = Calculator.Options(evolutionValues = Calculator.Options.EvolutionValues.STANDARD, stats = requiredStats) - val report = Calculator.computeStatsWithCriterias(realm, listOf(), query, options) + val options = Calculator.Options( + progressValues = Calculator.Options.ProgressValues.STANDARD, + stats = requiredStats, + query = query + ) + val report = Calculator.computeStats(realm, options) Timber.d("Report take: ${System.currentTimeMillis() - startDate.time}ms") report.results.firstOrNull()?.let { // Create rows + val dataSet1 = report.results.firstOrNull()?.defaultStatEntries(Stat.NET_RESULT, requireContext()) + val dataSet2 = report.results.firstOrNull()?.defaultStatEntries(Stat.STANDARD_DEVIATION, requireContext()) + val dataSet3 = report.results.firstOrNull()?.defaultStatEntries(Stat.HOURLY_DURATION, requireContext()) rowRepresentables.clear() rowRepresentables.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, resId = R.string.net_result)) - rowRepresentables.add(GraphRow(report, Stat.NET_RESULT)) + rowRepresentables.add(GraphRow(dataSet1, report = report, stat = Stat.NET_RESULT)) rowRepresentables.add(StatDoubleRow(it.computedStat(Stat.NET_RESULT), it.computedStat(Stat.HOURLY_RATE))) rowRepresentables.add(StatDoubleRow(it.computedStat(Stat.LOCATIONS_PLAYED), it.computedStat(Stat.LONGEST_STREAKS))) rowRepresentables.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, resId = R.string.distribution)) - rowRepresentables.add(GraphRow(report, Stat.STANDARD_DEVIATION, requireContext().getString(R.string.distribution))) + rowRepresentables.add( + GraphRow( + dataSet2, + requireContext().getString(R.string.distribution), + report = report, + stat = Stat.STANDARD_DEVIATION + ) + ) rowRepresentables.add(StatDoubleRow(it.computedStat(Stat.WIN_RATIO), it.computedStat(Stat.MAXIMUM_NETRESULT))) rowRepresentables.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, resId = R.string.volume)) - rowRepresentables.add(GraphRow(report, Stat.HOURLY_DURATION)) + rowRepresentables.add(GraphRow(dataSet3, report = report, stat = Stat.HOURLY_DURATION)) rowRepresentables.add(StatDoubleRow(it.computedStat(Stat.HOURLY_DURATION), it.computedStat(Stat.AVERAGE_HOURLY_DURATION))) rowRepresentables.add(StatDoubleRow(it.computedStat(Stat.DAYS_PLAYED), it.computedStat(Stat.MAXIMUM_DURATION))) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarFragment.kt index d04eada1..e74e97bb 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarFragment.kt @@ -7,25 +7,27 @@ import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.tabs.TabLayout import io.realm.Realm +import io.realm.RealmModel import kotlinx.android.synthetic.main.fragment_calendar.* -import kotlinx.android.synthetic.main.fragment_stats.recyclerView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import net.pokeranalytics.android.R import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.ComputedResults import net.pokeranalytics.android.calculus.Stat 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 import net.pokeranalytics.android.ui.extensions.hideWithAnimation import net.pokeranalytics.android.ui.extensions.showWithAnimation -import net.pokeranalytics.android.ui.fragment.components.SessionObserverFragment +import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.view.CalendarTabs import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType @@ -36,7 +38,8 @@ import java.util.* import kotlin.coroutines.CoroutineContext -class CalendarFragment : SessionObserverFragment(), CoroutineScope, StaticRowRepresentableDataSource, RowRepresentableDelegate { +class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentableDataSource, + RowRepresentableDelegate { enum class TimeFilter { MONTH, YEAR @@ -73,7 +76,8 @@ class CalendarFragment : SessionObserverFragment(), CoroutineScope, StaticRowRep // Life Cycle override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(net.pokeranalytics.android.R.layout.fragment_calendar, container, false) + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_calendar, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -94,7 +98,12 @@ class CalendarFragment : SessionObserverFragment(), CoroutineScope, StaticRowRep TimeFilter.MONTH -> { val date = datesForRows[row] sortedMonthlyReports[datesForRows[row]]?.let { - CalendarDetailsActivity.newInstance(requireContext(), it, sessionTypeCondition, date?.getMonthAndYear()) + CalendarDetailsActivity.newInstance( + requireContext(), + it, + sessionTypeCondition, + date?.getMonthAndYear() + ) } } TimeFilter.YEAR -> { @@ -106,7 +115,9 @@ class CalendarFragment : SessionObserverFragment(), CoroutineScope, StaticRowRep } } - override fun sessionsChanged() { + override val observedEntities: List> = listOf(ComputableResult::class.java) + + override fun entitiesChanged(clazz: Class) { launchStatComputation() } @@ -235,8 +246,8 @@ class CalendarFragment : SessionObserverFragment(), CoroutineScope, StaticRowRep val monthlyReports: HashMap = HashMap() val yearlyReports: HashMap = HashMap() - val requiredStats: List = listOf(Stat.LOCATIONS_PLAYED, Stat.LONGEST_STREAKS, Stat.DAYS_PLAYED, Stat.STANDARD_DEVIATION_HOURLY ) - val options = Calculator.Options(evolutionValues = Calculator.Options.EvolutionValues.STANDARD, stats = requiredStats) + val requiredStats: List = + listOf(Stat.LOCATIONS_PLAYED, Stat.LONGEST_STREAKS, Stat.DAYS_PLAYED, Stat.STANDARD_DEVIATION_HOURLY) // Compute data per AnyYear and AnyMonthOfYear @@ -249,15 +260,23 @@ class CalendarFragment : SessionObserverFragment(), CoroutineScope, StaticRowRep } monthlyQueries.forEach { query -> - val report = Calculator.computeStatsWithCriterias(realm, query = query, options = options) + val options = Calculator.Options( + progressValues = Calculator.Options.ProgressValues.STANDARD, + stats = requiredStats, + query = query + ) + val report = Calculator.computeStats(realm, options = options) report.results.forEach { computedResults -> if (!computedResults.isEmpty) { // Set date data query.conditions.forEach { condition -> - when (condition) { - is QueryCondition.AnyYear -> calendar.set(Calendar.YEAR, condition.listOfValues.first()) - is QueryCondition.AnyMonthOfYear -> calendar.set(Calendar.MONTH, condition.listOfValues.first()) - } + when (condition) { + is QueryCondition.AnyYear -> calendar.set(Calendar.YEAR, condition.listOfValues.first()) + is QueryCondition.AnyMonthOfYear -> calendar.set( + Calendar.MONTH, + condition.listOfValues.first() + ) + } } monthlyReports[calendar.time] = computedResults @@ -275,14 +294,19 @@ class CalendarFragment : SessionObserverFragment(), CoroutineScope, StaticRowRep } yearConditions.forEach { query -> - val report = Calculator.computeStatsWithCriterias(realm, query = query, options = options) + val options = Calculator.Options( + progressValues = Calculator.Options.ProgressValues.STANDARD, + stats = requiredStats, + query = query + ) + val report = Calculator.computeStats(realm, options = options) report.results.forEach { computedResults -> if (!computedResults.isEmpty) { // Set date data query.conditions.forEach { condition -> - when (condition) { - is QueryCondition.AnyYear -> calendar.set(Calendar.YEAR, condition.listOfValues.first()) - } + when (condition) { + is QueryCondition.AnyYear -> calendar.set(Calendar.YEAR, condition.listOfValues.first()) + } } yearlyReports[calendar.time] = computedResults } 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 5ae08654..f044678b 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 @@ -6,7 +6,6 @@ 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.activity.SettingsActivity -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity import net.pokeranalytics.android.ui.adapter.ComparisonChartPagerAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource @@ -37,7 +36,6 @@ class ComparisonChartFragment : PokerAnalyticsFragment(), StaticRowRepresentable } - private lateinit var parentActivity: PokerAnalyticsActivity private lateinit var viewPagerAdapter: ComparisonChartPagerAdapter private var comparisonChartMenu: Menu? = null @@ -94,21 +92,15 @@ class ComparisonChartFragment : PokerAnalyticsFragment(), StaticRowRepresentable */ private fun initUI() { - parentActivity = activity as PokerAnalyticsActivity + setDisplayHomeAsUpEnabled(true) + setToolbarTitle(getString(R.string.comparison_chart)) - toolbar.title = "" - - parentActivity.setSupportActionBar(toolbar) - parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true) - setHasOptionsMenu(true) - - toolbar.title = "Comparison chart" - - viewPagerAdapter = ComparisonChartPagerAdapter(requireContext(), parentActivity.supportFragmentManager) - viewPager.adapter = viewPagerAdapter - viewPager.offscreenPageLimit = 2 - - tabs.setupWithViewPager(viewPager) + parentActivity?.let { + viewPagerAdapter = ComparisonChartPagerAdapter(requireContext(), it.supportFragmentManager) + viewPager.adapter = viewPagerAdapter + viewPager.offscreenPageLimit = 2 + tabs.setupWithViewPager(viewPager) + } } 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 76d552e3..2fa0c3a7 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 @@ -1,6 +1,7 @@ package net.pokeranalytics.android.ui.fragment import android.app.Activity +import android.content.Context import android.content.Intent import android.os.Bundle import android.view.LayoutInflater @@ -9,7 +10,6 @@ import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.fragment_data_list.* import net.pokeranalytics.android.R -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource @@ -21,114 +21,108 @@ import java.util.* class CurrenciesFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { - companion object { - - const val INTENT_CURRENCY_CODE = "INTENT_CURRENCY_CODE" - - val rowRepresentation : List by lazy { - val rows = ArrayList() - rows.addAll(mostUsedCurrencies) - rows.add(SeparatorRow()) - rows.addAll(availableCurrencies) - rows - } - - private val mostUsedCurrencyCodes = arrayListOf("EUR", "USD", "CAD", "GBP", "AUD", "CNY") - private val systemCurrencies = Currency.getAvailableCurrencies() - - private val mostUsedCurrencies = this.mostUsedCurrencyCodes.map { code -> - CurrencyRow( - this.systemCurrencies.filter { - it.currencyCode == code - }.first() - ) - } - - private val availableCurrencies = this.systemCurrencies.filter { - !mostUsedCurrencyCodes.contains(it.currencyCode) - }.filter { - Locale.getAvailableLocales().filter {locale -> - try { - Currency.getInstance(locale).currencyCode == it.currencyCode - } catch (e: Exception) { - false - } - }.isNotEmpty() - }.sortedBy { - it.displayName - }.map { - CurrencyRow(it) - } - } - - private class CurrencyRow(var currency:Currency) : RowRepresentable { - - override fun getDisplayName(): String { - return currency.getDisplayName(Locale.getDefault()).capitalize() - } - - var currencyCode: String = currency.currencyCode - var currencySymbole: String = currency.getSymbol(Locale.getDefault()) - var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbole})" - - override val viewType: Int = RowViewType.TITLE_VALUE.ordinal - } - - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_currencies, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initData() - initUI() - } - - - // StaticRowRepresentableDataSource - - override fun adapterRows(): List? { - return CurrenciesFragment.rowRepresentation - - } - - override fun stringForRow(row: RowRepresentable): String { - return (row as CurrencyRow).currencyCodeAndSymbol - } - - // RowRepresentableDelegate - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - val intent = Intent() - intent.putExtra(INTENT_CURRENCY_CODE, (row as CurrencyRow).currency.currencyCode) - this.activity?.setResult(Activity.RESULT_OK, intent) - this.activity?.finish() - } - - private fun initData() { - } - - /** - * Init UI - */ - private fun initUI() { - - val activity = activity as PokerAnalyticsActivity - - // Avoid a bug during setting the title - toolbar.title = this.getString( R.string.currency) - - activity.setSupportActionBar(toolbar) - activity.supportActionBar?.setDisplayHomeAsUpEnabled(true) - setHasOptionsMenu(true) - - val viewManager = LinearLayoutManager(requireContext()) - val dataListAdapter = RowRepresentableAdapter(this, this) - - recyclerView.apply { - setHasFixedSize(true) - layoutManager = viewManager - adapter = dataListAdapter - } - } + companion object { + + const val INTENT_CURRENCY_CODE = "INTENT_CURRENCY_CODE" + + val rowRepresentation: List by lazy { + val rows = ArrayList() + rows.addAll(mostUsedCurrencies) + rows.add(SeparatorRow()) + rows.addAll(availableCurrencies) + rows + } + + private val mostUsedCurrencyCodes = arrayListOf("EUR", "USD", "CAD", "GBP", "AUD", "CNY") + private val systemCurrencies = Currency.getAvailableCurrencies() + + private val mostUsedCurrencies = this.mostUsedCurrencyCodes.map { code -> + CurrencyRow( + this.systemCurrencies.filter { + it.currencyCode == code + }.first() + ) + } + + private val availableCurrencies = this.systemCurrencies.filter { + !mostUsedCurrencyCodes.contains(it.currencyCode) + }.filter { + Locale.getAvailableLocales().filter { locale -> + try { + Currency.getInstance(locale).currencyCode == it.currencyCode + } catch (e: Exception) { + false + } + }.isNotEmpty() + }.sortedBy { + it.displayName + }.map { + CurrencyRow(it) + } + } + + private class CurrencyRow(var currency: Currency) : RowRepresentable { + + override fun getDisplayName(context: Context): String { + return currency.getDisplayName(Locale.getDefault()).capitalize() + } + + var currencyCode: String = currency.currencyCode + var currencySymbole: String = currency.getSymbol(Locale.getDefault()) + var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbole})" + + override val viewType: Int = RowViewType.TITLE_VALUE.ordinal + } + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_currencies, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initData() + initUI() + } + + + // StaticRowRepresentableDataSource + + override fun adapterRows(): List? { + return CurrenciesFragment.rowRepresentation + + } + + override fun stringForRow(row: RowRepresentable): String { + return (row as CurrencyRow).currencyCodeAndSymbol + } + + // RowRepresentableDelegate + override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + val intent = Intent() + intent.putExtra(INTENT_CURRENCY_CODE, (row as CurrencyRow).currency.currencyCode) + this.activity?.setResult(Activity.RESULT_OK, intent) + this.activity?.finish() + } + + private fun initData() { + } + + /** + * Init UI + */ + private fun initUI() { + + setDisplayHomeAsUpEnabled(true) + setToolbarTitle(getString(R.string.currency)) + + val viewManager = LinearLayoutManager(requireContext()) + val dataListAdapter = RowRepresentableAdapter(this, this) + + recyclerView.apply { + setHasFixedSize(true) + layoutManager = viewManager + adapter = dataListAdapter + } + } } \ No newline at end of file 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 index 0e143ef1..68ee1388 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/DataListFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/DataListFragment.kt @@ -6,109 +6,70 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast -import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.snackbar.Snackbar -import io.realm.RealmObject +import io.realm.Realm import io.realm.RealmResults import io.realm.kotlin.isValid import kotlinx.android.synthetic.main.fragment_data_list.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.Identifiable -import net.pokeranalytics.android.ui.activity.DataListActivity +import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.ui.activity.EditableDataActivity -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +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.fragment.components.PokerAnalyticsFragment +import net.pokeranalytics.android.ui.fragment.components.DeletableItemFragment import net.pokeranalytics.android.ui.helpers.SwipeToDeleteCallback +import net.pokeranalytics.android.ui.interfaces.FilterableType import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType -import net.pokeranalytics.android.ui.view.rowrepresentable.SettingRow +import net.pokeranalytics.android.util.extensions.sorted -class DataListFragment : PokerAnalyticsFragment(), LiveRowRepresentableDataSource, RowRepresentableDelegate { +open class DataListFragment : DeletableItemFragment(), LiveRowRepresentableDataSource, RowRepresentableDelegate { companion object { const val REQUEST_CODE_DETAILS = 1000 } - private lateinit var dataType: SettingRow - private lateinit var items: RealmResults<*> - private lateinit var dataListAdapter: RowRepresentableAdapter + private lateinit var identifiableClass: Class - private var deletedItem: RealmObject? = null - private var lastDeletedItemPosition: Int = 0 - private var lastItemClickedPosition: Int = 0 + private lateinit var dataType: LiveData + private lateinit var items: RealmResults - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_data_list, container, false) - } + /** + * Set fragment data + */ + fun setData(dataType: Int) { - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initUI() - } + this.dataType = LiveData.values()[dataType] + this.identifiableClass = this.dataType.relatedEntity + setToolbarTitle(this.dataType.pluralLocalizedTitle(requireContext())) - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == REQUEST_CODE_DETAILS && resultCode == Activity.RESULT_OK) { - val needToDeleteItem = data?.getBooleanExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName, false) ?: false - if (needToDeleteItem) { - GlobalScope.launch(Dispatchers.Main) { - delay(300) - deleteItem(lastItemClickedPosition) - } - } - } + this.items = this.retrieveItems(getRealm()) } - override fun onResume() { - super.onResume() - this.recyclerView?.adapter?.notifyDataSetChanged() + open fun retrieveItems(realm: Realm): RealmResults { + return realm.sorted(this.identifiableClass, editableOnly = true, filterableTypeUniqueIdentifier = dataType.subType) } - override fun rowRepresentableForPosition(position: Int): RowRepresentable? { - return this.items[position] as RowRepresentable + override fun deletableItems() : List { + return this.items } - 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 indexForRow(row: RowRepresentable): Int { - return this.items.indexOf(row) + 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 onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - - if (row is Identifiable && !row.isValid()) { - Toast.makeText(requireContext(), R.string.invalid_object, Toast.LENGTH_LONG) - return - } - - this.dataType.relatedResultsRepresentable?.let { - lastItemClickedPosition = position - EditableDataActivity.newInstanceForResult( - this, - it.ordinal, - (row as Identifiable).id, - REQUEST_CODE_DETAILS - ) - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUI() } /** @@ -116,20 +77,19 @@ class DataListFragment : PokerAnalyticsFragment(), LiveRowRepresentableDataSourc */ private fun initUI() { - val activity = activity as PokerAnalyticsActivity - - // Avoid a bug during setting the title - toolbar.title = "" - - activity.setSupportActionBar(toolbar) - activity.supportActionBar?.setDisplayHomeAsUpEnabled(true) - setHasOptionsMenu(true) + setDisplayHomeAsUpEnabled(true) val viewManager = LinearLayoutManager(requireContext()) dataListAdapter = RowRepresentableAdapter(this, this) val swipeToDelete = SwipeToDeleteCallback(dataListAdapter) { position -> - deleteItem(position) + val item = this.items[position] + if (item != null) { + val itemId = item.id + deleteItem(dataListAdapter, items, itemId) + } else { + throw IllegalStateException("Item with position $position not found") + } } val itemTouchHelper = ItemTouchHelper(swipeToDelete) @@ -142,74 +102,53 @@ class DataListFragment : PokerAnalyticsFragment(), LiveRowRepresentableDataSourc } this.addButton.setOnClickListener { - this.dataType.relatedResultsRepresentable?.let { - EditableDataActivity.newInstance( - requireContext(), - dataType = it.ordinal, - primaryKey = null - ) - } + EditableDataActivity.newInstance( + requireContext(), + dataType = this.dataType.ordinal, + primaryKey = null + ) } } - /** - * Delete item - */ - private fun deleteItem(position: Int) { - - if (isDetached || activity == null) { - return - } + override fun onResume() { + super.onResume() + this.recyclerView?.adapter?.notifyDataSetChanged() + } - // Save the delete position & create a copy of the object - val mRecentlyDeletedItem = rowRepresentableForPosition(position) - lastDeletedItemPosition = position + override fun rowRepresentableForPosition(position: Int): RowRepresentable? { + return this.items[position] as RowRepresentable + } - if (mRecentlyDeletedItem is RealmObject) { + override fun numberOfRows(): Int { + return this.items.size + } - // Check if the object is valid for the deletion - if ((mRecentlyDeletedItem as Deletable).isValidForDelete(this.getRealm())) { - deletedItem = getRealm().copyFromRealm(mRecentlyDeletedItem) - getRealm().executeTransaction { - mRecentlyDeletedItem.deleteFromRealm() - } - dataListAdapter.notifyItemRemoved(position) - showUndoSnackBar() - } else { - dataListAdapter.notifyItemChanged(position) - val builder = AlertDialog.Builder(requireContext()) - .setMessage((mRecentlyDeletedItem as Deletable).getFailedDeleteMessage()) - .setNegativeButton(R.string.ok, null) - builder.show() - } - } + override fun viewTypeForPosition(position: Int): Int { + val viewType = (this.items[position] as RowRepresentable).viewType + return if (viewType != -1) viewType else RowViewType.DATA.ordinal } - /** - * Show undo snack bar - */ - private fun showUndoSnackBar() { - val message = String.format(getString(R.string.data_deleted), this.dataType.localizedTitle(requireContext())) - val snackBar = Snackbar.make(constraintLayout, message, Snackbar.LENGTH_INDEFINITE) - snackBar.setAction(R.string.cancel) { - getRealm().executeTransaction { realm -> - deletedItem?.let { - realm.copyToRealmOrUpdate(it) - dataListAdapter.notifyItemInserted(lastDeletedItemPosition) - } + 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) } } - snackBar.show() } /** - * Set fragment data + * Update UI */ - fun setData(dataType: Int) { - this.dataType = SettingRow.values()[dataType] - this.toolbar.title = this.dataType.localizedTitle(requireContext()) - this.dataType.relatedResultsRepresentable?.let { - this.items = it.items(getRealm()) - } + fun updateUI(showAddButton: Boolean) { + this.addButton.isVisible = showAddButton } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/EditableDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/EditableDataFragment.kt deleted file mode 100644 index ba10376a..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/EditableDataFragment.kt +++ /dev/null @@ -1,222 +0,0 @@ -package net.pokeranalytics.android.ui.fragment - -import android.app.Activity.RESULT_OK -import android.content.Intent -import android.os.Bundle -import android.view.* -import androidx.appcompat.app.AlertDialog -import androidx.recyclerview.widget.LinearLayoutManager -import io.realm.RealmObject -import kotlinx.android.synthetic.main.fragment_editable_data.* -import kotlinx.android.synthetic.main.fragment_editable_data.view.* -import net.pokeranalytics.android.R -import net.pokeranalytics.android.exceptions.ConfigurationException -import net.pokeranalytics.android.model.LiveData -import net.pokeranalytics.android.model.interfaces.Deletable -import net.pokeranalytics.android.model.interfaces.Editable -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.activity.components.PokerAnalyticsActivity -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.PokerAnalyticsFragment -import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetFragment -import net.pokeranalytics.android.ui.view.RowRepresentable - - -open class EditableDataFragment : PokerAnalyticsFragment(), RowRepresentableDelegate { - - lateinit var parentActivity: PokerAnalyticsActivity - lateinit var item: RealmObject - lateinit var liveDataType: LiveData - lateinit var rowRepresentableAdapter: RowRepresentableAdapter - - private var editableMenu: Menu? = null - private var dataType: Int? = null - private var primaryKey: String? = null - - var isUpdating = false - var shouldOpenKeyboard = true - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_editable_data, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initUI() - initData() - } - - 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) - } - - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item!!.itemId) { - R.id.save -> saveData() - R.id.delete -> deleteData() - } - return true - } - - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - BottomSheetFragment.create(fragmentManager, row, this, getDataSource().editDescriptors(row)) - } - - override fun onRowValueChanged(value: Any?, row: RowRepresentable) { - this.getRealm().executeTransaction { - (this.item as Editable).updateValue(value, row) - } - rowRepresentableAdapter.refreshRow(row) - } - - /** - * Init UI - */ - private fun initUI() { - parentActivity = activity as PokerAnalyticsActivity - parentActivity.setSupportActionBar(toolbar) - parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true) - setHasOptionsMenu(true) - - val viewManager = LinearLayoutManager(requireContext()) - - recyclerView.apply { - setHasFixedSize(true) - layoutManager = viewManager - } - } - - /** - * Return the data source - */ - open fun getDataSource(): RowRepresentableDataSource { - return this.item as RowRepresentableDataSource - } - - /** - * Init data - */ - private fun initData() { - if (this.dataType != null) { - val proxyItem: RealmObject? = this.liveDataType.getData(this.getRealm(), primaryKey) - proxyItem?.let { - //TODO: Localize - this.appBar.toolbar.title = "Update ${this.liveDataType.localizedTitle(this.parentActivity).toLowerCase().capitalize()}" - isUpdating = true - } ?: run { - //TODO: Localize - this.appBar.toolbar.title = this.liveDataType.newEntityLocalizedTitle(requireContext()) - } - this.item = this.liveDataType.updateOrCreate(this.getRealm(), primaryKey) - - val dataSource = getDataSource() - this.rowRepresentableAdapter = RowRepresentableAdapter(getDataSource(), this) - this.recyclerView.adapter = rowRepresentableAdapter - - // When creating an object, open automatically the keyboard for the first row - if (!isUpdating && shouldOpenKeyboard) { - val row = dataSource.adapterRows()?.firstOrNull() - row?.let { - onRowSelected(0, it) - } - } - } - } - - /** - * Update menu UI - */ - private fun updateMenuUI() { - editableMenu?.findItem(R.id.delete)?.isVisible = isUpdating - editableMenu?.findItem(R.id.save)?.isVisible = true - } - - /** - * Save data - */ - fun saveData() { - - val savable = this.item - when (savable) { - is Savable -> { - val status = savable.getSaveValidityStatus(realm = this.getRealm()) - when (status) { - SaveValidityStatus.VALID -> { - this.getRealm().executeTransaction { - val managedItem = it.copyToRealmOrUpdate(this.item) - if (managedItem is Savable) { - val uniqueIdentifier = (managedItem as Savable).id - finishActivityWithResult(uniqueIdentifier) - } - - } - } - else -> { - val message = savable.getFailedSaveMessage(status) - val builder = AlertDialog.Builder(requireContext()) - .setMessage(message) - .setNegativeButton(R.string.ok, null) - builder.show() - } - } - - } else -> { - throw ConfigurationException("Save action called on un-Savable object") - } - } - - } - - /** - * Delete data - */ - private fun deleteData() { - - val deletable = this.item as Deletable - val realm = this.getRealm() - - if (deletable.isValidForDelete(realm)) { - val intent = Intent() - intent.putExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName, true) - activity?.setResult(RESULT_OK, intent) - activity?.finish() - } else { - val message = deletable.getFailedDeleteMessage() - val builder = AlertDialog.Builder(requireContext()) - .setMessage(message) - .setNegativeButton(R.string.ok, null) - builder.show() - } - } - - /** - * Finish the activity with a result - */ - private fun finishActivityWithResult(uniqueIdentifier: String) { - val intent = Intent() - intent.putExtra(EditableDataActivity.IntentKey.DATA_TYPE.keyName, dataType) - intent.putExtra(EditableDataActivity.IntentKey.PRIMARY_KEY.keyName, uniqueIdentifier) - activity?.setResult(RESULT_OK, intent) - activity?.finish() - } - - /** - * Set fragment data - */ - fun setData(dataType: Int, primaryKey: String?) { - this.dataType = dataType - this.liveDataType = LiveData.values()[dataType] - this.primaryKey = primaryKey - } - -} \ 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/fragment/FeedFragment.kt new file mode 100644 index 00000000..35454cc4 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/FeedFragment.kt @@ -0,0 +1,377 @@ +package net.pokeranalytics.android.ui.fragment + +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.isVisible +import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import com.google.android.material.tabs.TabLayout +import io.realm.RealmModel +import io.realm.RealmResults +import io.realm.Sort +import io.realm.kotlin.where +import kotlinx.android.synthetic.main.fragment_feed.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.LiveData +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.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.view.RowRepresentable +import net.pokeranalytics.android.ui.view.SmoothScrollLinearLayoutManager +import net.pokeranalytics.android.util.Preferences +import java.text.SimpleDateFormat +import java.util.* + + +class FeedFragment : FilterableFragment(), RowRepresentableDelegate { + + private enum class Tab { + SESSIONS, + TRANSACTIONS + } + + companion object { + + const val REQUEST_CODE_MENU = 100 + const val REQUEST_CODE_TRANSACTION_DETAILS = 101 + + fun newInstance(): FeedFragment { + val fragment = FeedFragment() + val bundle = Bundle() + fragment.arguments = bundle + return fragment + } + } + + private lateinit var feedSessionAdapter: FeedSessionRowRepresentableAdapter + private lateinit var feedTransactionAdapter: FeedTransactionRowRepresentableAdapter + private lateinit var realmSessions: RealmResults + private lateinit var realmTransactions: RealmResults + private lateinit var betaLimitDate: Date + + private var newSessionCreated: Boolean = false + private var adapterHasBeenSet: Boolean = false + private var selectedTransaction: Transaction? = null + private var selectedTransactionPosition: Int = -1 + + override val observedEntities: List> = listOf(Session::class.java, Transaction::class.java) + + override fun entitiesChanged(clazz: Class) { + super.entitiesChanged(clazz) + + when (clazz.kotlin) { + Session::class -> { + this.feedSessionAdapter.refreshData() + this.feedSessionAdapter.notifyDataSetChanged() + } + Transaction::class -> { + this.feedTransactionAdapter.refreshData() + this.feedTransactionAdapter.notifyDataSetChanged() + } + } + + } + + 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 onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUI() + initData() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE_MENU && resultCode == RESULT_OK && data != null) { + when (data.getIntExtra(NewDataMenuActivity.IntentKey.CHOICE.keyName, -1)) { + 0 -> createNewSession(false) + 1 -> createNewSession(true) + 2 -> createNewTransaction() + } + } else if (requestCode == REQUEST_CODE_TRANSACTION_DETAILS && resultCode == RESULT_OK && data != null) { + if (data.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName) != null) { + deleteSelectedTransaction() + } + } else if (requestCode == FilterActivityRequestCode.CREATE_FILTER.ordinal && resultCode == RESULT_OK) { + data?.let { + 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) + } + + } + + override fun onDestroyView() { + super.onDestroyView() + realmSessions.removeAllChangeListeners() + realmTransactions.removeAllChangeListeners() + } + + /* + override fun setUserVisibleHint(isVisibleToUser: Boolean) { + super.setUserVisibleHint(isVisibleToUser) + if (isVisibleToUser && view != null) { + if (FilterHandler.filterWasUpdated) { + applyFilter() + FilterHandler.filterWasUpdated = false + } + } + } + */ + + override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + when (row) { + is Session -> SessionActivity.newInstance(requireContext(), sessionId = (row as Editable).id) + is Transaction -> { + selectedTransaction = row + selectedTransactionPosition = position + EditableDataActivity.newInstanceForResult( + this, + LiveData.TRANSACTION, + row.id, + REQUEST_CODE_TRANSACTION_DETAILS + ) + } + } + } + + /** + * Init UI + */ + private fun initUI() { + + disclaimerContainer.isVisible = Preferences.shouldShowDisclaimer(requireContext()) + + disclaimerDismiss.setOnClickListener { + Preferences.setStopShowingDisclaimer(requireContext()) + + disclaimerContainer.animate().translationY(disclaimerContainer.height.toFloat()) + .setInterpolator(FastOutSlowInInterpolator()) + .withEndAction { disclaimerContainer?.isVisible = false } + .start() + } + + addButton.setOnClickListener { + activity?.let { + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(it) + val intent = Intent(requireContext(), NewDataMenuActivity::class.java) + startActivityForResult(intent, REQUEST_CODE_MENU, options.toBundle()) + } + } + + tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + when (tab.position) { + 0 -> { + currentFilterable = FilterableType.SESSION + recyclerView.adapter = feedSessionAdapter + } + 1 -> { + currentFilterable = FilterableType.TRANSACTION + recyclerView.adapter = feedTransactionAdapter + } + } + } + + override fun onTabUnselected(tab: TabLayout.Tab) { + } + + override fun onTabReselected(tab: TabLayout.Tab) { + } + }) + + } + + /** + * Init data + */ + private fun initData() { + + val sdf = SimpleDateFormat("dd/M/yyyy hh:mm", Locale.getDefault()) + betaLimitDate = sdf.parse("17/7/2019 10:00") + this.currentFilterable = FilterableType.SESSION + + val viewManager = SmoothScrollLinearLayoutManager(requireContext()) + recyclerView.apply { + setHasFixedSize(true) + layoutManager = viewManager + } + + applyFilter() + } + + private fun loadSessions(filter: Filter? = null) { + val sessionFilter: Filter? = filter?.let { + if (it.filterableType == FilterableType.SESSION) { + it + } else { + null + } + } + + // Sessions + this.realmSessions = + sessionFilter?.results() ?: run { getRealm().where().isNotNull("startDate").findAll() } + this.realmSessions = this.realmSessions.sort("startDate", Sort.DESCENDING) + + val pendingSessions = sessionFilter?.let { + getRealm().where().alwaysFalse().findAll() + } ?: run { + getRealm().where().isNull("year").isNull("month").findAll().sort("startDate", Sort.DESCENDING) + } + var distinctDateSessions = sessionFilter?.results("year", "month") ?: run { + getRealm().where().distinct("year", "month").findAll() + } + distinctDateSessions = distinctDateSessions.sort("startDate", Sort.DESCENDING) + this.feedSessionAdapter = + FeedSessionRowRepresentableAdapter(this, realmSessions, pendingSessions, distinctDateSessions) + } + + private fun loadTransactions(filter: Filter? = null) { + val transactionFilter: Filter? = filter?.let { + if (it.filterableType == FilterableType.TRANSACTION) { + it + } else { + null + } + } + + // Transactions + 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) + + } + + /** + * Create a new cash game + */ + private fun createNewSession(isTournament: Boolean) { + +// val sessionCount = this.feedSessionAdapter.realmResults.size +// if (!AppGuard.isProUser && sessionCount >= AppGuard.MAX_SESSIONS_BEFORE_REQUESTING_SUBSCRIPTION) { // && !BuildConfig.DEBUG +// Toast.makeText(context, "Please subscribe!", Toast.LENGTH_LONG).show() +// BillingActivity.newInstanceForResult(requireContext()) +// return +// } + + if (Date().after(betaLimitDate)) { + this.showEndOfBetaMessage() + return + } + + SessionActivity.newInstanceforResult(this, isTournament, requestCode = RequestCode.NEW_SESSION.value) + newSessionCreated = true + } + + /** + * Create a new transaction + */ + private fun createNewTransaction() { + + if (Date().after(betaLimitDate)) { + this.showEndOfBetaMessage() + return + } + EditableDataActivity.newInstanceForResult(this, LiveData.TRANSACTION, null, RequestCode.NEW_TRANSACTION.value) + +// EditableDataActivity.newInstance(requireContext(), LiveData.TRANSACTION.ordinal) + } + + /** + * Delete selected transaction + */ + private fun deleteSelectedTransaction() { + val realm = getRealm() + realm.beginTransaction() + selectedTransaction?.deleteFromRealm() + realm.commitTransaction() + selectedTransactionPosition = -1 + } + + /** + * Show end of beta message + */ + private fun showEndOfBetaMessage() { + Toast.makeText( + context, + "Beta has ended. Thanks a lot for your participation! Please update with the Google Play version to continue using the app", + Toast.LENGTH_LONG + ).show() + } + + + // Filter Handler + + override fun applyFilter() { + super.applyFilter() + + val filter: Filter? = this.currentFilter(this.requireContext(), getRealm()) + this.loadSessions(filter) + this.loadTransactions(filter) + + filter?.let { + when (it.filterableType) { + FilterableType.SESSION -> { + recyclerView.adapter = feedSessionAdapter + this.selectTab(Tab.SESSIONS) + } + FilterableType.TRANSACTION -> { + recyclerView.adapter = feedTransactionAdapter + this.selectTab(Tab.TRANSACTIONS) + } + else -> { + } + } + adapterHasBeenSet = true + } + + if (!adapterHasBeenSet) { + adapterHasBeenSet = true + recyclerView.adapter = feedSessionAdapter + } + } + + override fun removeFilter() { + super.removeFilter() + + this.loadSessions() + this.loadTransactions() + if (currentFilterable == FilterableType.SESSION) { + recyclerView.adapter = feedSessionAdapter + } else { + recyclerView.adapter = feedTransactionAdapter + } + } + + private fun selectTab(tab: Tab) { + this.tabs.getTabAt(tab.ordinal)?.select() + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FilterDetailsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/FilterDetailsFragment.kt index 0233ea82..7fe858e8 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FilterDetailsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/FilterDetailsFragment.kt @@ -1,10 +1,10 @@ package net.pokeranalytics.android.ui.fragment 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.Menu import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager @@ -14,11 +14,10 @@ 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.activity.components.PokerAnalyticsActivity 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.components.PokerAnalyticsFragment +import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetFragment import net.pokeranalytics.android.ui.helpers.DateTimePickerManager import net.pokeranalytics.android.ui.view.RowRepresentable @@ -27,30 +26,24 @@ 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 -import net.pokeranalytics.android.util.extensions.shortDate -import net.pokeranalytics.android.util.extensions.shortTime -import net.pokeranalytics.android.util.extensions.toMinutes import timber.log.Timber import java.util.* import kotlin.collections.ArrayList -open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { +open class FilterDetailsFragment : RealmFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { - lateinit var parentActivity: PokerAnalyticsActivity lateinit var rowRepresentableAdapter: RowRepresentableAdapter - private lateinit var primaryKey: String - private lateinit var filterCategoryRow: FilterCategoryRow + private lateinit var primaryKey: String + private lateinit var filterCategoryRow: FilterCategoryRow private var currentFilter: Filter? = null private var rows: ArrayList = ArrayList() private var rowsForFilterSubcategoryRow: HashMap> = HashMap() - private var filterMenu: Menu? = null private val selectedRows = ArrayList() - private var isUpdating = false - private var shouldOpenKeyboard = true override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) return inflater.inflate(R.layout.fragment_filter_details, container, false) } @@ -68,37 +61,46 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresent override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { super.onRowSelected(position, row, fromAction) - Timber.d("Row: $row") - - if (row.viewType == RowViewType.TITLE_CHECK.ordinal) { - updateRowsSelection(row) - return - } + if (row.viewType == RowViewType.TITLE_CHECK.ordinal) { + updateRowsSelection(row) + return + } when (row) { - is QueryCondition.DateQuery -> DateTimePickerManager.create(requireContext(), row, this, row.singleValue, onlyDate = !row.showTime, onlyTime = row.showTime) + is QueryCondition.DateQuery -> DateTimePickerManager.create( + requireContext(), + row, + this, + row.singleValue, + onlyDate = !row.showTime, + onlyTime = row.showTime + ) is QueryCondition.Duration -> { - val hours = if (row.minutes / 60 > 0) (row.minutes / 60).toString() else "" - val minutes = if (row.minutes % 60 > 0) (row.minutes % 60).toString() else "" + var hours: String? = null + var minutes: String? = null + row.minutes?.let { + hours = if (it / 60 > 0) (it / 60).toString() else null + 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) } - - is QueryCondition.ListOfValues<*> -> { - val valueAsString = row.listOfValues.firstOrNull()?.toString() ?: "" - val data = row.editingDescriptors(mapOf("valueAsString" to valueAsString)) - BottomSheetFragment.create(fragmentManager, row, this, data, true) - } + is QueryCondition.ListOfValues<*> -> { + var valueAsString: String? = null + row.listOfValues.firstOrNull()?.let { + valueAsString = row.listOfValues.firstOrNull()?.toString() + } + val data = row.editingDescriptors(mapOf("valueAsString" to valueAsString)) + BottomSheetFragment.create(fragmentManager, row, this, data, true) + } } } - override fun stringForRow(row: RowRepresentable): String { + override fun stringForRow(row: RowRepresentable, context: Context): String { return when (row) { - is QueryCondition.DateQuery -> if (row.showTime) row.singleValue.shortTime() else row.singleValue.shortDate() - is QueryCondition.Duration -> row.minutes.toMinutes(requireContext()) - is QueryCondition.ListOfValues<*> -> row.listOfValues.firstOrNull()?.toString() ?: NULL_TEXT + is QueryCondition.ListOfValues<*> -> row.firstValue(context) else -> super.stringForRow(row) - } + } ?: NULL_TEXT } override fun isSelected(row: RowRepresentable): Boolean { @@ -110,28 +112,40 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresent Timber.d("onRowValueChanged: $row $value") when (row) { - is QueryCondition.DateQuery -> row.singleValue = if (value != null && value is Date) value else Date() + is QueryCondition.DateQuery -> row.singleValue = if (value != null && value is Date) value else null is QueryCondition.Duration -> { if (value is ArrayList<*>) { - val hours = try { - (value[0] as String? ?: "0").toInt() + val hours: Int? = try { + (value[0] as String?)?.toInt() } catch (e: Exception) { - 0 + null } val minutes = try { - (value[1] as String? ?: "0").toInt() + (value[1] as String?)?.toInt() } catch (e: Exception) { - 0 + null + } + if (hours != null && minutes != null) { + row.minutes = hours * 60 + minutes + } else if (hours != null) { + row.minutes = hours * 60 + } else if (minutes != null) { + row.minutes = minutes } - - row.minutes = hours * 60 + minutes } else { - row.minutes = 0 + row.minutes = null } } - is QueryCondition.SingleInt -> row.singleValue = if (value != null && value is String) value.toInt() else 0 - is QueryCondition.ListOfDouble-> row.listOfValues = arrayListOf(if (value != null && value is String) value.toDouble() else 0.0) - is QueryCondition.ListOfInt-> row.listOfValues = arrayListOf(if (value != null && value is String) value.toInt() else 0) + is QueryCondition.SingleInt -> row.singleValue = if (value != null && value is String) value.toInt() else null + is QueryCondition.ListOfDouble -> row.listOfValues = arrayListOf().apply { + if (value != null && value is String) this.add(value.toDouble()) + } + is QueryCondition.ListOfInt -> row.listOfValues = arrayListOf().apply { + if (value != null && value is String) this.add(value.toInt()) + } + is QueryCondition.ListOfString -> row.listOfValues = arrayListOf().apply { + if (value != null && value is String) this.add(value) + } } // Remove the row before updating the selected rows list @@ -153,10 +167,8 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresent * Init UI */ private fun initUI() { - parentActivity = activity as PokerAnalyticsActivity - parentActivity.setSupportActionBar(toolbar) - parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true) - setHasOptionsMenu(true) + + setDisplayHomeAsUpEnabled(true) this.appBar.toolbar.title = getString(R.string.filter) @@ -173,24 +185,24 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresent */ private fun initData() { - currentFilter = Filter.getFilterBydId(getRealm(), primaryKey) + //currentFilter = Filter.getFilterBydId(getRealm(), primaryKey) + currentFilter = FiltersFragment.currentFilter + this.appBar.toolbar.title = filterCategoryRow.localizedTitle(requireContext()) - this.appBar.toolbar.title = filterCategoryRow.localizedTitle(requireContext()) + this.rows.clear() + this.rowsForFilterSubcategoryRow.clear() + this.rows.addAll(filterCategoryRow.filterElements) - 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.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 + this.rowRepresentableAdapter = RowRepresentableAdapter(this, this) + this.recyclerView.adapter = rowRepresentableAdapter } /** @@ -215,7 +227,7 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresent } } - println("list of selected rows : $selectedRows") + println("list of selected rows : $selectedRows") // Update UI rowRepresentableAdapter.refreshRow(row) @@ -233,7 +245,7 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresent val realm = getRealm() realm.beginTransaction() - currentFilter?.remove(filterCategoryRow) + currentFilter?.remove(filterCategoryRow) currentFilter?.createOrUpdateFilterConditions(selectedRows) realm.commitTransaction() @@ -241,7 +253,6 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresent Timber.d("Condition: $it") } - finishActivityWithResult(currentFilter?.id) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FiltersFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/FiltersFragment.kt index 499b52aa..eb36582f 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FiltersFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/FiltersFragment.kt @@ -1,47 +1,61 @@ package net.pokeranalytics.android.ui.fragment -import android.app.Activity import android.app.Activity.RESULT_OK import android.content.Intent import android.os.Bundle import android.view.* +import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager -import io.realm.kotlin.where -import kotlinx.android.synthetic.main.fragment_editable_data.* -import kotlinx.android.synthetic.main.fragment_filters.view.* +import com.google.android.material.chip.Chip +import kotlinx.android.synthetic.main.fragment_editable_data.appBar +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.model.LiveData import net.pokeranalytics.android.model.realm.Filter +import net.pokeranalytics.android.ui.activity.DataListActivity import net.pokeranalytics.android.ui.activity.FilterDetailsActivity import net.pokeranalytics.android.ui.activity.FiltersActivity -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity 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.components.PokerAnalyticsFragment +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.sorted import timber.log.Timber -open class FiltersFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { +open class FiltersFragment : RealmFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { companion object { const val REQUEST_CODE_FILTER_DETAILS = 100 + const val MOST_USED_FILTERS_DISPLAYED = 6 + + var currentFilter: Filter? = null + } - private lateinit var parentActivity: PokerAnalyticsActivity private lateinit var rowRepresentableAdapter: RowRepresentableAdapter - private var currentFilter: Filter? = null 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 override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) return inflater.inflate(R.layout.fragment_filters, container, false) } @@ -49,24 +63,34 @@ open class FiltersFragment : PokerAnalyticsFragment(), StaticRowRepresentableDat super.onViewCreated(view, savedInstanceState) initUI() initData() + updateMostUsedFilters() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - if (requestCode == REQUEST_CODE_FILTER_DETAILS && resultCode == Activity.RESULT_OK) { + if (requestCode == REQUEST_CODE_FILTER_DETAILS && resultCode == RESULT_OK) { + // Update object /* - Timber.d("onActivityResult: $requestCode") - if (data != null && data.hasExtra(FilterDetailsActivity.IntentKey.FILTER_ID.keyName)) { - val filterId = data.getStringExtra(FilterDetailsActivity.IntentKey.FILTER_ID.keyName) - Timber.d("Updated queryWith: ${filterId}") + currentFilter?.id?.let { currentFilterId -> + Filter.getFilterBydId(getRealm(), currentFilterId)?.let { filter -> + currentFilter = filter + } } */ selectedRow?.let { rowRepresentableAdapter.refreshRow(it) } + } else if (requestCode == FilterActivityRequestCode.SELECT_FILTER.ordinal) { + + updateMostUsedFilters() + + if (resultCode == RESULT_OK && data != null && data.hasExtra(FiltersActivity.IntentKey.FILTER_ID.keyName)) { + val filterId = data.getStringExtra(FiltersActivity.IntentKey.FILTER_ID.keyName) + finishActivityWithResult(filterId) + } } } @@ -82,14 +106,13 @@ open class FiltersFragment : PokerAnalyticsFragment(), StaticRowRepresentableDat if (isUpdating) { cancelUpdates() } else { - deleteFilter() + activity?.finish() } } override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item!!.itemId) { - R.id.save -> validUpdates() - R.id.delete -> deleteFilter() + R.id.save -> validateUpdates() } return true } @@ -123,19 +146,23 @@ open class FiltersFragment : PokerAnalyticsFragment(), StaticRowRepresentableDat * Init UI */ private fun initUI() { - parentActivity = activity as PokerAnalyticsActivity - parentActivity.setSupportActionBar(toolbar) - parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true) - setHasOptionsMenu(true) + + setDisplayHomeAsUpEnabled(true) this.appBar.toolbar.title = getString(R.string.filter) val viewManager = LinearLayoutManager(requireContext()) - recyclerView.apply { setHasFixedSize(true) layoutManager = viewManager } + + moreFilters.setOnClickListener { + LiveData.FILTER.subType = filterableType.uniqueIdentifier + DataListActivity.newSelectInstance(this, LiveData.FILTER.ordinal, false) + } + + mostUsedFiltersLayout.isVisible = showMostUsedFiltersLayout } /** @@ -145,45 +172,93 @@ open class FiltersFragment : PokerAnalyticsFragment(), StaticRowRepresentableDat val realm = getRealm() - //TODO: Remove that - val filters = realm.where().findAll() - Timber.d("Filters: ${filters.size}") - primaryKey?.let { - currentFilter = Filter.getFilterBydId(realm, it) + currentFilter = realm.copyFromRealm(Filter.getFilterBydId(realm, it)) isUpdating = true } ?: run { - realm.beginTransaction() - currentFilter = Filter.newInstance(realm) - realm.commitTransaction() + currentFilter = Filter.newInstance(this.filterableType.uniqueIdentifier) //realm.copyFromRealm(Filter.newInstanceForResult(realm, this.filterableType.ordinal)) } // Create a copy if the user cancels the updates currentFilter?.let { - filterCopy = getRealm().copyFromRealm(it) + if (it.isValid && it.isManaged) { + filterCopy = getRealm().copyFromRealm(it) + } } rows.clear() - rows.addAll(FilterCategoryRow.values()) + rows.addAll(FilterCategoryRow.values(this.filterableType)) this.rowRepresentableAdapter = RowRepresentableAdapter(this, this) this.recyclerView.adapter = rowRepresentableAdapter } + /** + * Update the most used filters chips + */ + private fun updateMostUsedFilters() { + + var nbChips = 0 + val filters = getRealm().sorted(Filter::class.java, editableOnly = false, filterableTypeUniqueIdentifier = this.filterableType.uniqueIdentifier) + val currentFilterId = Preferences.getActiveFilterId(requireContext()) + + if (isUpdating || filters.isEmpty() || (filters.size == 1 && filters.first()?.id == currentFilterId)) { + mostUsedFiltersLayout.visibility = View.GONE + return + } + + mostUsedFilters.removeAllViews() + + filters.forEach { filter -> + if (nbChips < MOST_USED_FILTERS_DISPLAYED) { + + if (filter.id != currentFilterId) { + val chip = Chip(requireContext()) + chip.id = View.generateViewId() + chip.tag = filter.id + chip.text = filter.getDisplayName(requireContext()) + chip.chipStartPadding = 8f.px + chip.chipEndPadding = 8f.px + + chip.isChecked = filter.id == currentFilterId + chip.setOnCloseIconClickListener { + chip.isChecked = false + } + chip.setOnClickListener { + if (chip.isChecked) { + finishActivityWithResult(filter.id) + } else { + finishActivityWithResult("") + } + } + mostUsedFilters.addView(chip) + nbChips++ + } + } + } + } + /** * Update menu UI */ private fun updateMenuUI() { - filterMenu?.findItem(R.id.delete)?.isVisible = isUpdating filterMenu?.findItem(R.id.save)?.isVisible = true + filterMenu?.findItem(R.id.delete)?.isVisible = false } /** - * Valid the updates of the queryWith + * Validate the updates of the queryWith */ - private fun validUpdates() { - Timber.d("Valid queryWith updates") + private fun validateUpdates() { + val realm = getRealm() + realm.beginTransaction() + currentFilter?.let { + it.name = it.query.getName(requireContext()) + realm.copyToRealmOrUpdate(it) + } + realm.commitTransaction() + val filterId = currentFilter?.id ?: "" finishActivityWithResult(filterId) } @@ -192,10 +267,7 @@ open class FiltersFragment : PokerAnalyticsFragment(), StaticRowRepresentableDat * Cancel the latest updates of the queryWith */ private fun cancelUpdates() { - Timber.d("Cancel queryWith updates") - val filterId = filterCopy?.id ?: "" - val realm = getRealm() realm.beginTransaction() filterCopy?.let { @@ -205,19 +277,6 @@ open class FiltersFragment : PokerAnalyticsFragment(), StaticRowRepresentableDat finishActivityWithResult(filterId) } - /** - * Delete data - */ - private fun deleteFilter() { - Timber.d("Delete queryWith") - val realm = getRealm() - realm.beginTransaction() - currentFilter?.deleteFromRealm() - realm.commitTransaction() - - finishActivityWithResult("") - } - /** * Finish the activity with a result */ @@ -231,8 +290,18 @@ open class FiltersFragment : PokerAnalyticsFragment(), StaticRowRepresentableDat /** * Set fragment data */ - fun setData(primaryKey: String?) { + fun setData(primaryKey: String?, filterableType: FilterableType) { this.primaryKey = primaryKey + this.filterableType = filterableType + } + + /** + * Update the most used filters visibility + */ + fun updateMostUsedFiltersVisibility(visible: Boolean) { + Timber.d("updateMostUsedFiltersVisibility: $visible") + showMostUsedFiltersLayout = visible + mostUsedFiltersLayout?.isVisible = visible } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt index de378bd6..9570c9cb 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt @@ -13,18 +13,17 @@ import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBub import com.github.mikephil.charting.listener.OnChartValueSelectedListener import kotlinx.android.synthetic.main.fragment_graph.* import net.pokeranalytics.android.R -import net.pokeranalytics.android.calculus.ObjectIdentifier import net.pokeranalytics.android.calculus.Stat -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity -import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment +import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.graph.AxisFormatting import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry +import net.pokeranalytics.android.ui.graph.ObjectIdentifier import net.pokeranalytics.android.ui.graph.setStyle import net.pokeranalytics.android.ui.view.LegendView import net.pokeranalytics.android.ui.view.MultiLineLegendView -class GraphFragment : PokerAnalyticsFragment(), OnChartValueSelectedListener { +class GraphFragment : RealmFragment(), OnChartValueSelectedListener { enum class Style { LINE, @@ -49,8 +48,6 @@ class GraphFragment : PokerAnalyticsFragment(), OnChartValueSelectedListener { } - private lateinit var parentActivity: PokerAnalyticsActivity - private var style: Style = Style.LINE private lateinit var legendView: LegendView @@ -63,6 +60,7 @@ class GraphFragment : PokerAnalyticsFragment(), OnChartValueSelectedListener { private var axisFormatting: AxisFormatting = AxisFormatting.DEFAULT override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) return inflater.inflate(R.layout.fragment_graph, container, false) } @@ -100,9 +98,6 @@ class GraphFragment : PokerAnalyticsFragment(), OnChartValueSelectedListener { */ private fun initUI() { - parentActivity = activity as PokerAnalyticsActivity - parentActivity.title = stat.localizedTitle(requireContext()) - this.legendView = when (this.style) { Style.MULTILINE -> MultiLineLegendView(requireContext()) else -> LegendView(requireContext()) @@ -129,15 +124,12 @@ class GraphFragment : PokerAnalyticsFragment(), OnChartValueSelectedListener { this.chartView = lineChart dataSets.firstOrNull()?.let { dataSet -> - this.legendView.prepareWithStat(this.stat, dataSet.entryCount, this.style) if (dataSet.entryCount > 0) { val entry = dataSet.getEntryForIndex(dataSet.entryCount - 1) this.selectValue(entry, dataSet) - } } - } this.barDataSetList?.let { dataSets -> @@ -146,10 +138,10 @@ class GraphFragment : PokerAnalyticsFragment(), OnChartValueSelectedListener { val barChart = BarChart(context) barChart.setOnChartValueSelectedListener(this) - if (stat.showXAxisZero) { + if (stat.graphShowsXAxisZero) { barChart.xAxis.axisMinimum = 0.0f } - if (stat.showYAxisZero) { + if (stat.graphShowsYAxisZero) { barChart.axisLeft.axisMinimum = 0.0f } this.chartView = barChart @@ -178,21 +170,13 @@ class GraphFragment : PokerAnalyticsFragment(), OnChartValueSelectedListener { override fun onValueSelected(e: Entry?, h: Highlight?) { - var groupName = "" - h?.let { highlight -> - this.chartView?.data?.getDataSetByIndex(highlight.dataSetIndex)?.let { - groupName = it.label - } - } - val dataSet = this.chartView?.data?.getDataSetForEntry(e) - e?.let { entry -> this.selectValue(entry, dataSet) } } - private fun selectValue(entry: Entry, dataSet: IBarLineScatterCandleBubbleDataSet? = null) { + private fun selectValue(entry: Entry, dataSet: IBarLineScatterCandleBubbleDataSet? = null) { val statEntry = when (entry.data) { is ObjectIdentifier -> { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/HistoryFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/HistoryFragment.kt deleted file mode 100644 index 06319966..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/HistoryFragment.kt +++ /dev/null @@ -1,152 +0,0 @@ -package net.pokeranalytics.android.ui.fragment - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.core.view.isVisible -import androidx.interpolator.view.animation.FastOutSlowInInterpolator -import io.realm.RealmResults -import io.realm.Sort -import io.realm.kotlin.where -import kotlinx.android.synthetic.main.fragment_history.* -import net.pokeranalytics.android.R -import net.pokeranalytics.android.model.interfaces.Editable -import net.pokeranalytics.android.model.realm.Session -import net.pokeranalytics.android.ui.activity.SessionActivity -import net.pokeranalytics.android.ui.adapter.HistorySessionRowRepresentableAdapter -import net.pokeranalytics.android.ui.adapter.LiveRowRepresentableDataSource -import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate -import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment -import net.pokeranalytics.android.ui.view.RowRepresentable -import net.pokeranalytics.android.ui.view.SmoothScrollLinearLayoutManager -import net.pokeranalytics.android.util.Preferences -import java.text.SimpleDateFormat -import java.util.* - -class HistoryFragment : PokerAnalyticsFragment(), LiveRowRepresentableDataSource, RowRepresentableDelegate { - - companion object { - fun newInstance(): HistoryFragment { - val fragment = HistoryFragment() - val bundle = Bundle() - fragment.arguments = bundle - return fragment - } - } - - private lateinit var historyAdapter: HistorySessionRowRepresentableAdapter - - private lateinit var realmSessions: RealmResults - private val rows: ArrayList = ArrayList() - private var newSessionCreated: Boolean = false - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_history, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initUI() - initData() - } - - override fun onDestroyView() { - super.onDestroyView() - realmSessions.removeAllChangeListeners() - } - - /** - * Init UI - */ - private fun initUI() { - - disclaimerContainer.isVisible = Preferences.shouldShowDisclaimer(requireContext()) - - val sdf = SimpleDateFormat("dd/M/yyyy hh:mm") - val betaLimitDate = sdf.parse("17/7/2019 10:00") - - newCashGame.setOnClickListener { - - if (Date().after(betaLimitDate)) { - this.showEndOfBetaMessage() - return@setOnClickListener - } - - SessionActivity.newInstance(requireContext(), false) - newSessionCreated = true - } - - newTournament.setOnClickListener { - - if (Date().after(betaLimitDate)) { - this.showEndOfBetaMessage() - return@setOnClickListener - } - - SessionActivity.newInstance(requireContext(), true) - newSessionCreated = true - } - - disclaimerDismiss.setOnClickListener { - Preferences.setStopShowingDisclaimer(requireContext()) - - disclaimerContainer.animate().translationY(disclaimerContainer.height.toFloat()) - .setInterpolator(FastOutSlowInInterpolator()) - .withEndAction { disclaimerContainer?.isVisible = false } - .start() - } - - } - - private fun showEndOfBetaMessage() { - Toast.makeText(context, "Beta has ended. Please update with the Google Play version", Toast.LENGTH_LONG).show() - - } - - /** - * Init data - */ - private fun initData() { - - this.realmSessions = getRealm().where().findAll().sort("startDate", Sort.DESCENDING) - this.realmSessions.addChangeListener { _, _ -> - this.historyAdapter.refreshData() - this.historyAdapter.notifyDataSetChanged() - } - - val startedSessions = getRealm().where().isNotNull("year").isNotNull("month").findAll().sort("startDate", Sort.DESCENDING) - val pendingSessions = getRealm().where().isNull("year").isNull("month").findAll().sort("startDate", Sort.DESCENDING) - val distinctDateSessions = getRealm().where().distinct("year", "month").findAll().sort("startDate", Sort.DESCENDING) - - this.historyAdapter = HistorySessionRowRepresentableAdapter(this, startedSessions, pendingSessions, distinctDateSessions) - - val viewManager = SmoothScrollLinearLayoutManager(requireContext()) - recyclerView.apply { - setHasFixedSize(true) - layoutManager = viewManager - adapter = historyAdapter - } - } - - override fun rowRepresentableForPosition(position: Int): RowRepresentable? { - return this.rows[position] - } - - override fun numberOfRows(): Int { - return this.rows.size - } - - override fun viewTypeForPosition(position: Int): Int { - return rows[position].viewType - } - - override fun indexForRow(row: RowRepresentable): Int { - return this.rows.indexOf(row) - } - - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - SessionActivity.newInstance(requireContext(), sessionId = (row as Editable).id) - } -} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt new file mode 100644 index 00000000..0727dd0d --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt @@ -0,0 +1,84 @@ +package net.pokeranalytics.android.ui.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.activity.components.ResultCode +import net.pokeranalytics.android.ui.fragment.components.RealmFragment +import net.pokeranalytics.android.util.csv.CSVImporter +import net.pokeranalytics.android.util.csv.ImportException +import timber.log.Timber +import java.io.InputStream +import java.util.* +import kotlin.coroutines.CoroutineContext + +class ImportFragment : RealmFragment() { + + val coroutineContext: CoroutineContext + get() = Dispatchers.Main + + private lateinit var filePath: String + private lateinit var inputStream: InputStream + + fun setData(path: String) { + this.filePath = path + } + + fun setData(inputStream: InputStream) { + this.inputStream = inputStream + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_import, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + this.startImport() + } + + fun startImport() { + + var shouldDismissActivity = false + + GlobalScope.launch(coroutineContext) { + + val test = GlobalScope.async { + val s = Date() + Timber.d(">>> Start Import...") + + try { + val csv = CSVImporter(inputStream) + csv.start() + } catch (e: ImportException) { + shouldDismissActivity = true + } + val e = Date() + val duration = (e.time - s.time) / 1000.0 + Timber.d(">>> Import ended in $duration seconds") + + } + test.await() + + if (shouldDismissActivity) { + + activity?.let { + it.setResult(ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) + it.finish() + } + + } + + } + + } + +} \ 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 faa0d030..997ff82c 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 @@ -5,7 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager -import kotlinx.android.synthetic.main.fragment_stats.* +import kotlinx.android.synthetic.main.fragment_more.* import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.activity.BankrollActivity import net.pokeranalytics.android.ui.activity.SettingsActivity 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 new file mode 100644 index 00000000..0584020b --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportCreationFragment.kt @@ -0,0 +1,406 @@ +package net.pokeranalytics.android.ui.fragment + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.* +import androidx.recyclerview.widget.LinearLayoutManager +import io.realm.Realm +import kotlinx.android.synthetic.main.fragment_report_creation.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.calculus.Calculator +import net.pokeranalytics.android.calculus.Stat +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.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.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowViewType +import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable +import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRow +import net.pokeranalytics.android.util.extensions.sorted +import timber.log.Timber + +class ReportCreationFragment : RealmFragment(), RowRepresentableDataSource, RowRepresentableDelegate { + + private lateinit var optionsAdapter: RowRepresentableAdapter + + private var assistant = Assistant() + private var currentRows: List = listOf() + private var reportCreationMenu: Menu? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_report_creation, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + this.initUI() + this.updateUIWithCurrentStep() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == FilterActivityRequestCode.CREATE_FILTER.ordinal && resultCode == Activity.RESULT_OK) { + this.updateUIWithCurrentStep() + val selectedFilterId = data?.getStringExtra(FiltersActivity.IntentKey.FILTER_ID.keyName) + val filterPosition = currentRows.indexOfFirst { it is Filter && it.id == selectedFilterId } + if (filterPosition != -1) { + onRowSelected(filterPosition, currentRows[filterPosition]) + } + } + } + + 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 + reportCreationMenu = menu + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.add -> { + if (this.assistant.step == Assistant.Step.FILTER) { + FiltersActivity.newInstanceForResult( + this, + currentFilterable = FilterableType.SESSION, + hideMostUsedFilters = true + ) + } + } + } + return super.onOptionsItemSelected(item) + } + + /** + * Init UI + */ + private fun initUI() { + + setDisplayHomeAsUpEnabled(true) + + this.optionsAdapter = RowRepresentableAdapter(this, this) + //this.optionsAdapter.setHasStableIds(true) + + val viewManager = LinearLayoutManager(requireContext()) + recyclerView.apply { + setHasFixedSize(true) + layoutManager = viewManager + adapter = optionsAdapter + } + + this.next.setOnClickListener { + if (assistant.nextEnabled) { + this.assistant.nextStep() + + if (this.assistant.step == Assistant.Step.FINALIZE) { + +// getRealm().executeTransaction { +// val rs = this.assistant.options.reportSetup("test") +// it.insert(rs) +// } + + // launch report + this.finishActivityWithOptions(this.assistant.options) + + } else { + this.updateUIWithCurrentStep() + } + } + } + } + + /** + * Update UI when changing the current step + */ + private fun updateUIWithCurrentStep() { + + this.next.visibility = if (this.assistant.nextButtonShouldAppear) View.VISIBLE else View.GONE + this.next.text = requireContext().getString(this.assistant.nextButtonTitleResId) + this.next.isEnabled = this.assistant.nextEnabled + this.reportCreationMenu?.findItem(R.id.add)?.isVisible = this.assistant.addButtonShouldAppear + + val rows = mutableListOf() + + this.assistant.titleResId?.let { titleResId -> + rows.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, resId = titleResId)) + } + + rows.addAll(this.assistant.dataSource) + + this.currentRows = rows + this.optionsAdapter.notifyDataSetChanged() + + } + + /** + * Finish activity + */ + private fun finishActivityWithOptions(options: Calculator.Options) { + ReportCreationActivity.options = options // temp object + val intent = Intent() + activity?.setResult(Activity.RESULT_OK, intent) + activity?.finish() + } + + + // RowRepresentableDataSource + + override fun adapterRows(): List? { + return this.currentRows + } + + override fun rowRepresentableForPosition(position: Int): RowRepresentable? { + return this.currentRows[position] + } + + override fun numberOfRows(): Int { + return this.currentRows.size + } + + override fun viewTypeForPosition(position: Int): Int { + return when (position) { + 0 -> RowViewType.HEADER_TITLE.ordinal + else -> { + val row = this.currentRows[position] + when (row) { + is SeparatorRow -> row.viewType + else -> RowViewType.TITLE_CHECK.ordinal + } + } + } + } + + override fun isSelected(row: RowRepresentable): Boolean { + return this.assistant.isSelected(row) + } + + // RowRepresentableDelegate + + override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + + val newStep = this.assistant.onRowSelected(position - 1) + if (newStep) { + this.updateUIWithCurrentStep() + } else { + this.next.isEnabled = this.assistant.nextEnabled + this.optionsAdapter.notifyDataSetChanged() + } + + } + +} + +class Assistant { + + var step: Step = Step.TYPE + private var display: Calculator.Options.Display = Calculator.Options.Display.TABLE + private var stats = mutableListOf() + private var comparators = mutableListOf() + private var useFilter: Boolean? = null + private var filter: Filter? = null + + val options: Calculator.Options + get() { + return Calculator.Options( + this.display, + stats = this.stats, + criterias = this.comparators, + filter = this.filter, + userGenerated = true + ) + } + + enum class Step { + TYPE, + STAT, + COMPARATOR, + FILTER, + FINALIZE + } + + /** + * Notifies the Assistant a row has been selected with a [position] + * Returns whether the step has changed + */ + fun onRowSelected(position: Int): Boolean { + + val stepChanges = !this.nextButtonShouldAppear + + when (this.step) { + Step.TYPE -> { + this.display = this.dataSource[position] as Calculator.Options.Display + this.nextStep() + } + Step.STAT -> { + val stat = this.dataSource[position] as Stat + if (this.stats.contains(stat)) { + this.stats.remove(stat) + } else { + this.stats.add(stat) + } + } + Step.COMPARATOR -> { + val item = this.dataSource[position] + + val comparator = when (item) { + is CustomField -> { + val criteria: Criteria = when (item.type) { + CustomField.Type.LIST.ordinal -> Criteria.ListCustomFields(item.id) + else -> Criteria.ValueCustomFields(item.id) + } + criteria + } + is Criteria -> item + else -> { + Timber.d("onRowSelected: unmanaged data type: $item") + return false + } + } + + if (this.comparators.contains(comparator)) { + this.comparators.remove(comparator) + } else { + this.comparators.add(comparator) + } + } + Step.FILTER -> { + this.filter = this.dataSource[position] as Filter + } + else -> { + } + } + return stepChanges + } + + fun isSelected(row: RowRepresentable): Boolean { + return when (this.step) { + Step.STAT -> this.stats.contains(row as Stat) + Step.COMPARATOR -> { + when (row) { + is Criteria -> this.comparators.contains(row) + is CustomField -> { + val cfCriteria = this.comparators.filterIsInstance() + val cfCriterion = cfCriteria.firstOrNull { it.customFieldId == row.id } + return cfCriterion != null + } + else -> false + } + } + Step.FILTER -> this.filter == row + else -> false + } + } + + fun nextStep() { + this.step = this.nextStep + } + + private val nextStep: Step + get() { + return if (this.stats.isEmpty()) { + Step.STAT + } else if (this.display == Calculator.Options.Display.COMPARISON && this.comparators.isEmpty()) { + Step.COMPARATOR + } else if (this.step == Step.FILTER) { + Step.FINALIZE + } else if (this.useFilter == null) { + Step.FILTER + } else { + Step.FINALIZE + } + + } + + val titleResId: Int? + get() { + return when (step) { + Step.TYPE -> R.string.new_report_step_type + Step.STAT -> R.string.new_report_step_stat + Step.COMPARATOR -> R.string.new_report_step_comparator + Step.FILTER -> R.string.new_report_step_filter + else -> null + } + } + + val dataSource: List + get() { + return when (this.step) { + Step.TYPE -> listOf( + Calculator.Options.Display.TABLE, + Calculator.Options.Display.PROGRESS, + Calculator.Options.Display.COMPARISON + ) + Step.STAT -> { + when (this.display) { + Calculator.Options.Display.PROGRESS -> Stat.evolutionValuesList + else -> Stat.userSelectableList + } + } + Step.COMPARATOR -> { + val list = mutableListOf() + + val realm = Realm.getDefaultInstance() + val customFields = realm.sorted() + list.addAll(customFields) + list.add(SeparatorRow()) + list.addAll(Criteria.all) + realm.close() + return list + } + Step.FILTER -> { + val realm = Realm.getDefaultInstance() + val filters = realm.sorted(Filter::class.java) + realm.close() + filters + } + else -> listOf() + } + } + + val nextEnabled: Boolean + get() { + return when (this.step) { + Step.STAT -> this.stats.isNotEmpty() + Step.COMPARATOR -> this.comparators.isNotEmpty() + Step.FILTER -> true + else -> false + } + } + + val nextButtonShouldAppear: Boolean + get() { + return when (this.step) { + Step.STAT, Step.COMPARATOR, Step.FILTER -> true + else -> false + } + } + + val nextButtonTitleResId: Int + get() { + return when (this.step) { + Step.FILTER -> R.string.launch_report + else -> R.string.next + } + } + + val addButtonShouldAppear: Boolean + get() { + return when (this.step) { + Step.FILTER -> true + else -> false + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportDetailsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportDetailsFragment.kt deleted file mode 100644 index 64475656..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportDetailsFragment.kt +++ /dev/null @@ -1,92 +0,0 @@ -package net.pokeranalytics.android.ui.fragment - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.google.android.material.tabs.TabLayout -import kotlinx.android.synthetic.main.fragment_report_details.* -import kotlinx.android.synthetic.main.fragment_statistic_details.toolbar -import net.pokeranalytics.android.R -import net.pokeranalytics.android.calculus.AggregationType -import net.pokeranalytics.android.calculus.Report -import net.pokeranalytics.android.calculus.Stat -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity -import net.pokeranalytics.android.ui.adapter.ReportPagerAdapter -import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment - -class ReportDetailsFragment : PokerAnalyticsFragment() { - - companion object { - fun newInstance(report: Report?, reportTitle: String): ReportDetailsFragment { - val fragment = ReportDetailsFragment() - fragment.reportTitle = reportTitle - report?.let { - fragment.selectedReport = it - } - val bundle = Bundle() - fragment.arguments = bundle - return fragment - - } - } - - private lateinit var parentActivity: PokerAnalyticsActivity - private lateinit var selectedReport: Report - - private var reports: MutableMap = hashMapOf() - private var stat: Stat = Stat.NET_RESULT - private var displayAggregationChoices: Boolean = true - private var reportTitle: String = "" - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_report_details, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initUI() - } - - /** - * Init UI - */ - private fun initUI() { - - parentActivity = activity as PokerAnalyticsActivity - - // Avoid a bug during setting the title - toolbar.title = "" - - parentActivity.setSupportActionBar(toolbar) - parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true) - setHasOptionsMenu(true) - - toolbar.title = reportTitle - - val reportPagerAdapter = ReportPagerAdapter(requireContext(), parentActivity.supportFragmentManager, selectedReport) - viewPager.adapter = reportPagerAdapter - viewPager.offscreenPageLimit = 3 - - tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { - override fun onTabSelected(tab: TabLayout.Tab) { - viewPager.setCurrentItem(tab.position, false) - } - - override fun onTabUnselected(tab: TabLayout.Tab) { - } - - override fun onTabReselected(tab: TabLayout.Tab) { - } - }) - } - - - /** - * Set data - */ - fun setData(report: Report) { - this.selectedReport = report - } - -} \ No newline at end of file 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 e5f22039..a13ddcc7 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 @@ -1,5 +1,7 @@ package net.pokeranalytics.android.ui.fragment +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -7,27 +9,42 @@ import android.view.ViewGroup import android.widget.Toast import androidx.recyclerview.widget.LinearLayoutManager import io.realm.Realm -import kotlinx.android.synthetic.main.fragment_stats.* +import io.realm.RealmResults +import kotlinx.android.synthetic.main.fragment_data_list.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import net.pokeranalytics.android.R import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.combined -import net.pokeranalytics.android.ui.activity.ReportDetailsActivity +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.activity.ReportCreationActivity +import net.pokeranalytics.android.ui.activity.components.ReportActivity +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.adapter.StaticRowRepresentableDataSource -import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment +import net.pokeranalytics.android.ui.fragment.components.DeletableItemFragment 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.ReportRow import timber.log.Timber import java.util.* -import kotlin.collections.ArrayList -class ReportsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { +class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { + + private lateinit var reportSetups: RealmResults + private var adapterRows = mutableListOf() + + override fun deletableItems(): List { + return this.reportSetups + } companion object { @@ -41,19 +58,12 @@ class ReportsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSour return fragment } - val rowRepresentation: List by lazy { - val rows = ArrayList() - rows.addAll(ReportRow.getRows()) - rows - } } - private lateinit var reportsAdapter: RowRepresentableAdapter - - // Life Cycle override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) return inflater.inflate(R.layout.fragment_reports, container, false) } @@ -61,22 +71,28 @@ class ReportsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSour super.onViewCreated(view, savedInstanceState) initData() initUI() + this.updateRows() } - // Rows - - override fun adapterRows(): List? { - return rowRepresentation - } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - super.onRowSelected(position, row, fromAction) - if (row is ReportRow) { - val reportName = row.localizedTitle(requireContext()) - launchComputation(row.criteria, reportName) + if (requestCode == RequestCode.NEW_REPORT.value && resultCode == Activity.RESULT_OK) { + ReportCreationActivity.options?.let { options -> + this.launchReportWithOptions(options, options.getName(requireContext())) + } + ReportCreationActivity.options = null + } else if (requestCode == RequestCode.DEFAULT.value && resultCode == Activity.RESULT_OK) { + val itemToDeleteId = data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName) + itemToDeleteId?.let { id -> + GlobalScope.launch(Dispatchers.Main) { + delay(300) + deleteItem(dataListAdapter, reportSetups, id) + } + } } - } + } // Business @@ -84,6 +100,10 @@ class ReportsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSour * Init data */ private fun initData() { + this.reportSetups = getRealm().where(ReportSetup::class.java).findAll().sort("name") + this.reportSetups.addChangeListener { _, _ -> + this.updateRows() + } } /** @@ -91,27 +111,79 @@ class ReportsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSour */ private fun initUI() { - reportsAdapter = RowRepresentableAdapter(this, this) + dataListAdapter = RowRepresentableAdapter(this, this) val viewManager = LinearLayoutManager(requireContext()) recyclerView.apply { setHasFixedSize(true) layoutManager = viewManager - adapter = reportsAdapter + adapter = dataListAdapter + } + + this.addButton.setOnClickListener { + ReportCreationActivity.newInstanceForResult(this, requireContext()) + } + + } + + // Rows + + private fun updateRows() { + this.adapterRows.clear() + if (this.reportSetups.size > 0) { + adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.custom)) + adapterRows.addAll(this.reportSetups) + } + adapterRows.addAll(ReportRow.getRows()) + this.dataListAdapter.notifyDataSetChanged() + } + + override fun adapterRows(): List? { + return this.adapterRows + } + + override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + super.onRowSelected(position, row, fromAction) + + when (row) { + is ReportRow -> { + val reportName = row.localizedTitle(requireContext()) + launchComputation(row.criteria, reportName) + } + is ReportSetup -> { + launchReportWithOptions(row.options, row.name) + } } } /** * Launch computation */ - private fun launchComputation(criteria: List, reportName: String) { + private fun launchComputation(criteriaList: List, reportName: String) { - if (criteria.combined().size < 2) { + if (criteriaList.combined().size < 2) { Toast.makeText(context, R.string.less_then_2_values_for_display, Toast.LENGTH_LONG).show() return } + val requiredStats: List = listOf(Stat.NET_RESULT) + val options = Calculator.Options( + display = Calculator.Options.Display.COMPARISON, + progressValues = Calculator.Options.ProgressValues.STANDARD, + stats = requiredStats, + criterias = criteriaList + ) + + this.launchReportWithOptions(options, reportName) + + } + + /** + * Launch and display a report with some [options] and a [reportName] + */ + private fun launchReportWithOptions(options: Calculator.Options, reportName: String) { + showLoader() GlobalScope.launch { @@ -119,23 +191,18 @@ class ReportsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSour val startDate = Date() val realm = Realm.getDefaultInstance() - val requiredStats: List = listOf(Stat.NET_RESULT) - val options = Calculator.Options(evolutionValues = Calculator.Options.EvolutionValues.STANDARD, stats = requiredStats) - - val report = Calculator.computeStatsWithCriterias(realm, criteria, options = options) -// val report = Calculator.computeStatsWithComparators(realm, criteria = criteria, options = options) + val report = Calculator.computeStats(realm, options = options) Timber.d("launchComputation: ${System.currentTimeMillis() - startDate.time}ms") launch(Dispatchers.Main) { if (!isDetached) { hideLoader() - ReportDetailsActivity.newInstance(requireContext(), report, reportName) + ReportActivity.newInstanceForResult(this@ReportsFragment, report, reportName) } } realm.close() } } - } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SessionFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SessionFragment.kt index 05c5d691..cb13262e 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SessionFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SessionFragment.kt @@ -8,31 +8,38 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.recyclerview.widget.DiffUtil -import io.realm.kotlin.where import kotlinx.android.synthetic.main.fragment_session.* import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.extensions.SessionState import net.pokeranalytics.android.model.extensions.getState import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.realm.Location import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.utils.FavoriteSessionFinder -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +import net.pokeranalytics.android.ui.activity.EditableDataActivity import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate -import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment +import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetFragment import net.pokeranalytics.android.ui.helpers.DateTimePickerManager import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentableDiffCallback import net.pokeranalytics.android.ui.view.SmoothScrollLinearLayoutManager import net.pokeranalytics.android.ui.view.rowrepresentable.SessionRow +import net.pokeranalytics.android.util.extensions.findById +import net.pokeranalytics.android.util.extensions.getNextMinuteInMilliseconds import java.util.* -class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate { +class SessionFragment : RealmFragment(), RowRepresentableDelegate { + + companion object { + const val TIMER_DELAY = 60000L + + const val REQUEST_CODE_NEW_CUSTOM_FIELD = 1000 + } - private lateinit var parentActivity: PokerAnalyticsActivity private lateinit var currentSession: Session private lateinit var sessionAdapter: RowRepresentableAdapter @@ -40,25 +47,66 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate { private val oldRows: ArrayList = ArrayList() private var sessionHasBeenCustomized = false private val handler: Handler = Handler() + private val refreshTimer: Runnable = object : Runnable { override fun run() { - // Refresh header each 30 seconds refreshTimer() - handler.postDelayed(this, 60000) + handler.postDelayed(this, TIMER_DELAY) } } + + /** + * Set fragment data + */ + fun setData(isTournament: Boolean, sessionId: String) { + + val realm = getRealm() + val sessionRealm = realm.findById(sessionId) + if (sessionRealm != null) { + currentSession = sessionRealm + sessionHasBeenCustomized = true + } else { + realm.beginTransaction() + currentSession = Session.newInstance(realm, isTournament) + FavoriteSessionFinder.copyParametersFromFavoriteSession(currentSession, null, requireContext()) + realm.commitTransaction() + + // Find the nearest location around the user + parentActivity?.findNearestLocation { + it?.let { location -> + realm.beginTransaction() + val realmLocation = realm.findById(location.id) + FavoriteSessionFinder.copyParametersFromFavoriteSession(currentSession, realmLocation, requireContext()) + + currentSession.location = realmLocation + realm.commitTransaction() + updateSessionUI(true) + } + } + sessionHasBeenCustomized = false + } + + toolbar.title = if (currentSession.isTournament()) getString(R.string.tournament) else getString(R.string.cash_game) + collapsingToolbar.title = toolbar.title + + sessionAdapter = RowRepresentableAdapter(currentSession, this) + recyclerView.adapter = sessionAdapter + + updateSessionUI(true) + } + override fun onResume() { super.onResume() this.refreshTimer() } private fun refreshTimer() { - currentSession.updateRowRepresentation() - sessionAdapter.notifyItemChanged(0) + this.updateSessionUI(false) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) return inflater.inflate(R.layout.fragment_session, container, false) } @@ -83,6 +131,7 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate { override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item!!.itemId) { R.id.stop -> stopSession() + R.id.newCustomField -> addNewCustomField() R.id.restart -> restartTimer() R.id.delete -> deleteSession() } @@ -140,14 +189,7 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate { */ private fun initUI() { - parentActivity = activity as PokerAnalyticsActivity - - // Avoid a bug during setting the title - toolbar.title = "" - - parentActivity.setSupportActionBar(toolbar) - parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true) - setHasOptionsMenu(true) + setDisplayHomeAsUpEnabled(true) val viewManager = SmoothScrollLinearLayoutManager(requireContext()) recyclerView.apply { @@ -180,7 +222,8 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate { val animationDuration = if (firstDisplay) 0L else 300L - when (currentSession.getState()) { + val state = currentSession.getState() + when (state) { SessionState.PENDING, SessionState.PLANNED -> { sessionMenu?.findItem(R.id.restart)?.isVisible = false floatingActionButton.setImageResource(R.drawable.ic_outline_play) @@ -188,6 +231,7 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate { floatingActionButton.animate().scaleX(1f).scaleY(1f).alpha(1f) .setDuration(animationDuration) .setInterpolator(OvershootInterpolator()).start() + handler.postDelayed(refreshTimer, Date().getNextMinuteInMilliseconds()) } SessionState.STARTED -> { sessionMenu?.findItem(R.id.restart)?.isVisible = true @@ -196,7 +240,7 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate { floatingActionButton.animate().scaleX(1f).scaleY(1f).alpha(1f) .setDuration(animationDuration) .setInterpolator(OvershootInterpolator()).start() - handler.postDelayed(refreshTimer, 60000) + handler.postDelayed(refreshTimer, Date().getNextMinuteInMilliseconds()) } SessionState.PAUSED -> { sessionMenu?.findItem(R.id.restart)?.isVisible = true @@ -263,6 +307,7 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate { when (currentSession.getState()) { SessionState.PENDING, SessionState.PLANNED, SessionState.PAUSED -> { currentSession.startOrContinue() + this.recyclerView.smoothScrollToPosition(0) } SessionState.STARTED -> { currentSession.pause() @@ -281,6 +326,13 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate { updateSessionUI() } + /** + * Add new custom field + */ + private fun addNewCustomField() { + EditableDataActivity.newInstanceForResult(this, LiveData.CUSTOM_FIELD, requestCode = REQUEST_CODE_NEW_CUSTOM_FIELD) + } + /** * Restart timer */ @@ -297,47 +349,6 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate { activity?.finish() } - /** - * Set fragment data - */ - fun setData(isTournament: Boolean, sessionId: String) { - - val realm = getRealm() - val sessionRealm = realm.where().equalTo("id", sessionId).findFirst() - if (sessionRealm != null) { - currentSession = sessionRealm - sessionHasBeenCustomized = true - } else { - realm.beginTransaction() - currentSession = Session.newInstance(realm, isTournament) - FavoriteSessionFinder.copyParametersFromFavoriteSession(currentSession, null, requireContext()) - realm.commitTransaction() - - // Find the nearest location around the user - parentActivity.findNearestLocation { location -> - location?.let { - realm.beginTransaction() - val location = realm.where().equalTo("id", it.id).findFirst() - FavoriteSessionFinder.copyParametersFromFavoriteSession(currentSession, location, requireContext()) - - currentSession.location = location - realm.commitTransaction() - updateSessionUI(true) - } - } - sessionHasBeenCustomized = false - } - - - toolbar.title = - if (currentSession.isTournament()) getString(R.string.tournament) else getString(R.string.cash_game) - - sessionAdapter = RowRepresentableAdapter(currentSession, this) - recyclerView.adapter = sessionAdapter - - updateSessionUI(true) - } - /** * Called when the user pressed back on the parent activity */ 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 d4cbd1b5..125fac2a 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 @@ -1,7 +1,9 @@ package net.pokeranalytics.android.ui.fragment import android.app.Activity +import android.content.ActivityNotFoundException import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -12,10 +14,10 @@ import kotlinx.android.synthetic.main.fragment_settings.* import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.R import net.pokeranalytics.android.model.realm.Session +import net.pokeranalytics.android.ui.activity.BillingActivity import net.pokeranalytics.android.ui.activity.CurrenciesActivity import net.pokeranalytics.android.ui.activity.DataListActivity import net.pokeranalytics.android.ui.activity.GDPRActivity -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource @@ -28,6 +30,9 @@ import net.pokeranalytics.android.ui.view.rowrepresentable.SettingRow import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.URL import net.pokeranalytics.android.util.UserDefaults +import net.pokeranalytics.android.util.billing.AppGuard +import net.pokeranalytics.android.util.billing.IAPProducts +import timber.log.Timber import java.util.* @@ -56,7 +61,6 @@ class SettingsFragment : PokerAnalyticsFragment(), RowRepresentableDelegate, Sta } private lateinit var settingsAdapterRow: RowRepresentableAdapter - private lateinit var parentActivity: PokerAnalyticsActivity override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_settings, container, false) @@ -90,6 +94,7 @@ class SettingsFragment : PokerAnalyticsFragment(), RowRepresentableDelegate, Sta override fun stringForRow(row: RowRepresentable): String { return when (row) { + SettingRow.SUBSCRIPTION -> AppGuard.subscriptionStatus(requireContext()) SettingRow.VERSION -> BuildConfig.VERSION_NAME + if (BuildConfig.DEBUG) " (${BuildConfig.VERSION_CODE}) DEBUG" else "" SettingRow.CURRENCY -> UserDefaults.currency.symbol else -> "" @@ -98,25 +103,32 @@ class SettingsFragment : PokerAnalyticsFragment(), RowRepresentableDelegate, Sta override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { when (row) { - SettingRow.RATE_APP -> parentActivity.openPlayStorePage() - SettingRow.CONTACT_US -> parentActivity.openContactMail(R.string.contact) - SettingRow.BUG_REPORT -> parentActivity.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path) + SettingRow.SUBSCRIPTION -> { + if (!AppGuard.isProUser) { + BillingActivity.newInstance(requireContext()) + } else { + this.openPlaystoreAccount() + } + } + SettingRow.RATE_APP -> parentActivity?.openPlayStorePage() + SettingRow.CONTACT_US -> parentActivity?.openContactMail(R.string.contact) + SettingRow.BUG_REPORT -> parentActivity?.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path) SettingRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, REQUEST_CODE_CURRENCY) SettingRow.FOLLOW_US -> { when (position) { - 0 -> parentActivity.openUrl(URL.BLOG.value) - 1 -> parentActivity.openUrl(URL.INSTAGRAM.value) - 2 -> parentActivity.openUrl(URL.TWITTER.value) - 3 -> parentActivity.openUrl(URL.FACEBOOK.value) + 0 -> parentActivity?.openUrl(URL.BLOG.value) + 1 -> parentActivity?.openUrl(URL.INSTAGRAM.value) + 2 -> parentActivity?.openUrl(URL.TWITTER.value) + 3 -> parentActivity?.openUrl(URL.FACEBOOK.value) } } - SettingRow.PRIVACY_POLICY -> parentActivity.openUrl(URL.PRIVACY_POLICY.value) - SettingRow.TERMS_OF_USE -> parentActivity.openUrl(URL.TERMS.value) + SettingRow.PRIVACY_POLICY -> parentActivity?.openUrl(URL.PRIVACY_POLICY.value) + SettingRow.TERMS_OF_USE -> parentActivity?.openUrl(URL.TERMS.value) SettingRow.GDPR -> openGDPRActivity() } row.relatedResultsRepresentable?.let { - DataListActivity.newInstance(requireContext(), (row as SettingRow).ordinal) + DataListActivity.newInstance(requireContext(), it.ordinal) } } @@ -125,11 +137,7 @@ class SettingsFragment : PokerAnalyticsFragment(), RowRepresentableDelegate, Sta */ private fun initUI() { - parentActivity = activity as PokerAnalyticsActivity - -// parentActivity.setSupportActionBar(toolbar) -// parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true) -// setHasOptionsMenu(true) + setDisplayHomeAsUpEnabled(true) val viewManager = LinearLayoutManager(requireContext()) settingsAdapterRow = RowRepresentableAdapter( @@ -158,4 +166,19 @@ class SettingsFragment : PokerAnalyticsFragment(), RowRepresentableDelegate, Sta startActivity(intent) } + /** + * Open Google Play account + */ + private fun openPlaystoreAccount() { + + val packageName = "net.pokeranalytics.android" + val sku = IAPProducts.PRO.identifier + + try { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/account/subscriptions?sku=$sku&package=$packageName"))) + } catch (e: ActivityNotFoundException) { + Timber.d(e) + } + } + } \ No newline at end of file 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 42eb98ec..dfcc828a 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 @@ -1,75 +1,116 @@ package net.pokeranalytics.android.ui.fragment +import android.app.Activity +import android.content.Intent import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import io.realm.Realm +import io.realm.RealmModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.launch +import net.pokeranalytics.android.R import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.ComputableGroup import net.pokeranalytics.android.calculus.Report import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.QueryCondition -import net.pokeranalytics.android.ui.view.RowRepresentable -import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable -import net.pokeranalytics.android.ui.view.rowrepresentable.StatRow +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.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 timber.log.Timber import java.util.* import kotlin.coroutines.CoroutineContext -class StatisticsFragment : TableReportFragment() { +class StatisticsFragment : FilterableFragment() { - override val coroutineContext: CoroutineContext + val coroutineContext: CoroutineContext get() = Dispatchers.Main - private var stringAll = "" - private var stringCashGame = "" - private var stringTournament = "" - - companion object { - - /** - * Create new instance - */ - fun newInstance(report: Report? = null): StatisticsFragment { - val fragment = StatisticsFragment() - report?.let { - fragment.report = it - } - val bundle = Bundle() - fragment.arguments = bundle - return fragment - } - } + private lateinit var tableReportFragment: ComposableTableReportFragment + + companion object { + + /** + * Create new instance + */ + fun newInstance(): StatisticsFragment { + val fragment = StatisticsFragment() + val bundle = Bundle() + fragment.arguments = bundle + return fragment + } + } - // Life Cycle + // Life Cycle + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_stats, container, false) + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - launchStatComputation() + initUI() + this.currentFilterable = FilterableType.SESSION + applyFilter() } - override fun sessionsChanged() { + private fun initUI() { + val fragmentTransaction = requireFragmentManager().beginTransaction() + val fragment = ComposableTableReportFragment.newInstance(null) + fragmentTransaction.add(R.id.tableContainer, fragment) + fragmentTransaction.commit() + this.tableReportFragment = fragment + } + + override val observedEntities: List> = listOf(ComputableResult::class.java) + + override fun entitiesChanged(clazz: Class) { this.launchStatComputation() - this.statsAdapter?.notifyDataSetChanged() } - override fun convertReportIntoRepresentables(report: Report): ArrayList { - val rows: ArrayList = ArrayList() - report.results.forEach { result -> - rows.add(CustomizableRowRepresentable(title = result.group.name)) - result.group.stats?.forEach { stat -> - rows.add(StatRow(stat, result.computedStat(stat), result.group.name)) +// override fun convertReportIntoRepresentables(report: Report): ArrayList { +// val rows: ArrayList = ArrayList() +// report.results.forEach { result -> +// rows.add(CustomizableRowRepresentable(title = result.group.name)) +// result.group.stats?.forEach { stat -> +// rows.add(StatRow(stat, result.computedStat(stat), result.group.name)) +// } +// } +// return rows +// } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == FilterActivityRequestCode.CREATE_FILTER.ordinal && resultCode == Activity.RESULT_OK) { + data?.let { + this.saveFilter(this.requireContext(), it.getStringExtra(FiltersActivity.IntentKey.FILTER_ID.keyName)) } } - return rows } + // Filter Handler - // Business + override fun applyFilter() { + super.applyFilter() + this.launchStatComputation() + } + + override fun removeFilter() { + super.removeFilter() + this.launchStatComputation() + } + + // Business /** * Launch stat computation @@ -83,7 +124,10 @@ class StatisticsFragment : TableReportFragment() { Timber.d(">>> start...") val realm = Realm.getDefaultInstance() - report = createSessionGroupsAndStartCompute(realm) + + val report = createSessionGroupsAndStartCompute(realm) + tableReportFragment.report = report + realm.close() val e = Date() @@ -94,7 +138,7 @@ class StatisticsFragment : TableReportFragment() { test.await() if (!isDetached) { - showResults() + tableReportFragment.showResults() } } } @@ -104,6 +148,10 @@ class StatisticsFragment : TableReportFragment() { */ private fun createSessionGroupsAndStartCompute(realm: Realm): Report { + val filter: Filter? = this.currentFilter(this.requireContext(), realm)?.let { + if (it.filterableType == currentFilterable) { it } else { null } + } + val allStats: List = listOf( Stat.NET_RESULT, Stat.HOURLY_RATE, @@ -112,7 +160,9 @@ class StatisticsFragment : TableReportFragment() { Stat.AVERAGE_HOURLY_DURATION, Stat.HOURLY_DURATION ) - val allSessionGroup = ComputableGroup(Query(), allStats) + + val query = filter?.query ?: Query() + val allSessionGroup = ComputableGroup(query, allStats) val cgStats: List = listOf( Stat.NET_RESULT, @@ -125,10 +175,17 @@ class StatisticsFragment : TableReportFragment() { Stat.NUMBER_OF_GAMES, Stat.AVERAGE_BUYIN ) - val cgSessionGroup = ComputableGroup(Query(QueryCondition.IsCash), cgStats) + val cgSessionGroup = ComputableGroup(Query(QueryCondition.IsCash).merge(query), cgStats) val tStats: List = - listOf(Stat.NET_RESULT, Stat.HOURLY_RATE, Stat.ROI, Stat.WIN_RATIO, Stat.NUMBER_OF_GAMES, Stat.AVERAGE_BUYIN) - val tSessionGroup = ComputableGroup(Query(QueryCondition.IsTournament), tStats) + listOf( + Stat.NET_RESULT, + Stat.HOURLY_RATE, + Stat.ROI, + Stat.WIN_RATIO, + Stat.NUMBER_OF_GAMES, + Stat.AVERAGE_BUYIN + ) + val tSessionGroup = ComputableGroup(Query(QueryCondition.IsTournament).merge(query), tStats) Timber.d(">>>>> Start computations...") @@ -137,7 +194,7 @@ class StatisticsFragment : TableReportFragment() { computedStats.addAll(allStats) computedStats.addAll(cgStats) computedStats.addAll(tStats) - options.displayedStats = computedStats + options.stats = computedStats return Calculator.computeGroups(realm, listOf(allSessionGroup, cgSessionGroup, tSessionGroup), options) } 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 new file mode 100644 index 00000000..3f26b19d --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt @@ -0,0 +1,230 @@ +package net.pokeranalytics.android.ui.fragment + +import android.graphics.drawable.GradientDrawable +import android.os.Build +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.TypefaceSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.Toast +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.children +import androidx.core.view.setMargins +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.viewpager.widget.ViewPager +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.Purchase +import com.android.billingclient.api.SkuDetails +import com.android.billingclient.api.SkuDetailsResponseListener +import kotlinx.android.synthetic.main.fragment_subscription.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.extensions.px +import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment +import net.pokeranalytics.android.ui.fragment.components.ScreenSlidePageFragment +import net.pokeranalytics.android.util.Preferences +import net.pokeranalytics.android.util.billing.AppGuard +import net.pokeranalytics.android.util.billing.IAPProducts +import net.pokeranalytics.android.util.billing.PurchaseDelegate +import java.lang.ref.WeakReference +import java.time.Period + +class SubscriptionFragment : PokerAnalyticsFragment(), SkuDetailsResponseListener, PurchaseDelegate, ViewPager.OnPageChangeListener { + + companion object { + val parallax: Float = 64f.px + } + + private var pagerAdapter: ScreenSlidePagerAdapter? = null + private var selectedProduct: SkuDetails? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + this.showLoader(R.string.loading_please_wait) + if (!AppGuard.requestProducts(this)) { + this.hideLoader() + Toast.makeText(requireContext(), R.string.billingclient_unavailable, Toast.LENGTH_LONG).show() + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_subscription, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initUI() + } + + private fun initUI() { + + val upgradeString = requireContext().getString(R.string.pro_upgrade) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + + 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 (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 + + } else { + this.title.text = upgradeString + } + + // Pager + + // The pager adapter, which provides the pages to the view pager widget. + this.pagerAdapter = ScreenSlidePagerAdapter(requireFragmentManager()) + this.pager.adapter = pagerAdapter + this.pager.addOnPageChangeListener(this) + + this.purchase.isEnabled = false + this.purchase.setOnClickListener { + + this.selectedProduct?.let { + AppGuard.initiatePurchase(this.requireActivity(), it, this) + } ?: run { + throw IllegalStateException("Attempt to initiate purchase while no product has been chosen") + } + } + + val count = this.pager.adapter?.count ?: 0 + for (i in 1..count) { + val view = View(requireContext()) + view.background = requireContext().getDrawable(R.drawable.circle_green) + val layoutParam = LinearLayout.LayoutParams(10.px, 10.px) + layoutParam.setMargins(6.px) + this.pageIndicator.addView(view, layoutParam) + } + + this.updatePagerIndicators(0) + + } + + /** + * A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in + * sequence. + */ + private inner class ScreenSlidePagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { + + private var fragments: HashMap> = HashMap() + + private inner class FeatureDescriptor(var iconResId: Int, var titleResId: Int, var descResId: Int) + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + val fragment = super.instantiateItem(container, position) as ScreenSlidePageFragment + fragments[position] = WeakReference(fragment) + return super.instantiateItem(container, position) + } + + private val dataSource: List = listOf( + FeatureDescriptor(R.drawable.ic_baseline_all_inclusive, R.string.f_unlimited, R.string.f_unlimited_desc), + FeatureDescriptor(R.drawable.ic_baseline_wifi_off, R.string.f_offline, R.string.f_offline_desc), + FeatureDescriptor(R.drawable.ic_baseline_vpn_key, R.string.f_privacy, R.string.f_privacy_desc), + FeatureDescriptor(R.drawable.ic_baseline_email, R.string.f_support, R.string.f_support_desc) + ) + + override fun getCount(): Int = this.dataSource.size + override fun getItem(position: Int): Fragment { + val d = this.dataSource[position] + return ScreenSlidePageFragment(d.iconResId, d.titleResId, d.descResId) + } + + /** + * Return the fragment at [position] + */ + fun getFragment(position: Int): ScreenSlidePageFragment? { + if (fragments.contains(position)) { + return fragments[position]?.get() + } + return null + } + } + + // SkuDetailsResponseListener + override fun onSkuDetailsResponse(responseCode: Int, skuDetailsList: MutableList?) { + if (responseCode == BillingClient.BillingResponse.OK && skuDetailsList != null) { + this.hideLoader() + selectedProduct = skuDetailsList.first { it.sku == IAPProducts.PRO.identifier } + updateUI() + } + } + + private fun updateUI() { + this.selectedProduct?.let { + this.purchase.isEnabled = true + val perYearString = requireContext().getString(R.string.year_subscription) + val formattedPrice = it.price + " / " + perYearString + this.price.text = formattedPrice + + var freeTrialDays = 30 // initial, should be more, no less + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val p = Period.parse(it.freeTrialPeriod) + freeTrialDays = p.days + } + val formattedFreeTrial = + "$freeTrialDays " + requireContext().getString(R.string.days) + " " + requireContext().getString(R.string.free_trial) + this.freetrial.text = formattedFreeTrial + } + } + + // PurchaseDelegate + + override fun purchaseDidSucceed(purchase: Purchase) { + + // record purchase in preferences for troubleshooting / verification + val purchaseInfos = listOf(purchase.sku, purchase.orderId, purchase.purchaseToken) + Preferences.setString(Preferences.Keys.LATEST_PURCHASE, purchaseInfos.joinToString("/"), requireContext()) + + this.activity?.finish() + } + + // OnPageChangeListener + + override fun onPageScrollStateChanged(state: Int) {} + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + pagerAdapter?.getFragment(position)?.let { + it.updateViewsPosition(-positionOffset * parallax) + } + pagerAdapter?.getFragment(position + 1)?.let { + it.updateViewsPosition((1 - positionOffset) * parallax) + } + } + + override fun onPageSelected(position: Int) { + updatePagerIndicators(position) + } + + private fun updatePagerIndicators(position: Int) { + this.pageIndicator.children.forEachIndexed { index, view -> + val drawable = view.background + when (drawable) { + is GradientDrawable -> { + val color = if (position == index) R.color.white else R.color.quantum_grey + drawable.setColor(requireContext().getColor(color)) + } + else -> { + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/TableReportFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/TableReportFragment.kt deleted file mode 100644 index 71a1243b..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/TableReportFragment.kt +++ /dev/null @@ -1,207 +0,0 @@ -package net.pokeranalytics.android.ui.fragment - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.recyclerview.widget.LinearLayoutManager -import io.realm.Realm -import kotlinx.android.synthetic.main.fragment_stats.* -import kotlinx.coroutines.* -import net.pokeranalytics.android.R -import net.pokeranalytics.android.calculus.* -import net.pokeranalytics.android.model.realm.ComputableResult -import net.pokeranalytics.android.ui.activity.StatisticDetailsActivity -import net.pokeranalytics.android.ui.adapter.DisplayDescriptor -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.components.SessionObserverFragment -import net.pokeranalytics.android.ui.view.RowRepresentable -import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable -import net.pokeranalytics.android.ui.view.rowrepresentable.StatRow -import net.pokeranalytics.android.util.NULL_TEXT -import timber.log.Timber -import java.util.* -import kotlin.coroutines.CoroutineContext - -open class TableReportFragment : SessionObserverFragment(), StaticRowRepresentableDataSource, CoroutineScope, - RowRepresentableDelegate { - - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main - - private var rowRepresentables: ArrayList = ArrayList() - - var statsAdapter: RowRepresentableAdapter? = null - var report : Report? = null - - companion object { - - /** - * Create new instance - */ - fun newInstance(report: Report? = null): TableReportFragment { - val fragment = TableReportFragment() - report?.let { - fragment.report = it - } - val bundle = Bundle() - fragment.arguments = bundle - return fragment - } - } - - // Life Cycle - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_stats, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initData() - initUI() - - report?.let { - showResults() - } - } - - // Row Representable DS - - override fun adapterRows(): List? { - return this.rowRepresentables - } - - override fun contentDescriptorForRow(row: RowRepresentable): DisplayDescriptor? { - val dc = DisplayDescriptor() - dc.textFormat = TextFormat(NULL_TEXT) - if (row is StatRow) { - context?.let { _ -> - row.computedStat?.let { - dc.textFormat = it.format() - } - } - } - return dc - } - - override fun statFormatForRow(row: RowRepresentable): TextFormat { - if (row is StatRow) { - context?.let { _ -> - row.computedStat?.let { return it.format() } - } - } - return TextFormat(NULL_TEXT) - } - - override fun onResume() { - super.onResume() - statsAdapter?.notifyDataSetChanged() - } - - // Business - - /** - * Init data - */ - open fun initData() { - this.statsAdapter = RowRepresentableAdapter(this, this) - } - - /** - * Init UI - */ - open fun initUI() { - val viewManager = LinearLayoutManager(requireContext()) - recyclerView.apply { - setHasFixedSize(true) - layoutManager = viewManager - adapter = statsAdapter - } - } - - /** - * Show results - */ - fun showResults() { - report?.let { - this.rowRepresentables = this.convertReportIntoRepresentables(it) - statsAdapter?.notifyDataSetChanged() - } - } - - open fun convertReportIntoRepresentables(report: Report): ArrayList { - val rows: ArrayList = ArrayList() - report.options.displayedStats.forEach {stat -> - rows.add(CustomizableRowRepresentable(title = stat.localizedTitle(requireContext()))) - report.results.forEach { - val title = it.group.name - rows.add(StatRow(stat, it.computedStat(stat), it.group.name, title)) - } - - } - return rows - } - - // RowRepresentableDelegate - - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - - val cr = getRealm().where(ComputableResult::class.java).findAll() - if (cr.size < 2) { - Toast.makeText(context, R.string.less_then_2_values_for_display, Toast.LENGTH_LONG).show() - return - } - - if (row is StatRow && row.stat.hasEvolutionGraph) { - - // queryWith groups - val groupResults = this.report?.results?.filter { - it.group.name == row.groupName - } - - groupResults?.firstOrNull()?.let { - this.launchStatComputationWithEvolution(row.stat, it.group) - } - } - - } - - private fun launchStatComputationWithEvolution(stat: Stat, computableGroup: ComputableGroup) { - - showLoader() - - GlobalScope.launch(coroutineContext) { - - var report: Report? = null - val test = GlobalScope.async { - val s = Date() - Timber.d(">>> start...") - - val realm = Realm.getDefaultInstance() - - val aggregationType = stat.aggregationTypes.first() - report = Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, computableGroup, aggregationType) - - realm.close() - - val e = Date() - val duration = (e.time - s.time) / 1000.0 - Timber.d(">>> ended in $duration seconds") - - } - test.await() - - if (!isDetached) { - hideLoader() - report?.let { - StatisticDetailsActivity.newInstance(requireContext(), stat, computableGroup, it) - } - } - } - } - -} \ No newline at end of file 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 new file mode 100644 index 00000000..232a7d46 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/DeletableItemFragment.kt @@ -0,0 +1,147 @@ +package net.pokeranalytics.android.ui.fragment.components + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import com.google.android.material.snackbar.Snackbar +import io.realm.RealmObject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.interfaces.Deletable +import net.pokeranalytics.android.ui.activity.DataListActivity +import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter + +/** + * Deletable Item Fragment + * Don't forget to add a CoordinatorLayout at the top of your XML + * if you want to display correctly the snack bar + */ +abstract class DeletableItemFragment : RealmFragment() { + + companion object { + const val REQUEST_CODE_DELETION = 1000 + } + + lateinit var dataListAdapter: RowRepresentableAdapter + + private var deletedItem: RealmObject? = null + private var itemHasBeenReInserted: Boolean = false + private var lastDeletedItemPosition: Int = 0 + + private var mainLayout: ViewGroup? = null + private var snackBar: Snackbar? = null + + abstract fun deletableItems() : List + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + this.mainLayout = view.findViewById(R.id.mainLayout) + } + + override fun onPause() { + super.onPause() + snackBar?.dismiss() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE_DELETION && resultCode == Activity.RESULT_OK) { + + val itemToDeleteId = data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName) + itemToDeleteId?.let { id -> + GlobalScope.launch(Dispatchers.Main) { + delay(300) + deleteItem(dataListAdapter, deletableItems(), id) + } + } + } + } + + /** + * Delete item + * [dataListAdapter]: Adapter to update + * [items]: List of items which contains the element to delete + * [itemId]: Id of the item to delete + */ + fun deleteItem(dataListAdapter: RowRepresentableAdapter, items: List, itemId: String) { + + if (isDetached || activity == null) { + return + } + + this.dataListAdapter = dataListAdapter + + // Save the delete position & create a copy of the object + val itemPosition = items.indexOfFirst { it.id == itemId } + val itemToDelete = items.find { it.id == itemId } + + if (itemToDelete is RealmObject && itemPosition != -1) { + + // Check if the object is valid for the deletion + if (itemToDelete.isValidForDelete(this.getRealm())) { + deletedItem = getRealm().copyFromRealm(itemToDelete) + lastDeletedItemPosition = itemPosition + getRealm().executeTransaction { + itemToDelete.deleteDependencies() + itemToDelete.deleteFromRealm() + } + itemHasBeenReInserted = false + updateUIAfterDeletion(itemPosition) + showUndoSnackBar() + } else { + dataListAdapter.notifyItemChanged(itemPosition) + val status = itemToDelete.getDeleteStatus(requireContext(), this.getRealm()) + val message = itemToDelete.getFailedDeleteMessage(status) + val builder = AlertDialog.Builder(requireContext()) + .setMessage(message) + .setNegativeButton(R.string.ok, null) + builder.show() + } + } + } + + /** + * Show undo snack bar + */ + private fun showUndoSnackBar() { + val message = String.format(getString(R.string.data_deleted)) + this.mainLayout?.let { view -> + snackBar = Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE) + snackBar?.setAction(R.string.cancel) { + if (!itemHasBeenReInserted) { + itemHasBeenReInserted = true + getRealm().executeTransaction { realm -> + deletedItem?.let { + val item = realm.copyToRealmOrUpdate(it) + updateUIAfterUndoDeletion(item) + } + } + } + } + snackBar?.show() + } ?: run { + throw IllegalStateException("mainLayout is not defined") + } + } + + /** + * Called once the object has been deleted + */ + open fun updateUIAfterDeletion(itemPosition: Int) { + dataListAdapter.notifyItemRemoved(itemPosition) + } + + /** + * Called once the object has been restored + */ + open fun updateUIAfterUndoDeletion(newItem: RealmObject) { + dataListAdapter.notifyItemInserted(lastDeletedItemPosition) + } + +} \ No newline at end of file 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 new file mode 100644 index 00000000..fb6dd3a8 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/FilterableFragment.kt @@ -0,0 +1,159 @@ +package net.pokeranalytics.android.ui.fragment.components + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.view.* +import androidx.appcompat.widget.Toolbar +import kotlinx.android.synthetic.main.view_selected_filter.view.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +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.util.Preferences +import timber.log.Timber + + +/** + * A class which define the fragment as Filterable + * - Add a filter icon menu + * - Listen for INTENT_FILTER_UPDATE_FILTER_UI + * - + */ +open class FilterableFragment : RealmFragment(), FilterHandler { + + override var currentFilterable: FilterableType = FilterableType.ALL + set(value) { + field = value + this.currentFilter(this.requireContext(), getRealm())?.let { + if (this.shouldHideCurrentFilter(it)) { + hideSelectedFilter() + } else { + displaySelectedFilter() + } + } + } + + private var filterMenuItem: MenuItem? = null + + private val updateFilterUIBroadcast = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + context?.let { + if (Preferences.getActiveFilterId(context)?.isNotEmpty() == true) { + applyFilter() + } else { + removeFilter() + } + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + parentActivity?.registerReceiver(updateFilterUIBroadcast, IntentFilter(INTENT_FILTER_UPDATE_FILTER_UI)) + } + + override fun onDestroy() { + super.onDestroy() + parentActivity?.unregisterReceiver(updateFilterUIBroadcast) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + setHasOptionsMenu(true) + return super.onCreateView(inflater, container, savedInstanceState) + } + + 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) + filterMenuItem = toolbar.menu?.add(0, R.id.menu_item_filter, 0, R.string.filter) + filterMenuItem?.setIcon(R.drawable.ic_outline_filter_list) + filterMenuItem?.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM) + } + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_item_filter -> { + manageFilters(this) + } + } + return super.onOptionsItemSelected(item) + } + + override fun applyFilter() { + updateFilterUI() + } + + override fun removeFilter() { + hideSelectedFilter() + } + + /** + * Show or hide the filter UI + */ + private fun updateFilterUI() { + if (Preferences.getActiveFilterId(requireContext())?.isNotEmpty() == true) { + displaySelectedFilter() + } else { + hideSelectedFilter() + } + } + + /** + * Display the filter UI + */ + private fun displaySelectedFilter() { + + currentFilter(requireContext(), getRealm())?.let { filter -> + if (this.shouldHideCurrentFilter(filter)) { + return + } + view?.findViewById(R.id.selectedFilter)?.let { viewGroup -> + + val layoutCurrentFilter = LayoutInflater.from(requireContext()).inflate(R.layout.view_selected_filter, viewGroup, false) + layoutCurrentFilter.filterName.text = filter.getDisplayName(requireContext()) + layoutCurrentFilter.deselectFilter.setOnClickListener { + saveFilter(requireContext(), "") + } + + viewGroup.removeAllViews() + viewGroup.addView(layoutCurrentFilter) + + GlobalScope.launch(Dispatchers.Main) { + delay(300) + viewGroup.visibility = View.VISIBLE + } + } + } + } + + /** + * Hide the filter UI + */ + private fun hideSelectedFilter() { + view?.findViewById(R.id.selectedFilter).let { + GlobalScope.launch(Dispatchers.Main) { + it?.visibility = View.GONE + } + } + } + + /** + * Check if the current filter's header should be displayed + */ + private fun shouldHideCurrentFilter(currentFilter: Filter? = null): Boolean { + currentFilter?.let { + return it.filterableType != this.currentFilterable + } + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/PokerAnalyticsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/PokerAnalyticsFragment.kt index 12a92fa0..3bbf1d13 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/PokerAnalyticsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/PokerAnalyticsFragment.kt @@ -1,41 +1,63 @@ package net.pokeranalytics.android.ui.fragment.components import android.os.Bundle +import android.view.View +import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment -import io.realm.Realm +import com.crashlytics.android.Crashlytics +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity -open class PokerAnalyticsFragment: Fragment() { +open class PokerAnalyticsFragment : Fragment() { - private var realm: Realm? = null private var loaderDialogFragment: LoaderDialogFragment? = null + var parentActivity: PokerAnalyticsActivity? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - realm = Realm.getDefaultInstance() - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Crashlytics.log("$this.localClassName onCreate") + } - override fun onDestroyView() { - super.onDestroyView() - this.realm?.close() - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUI() + } - /** - * Get the realm instance - */ - fun getRealm(): Realm { - this.realm?.let { - return it - } ?: run { - val realm = Realm.getDefaultInstance() - this.realm = realm - return realm - } + override fun onResume() { + super.onResume() + Crashlytics.log("$this.localClassName onResume") + } + + override fun onPause() { + super.onPause() + Crashlytics.log("$this.localClassName onPause") + } + + override fun onDestroy() { + super.onDestroy() + Crashlytics.log("$this.localClassName onDestroy") } /** * Method called when the activity override onBackPressed and send the information to the fragment */ - open fun onBackPressed(){} + open fun onBackPressed() {} + + /** + * Init UI + */ + private fun initUI() { + + setHasOptionsMenu(true) + + parentActivity = activity as PokerAnalyticsActivity + + // Set the toolbar to the parent activity + + view?.findViewById(R.id.toolbar)?.let { toolbar -> + parentActivity?.setSupportActionBar(toolbar) + } + } /** * Show the loader @@ -55,4 +77,21 @@ open class PokerAnalyticsFragment: Fragment() { loaderDialogFragment = null } + /** + * Set the toolbar title + */ + fun setToolbarTitle(title: String?) { + view?.findViewById(R.id.toolbar)?.let { toolbar -> + toolbar.title = title + } + parentActivity?.supportActionBar?.title = title + } + + /** + * Display or not the back arrow icon + */ + fun setDisplayHomeAsUpEnabled(enabled: Boolean) { + parentActivity?.supportActionBar?.setDisplayHomeAsUpEnabled(enabled) + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/RealmFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/RealmFragment.kt new file mode 100644 index 00000000..8c583927 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/RealmFragment.kt @@ -0,0 +1,66 @@ +package net.pokeranalytics.android.ui.fragment.components + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import io.realm.Realm +import io.realm.RealmModel +import io.realm.RealmResults + +open class RealmFragment : PokerAnalyticsFragment() { + + /** + * A realm instance + */ + private lateinit var realm: Realm + + /** + * A List of observed RealmResults + */ + private var observedRealmResults: MutableList> = mutableListOf() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + realm = Realm.getDefaultInstance() + + this.observedEntities.forEach { + val realmResults = realm.where(it).findAll() + + realmResults.addChangeListener { _,_ -> + this.entitiesChanged(it) + } + + this.observedRealmResults.add(realmResults) + } + + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onDestroyView() { + super.onDestroyView() + + this.observedRealmResults.forEach { + it.removeAllChangeListeners() + } + + this.realm.close() + } + + /** + * Get the realm instance + */ + fun getRealm(): Realm { + return this.realm + } + + /** + * A list of RealmModel classes to observe + */ + open val observedEntities: List> = listOf() + + /** + * The method called when a change happened in any RealmResults + */ + open fun entitiesChanged(clazz: Class) {} + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/ScreenSlidePageFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/ScreenSlidePageFragment.kt new file mode 100644 index 00000000..534fed12 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/ScreenSlidePageFragment.kt @@ -0,0 +1,37 @@ +package net.pokeranalytics.android.ui.fragment.components + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import kotlinx.android.synthetic.main.fragment_screen_slide_page.* +import net.pokeranalytics.android.R + + +class ScreenSlidePageFragment(var iconResId: Int, var titleResId: Int, var descriptionResId: Int) : PokerAnalyticsFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_screen_slide_page, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + this.icon.setImageResource(this.iconResId) + this.title.text = requireContext().getString(this.titleResId) + this.description.text = requireContext().getString(this.descriptionResId) + } + + /** + * Update views position + */ + fun updateViewsPosition(position: Float) { + view?.findViewById(R.id.title)?.translationX = position + view?.findViewById(R.id.icon)?.translationX = position + view?.findViewById(R.id.description)?.translationX = position * 2f + + } + +} diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/SessionObserverFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/SessionObserverFragment.kt deleted file mode 100644 index 63e688a6..00000000 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/SessionObserverFragment.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.pokeranalytics.android.ui.fragment.components - -import android.os.Bundle -import io.realm.RealmResults -import net.pokeranalytics.android.model.realm.ComputableResult - -open class SessionObserverFragment : PokerAnalyticsFragment() { - - private lateinit var endedSessions: RealmResults - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - this.endedSessions = getRealm().where(ComputableResult::class.java).findAll() // ComputableResult are existing only if sessions are ended - this.endedSessions.addChangeListener { _, _ -> - this.sessionsChanged() - } - } - - override fun onDestroyView() { - super.onDestroyView() - endedSessions.removeAllChangeListeners() - } - - open fun sessionsChanged() { - // to override - } - -} \ 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 38014ea2..74c8d6b8 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 @@ -27,7 +27,6 @@ class BottomSheetDoubleEditTextFragment : BottomSheetFragment() { override fun onStart() { super.onStart() - if (isEditingBlinds) { editText2.requestFocus() } else { @@ -43,6 +42,7 @@ class BottomSheetDoubleEditTextFragment : BottomSheetFragment() { * Init data */ private fun initData() { + valueHasPlaceholder = true isEditingBlinds = row == SessionRow.BLINDS } @@ -64,17 +64,23 @@ class BottomSheetDoubleEditTextFragment : BottomSheetFragment() { values[0] = (data[0].defaultValue ?: "").toString() values[1] = (data[1].defaultValue ?: "").toString() - editText1.setText(values[0]) - editText2.setText(values[1]) - data[0].hint?.let { editText1.hint = getString(it) } editText1.inputType = data[0].inputType ?: InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + data[1].hint?.let { editText2.hint = getString(it) } + editText2.inputType = data[1].inputType ?: InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + + if (valueHasPlaceholder) { + editText1.hint = values[0] + editText2.hint = values[1] + } else { + editText1.setText(values[0]) + editText2.setText(values[1]) + } + editText1.addTextChangedListener { values[0] = it?.toString() ?: "" } - data[1].hint?.let { editText2.hint = getString(it) } - editText2.inputType = data[1].inputType ?: InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES editText2.addTextChangedListener { values[1] = it?.toString() ?: "" if (isEditingBlinds) { 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 d742fbd0..537da59b 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 @@ -43,12 +43,14 @@ class BottomSheetEditTextFragment : BottomSheetFragment() { * Init data */ private fun initData() { + valueHasPlaceholder = true } /** * Init UI */ private fun initUI() { + val data = getData()?:throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor not found") if (data.size != 1) { throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor inconsistency") @@ -62,7 +64,12 @@ class BottomSheetEditTextFragment : BottomSheetFragment() { this.value = it?.toString() } data[0].defaultValue?.let { - editText1.setText(it.toString()) + if (valueHasPlaceholder) { + this.value = it.toString() + editText1.hint = it.toString() + } else { + editText1.setText(it.toString()) + } } editText1.setOnEditorActionListener { _, actionId, _ -> 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 f883c7f2..8c33bf13 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 @@ -12,7 +12,7 @@ import android.view.WindowManager import androidx.appcompat.view.ContextThemeWrapper import androidx.fragment.app.FragmentManager import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import io.realm.RealmObject +import io.realm.RealmModel import kotlinx.android.synthetic.main.fragment_bottom_sheet.* import net.pokeranalytics.android.R import net.pokeranalytics.android.model.LiveData @@ -30,8 +30,10 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { lateinit var row: RowRepresentable lateinit var delegate: RowRepresentableDelegate var currentCurrency: Currency? = null + var valueHasPlaceholder: Boolean = false private var isClearable: Boolean = true + private var isDeletable: Boolean = false private var rowRepresentableEditDescriptors: ArrayList? = null companion object { @@ -44,7 +46,9 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { delegate: RowRepresentableDelegate, rowRepresentableEditDescriptors: ArrayList?, isClearable: Boolean? = true, - currentCurrency: Currency? = null + currentCurrency: Currency? = null, + isDeletable: Boolean? = false, + valueHasPlaceholder: Boolean? = false ): BottomSheetFragment { val bottomSheetFragment = row.bottomSheetType.newInstance() bottomSheetFragment.show(fragmentManager, "bottomSheet") @@ -52,6 +56,8 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { bottomSheetFragment.delegate = delegate bottomSheetFragment.rowRepresentableEditDescriptors = rowRepresentableEditDescriptors bottomSheetFragment.isClearable = isClearable ?: true + bottomSheetFragment.isDeletable = isDeletable ?: true + bottomSheetFragment.valueHasPlaceholder = valueHasPlaceholder ?: true bottomSheetFragment.currentCurrency = currentCurrency return bottomSheetFragment } @@ -79,7 +85,7 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { val primaryKey = data.getStringExtra(EditableDataActivity.IntentKey.PRIMARY_KEY.keyName) val pokerAnalyticsActivity = activity as PokerAnalyticsActivity val liveDataType = LiveData.values()[dataType] - val proxyItem: RealmObject? = liveDataType.getData(pokerAnalyticsActivity.getRealm(), primaryKey) + val proxyItem: RealmModel? = liveDataType.getData(pokerAnalyticsActivity.getRealm(), primaryKey) this.delegate.onRowValueChanged(proxyItem, this.row) dismiss() } @@ -111,39 +117,30 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { dismiss() true } + + bottomSheetToolbar.menu.findItem(R.id.actionDelete).setOnMenuItemClickListener { + delegate.onRowDeleted(row) + dismiss() + true + } + bottomSheetToolbar.menu.findItem(R.id.actionAdd).setOnMenuItemClickListener { - when (row) { - SessionRow.GAME -> EditableDataActivity.newInstanceForResult( - this, - LiveData.GAME.ordinal, - requestCode = REQUEST_CODE_ADD_NEW_OBJECT - ) - SessionRow.BANKROLL, TransactionRow.BANKROLL -> EditableDataActivity.newInstanceForResult( - this, - LiveData.BANKROLL.ordinal, - requestCode = REQUEST_CODE_ADD_NEW_OBJECT - ) - SessionRow.LOCATION -> EditableDataActivity.newInstanceForResult( - this, - LiveData.LOCATION.ordinal, - requestCode = REQUEST_CODE_ADD_NEW_OBJECT - ) - SessionRow.TOURNAMENT_NAME -> EditableDataActivity.newInstanceForResult( - this, - LiveData.TOURNAMENT_NAME.ordinal, - requestCode = REQUEST_CODE_ADD_NEW_OBJECT - ) - SessionRow.TOURNAMENT_FEATURE -> EditableDataActivity.newInstanceForResult( - this, - LiveData.TOURNAMENT_FEATURE.ordinal, - requestCode = REQUEST_CODE_ADD_NEW_OBJECT - ) - TransactionRow.TYPE -> EditableDataActivity.newInstanceForResult( - this, - LiveData.TRANSACTION_TYPE.ordinal, - requestCode = REQUEST_CODE_ADD_NEW_OBJECT - ) + val liveData = when (row) { + SessionRow.GAME -> LiveData.GAME + SessionRow.BANKROLL, TransactionRow.BANKROLL -> LiveData.BANKROLL + SessionRow.LOCATION -> LiveData.LOCATION + SessionRow.TOURNAMENT_NAME -> LiveData.TOURNAMENT_NAME + SessionRow.TOURNAMENT_FEATURE -> LiveData.TOURNAMENT_FEATURE + TransactionRow.TYPE -> LiveData.TRANSACTION_TYPE + else -> throw IllegalStateException("row $row does not have an associated LiveData value") } + + EditableDataActivity.newInstanceForResult( + this, + liveData, + requestCode = REQUEST_CODE_ADD_NEW_OBJECT + ) + true } bottomSheetToolbar.menu.findItem(R.id.actionCheck).setOnMenuItemClickListener { @@ -153,6 +150,7 @@ open class BottomSheetFragment : BottomSheetDialogFragment() { } bottomSheetToolbar.menu.findItem(R.id.actionClear).isVisible = isClearable + bottomSheetToolbar.menu.findItem(R.id.actionDelete).isVisible = isDeletable } } 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 6eaee3b8..eeece10e 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 @@ -18,7 +18,11 @@ import net.pokeranalytics.android.ui.view.RowViewType open class BottomSheetListFragment : BottomSheetFragment(), LiveRowRepresentableDataSource, RowRepresentableDelegate { lateinit var dataAdapter: RowRepresentableAdapter - var realmData: RealmResults<*>? = null + var realmData: RealmResults? = null + + override fun adapterRows(): List? { + return this.realmData + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -49,13 +53,6 @@ open class BottomSheetListFragment : BottomSheetFragment(), LiveRowRepresentable return RowViewType.BOTTOM_SHEET_DATA.ordinal } - override fun indexForRow(row: RowRepresentable): Int { - realmData?.let { - return it.indexOf(row) - } - throw IllegalStateException("Need to implement Data Source") - } - override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { realmData?.let { val selectedData = it[position] @@ -78,7 +75,7 @@ open class BottomSheetListFragment : BottomSheetFragment(), LiveRowRepresentable if (bottomSheetData.first().data == null) { throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor inconsistency") } - this.realmData = bottomSheetData.first().data as RealmResults<*> + this.realmData = bottomSheetData.first().data as RealmResults } /** 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 3782f246..9c3b7ca8 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 @@ -6,15 +6,14 @@ import android.view.View import androidx.core.view.get import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.chip.Chip -import io.realm.RealmResults import kotlinx.android.synthetic.main.bottom_sheet_game_list.* import kotlinx.android.synthetic.main.fragment_bottom_sheet.view.* import net.pokeranalytics.android.R import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter -import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.extensions.px +import net.pokeranalytics.android.ui.view.RowRepresentable /** * Bottom Sheet List Game Fragment @@ -58,7 +57,7 @@ class BottomSheetListGameFragment : BottomSheetListFragment() { throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor inconsistency") } this.limit = bottomSheetData[0].defaultValue as Int? - this.realmData = bottomSheetData[1].data as RealmResults<*> + this.realmData = bottomSheetData[1].data } /** 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 42872569..7bbe3c9e 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 @@ -3,7 +3,7 @@ package net.pokeranalytics.android.ui.fragment.components.bottomsheet import android.app.Activity import android.content.Intent import io.realm.RealmList -import io.realm.RealmObject +import io.realm.RealmModel import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.ui.activity.EditableDataActivity @@ -28,7 +28,7 @@ open class BottomSheetMultiSelectionFragment : BottomSheetListFragment() { val primaryKey = data.getStringExtra(EditableDataActivity.IntentKey.PRIMARY_KEY.keyName) val pokerAnalyticsActivity = activity as PokerAnalyticsActivity val liveDataType = LiveData.values()[dataType] - val proxyItem: RealmObject? = liveDataType.getData(pokerAnalyticsActivity.getRealm(), primaryKey) + val proxyItem: RealmModel? = liveDataType.getData(pokerAnalyticsActivity.getRealm(), primaryKey) selectedRows.add(proxyItem as RowRepresentable) dataAdapter.refreshRow(proxyItem as RowRepresentable) } @@ -53,14 +53,15 @@ open class BottomSheetMultiSelectionFragment : BottomSheetListFragment() { override fun initData() { super.initData() - val bottomSheetData = getData()?:throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor not found") + val bottomSheetData = + getData() ?: throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor not found") if (bottomSheetData.size != 1) { throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor inconsistency") } bottomSheetData.first().defaultValue?.let { - (it as RealmList<*>).forEach{row -> - this.selectedRows.add(row as RowRepresentable) - } + (it as RealmList<*>).forEach { row -> + this.selectedRows.add(row as RowRepresentable) + } } } } \ No newline at end of file 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 da8f8510..a5213f7b 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,6 +36,7 @@ class BottomSheetNumericTextFragment : BottomSheetFragment() { * Init data */ private fun initData() { + valueHasPlaceholder = true } /** @@ -61,13 +62,20 @@ class BottomSheetNumericTextFragment : BottomSheetFragment() { } data[0].defaultValue?.let { - if (it is Double || it is Long) { + val valueString = if (it is Double || it is Long) { val formatter = NumberFormat.getNumberInstance() formatter.maximumFractionDigits = 6 formatter.isGroupingUsed = false - editText1.setText(formatter.format(it)) + formatter.format(it) } else { - editText1.setText(it.toString()) + it.toString() + } + + if (valueHasPlaceholder) { + this.value = it.toString().toDoubleOrNull() + editText1.hint = valueString + } else { + editText1.setText(valueString) } } 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 a60f51f0..7af3f1da 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 @@ -8,7 +8,6 @@ import kotlinx.android.synthetic.main.bottom_sheet_list.* import kotlinx.android.synthetic.main.fragment_bottom_sheet.view.* import net.pokeranalytics.android.R import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException -import net.pokeranalytics.android.model.TournamentType import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource @@ -36,7 +35,7 @@ class BottomSheetStaticListFragment : BottomSheetFragment(), StaticRowRepresenta } override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { - this.delegate.onRowValueChanged((row as TournamentType).ordinal, this.row) + this.delegate.onRowValueChanged(row, this.row) dismiss() super.onRowSelected(position, row, fromAction) } 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 81684a46..a672e2c3 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 @@ -12,6 +12,7 @@ import net.pokeranalytics.android.R import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException import net.pokeranalytics.android.util.extensions.round import net.pokeranalytics.android.util.extensions.toCurrency +import java.text.NumberFormat class BottomSheetSumFragment : BottomSheetFragment() { @@ -63,7 +64,10 @@ class BottomSheetSumFragment : BottomSheetFragment() { 0.0 } - currentValue.text = currentDefaultValue.toCurrency(currentCurrency) + val formatter = NumberFormat.getNumberInstance() + formatter.maximumFractionDigits = 6 + formatter.isGroupingUsed = false + currentValue.text = formatter.format(currentDefaultValue) // First value val defaultValue1 = try { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetType.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetType.kt index 41ce26f5..447453b0 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetType.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetType.kt @@ -1,33 +1,48 @@ package net.pokeranalytics.android.ui.fragment.components.bottomsheet enum class BottomSheetType { - NONE { override fun newInstance() = BottomSheetFragment()}, - LIST { override fun newInstance() = BottomSheetListFragment()}, - LIST_STATIC { override fun newInstance() = BottomSheetStaticListFragment()}, - LIST_GAME { override fun newInstance() = BottomSheetListGameFragment()}, - DOUBLE_LIST { override fun newInstance() = BottomSheetListGameFragment()}, - MULTI_SELECTION { override fun newInstance() = BottomSheetMultiSelectionFragment()}, - GRID { override fun newInstance() = BottomSheetTableSizeGridFragment()}, - EDIT_TEXT { override fun newInstance() = BottomSheetEditTextFragment()}, - EDIT_TEXT_MULTI_LINES { override fun newInstance() = BottomSheetEditTextMultiLinesFragment()}, - DOUBLE_EDIT_TEXT { override fun newInstance() = BottomSheetDoubleEditTextFragment()}, - NUMERIC_TEXT { override fun newInstance() = BottomSheetNumericTextFragment()}, - SUM { override fun newInstance() = BottomSheetSumFragment()}; + NONE, + LIST, + LIST_STATIC, + LIST_GAME, + DOUBLE_LIST, + MULTI_SELECTION, + GRID, + EDIT_TEXT, + EDIT_TEXT_MULTI_LINES, + DOUBLE_EDIT_TEXT, + NUMERIC_TEXT, + SUM; - abstract fun newInstance(): BottomSheetFragment + fun newInstance(): BottomSheetFragment { + return when (this) { + NONE -> BottomSheetFragment() + LIST -> BottomSheetListFragment() + LIST_STATIC -> BottomSheetStaticListFragment() + LIST_GAME -> BottomSheetListGameFragment() + DOUBLE_LIST -> BottomSheetListGameFragment() + MULTI_SELECTION -> BottomSheetMultiSelectionFragment() + GRID -> BottomSheetTableSizeGridFragment() + EDIT_TEXT -> BottomSheetEditTextFragment() + EDIT_TEXT_MULTI_LINES -> BottomSheetEditTextMultiLinesFragment() + DOUBLE_EDIT_TEXT -> BottomSheetDoubleEditTextFragment() + NUMERIC_TEXT -> BottomSheetNumericTextFragment() + SUM -> BottomSheetSumFragment() + } + } - val validationRequired : Boolean - get() = when (this) { - LIST, LIST_GAME, LIST_STATIC, GRID, DOUBLE_LIST -> false - else -> true - } + val validationRequired: Boolean + get() = when (this) { + LIST, LIST_GAME, LIST_STATIC, GRID, DOUBLE_LIST -> false + else -> true + } - val clearRequired : Boolean - get() = true + val clearRequired: Boolean + get() = true - val addRequired : Boolean - get() = when (this) { - EDIT_TEXT, NUMERIC_TEXT, DOUBLE_EDIT_TEXT, EDIT_TEXT_MULTI_LINES, GRID, LIST_STATIC, SUM -> false - else -> true - } + val addRequired: Boolean + get() = when (this) { + EDIT_TEXT, NUMERIC_TEXT, DOUBLE_EDIT_TEXT, EDIT_TEXT_MULTI_LINES, GRID, LIST_STATIC, SUM -> false + else -> true + } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/BankrollDataFragment.kt similarity index 93% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollDataFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/fragment/data/BankrollDataFragment.kt index 7f15c747..eb790102 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/BankrollDataFragment.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.fragment +package net.pokeranalytics.android.ui.fragment.data import android.app.Activity.RESULT_OK import android.content.Intent @@ -11,6 +11,7 @@ import net.pokeranalytics.android.model.retrofit.CurrencyConverterValue import net.pokeranalytics.android.ui.activity.CurrenciesActivity import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource +import net.pokeranalytics.android.ui.fragment.CurrenciesFragment import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.RowViewType @@ -96,7 +97,11 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS } } BankrollRow.INITIAL_VALUE -> { - this.bankroll.initialValue.toCurrency() + var c: Currency? = null + this.bankroll.currency?.code?.let { + c = Currency.getInstance(it) + } + this.bankroll.initialValue.toCurrency(c) } BankrollRow.RATE -> { val rate = this.bankroll.currency?.rate ?: 1.0 @@ -130,7 +135,9 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { when (row) { - BankrollRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@BankrollDataFragment, REQUEST_CODE_CURRENCY) + BankrollRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@BankrollDataFragment, + REQUEST_CODE_CURRENCY + ) BankrollRow.REFRESH_RATE -> refreshRate() else -> super.onRowSelected(position, row, fromAction) } @@ -150,11 +157,12 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS /** * Init data */ - private fun initData() { + override fun initData() { + super.initData() defaultCurrency = UserDefaults.currency - if (!isUpdating) { + if (!deleteButtonShouldAppear) { bankroll.currency = net.pokeranalytics.android.model.realm.Currency() bankroll.currency?.code = defaultCurrency.currencyCode bankroll.currency?.rate = 1.0 @@ -195,7 +203,7 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS } /** - * Refresh the rate with the Currency Converter API + * Refresh the rate with the CurrencyCode Converter API */ private fun refreshRate() { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt new file mode 100644 index 00000000..768b8088 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt @@ -0,0 +1,274 @@ +package net.pokeranalytics.android.ui.fragment.data + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.chip.ChipGroup +import kotlinx.android.synthetic.main.fragment_custom_view.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.realm.CustomField +import net.pokeranalytics.android.model.realm.CustomFieldEntry +import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource +import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource +import net.pokeranalytics.android.ui.extensions.ChipGroupExtension +import net.pokeranalytics.android.ui.extensions.px +import net.pokeranalytics.android.ui.extensions.showAlertDialog +import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetFragment +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor +import net.pokeranalytics.android.ui.view.rowrepresentable.CustomFieldRow +import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow +import net.pokeranalytics.android.util.NULL_TEXT +import java.util.* + +/** + * Custom EditableDataFragment to manage the Transaction data + */ +class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource { + + // Return the item as a Custom TypedCSVField object + private val customField: CustomField + get() { + return this.item as CustomField + } + + private val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() { + + var dragFrom = -1 + var dragTo = -1 + + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + return if (viewHolder.adapterPosition <= 2) { + return 0 + } else { + makeFlag( + ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.DOWN + or ItemTouchHelper.UP or ItemTouchHelper.START or ItemTouchHelper.END + ) + } + } + + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + + if (target.adapterPosition <= 2) { + return false + } + + val fromPosition = viewHolder.adapterPosition + val toPosition = target.adapterPosition + + if (dragFrom == -1) { + dragFrom = viewHolder.adapterPosition - 1 + } + dragTo = target.adapterPosition - 1 + + rowRepresentableAdapter.notifyItemMoved(fromPosition, toPosition) + + return true + } + + override fun onMoved( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + fromPos: Int, + target: RecyclerView.ViewHolder, + toPos: Int, + x: Int, + y: Int + ) { + super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y) + + Collections.swap(customField.entries, fromPos - (CustomFieldRow.values().size + 1), toPos - (CustomFieldRow.values().size + 1)) + customField.entries.forEachIndexed { index, rowRepresentable -> + val entry = rowRepresentable as CustomFieldEntry + entry.order = index + } + + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + dragFrom = -1 + dragTo = -1 + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + } + }) + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_custom_view, container, false) + } + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUI() + } + + override fun getDataSource(): RowRepresentableDataSource { + return this + } + + override fun adapterRows(): List? { + return customField.adapterRows() + } + + override fun stringForRow(row: RowRepresentable): String { + return when (row) { + SimpleRow.NAME -> if (customField.name.isNotEmpty()) customField.name else NULL_TEXT + else -> super.stringForRow(row) + } + } + + override fun boolForRow(row: RowRepresentable): Boolean { + return when (row) { + CustomFieldRow.COPY_ON_DUPLICATE -> customField.duplicateValue + CustomFieldRow.TYPE -> isUpdating + else -> super.boolForRow(row) + } + } + + override fun intForRow(row: RowRepresentable): Int { + return when (row) { + CustomFieldRow.TYPE -> customField.type + else -> super.intForRow(row) + } + } + + override fun editDescriptors(row: RowRepresentable): ArrayList? { + return when (row) { + SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.customField.name)) + is CustomFieldEntry -> row.editingDescriptors(mapOf("defaultValue" to row.value)) + else -> null + } + } + + override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + when (row) { + is CustomFieldEntry -> { + val data = customField.editDescriptors(row) + BottomSheetFragment.create(fragmentManager, row, this, data, isClearable = false, isDeletable = true) + } + else -> super.onRowSelected(position, row, fromAction) + } + } + + override fun onRowValueChanged(value: Any?, row: RowRepresentable) { + when (row) { + is CustomFieldEntry -> { + row.updateValue(value, row) + customField.updateRowRepresentation() + rowRepresentableAdapter.notifyDataSetChanged() + } + CustomFieldRow.TYPE -> { + customField.updateValue(value, row) + updateUI() + rowRepresentableAdapter.notifyDataSetChanged() + } + else -> super.onRowValueChanged(value, row) + } + } + + override fun onRowDeleted(row: RowRepresentable) { + super.onRowDeleted(row) + when (row) { + is CustomFieldEntry -> { + if (!row.isValidForDelete(getRealm())) { + val status = row.getDeleteStatus(requireContext(), getRealm()) + val message = row.getFailedDeleteMessage(status) + showAlertDialog(requireContext(), R.string.cf_entry_delete_popup_title, message, showCancelButton = true, positiveAction = { + customField.deleteEntry(row) + rowRepresentableAdapter.notifyDataSetChanged() + }) + return + } + customField.deleteEntry(row) + rowRepresentableAdapter.notifyDataSetChanged() + } + } + } + + /** + * Init UI + */ + private fun initUI() { + customField.updateRowRepresentation() + bottomBar.translationY = 72f.px + bottomBar.visibility = View.VISIBLE + + if (customField.sortCondition == CustomField.Sort.DEFAULT.uniqueIdentifier) { + itemTouchHelper.attachToRecyclerView(recyclerView) + } else { + itemTouchHelper.attachToRecyclerView(null) + } + + when (customField.sortCondition) { + CustomField.Sort.DEFAULT.uniqueIdentifier -> sortDefault.isChecked = true + CustomField.Sort.ASCENDING.uniqueIdentifier -> sortAscending.isChecked = true + CustomField.Sort.DESCENDING.uniqueIdentifier -> sortDescending.isChecked = true + } + + addItem.setOnClickListener { + val customFieldEntry = customField.addEntry() + rowRepresentableAdapter.notifyDataSetChanged() + onRowSelected(-1, customFieldEntry) + } + + sortChoices.setOnCheckedChangeListener(object : ChipGroupExtension.SingleSelectionOnCheckedListener() { + override fun onCheckedChanged(group: ChipGroup, checkedId: Int) { + super.onCheckedChanged(group, checkedId) + + @SuppressWarnings + if (checkedId < 0) { // when unchecked, checkedId returns -1, causing a crash + return + } + + when (checkedId) { + R.id.sortDefault -> customField.sortCondition = CustomField.Sort.DEFAULT.uniqueIdentifier + R.id.sortAscending -> customField.sortCondition = CustomField.Sort.ASCENDING.uniqueIdentifier + R.id.sortDescending -> customField.sortCondition = CustomField.Sort.DESCENDING.uniqueIdentifier + } + + if (customField.sortCondition == CustomField.Sort.DEFAULT.uniqueIdentifier) { + itemTouchHelper.attachToRecyclerView(recyclerView) + } else { + itemTouchHelper.attachToRecyclerView(null) + } + rowRepresentableAdapter.notifyDataSetChanged() + } + }) + + updateUI() + rowRepresentableAdapter.notifyDataSetChanged() + + if (!this.deleteButtonShouldAppear) { + rowRepresentableForPosition(0)?.let { + onRowSelected(0, it) + } + } + } + + + /** + * Update UI + */ + private fun updateUI() { + if (customField.type == CustomField.Type.LIST.uniqueIdentifier) { + bottomBar.animate().translationY(0f.px) + .setInterpolator(FastOutSlowInInterpolator()) + .start() + } else { + bottomBar.animate().translationY(72f.px) + .setInterpolator(FastOutSlowInInterpolator()) + .start() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/DataManagerFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/DataManagerFragment.kt new file mode 100644 index 00000000..a7ee9216 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/DataManagerFragment.kt @@ -0,0 +1,162 @@ +package net.pokeranalytics.android.ui.fragment.data + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.AlertDialog +import net.pokeranalytics.android.R +import net.pokeranalytics.android.exceptions.ConfigurationException +import net.pokeranalytics.android.model.LiveData +import net.pokeranalytics.android.model.interfaces.Deletable +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 + +open class DataManagerFragment : RealmFragment() { + + lateinit var item: Deletable + + lateinit var liveDataType: LiveData + protected var primaryKey: String? = null + + var deleteButtonShouldAppear = false + set(value) { + field = value + this.updateMenuUI() + } + + var saveButtonShouldAppear = true + set(value) { + field = value + this.updateMenuUI() + } + + protected var dataType: Int? = null + + private var editableMenu: Menu? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + loadItem() + } + + 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 + */ + 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) { + R.id.save -> saveData() + R.id.delete -> deleteData() + } + return true + } + + /** + * Init data + */ + private fun loadItem() { + + this.item = this.liveDataType.updateOrCreate(this.getRealm(), primaryKey) + this.deleteButtonShouldAppear = this.primaryKey != null + + } + + /** + * Save data + */ + protected open fun saveData() { + + val savable = this.item + this.willSaveData() + + when (savable) { + is Savable -> { + val status = savable.getSaveValidityStatus(realm = this.getRealm()) + when (status) { + SaveValidityStatus.VALID -> { + this.getRealm().executeTransaction { + val managedItem = it.copyToRealmOrUpdate(this.item) + if (managedItem is Savable) { + val uniqueIdentifier = managedItem.id + finishActivityWithResult(uniqueIdentifier) + } + } + onDataSaved() + } + else -> { + val message = savable.getFailedSaveMessage(status) + val builder = AlertDialog.Builder(requireContext()) + .setMessage(message) + .setNegativeButton(R.string.ok, null) + builder.show() + } + } + + } + else -> { + throw ConfigurationException("Save action called on un-Savable object") + } + } + + } + + open fun willSaveData() { + + } + + /** + * Delete data + */ + protected open fun deleteData() { + + val realm = this.getRealm() + + if (this.item.isValidForDelete(realm)) { + val intent = Intent() + intent.putExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName, this.item.id) + activity?.setResult(Activity.RESULT_OK, intent) + activity?.finish() + } else { + val status = this.item.getDeleteStatus(requireContext(), realm) + val message = this.item.getFailedDeleteMessage(status) + val builder = AlertDialog.Builder(requireContext()) + .setMessage(message) + .setNegativeButton(R.string.ok, null) + builder.show() + } + } + + /** + * Finish the activity with a result + */ + private fun finishActivityWithResult(uniqueIdentifier: String) { + val intent = Intent() + intent.putExtra(EditableDataActivity.IntentKey.DATA_TYPE.keyName, dataType) + intent.putExtra(EditableDataActivity.IntentKey.PRIMARY_KEY.keyName, uniqueIdentifier) + activity?.setResult(Activity.RESULT_OK, intent) + activity?.finish() + } + + open fun onDataSaved() {} + + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/EditableDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/EditableDataFragment.kt new file mode 100644 index 00000000..bbc9ec70 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/EditableDataFragment.kt @@ -0,0 +1,105 @@ +package net.pokeranalytics.android.ui.fragment.data + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import io.realm.RealmModel +import kotlinx.android.synthetic.main.fragment_editable_data.* +import kotlinx.android.synthetic.main.fragment_editable_data.view.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.LiveData +import net.pokeranalytics.android.model.interfaces.Editable +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.bottomsheet.BottomSheetFragment +import net.pokeranalytics.android.ui.view.RowRepresentable + + +open class EditableDataFragment : DataManagerFragment(), RowRepresentableDelegate { + + lateinit var rowRepresentableAdapter: RowRepresentableAdapter + + var shouldOpenKeyboard = true + var isUpdating = false + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_editable_data, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initData() + initUI() + } + + open fun initData() { + + 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 row = dataSource.adapterRows()?.firstOrNull() + row?.let { + onRowSelected(0, it) + } + } + } + + /** + * Set fragment data + */ + fun setData(dataType: Int, primaryKey: String?) { + this.dataType = dataType + this.liveDataType = LiveData.values()[dataType] + this.primaryKey = primaryKey + } + + override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + BottomSheetFragment.create(fragmentManager, row, this, getDataSource().editDescriptors(row)) + } + + override fun onRowValueChanged(value: Any?, row: RowRepresentable) { + this.getRealm().executeTransaction { + (this.item as Editable).updateValue(value, row) + } + rowRepresentableAdapter.refreshRow(row) + } + + /** + * Init UI + */ + private fun initUI() { + + setDisplayHomeAsUpEnabled(true) + + val proxyItem: RealmModel? = this.liveDataType.getData(this.getRealm(), primaryKey) + proxyItem?.let { + this.appBar.toolbar.title = this.liveDataType.updateEntityLocalizedTitle(requireContext()) + deleteButtonShouldAppear = true + isUpdating = true + } ?: run { + this.appBar.toolbar.title = this.liveDataType.newEntityLocalizedTitle(requireContext()) + } + + val viewManager = LinearLayoutManager(requireContext()) + recyclerView.apply { + setHasFixedSize(true) + layoutManager = viewManager + } + } + + /** + * Return the data source + */ + open fun getDataSource(): RowRepresentableDataSource { + return this.item as RowRepresentableDataSource + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/LocationDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/LocationDataFragment.kt similarity index 95% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/LocationDataFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/fragment/data/LocationDataFragment.kt index 2fc8c929..34853369 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/LocationDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/LocationDataFragment.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.fragment +package net.pokeranalytics.android.ui.fragment.data import android.os.Bundle import android.view.View @@ -38,9 +38,9 @@ class LocationDataFragment : EditableDataFragment(), StaticRowRepresentableDataS super.onViewCreated(view, savedInstanceState) shouldOpenKeyboard = false - locationActivated = parentActivity.hasLocationPermissionGranted() + locationActivated = parentActivity?.hasLocationPermissionGranted() ?: false - if (isUpdating) { + if (deleteButtonShouldAppear) { // If we update a location, we set the switch to the correct value locationActivated = location.latitude != null && location.longitude != null @@ -168,11 +168,11 @@ class LocationDataFragment : EditableDataFragment(), StaticRowRepresentableDataS val maxResults = 5 - parentActivity.askForPlacesRequest { success, places -> + parentActivity?.askForPlacesRequest { success, places -> if (success) { // Try to get the location of the user - parentActivity.findCurrentLocation {currentLocation -> + parentActivity?.findCurrentLocation {currentLocation -> currentLocation?.let { location.latitude = currentLocation.latitude location.longitude = currentLocation.longitude diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/TransactionDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionDataFragment.kt similarity index 67% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/TransactionDataFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionDataFragment.kt index c2474374..789c00ed 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/TransactionDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionDataFragment.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.fragment +package net.pokeranalytics.android.ui.fragment.data import android.os.Bundle import android.view.View @@ -6,8 +6,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import net.pokeranalytics.android.model.LiveData +import net.pokeranalytics.android.model.realm.Bankroll import net.pokeranalytics.android.model.realm.Transaction +import net.pokeranalytics.android.model.realm.TransactionType import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.helpers.DateTimePickerManager @@ -17,6 +18,7 @@ import net.pokeranalytics.android.ui.view.rowrepresentable.TransactionRow 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.* /** @@ -56,8 +58,18 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa override fun editDescriptors(row: RowRepresentable): ArrayList? { return when (row) { - TransactionRow.BANKROLL -> row.editingDescriptors(mapOf("defaultValue" to this.transaction.bankroll, "data" to LiveData.BANKROLL.items(getRealm()))) - TransactionRow.TYPE -> row.editingDescriptors(mapOf("defaultValue" to this.transaction.type, "data" to LiveData.TRANSACTION_TYPE.items(getRealm()))) + TransactionRow.BANKROLL -> row.editingDescriptors( + mapOf( + "defaultValue" to this.transaction.bankroll, + "data" to getRealm().sorted() + ) + ) + TransactionRow.TYPE -> row.editingDescriptors( + mapOf( + "defaultValue" to this.transaction.type, + "data" to getRealm().sorted() + ) + ) TransactionRow.AMOUNT -> row.editingDescriptors(mapOf("defaultValue" to (if (this.transaction.amount != 0.0) this.transaction.amount.round() else ""))) TransactionRow.COMMENT -> row.editingDescriptors(mapOf("defaultValue" to this.transaction.comment)) else -> super.editDescriptors(row) @@ -66,7 +78,14 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { when (row) { - TransactionRow.DATE -> DateTimePickerManager.create(requireContext(), row, this, this.transaction.date, onlyDate = true, isClearable = false) + TransactionRow.DATE -> DateTimePickerManager.create( + requireContext(), + row, + this, + this.transaction.date, + onlyDate = true, + isClearable = false + ) else -> super.onRowSelected(position, row, fromAction) } } @@ -75,15 +94,25 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa super.onRowValueChanged(value, row) rowRepresentableAdapter.refreshRow(row) - GlobalScope.launch(Dispatchers.Main) { - delay(200) - when(row) { - TransactionRow.BANKROLL -> onRowSelected(0, TransactionRow.TYPE) - TransactionRow.TYPE -> onRowSelected(0, TransactionRow.AMOUNT) - TransactionRow.AMOUNT -> onRowSelected(0, TransactionRow.COMMENT) - TransactionRow.COMMENT -> onRowSelected(0, TransactionRow.DATE) + if (primaryKey == null) { // automatically change the row for new data + GlobalScope.launch(Dispatchers.Main) { + delay(200) + when (row) { + TransactionRow.BANKROLL -> onRowSelected(0, TransactionRow.TYPE) + TransactionRow.TYPE -> onRowSelected(0, TransactionRow.AMOUNT) +// TransactionRow.AMOUNT -> onRowSelected(0, TransactionRow.DATE) +// TransactionRow.DATE -> onRowSelected(0, TransactionRow.COMMENT) + } } } } + override fun willSaveData() { + super.willSaveData() + val additive = this.transaction.type?.additive ?: true + if (!additive) { + this.transaction.amount *= -1 + } + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionTypeDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionTypeDataFragment.kt new file mode 100644 index 00000000..a5000654 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionTypeDataFragment.kt @@ -0,0 +1,79 @@ +package net.pokeranalytics.android.ui.fragment.data + +import net.pokeranalytics.android.model.realm.Transaction +import net.pokeranalytics.android.model.realm.TransactionType +import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor +import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow +import net.pokeranalytics.android.ui.view.rowrepresentable.TransactionTypeRow + +class TransactionTypeDataFragment : EditableDataFragment(), RowRepresentableDataSource { + + val transactionType: TransactionType + get() { + return this.item as TransactionType + } + + // RowRepresentableDataSource + + override fun adapterRows(): List? { + return this.transactionType.adapterRows() + } + + override fun rowRepresentableForPosition(position: Int): RowRepresentable? { + return this.transactionType.rowRepresentableForPosition(position) + } + + override fun numberOfRows(): Int { + return this.transactionType.numberOfRows() + } + + override fun viewTypeForPosition(position: Int): Int { + return this.transactionType.viewTypeForPosition(position) + } + + override fun stringForRow(row: RowRepresentable): String { + return when (row) { + SimpleRow.NAME -> this.transactionType.name + else -> return super.stringForRow(row) + } + } + + override fun boolForRow(row: RowRepresentable): Boolean { + return when (row) { + TransactionTypeRow.TRANSACTION_ADDITIVE -> this.transactionType.additive + else -> super.boolForRow(row) + } + } + + override fun editDescriptors(row: RowRepresentable): ArrayList? { + return row.editingDescriptors(mapOf("defaultValue" to this.transactionType.name)) + } + + override fun isEnabled(row: RowRepresentable): Boolean { + return when (row) { + TransactionTypeRow.TRANSACTION_ADDITIVE -> { + return this.transactionType.isValidForDelete(getRealm()) + } + else -> super.isEnabled(row) + } + } + + override fun isSelectable(row: RowRepresentable): Boolean { + return when (row) { + TransactionTypeRow.TRANSACTION_ADDITIVE -> { + val realm = getRealm() + val useCount = realm.where(Transaction::class.java) + .equalTo("type.id", this.transactionType.id).findAll().size + return useCount == 0 + } + else -> super.isSelectable(row) + } + } + + override fun getDataSource(): RowRepresentableDataSource { + return this + } + +} \ 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 new file mode 100644 index 00000000..3addaa19 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt @@ -0,0 +1,135 @@ +package net.pokeranalytics.android.ui.fragment.report + +import android.os.Bundle +import android.text.InputType +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import kotlinx.android.synthetic.main.fragment_progress_report.* +import net.pokeranalytics.android.calculus.Report +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.util.extensions.findById + + +abstract class AbstractReportFragment : DataManagerFragment() { + + private lateinit var _selectedReport: Report + + val selectedReport: Report + get() { + return this._selectedReport + } + + fun setReport(report: Report) { + this._selectedReport = report + this.primaryKey = report.options.reportSetupId + } + + protected var reportTitle: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + this.liveDataType = LiveData.REPORT_SETUP + this.saveButtonShouldAppear = this._selectedReport.options.userGenerated + this.deleteButtonShouldAppear = (this.primaryKey != null) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setDisplayHomeAsUpEnabled(true) + setToolbarTitle(reportTitle) + } + + override fun saveData() { + + activity?.let { + val builder = AlertDialog.Builder(it) + + // Get the layout inflater + val inflater = requireActivity().layoutInflater + + // Inflate and set the layout for the dialog + // Pass null as the parent view because its going in the dialog layout + val view = inflater.inflate(net.pokeranalytics.android.R.layout.dialog_edit_text, null) + val nameEditText = view.findViewById(net.pokeranalytics.android.R.id.reportName) + nameEditText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + + this.primaryKey?.let { id -> + getRealm().findById(id)?.let { reportSetup -> + nameEditText.hint = reportSetup.name + } + } + + builder.setView(view) + // Add action buttons + .setPositiveButton(net.pokeranalytics.android.R.string.save) { dialog, _ -> + saveReport(nameEditText.text.toString()) + dialog.dismiss() + } + .setNegativeButton(net.pokeranalytics.android.R.string.cancel) { dialog, _ -> + dialog.cancel() + } + + val dialog = builder.create() + dialog.setOnShowListener { + nameEditText.requestFocus() + val s = ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java) + s?.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) + } + + dialog.setOnDismissListener { + val s = ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java) + s?.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0) + } + + dialog.show() + + } ?: throw IllegalStateException("Activity cannot be null") + + } + + private fun saveReport(name: String) { + + getRealm().executeTransaction { realm -> + + val rs = this.item as ReportSetup + val firstSave = (this.primaryKey == null) + if (firstSave) { + val options = this._selectedReport.options + rs.name = name + rs.display = options.display.ordinal + options.stats.forEach { + rs.statIds.add(it.uniqueIdentifier) + } + options.criterias.forEach { criteria -> + when (criteria) { + is CustomFieldCriteria -> rs.criteriaCustomFieldIds.add(criteria.customFieldId) + else -> rs.criteriaIds.add(criteria.uniqueIdentifier) + } + } + + options.filterId?.let { id -> + rs.filter = realm.findById(id) + } + realm.copyToRealmOrUpdate(rs) + } else { + rs.name = name + realm.insertOrUpdate(rs) + } + + this.primaryKey = rs.id + + this.deleteButtonShouldAppear = true + toolbar.title = name + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComparisonReportFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComparisonReportFragment.kt new file mode 100644 index 00000000..5750e3c7 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComparisonReportFragment.kt @@ -0,0 +1,71 @@ +package net.pokeranalytics.android.ui.fragment.report + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.tabs.TabLayout +import kotlinx.android.synthetic.main.fragment_report_details.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.calculus.Report +import net.pokeranalytics.android.ui.adapter.ReportPagerAdapter + +class ComparisonReportFragment : AbstractReportFragment() { + + companion object { + + fun newInstance(report: Report, reportTitle: String): ComparisonReportFragment { + val fragment = ComparisonReportFragment() + fragment.reportTitle = reportTitle + fragment.setReport(report) + val bundle = Bundle() + fragment.arguments = bundle + return fragment + } + } + + /** + * Set data + */ + fun setData(report: Report) { + this.setReport(report) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_report_details, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUI() + } + + /** + * Init UI + */ + private fun initUI() { + + setDisplayHomeAsUpEnabled(true) + setToolbarTitle(reportTitle) + + parentActivity?.let { + val reportPagerAdapter = ReportPagerAdapter(requireContext(), it.supportFragmentManager, selectedReport) + viewPager.adapter = reportPagerAdapter + viewPager.offscreenPageLimit = 3 + } + + tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + viewPager.setCurrentItem(tab.position, false) + } + + override fun onTabUnselected(tab: TabLayout.Tab) { + } + + override fun onTabReselected(tab: TabLayout.Tab) { + } + }) + } + +} \ No newline at end of file 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 new file mode 100644 index 00000000..04036972 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt @@ -0,0 +1,226 @@ +package net.pokeranalytics.android.ui.fragment.report + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.recyclerview.widget.LinearLayoutManager +import io.realm.Realm +import kotlinx.android.synthetic.main.fragment_composable_table_report.* +import kotlinx.coroutines.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.calculus.* +import net.pokeranalytics.android.model.realm.ComputableResult +import net.pokeranalytics.android.ui.activity.components.ReportActivity +import net.pokeranalytics.android.ui.adapter.DisplayDescriptor +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.components.RealmFragment +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable +import net.pokeranalytics.android.ui.view.rowrepresentable.StatRow +import net.pokeranalytics.android.util.NULL_TEXT +import net.pokeranalytics.android.util.TextFormat +import timber.log.Timber +import java.util.* +import kotlin.coroutines.CoroutineContext + +open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentableDataSource, CoroutineScope, + RowRepresentableDelegate { + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + + private var rowRepresentables: ArrayList = ArrayList() + + var statsAdapter: RowRepresentableAdapter? = null + var report: Report? = null + + companion object { + + /** + * Create new instance + */ + fun newInstance(report: Report? = null): ComposableTableReportFragment { + val fragment = ComposableTableReportFragment() + fragment.report = report + + val bundle = Bundle() + fragment.arguments = bundle + return fragment + } + } + + // Life Cycle + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_composable_table_report, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initData() + initUI() + + report?.let { + showResults() + } + } + + // Row Representable DS + + override fun adapterRows(): List? { + return this.rowRepresentables + } + + override fun contentDescriptorForRow(row: RowRepresentable): DisplayDescriptor? { + val dc = DisplayDescriptor() + dc.textFormat = TextFormat(NULL_TEXT) + if (row is StatRow) { + context?.let { _ -> + row.computedStat?.let { + dc.textFormat = it.format() + } + } + } + return dc + } + + override fun statFormatForRow(row: RowRepresentable): TextFormat { + if (row is StatRow) { + context?.let { _ -> + row.computedStat?.let { return it.format() } + } + } + return TextFormat(NULL_TEXT) + } + + override fun onResume() { + super.onResume() + statsAdapter?.notifyDataSetChanged() + } + + // Business + + /** + * Init data + */ + open fun initData() { + this.statsAdapter = RowRepresentableAdapter(this, this) + } + + /** + * Init UI + */ + open fun initUI() { + val viewManager = LinearLayoutManager(requireContext()) + + recyclerView.apply { + setHasFixedSize(true) + layoutManager = viewManager + adapter = statsAdapter + } + + } + + /** + * Show results + */ + fun showResults() { + report?.let { + this.rowRepresentables = this.convertReportIntoRepresentables(it) + statsAdapter?.notifyDataSetChanged() + } + } + + open fun convertReportIntoRepresentables(report: Report): ArrayList { + + val rows: ArrayList = ArrayList() + report.results.forEach { result -> + val title = result.group.query.getName(requireContext()).capitalize() + rows.add(CustomizableRowRepresentable(title = title)) + val statList = result.group.stats ?: report.options.stats + statList.forEach { stat -> + rows.add(StatRow(stat, result.computedStat(stat), result.group.query.getName(requireContext()))) + } + } + return rows + +// val rows: ArrayList = ArrayList() +// report.options.stats.forEach {stat -> +// rows.add(CustomizableRowRepresentable(title = stat.localizedTitle(requireContext()))) +// report.results.forEach { +// val title = it.group.name +// rows.add(StatRow(stat, it.computedStat(stat), it.group.name, title)) +// } +// } +// return rows + } + + // RowRepresentableDelegate + + override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + + val cr = getRealm().where(ComputableResult::class.java).findAll() + if (cr.size < 2) { + Toast.makeText(context, R.string.less_then_2_values_for_display, Toast.LENGTH_LONG).show() + return + } + + if (row is StatRow && row.stat.hasProgressGraph) { + + // queryWith groups + val groupResults = this.report?.results?.filter { + row.groupName == it.group.query.getName(requireContext()) + } + + groupResults?.firstOrNull()?.let { + this.launchStatComputationWithEvolution(row.stat, it.group) + } + } + + } + + private fun launchStatComputationWithEvolution(stat: Stat, computableGroup: ComputableGroup) { + + showLoader() + + GlobalScope.launch(coroutineContext) { + + var report: Report? = null + val test = GlobalScope.async { + val s = Date() + Timber.d(">>> start...") + + val realm = Realm.getDefaultInstance() + + val aggregationType = stat.aggregationTypes.first() + report = + Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, computableGroup, aggregationType) + + realm.close() + + val e = Date() + val duration = (e.time - s.time) / 1000.0 + Timber.d(">>> ended in $duration seconds") + + } + test.await() + + if (!isDetached) { + hideLoader() + report?.let { + val title = stat.localizedTitle(requireContext()) + + ReportActivity.newInstance(requireContext(), it, title, stat) + +// ProgressReportActivity.newInstanceForResult(requireContext(), stat, it, title = title) + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticDetailsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt similarity index 73% rename from app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticDetailsFragment.kt rename to app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt index 962a29de..c256db17 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticDetailsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.ui.fragment +package net.pokeranalytics.android.ui.fragment.report import android.os.Bundle import android.view.LayoutInflater @@ -11,51 +11,49 @@ import com.github.mikephil.charting.data.LineDataSet import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup import io.realm.Realm -import kotlinx.android.synthetic.main.fragment_statistic_details.* +import kotlinx.android.synthetic.main.fragment_progress_report.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import net.pokeranalytics.android.R -import net.pokeranalytics.android.calculus.* +import net.pokeranalytics.android.calculus.AggregationType +import net.pokeranalytics.android.calculus.Calculator +import net.pokeranalytics.android.calculus.Report +import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.model.combined -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity import net.pokeranalytics.android.ui.extensions.ChipGroupExtension import net.pokeranalytics.android.ui.extensions.hideWithAnimation import net.pokeranalytics.android.ui.extensions.px import net.pokeranalytics.android.ui.extensions.showWithAnimation -import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment +import net.pokeranalytics.android.ui.fragment.GraphFragment import timber.log.Timber import java.util.* -class StatisticDetailsFragment : PokerAnalyticsFragment() { +class ProgressReportFragment : AbstractReportFragment() { companion object { /** - * Create new instance + * Creates new instance */ - fun newInstance(): StatisticDetailsFragment { - val fragment = StatisticDetailsFragment() + fun newInstance(): ProgressReportFragment { + val fragment = ProgressReportFragment() val bundle = Bundle() fragment.arguments = bundle return fragment } } - private lateinit var parentActivity: PokerAnalyticsActivity - private lateinit var computableGroup: ComputableGroup private lateinit var graphFragment: GraphFragment - private lateinit var selectedReport: Report - private lateinit var aggregationTypes: List - private var title: String? = null private var reports: MutableMap = hashMapOf() private var stat: Stat = Stat.NET_RESULT private var displayAggregationChoices: Boolean = true override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_statistic_details, container, false) + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_progress_report, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -66,12 +64,11 @@ class StatisticDetailsFragment : PokerAnalyticsFragment() { /** * Set data */ - fun setData(stat: Stat, computableGroup: ComputableGroup, report: Report, displayAggregationChoices: Boolean, title: String? = null) { + fun setData(report: Report, stat: Stat, displayAggregationChoices: Boolean, title: String) { this.stat = stat - this.computableGroup = computableGroup - this.selectedReport = report + this.setReport(report) this.displayAggregationChoices = displayAggregationChoices - this.title = title + this.reportTitle = title stat.aggregationTypes.firstOrNull()?.let { reports[it] = report @@ -83,22 +80,12 @@ class StatisticDetailsFragment : PokerAnalyticsFragment() { */ private fun initUI() { - parentActivity = activity as PokerAnalyticsActivity - - // Avoid a bug during setting the title - toolbar.title = "" - - parentActivity.setSupportActionBar(toolbar) - parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true) - setHasOptionsMenu(true) - - - val fragmentManager = parentActivity.supportFragmentManager - val fragmentTransaction = fragmentManager.beginTransaction() + val fragmentManager = parentActivity?.supportFragmentManager + val fragmentTransaction = fragmentManager?.beginTransaction() graphFragment = GraphFragment() - fragmentTransaction.add(R.id.graphContainer, graphFragment) - fragmentTransaction.commit() + fragmentTransaction?.add(R.id.graphContainer, graphFragment) + fragmentTransaction?.commit() this.aggregationTypes = stat.aggregationTypes @@ -108,7 +95,7 @@ class StatisticDetailsFragment : PokerAnalyticsFragment() { } } - toolbar.title = this.title ?: stat.localizedTitle(requireContext()) + val aggregationTypes = stat.aggregationTypes this.aggregationTypes.forEachIndexed { index, type -> val chip = Chip(requireContext()) @@ -120,7 +107,7 @@ class StatisticDetailsFragment : PokerAnalyticsFragment() { } this.chipGroup.isVisible = displayAggregationChoices - this.chipGroup.setSingleSelection(true) + this.chipGroup.isSingleSelection = true this.chipGroup.check(0) this.chipGroup.setOnCheckedChangeListener(object : ChipGroupExtension.SingleSelectionOnCheckedListener() { @@ -170,7 +157,9 @@ class StatisticDetailsFragment : PokerAnalyticsFragment() { val realm = Realm.getDefaultInstance() - val report = Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, computableGroup, aggregationType) + val group = selectedReport.results.first().group + + val report = Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, group, aggregationType) reports[aggregationType] = report realm.close() diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/TableReportFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/TableReportFragment.kt new file mode 100644 index 00000000..34e9e225 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/TableReportFragment.kt @@ -0,0 +1,50 @@ +package net.pokeranalytics.android.ui.fragment.report + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import net.pokeranalytics.android.R +import net.pokeranalytics.android.calculus.Report + +class TableReportFragment : AbstractReportFragment() { + + private lateinit var tableReportFragment: ComposableTableReportFragment + + companion object { + + fun newInstance(report: Report, title: String): TableReportFragment { + val fragment = TableReportFragment() + fragment.reportTitle = title + fragment.setReport(report) + val bundle = Bundle() + fragment.arguments = bundle + return fragment + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_table_report, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUI() + } + + private fun initUI() { + + setDisplayHomeAsUpEnabled(true) + setToolbarTitle(reportTitle) + + val fragmentTransaction = parentActivity?.supportFragmentManager?.beginTransaction() + val fragment = ComposableTableReportFragment.newInstance(this.selectedReport) + fragmentTransaction?.add(R.id.tableReportContainer, fragment) + fragmentTransaction?.commit() + + this.tableReportFragment = fragment + + } + +} \ No newline at end of file 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 addddf61..897ff3f9 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 @@ -3,14 +3,19 @@ package net.pokeranalytics.android.ui.graph import android.content.Context import com.github.mikephil.charting.data.Entry import net.pokeranalytics.android.calculus.Stat -import net.pokeranalytics.android.calculus.TextFormat +import net.pokeranalytics.android.model.interfaces.Timed import net.pokeranalytics.android.ui.fragment.GraphFragment import net.pokeranalytics.android.ui.view.DefaultLegendValues import net.pokeranalytics.android.ui.view.LegendContent +import net.pokeranalytics.android.util.TextFormat + +class ObjectIdentifier(var id: String, var clazz: Class) { + +} interface GraphUnderlyingEntry { - val entryTitle: String + fun entryTitle(context: Context): String fun formattedValue(stat: Stat): TextFormat fun legendValues( @@ -21,15 +26,16 @@ interface GraphUnderlyingEntry { context: Context ): LegendContent { + val leftName = stat.localizedTitle(context) return when (stat) { Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO, Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> { val totalStatValue = stat.format(entry.y.toDouble(), currency = null) - DefaultLegendValues(this.entryTitle, totalStatValue) + DefaultLegendValues(this.entryTitle(context), totalStatValue, leftName = leftName) } else -> { val entryValue = this.formattedValue(stat) val totalStatValue = stat.format(entry.y.toDouble(), currency = null) - DefaultLegendValues(this.entryTitle, entryValue, totalStatValue) + DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue, leftName = leftName) } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/helpers/DateTimePickerManager.kt b/app/src/main/java/net/pokeranalytics/android/ui/helpers/DateTimePickerManager.kt index d5e26d36..0e1d1e45 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/helpers/DateTimePickerManager.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/helpers/DateTimePickerManager.kt @@ -65,6 +65,8 @@ class DateTimePickerManager : DatePickerDialog.OnDateSetListener, calendar.set(Calendar.YEAR, year) calendar.set(Calendar.MONTH, month) calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MILLISECOND, 0) if (!onlyDate) { showTimePicker() } else { @@ -75,6 +77,8 @@ class DateTimePickerManager : DatePickerDialog.OnDateSetListener, override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) { calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) calendar.set(Calendar.MINUTE, minute) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MILLISECOND, 0) if (minimumDate != null) { minimumDate?.let { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/interfaces/FilterHandler.kt b/app/src/main/java/net/pokeranalytics/android/ui/interfaces/FilterHandler.kt new file mode 100644 index 00000000..64f687ba --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/interfaces/FilterHandler.kt @@ -0,0 +1,80 @@ +package net.pokeranalytics.android.ui.interfaces + +import android.content.Context +import android.content.Intent +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 + +enum class FilterActivityRequestCode { + SELECT_FILTER, + CREATE_FILTER, + ; +} + +enum class FilterableType(override var uniqueIdentifier: Int): IntIdentifiable { + ALL(0), + SESSION(1), + TRANSACTION(2), + BANKROLL(3), + HAND_HISTORY(4), + ; + + companion object : IntSearchable { + + override fun valuesInternal(): Array { + return values() + } + } + +} + +interface FilterHandler { + companion object { + const val INTENT_FILTER_UPDATE_FILTER_UI = "net.pokeranalytics.android.UPDATE_FILTER_UI" + } + + fun applyFilter() + fun removeFilter() + + fun saveFilter(context: Context, filterId:String) { + Preferences.setActiveFilterId(filterId, context) + + + val realm = Realm.getDefaultInstance() + realm.beginTransaction() + currentFilter(context, realm)?.let { + it.useCount++ + } + realm.commitTransaction() + realm.close() + + // Send broadcast + val intent = Intent() + intent.action = INTENT_FILTER_UPDATE_FILTER_UI + context.sendBroadcast(intent) + } + + fun currentFilter(context: Context, realm: Realm): Filter? { + return Preferences.getActiveFilterId(context)?.let { + realm.where().equalTo("id", it).findFirst() + } ?: run { + null + } + } + + var currentFilterable: FilterableType + + + /** + * Manage filters + */ + fun manageFilters(fragment: Fragment) { + FiltersActivity.newInstanceForResult(fragment = fragment, currentFilterable = currentFilterable) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/HistorySessionDiffCallback.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/HistorySessionDiffCallback.kt index 09ecd30a..df0ff0b3 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/HistorySessionDiffCallback.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/HistorySessionDiffCallback.kt @@ -35,11 +35,7 @@ class HistorySessionDiffCallback(var newRows: List, var oldRow // Force to update all the rows that were already there if (oldRows[oldItemPosition] is Session && newRows[newItemPosition] is Session) { - val session1 = oldRows[oldItemPosition] as Session - val session2 = newRows[newItemPosition] as Session - - return false //session1.id == session2.id - + return false } else if (oldRows[oldItemPosition] is CustomizableRowRepresentable && newRows[newItemPosition] is CustomizableRowRepresentable) { val header1 = oldRows[oldItemPosition] as CustomizableRowRepresentable val header2 = newRows[newItemPosition] as CustomizableRowRepresentable diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/LegendView.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/LegendView.kt index f00f98cc..3ab9580d 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/LegendView.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/LegendView.kt @@ -9,16 +9,17 @@ import androidx.core.view.isVisible import kotlinx.android.synthetic.main.layout_legend_default.view.* import net.pokeranalytics.android.R import net.pokeranalytics.android.calculus.Stat -import net.pokeranalytics.android.calculus.TextFormat import net.pokeranalytics.android.ui.extensions.setTextFormat import net.pokeranalytics.android.ui.fragment.GraphFragment +import net.pokeranalytics.android.util.TextFormat interface LegendContent data class DefaultLegendValues( var title: String, var leftFormat: TextFormat, - var rightFormat: TextFormat? = null + var rightFormat: TextFormat? = null, + var leftName: String? = null ) : LegendContent /** @@ -26,7 +27,7 @@ data class DefaultLegendValues( */ open class LegendView : FrameLayout { -// open class Values(var title: String, var leftFormat: TextFormat, var rightFormat: TextFormat? = null) +// open class Values(var titleResId: String, var leftFormat: TextFormat, var rightFormat: TextFormat? = null) // class MultiLineValues( // var firstTitle: String, // var secondTitle: String, @@ -51,7 +52,7 @@ open class LegendView : FrameLayout { init() } - open protected fun getResourceLayout(): Int { + protected open fun getResourceLayout(): Int { return R.layout.layout_legend_default } @@ -77,7 +78,7 @@ open class LegendView : FrameLayout { this.counter.isVisible = false } GraphFragment.Style.LINE -> { - if (stat.significantIndividualValue) { + if (stat.graphSignificantIndividualValue) { this.stat1Name.text = stat.localizedTitle(context) this.stat2Name.text = stat.cumulativeLabelResId(context) } else { @@ -86,9 +87,9 @@ open class LegendView : FrameLayout { } counter?.let { - val counterText = "$it ${context.getString(R.string.sessions)}" + val counterText = "$it ${context.getString(R.string.points)}" this.counter.text = counterText - this.counter.isVisible = stat.shouldShowNumberOfSessions + this.counter.isVisible = stat.graphShouldShowNumberOfSessions } } else -> { @@ -110,6 +111,9 @@ open class LegendView : FrameLayout { content.rightFormat?.let { this.stat2Value.setTextFormat(it, context) } + content.leftName?.let { name -> + this.stat1Name.text = name + } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/MultiLineLegendView.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/MultiLineLegendView.kt index 53bc4237..42016bb6 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/MultiLineLegendView.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/MultiLineLegendView.kt @@ -9,7 +9,7 @@ import kotlinx.android.synthetic.main.layout_legend_default.view.stat2Name import kotlinx.android.synthetic.main.layout_legend_default.view.stat2Value import net.pokeranalytics.android.R import net.pokeranalytics.android.calculus.Stat -import net.pokeranalytics.android.calculus.TextFormat +import net.pokeranalytics.android.util.TextFormat import net.pokeranalytics.android.ui.extensions.setTextFormat import net.pokeranalytics.android.ui.fragment.GraphFragment diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt index 0aa25f23..1f6ce50f 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt @@ -10,7 +10,7 @@ import net.pokeranalytics.android.util.NULL_TEXT */ interface RowRepresentable : Displayable, EditDataSource, ImageDecorator { - fun getDisplayName(): String { + fun getDisplayName(context: Context): String { return NULL_TEXT } } @@ -34,9 +34,13 @@ interface DefaultEditDataSource : EditDataSource, Localizable { interface ImageDecorator { val imageRes: Int? - get() { - return null - } + get() = null + + val imageTint: Int? + get() = null + + val imageClickable: Boolean? + get() = false } /** @@ -62,6 +66,16 @@ interface Displayable : Localizable { get() { return BottomSheetType.NONE } + + val selectedChoice: Int + get() { + return -1 + } + + val choices: List<*>? + get() { + return null + } } /** @@ -70,7 +84,7 @@ interface Displayable : Localizable { interface Localizable { /** - * The resource identifier of the localized title + * The resource uniqueIdentifier of the localized titleResId */ val resId: Int? get() { @@ -78,7 +92,7 @@ interface Localizable { } /** - * The localized title of the row + * The localized titleResId of the row */ fun localizedTitle(context: Context): String { this.resId?.let { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentableEditDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentableEditDescriptor.kt index 9d18652c..bdb6bf4b 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentableEditDescriptor.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentableEditDescriptor.kt @@ -9,6 +9,6 @@ class RowRepresentableEditDescriptor( var defaultValue: Any? = null, var hint: Int? = null, var inputType: Int? = null, - var data: RealmResults<*>? = null, + var data: RealmResults? = null, var staticData: List? = null ) \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt index a76a4d83..12ccaece 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt @@ -8,6 +8,7 @@ import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.SwitchCompat import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar import androidx.recyclerview.widget.RecyclerView @@ -17,19 +18,25 @@ import com.github.mikephil.charting.data.BarData import com.github.mikephil.charting.data.BarDataSet import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet -import kotlinx.android.synthetic.main.row_history_session.view.* +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import kotlinx.android.synthetic.main.row_feed_session.view.* import kotlinx.android.synthetic.main.row_transaction.view.* import net.pokeranalytics.android.R +import net.pokeranalytics.android.calculus.ComputedStat +import net.pokeranalytics.android.calculus.Stat +import net.pokeranalytics.android.calculus.bankroll.BankrollReport +import net.pokeranalytics.android.model.realm.CustomField import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter +import net.pokeranalytics.android.ui.extensions.ChipGroupExtension +import net.pokeranalytics.android.ui.extensions.addCircleRipple +import net.pokeranalytics.android.ui.extensions.px import net.pokeranalytics.android.ui.extensions.setTextFormat import net.pokeranalytics.android.ui.graph.AxisFormatting import net.pokeranalytics.android.ui.graph.setStyle -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.view.rowrepresentable.StatRow +import net.pokeranalytics.android.ui.view.rowrepresentable.* /** * An interface used to factor the configuration of RecyclerView.ViewHolder @@ -59,6 +66,7 @@ enum class RowViewType(private var layoutRes: Int) { TITLE_ICON_ARROW(R.layout.row_title_icon_arrow), TITLE_VALUE(R.layout.row_title_value), TITLE_VALUE_ARROW(R.layout.row_title_value_arrow), + TITLE_VALUE_ACTION(R.layout.row_title_value_action), TITLE_SWITCH(R.layout.row_title_switch), TITLE_GRID(R.layout.row_bottom_sheet_grid_title), DATA(R.layout.row_title), @@ -68,13 +76,15 @@ enum class RowViewType(private var layoutRes: Int) { LOADER(R.layout.row_loader), // Custom row - ROW_SESSION(R.layout.row_history_session), + ROW_SESSION(R.layout.row_feed_session), ROW_TRANSACTION(R.layout.row_transaction), ROW_BUTTON(R.layout.row_button), ROW_FOLLOW_US(R.layout.row_follow_us), STATS(R.layout.row_stats_title_value), STATS_DOUBLE(R.layout.row_stats_double), GRAPH(R.layout.row_graph), + LEGEND_DEFAULT(R.layout.row_legend_default), + LIST(R.layout.row_list), // Separator SEPARATOR(R.layout.row_separator); @@ -89,8 +99,8 @@ enum class RowViewType(private var layoutRes: Int) { return when (this) { // Row View Holder - HEADER_TITLE, HEADER_TITLE_VALUE, HEADER_TITLE_AMOUNT, HEADER_TITLE_AMOUNT_BIG, LOCATION_TITLE, - INFO, TITLE, TITLE_ARROW, TITLE_ICON_ARROW, TITLE_VALUE, TITLE_VALUE_ARROW, TITLE_GRID, + HEADER_TITLE, HEADER_TITLE_VALUE, HEADER_TITLE_AMOUNT, HEADER_TITLE_AMOUNT_BIG, LOCATION_TITLE, + INFO, TITLE, TITLE_ARROW, TITLE_ICON_ARROW, TITLE_VALUE, TITLE_VALUE_ARROW, TITLE_VALUE_ACTION, TITLE_GRID, TITLE_SWITCH, TITLE_CHECK, TITLE_VALUE_CHECK, DATA, BOTTOM_SHEET_DATA, LOADER -> RowViewHolder(layout) @@ -106,10 +116,14 @@ enum class RowViewType(private var layoutRes: Int) { // Row Follow Us ROW_FOLLOW_US -> RowFollowUsViewHolder(layout) + // Row List - ChipGroup + LIST -> ListViewHolder(layout) + // Row Stats STATS -> StatsTitleValueViewHolder(layout) STATS_DOUBLE -> StatsDoubleViewHolder(layout) GRAPH -> GraphViewHolder(layout) + LEGEND_DEFAULT -> LegendDefaultViewHolder(layout) // Separator SEPARATOR -> SeparatorViewHolder(layout) @@ -119,12 +133,12 @@ enum class RowViewType(private var layoutRes: Int) { } /** - * Display a generic row (title, value, container) + * Display a generic row (titleResId, value, container) */ + @SuppressWarnings("ResourceType") inner class RowViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { override fun bind(position: Int, row: RowRepresentable, adapter: RowRepresentableAdapter) { - if (row is CustomizableRowRepresentable) { // Customizable Row @@ -163,7 +177,7 @@ enum class RowViewType(private var layoutRes: Int) { if (row.resId != null) { it.text = row.localizedTitle(itemView.context) } else { - it.text = row.getDisplayName() + it.text = row.getDisplayName(itemView.context) } } @@ -174,15 +188,38 @@ enum class RowViewType(private var layoutRes: Int) { // Icon itemView.findViewById(R.id.icon)?.let { imageView -> + imageView.setImageDrawable(null) + row.imageRes?.let { imageRes -> + imageView.setImageResource(imageRes) + } + } + + // Action + itemView.findViewById(R.id.action)?.let { imageView -> + imageView.setImageDrawable(null) row.imageRes?.let { imageRes -> + imageView.visibility = View.VISIBLE imageView.setImageResource(imageRes) } + row.imageTint?.let { color -> + imageView.setColorFilter(ContextCompat.getColor(imageView.context, color)) + } + if (row.imageClickable == true) { + imageView.addCircleRipple() + imageView.setOnClickListener { + adapter.delegate?.onRowSelected(position, row, true) + } + } else { + imageView.setBackgroundResource(0) + } } // Listener val listener = View.OnClickListener { itemView.findViewById(R.id.switchView)?.let { - it.isChecked = !it.isChecked + if (adapter.dataSource.isEnabled(row)) { + it.isChecked = !it.isChecked + } } ?: run { adapter.delegate?.onRowSelected(position, row) } @@ -194,6 +231,7 @@ enum class RowViewType(private var layoutRes: Int) { // Switch itemView.findViewById(R.id.switchView)?.let { it.isChecked = adapter.dataSource.boolForRow(row) + it.isEnabled = adapter.dataSource.isEnabled(row) it.setOnCheckedChangeListener { _, isChecked -> adapter.delegate?.onRowValueChanged(isChecked, row) } @@ -246,7 +284,7 @@ enum class RowViewType(private var layoutRes: Int) { } if (row is StatRow) { - itemView.findViewById(R.id.nextArrow)?.isVisible = row.stat.hasEvolutionGraph + itemView.findViewById(R.id.nextArrow)?.isVisible = row.stat.hasProgressGraph } // Listener @@ -306,7 +344,7 @@ enum class RowViewType(private var layoutRes: Int) { } /** - * Display a stat + * Display a graph */ inner class GraphViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { @@ -314,7 +352,7 @@ enum class RowViewType(private var layoutRes: Int) { if (row is GraphRow) { - row.report.results.firstOrNull()?.defaultStatEntries(row.stat, itemView.context)?.let { dataSet -> + row.dataSet?.let { dataSet -> val context = itemView.context @@ -355,6 +393,84 @@ enum class RowViewType(private var layoutRes: Int) { } } + /** + * Display a legend + */ + inner class LegendDefaultViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + BindableHolder { + override fun bind(position: Int, row: RowRepresentable, adapter: RowRepresentableAdapter) { + + if (row is BankrollReport) { + itemView.findViewById(R.id.stat1Name)?.let { + it.text = itemView.context.getString(R.string.total) + } + itemView.findViewById(R.id.stat1Value)?.let { + val formattedStat = ComputedStat(Stat.NET_RESULT, row.total).format() + it.setTextFormat(formattedStat, itemView.context) + } + itemView.findViewById(R.id.stat2Name)?.let { + it.text = itemView.context.getString(R.string.risk_of_ruin) + } + itemView.findViewById(R.id.stat2Value)?.let { + val riskOfRuin = row.riskOfRuin ?: 0.0 + val formattedStat = ComputedStat(Stat.RISK_OF_RUIN, riskOfRuin).format() + it.setTextFormat(formattedStat, itemView.context) + } + val listener = View.OnClickListener { + adapter.delegate?.onRowSelected(position, row) + } + itemView.findViewById(R.id.container)?.setOnClickListener(listener) + + } + } + } + + /** + * Display a list of chips + */ + inner class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + BindableHolder { + override fun bind(position: Int, row: RowRepresentable, adapter: RowRepresentableAdapter) { + + if (row is CustomFieldRow) { + + itemView.findViewById(R.id.chipGroup)?.let { chipGroup -> + + chipGroup.removeAllViews() + chipGroup.setOnCheckedChangeListener(null) + + val currentChoice = adapter.dataSource.intForRow(row) + val isUpdating = adapter.dataSource.boolForRow(row) + + row.getChoices(currentChoice, isUpdating)?.forEach { type -> + val chip = Chip(itemView.context) + chip.id = type.ordinal + chip.text = itemView.context.getString(type.resId) + chip.chipStartPadding = 8f.px + chip.chipEndPadding = 8f.px + chip.isEnabled = type.isEnabled + chip.alpha = if (type.isEnabled) 1f else 0.5f + + chip.isChecked = adapter.dataSource.intForRow(row) == type.ordinal + chipGroup.addView(chip) + } + + chipGroup.setOnCheckedChangeListener(object : ChipGroupExtension.SingleSelectionOnCheckedListener() { + override fun onCheckedChanged(group: ChipGroup, checkedId: Int) { + super.onCheckedChanged(group, checkedId) + + @SuppressWarnings + if (checkedId < 0) { // when unchecked, checkedId returns -1, causing a crash + return + } + + adapter.delegate?.onRowValueChanged(CustomField.Type.values()[checkedId], row) + } + }) + } + } + } + } /** * Display a button in a row diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/SessionRowView.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/SessionRowView.kt index fc9b1c39..40db96da 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/SessionRowView.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/SessionRowView.kt @@ -26,7 +26,7 @@ import net.pokeranalytics.android.util.extensions.toCurrency */ class SessionRowView : FrameLayout { - private lateinit var rowHistorySession: ConstraintLayout + private lateinit var rowSession: ConstraintLayout /** * Constructors @@ -48,9 +48,9 @@ class SessionRowView : FrameLayout { */ private fun init() { val layoutInflater = LayoutInflater.from(context) - rowHistorySession = layoutInflater.inflate(R.layout.row_session_view, this, false) as ConstraintLayout + rowSession = layoutInflater.inflate(R.layout.row_session_view, this, false) as ConstraintLayout val layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT) - addView(rowHistorySession, layoutParams) + addView(rowSession, layoutParams) } /** @@ -61,8 +61,8 @@ class SessionRowView : FrameLayout { val date = session.startDate ?: session.creationDate // Date - rowHistorySession.dateDay.text = date.getShortDayName() - rowHistorySession.dateNumber.text = date.getDayNumber() + rowSession.dateDay.text = date.getShortDayName() + rowSession.dateNumber.text = date.getDayNumber() // Title / Game type @@ -99,60 +99,55 @@ class SessionRowView : FrameLayout { } val title = parameters.joinToString(separator = " ") - rowHistorySession.sessionTitle.text = title + rowSession.sessionTitle.text = title // Duration -// rowHistorySession.sessionInfoDurationIcon.isVisible = session.timeFrame != null -// rowHistorySession.sessionInfoDurationValue.isVisible = session.timeFrame != null -// session.timeFrame?.let { -// rowHistorySession.sessionInfoDurationValue.text = session.getFormattedDuration() -// } - rowHistorySession.sessionInfoDurationValue.text = session.getFormattedDuration() + rowSession.sessionInfoDurationValue.text = session.getFormattedDuration() // Location - rowHistorySession.sessionInfoLocationIcon.isVisible = session.location != null - rowHistorySession.sessionInfoLocationValue.isVisible = session.location != null + rowSession.sessionInfoLocationIcon.isVisible = session.location != null + rowSession.sessionInfoLocationValue.isVisible = session.location != null session.location?.let { - rowHistorySession.sessionInfoLocationValue.text = it.name + rowSession.sessionInfoLocationValue.text = it.name } // Table size - rowHistorySession.sessionInfoTableIcon.isVisible = session.tableSize != null - rowHistorySession.sessionInfoTableValue.isVisible = session.tableSize != null + rowSession.sessionInfoTableIcon.isVisible = session.tableSize != null + rowSession.sessionInfoTableValue.isVisible = session.tableSize != null session.tableSize?.let { - rowHistorySession.sessionInfoTableValue.text = TableSize(it).localizedTitle(context) + rowSession.sessionInfoTableValue.text = TableSize(it).localizedTitle(context) } val state = session.getState() - rowHistorySession.sessionInfoDurationIcon.isVisible = state.hasStarted - rowHistorySession.sessionInfoDurationValue.isVisible = state.hasStarted + rowSession.sessionInfoDurationIcon.isVisible = state.hasStarted + rowSession.sessionInfoDurationValue.isVisible = state.hasStarted // State if (state == SessionState.STARTED) { - rowHistorySession.gameResult.isVisible = false - rowHistorySession.infoIcon.isVisible = true - rowHistorySession.infoIcon.setImageResource(R.drawable.chip) - rowHistorySession.infoTitle.isVisible = true - rowHistorySession.infoTitle.text = context.getString(R.string.running_session_state) + rowSession.gameResult.isVisible = false + rowSession.infoIcon.isVisible = true + rowSession.infoIcon.setImageResource(R.drawable.chip) + rowSession.infoTitle.isVisible = true + rowSession.infoTitle.text = context.getString(R.string.running_session_state) } else if (state == SessionState.PLANNED) { - rowHistorySession.gameResult.isVisible = false - rowHistorySession.infoIcon.isVisible = true - rowHistorySession.infoIcon.setImageResource(R.drawable.ic_planned) - rowHistorySession.infoTitle.isVisible = true - rowHistorySession.infoTitle.text = session.startDate!!.shortTime() + rowSession.gameResult.isVisible = false + rowSession.infoIcon.isVisible = true + rowSession.infoIcon.setImageResource(R.drawable.ic_planned) + rowSession.infoTitle.isVisible = true + rowSession.infoTitle.text = session.startDate!!.shortTime() } else if (state == SessionState.PENDING) { - rowHistorySession.gameResult.isVisible = false - rowHistorySession.infoIcon.isVisible = false - rowHistorySession.infoTitle.isVisible = false + rowSession.gameResult.isVisible = false + rowSession.infoIcon.isVisible = false + rowSession.infoTitle.isVisible = false } else { - rowHistorySession.gameResult.isVisible = true - rowHistorySession.infoIcon.isVisible = false - rowHistorySession.infoTitle.isVisible = false + rowSession.gameResult.isVisible = true + rowSession.infoIcon.isVisible = false + rowSession.infoTitle.isVisible = false val result = session.result?.net ?: 0.0 val formattedStat = ComputedStat(Stat.NET_RESULT, result, currency = session.currency).format() - rowHistorySession.gameResult.setTextFormat(formattedStat, context) + rowSession.gameResult.setTextFormat(formattedStat, context) } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/CustomFieldRow.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/CustomFieldRow.kt new file mode 100644 index 00000000..f85d24c9 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/CustomFieldRow.kt @@ -0,0 +1,58 @@ +package net.pokeranalytics.android.ui.view.rowrepresentable + +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.realm.CustomField +import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType +import net.pokeranalytics.android.ui.view.DefaultEditDataSource +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowViewType + +enum class CustomFieldRow : RowRepresentable, DefaultEditDataSource { + TYPE, + COPY_ON_DUPLICATE; + + override val resId: Int? + get() { + return when (this) { + COPY_ON_DUPLICATE -> R.string.copy_on_duplicate + else -> null + } + } + + override val viewType: Int + get() { + return when (this) { + TYPE -> RowViewType.LIST.ordinal + COPY_ON_DUPLICATE -> RowViewType.TITLE_SWITCH.ordinal + } + } + + override val bottomSheetType: BottomSheetType + get() { + return when (this) { + TYPE -> BottomSheetType.NONE + COPY_ON_DUPLICATE -> BottomSheetType.NONE + } + } + + + override val choices: List? + get() { + return when (this) { + TYPE -> CustomField.Type.values().toList() + else -> null + } + } + + fun getChoices(currentChoiceIndex: Int, isUpdating: Boolean): List? { + val list = CustomField.Type.values().toList() + list.forEach { type -> + type.isEnabled = if (!isUpdating) { + true + } else { + !(currentChoiceIndex == CustomFieldRow.TYPE.ordinal || type == CustomField.Type.LIST) + } + } + return list + } +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/CustomizableRowRepresentable.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/CustomizableRowRepresentable.kt index 50a23ee9..3cdc0ace 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/CustomizableRowRepresentable.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/CustomizableRowRepresentable.kt @@ -2,11 +2,12 @@ package net.pokeranalytics.android.ui.view.rowrepresentable import android.content.Context import net.pokeranalytics.android.calculus.ComputedStat +import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType /** - * A class to display a title (and a value) as a Row Representable object + * A class to display a titleResId (and a value) as a Row Representable object */ class CustomizableRowRepresentable( var customViewType: RowViewType? = RowViewType.HEADER_TITLE, @@ -15,7 +16,7 @@ class CustomizableRowRepresentable( var value: String? = null, var computedStat: ComputedStat? = null, var isSelectable: Boolean? = false - ) : RowRepresentable { + ) : RowRepresentable, Identifiable { override fun localizedTitle(context: Context): String { @@ -30,4 +31,5 @@ class CustomizableRowRepresentable( override val viewType: Int = customViewType?.ordinal ?: RowViewType.HEADER_TITLE.ordinal + override var id: String = "" } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/FilterCategoryRow.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/FilterCategoryRow.kt index 066d7410..a0d7cd41 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/FilterCategoryRow.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/FilterCategoryRow.kt @@ -1,25 +1,65 @@ package net.pokeranalytics.android.ui.view.rowrepresentable +import io.realm.Realm import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.realm.CustomField +import net.pokeranalytics.android.ui.interfaces.FilterableType import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow.* +import net.pokeranalytics.android.util.extensions.sorted enum class FilterCategoryRow(override val resId: Int?, override val viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal) : RowRepresentable { GENERAL(R.string.general), DATE(R.string.date), TIME_FRAME(R.string.duration), - SESSIONS(R.string.sessions), + //Sessions(R.string.sessions), CASH(R.string.cash), TOURNAMENT(R.string.tournament), - ONLINE(R.string.online), + //ONLINE(R.string.online), RESULT(R.string.result), - //TRANSACTION_TYPES(R.string.operation_types), + TRANSACTION_TYPES(R.string.operation_types), LOCATIONS(R.string.locations), BANKROLLS(R.string.bankrolls), //PLAYERS(R.string.players), + CUSTOM_FIELDS(R.string.custom_fields), ; + companion object { + fun values(filterableType:FilterableType): ArrayList { + val list = arrayListOf() + for (category in values()) { + if (category.isAllowed(filterableType)) { + list.add(category) + } + } + return list + } + } + + fun isAllowed(filterableType:FilterableType): Boolean { + return when (filterableType) { + FilterableType.SESSION -> { + when (this) { + TRANSACTION_TYPES -> false + else -> true + } + } + FilterableType.TRANSACTION -> { + when (this) { + DATE, TRANSACTION_TYPES -> true + else -> false + } + } + else -> { + when (this) { + DATE -> true + else -> false + } + } + } + } + val filterElements: List get() { return filterSectionRows.flatMap { @@ -31,53 +71,60 @@ enum class FilterCategoryRow(override val resId: Int?, override val viewType: In get() { return when (this) { GENERAL -> arrayListOf( - CASH_TOURNAMENT, - LIVE_ONLINE, - GAME, - LIMIT_TYPE, - TABLE_SIZE + CashOrTournament, + LiveOrOnline, + Game, + LimitType, + TableSize ) DATE -> arrayListOf( - DYNAMIC_DATE, - FIXED_DATE, - DURATION, - YEAR, - WEEKDAYS_OR_WEEKEND, - DAY_OF_WEEK, - MONTH_OF_YEAR + DynamicDate, + FixedDate, + Duration, + Year, + WeekdayOrWeekend, + DayOfWeek, + MonthOfYear ) TIME_FRAME -> arrayListOf( - SESSION_DURATION, - TIME_FRAME_RANGE + SessionDuration, + TimeFrameRange ) - SESSIONS -> arrayListOf(FilterSectionRow.SESSIONS) + //Sessions -> arrayListOf(FilterSectionRow.Sessions) BANKROLLS -> arrayListOf( - BANKROLL + Bankroll ) CASH -> arrayListOf( - BLIND + Blind ) TOURNAMENT -> arrayListOf( - TOURNAMENT_TYPE, - TOURNAMENT_NAME, - TOURNAMENT_FEATURE, - TOURNAMENT_ENTRY_FEE, - TOURNAMENT_NUMBER_OF_PLAYER, - TOURNAMENT_FINAL_POSITION - ) - ONLINE -> arrayListOf( - MULTI_TABLING + TournamentType, + TournamentName, + TournamentFeature, + TournamentEntryFee, + TournamentNumberOfPlayer, + TournamentFinalPosition ) + //ONLINE -> arrayListOf(MultiTabling) LOCATIONS -> arrayListOf( - LOCATION + Location ) - //PLAYERS -> arrayListOf(MULTI_PLAYER) + //PLAYERS -> arrayListOf(MultiPlayer) RESULT -> arrayListOf( - VALUE, - NUMBER_OF_REBUY + Value, + NumberOfRebuy ) - //TRANSACTION_TYPES -> arrayListOf() + CUSTOM_FIELDS -> { + val realm = Realm.getDefaultInstance() + val sections = realm.sorted(CustomField::class.java).map { + CustomField(it) + } + realm.close() + sections + } + + TRANSACTION_TYPES -> arrayListOf(TransactionType) } } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/FilterSectionRow.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/FilterSectionRow.kt index f5ada853..319eee8d 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/FilterSectionRow.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/FilterSectionRow.kt @@ -1,46 +1,52 @@ package net.pokeranalytics.android.ui.view.rowrepresentable +import android.content.Context import net.pokeranalytics.android.R +import net.pokeranalytics.android.exceptions.PokerAnalyticsException import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.mapFirstCondition import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType -enum class FilterSectionRow(override val resId: Int?) : RowRepresentable { - CASH_TOURNAMENT(R.string.cash_or_tournament), - LIVE_ONLINE(R.string.live_or_online), - GAME(R.string.games), - LIMIT_TYPE(R.string.limits), - TABLE_SIZE(R.string.table_sizes), - DYNAMIC_DATE(R.string.dynamic_date), - FIXED_DATE(R.string.fixed_date), - DURATION(R.string.duration), - YEAR(R.string.year), - WEEKDAYS_OR_WEEKEND(R.string.weekdays_or_weekend), - DAY_OF_WEEK(R.string.day_of_the_week), - MONTH_OF_YEAR(R.string.month_of_the_year), - SESSION_DURATION(R.string.session_duration), - TIME_FRAME_RANGE(R.string.hour_slot), - SESSIONS(R.string.sessions), - BLIND(R.string.blinds), - CASH_RE_BUY_COUNT(R.string.rebuy_count), - TOURNAMENT_TYPE(R.string.tournament_types), - TOURNAMENT_NAME(R.string.tournament_name), - TOURNAMENT_FEATURE(R.string.tournament_feature), - COMPLETION_PERCENTAGE(R.string.tournament_completion_percentage_interval), - TOURNAMENT_FINAL_POSITION(R.string.final_position), - TOURNAMENT_NUMBER_OF_PLAYER(R.string.players_count), - TOURNAMENT_RE_BUY_COUNT(R.string.rebuy_count), - TOURNAMENT_ENTRY_FEE(R.string.buyin), - MULTI_TABLING(R.string.number_of_tables), - VALUE(R.string.value), - LOCATION(R.string.locations), - BANKROLL(R.string.bankrolls), - NUMBER_OF_PLAYERS(R.string.number_of_players), - MULTI_PLAYER(R.string.multiplayer), - NUMBER_OF_REBUY(R.string.number_of_buyins) - ; +sealed class FilterSectionRow(override val resId: Int?) : RowRepresentable { + object CashOrTournament: FilterSectionRow(R.string.cash_or_tournament) + object LiveOrOnline: FilterSectionRow(R.string.live_or_online) + object Game: FilterSectionRow(R.string.games) + object LimitType: FilterSectionRow(R.string.limits) + object TableSize: FilterSectionRow(R.string.table_sizes) + object DynamicDate: FilterSectionRow(R.string.dynamic_date) + object FixedDate: FilterSectionRow(R.string.fixed_date) + object Duration: FilterSectionRow(R.string.duration) + object Year: FilterSectionRow(R.string.year) + object WeekdayOrWeekend: FilterSectionRow(R.string.weekdays_or_weekend) + object DayOfWeek: FilterSectionRow(R.string.day_of_the_week) + object MonthOfYear: FilterSectionRow(R.string.month_of_the_year) + object SessionDuration: FilterSectionRow(R.string.session_duration) + object TimeFrameRange: FilterSectionRow(R.string.hour_slot) + object Sessions: FilterSectionRow(R.string.sessions) + object Blind: FilterSectionRow(R.string.blinds) + object CashRebuyCount: FilterSectionRow(R.string.rebuy_count) + object TournamentType: FilterSectionRow(R.string.tournament_types) + object TournamentName: FilterSectionRow(R.string.tournament_name) + object TournamentFeature: FilterSectionRow(R.string.tournament_feature) + object CompletionPercentage: FilterSectionRow(R.string.tournament_completion_percentage_interval) + object TournamentFinalPosition: FilterSectionRow(R.string.final_position) + object TournamentNumberOfPlayer: FilterSectionRow(R.string.players_count) + object TournamentRebuyCount: FilterSectionRow(R.string.rebuy_count) + object TournamentEntryFee: FilterSectionRow(R.string.buyin) + object MultiTabling: FilterSectionRow(R.string.number_of_tables) + object Value: FilterSectionRow(R.string.value) + object Location: FilterSectionRow(R.string.locations) + object Bankroll: FilterSectionRow(R.string.bankrolls) + object NumberOfPlayers: FilterSectionRow(R.string.number_of_players) + object MultiPlayer: FilterSectionRow(R.string.multiplayer) + object NumberOfRebuy: FilterSectionRow(R.string.number_of_buyins) + object TransactionType: FilterSectionRow(R.string.operation_types) + + data class CustomField(var customField:net.pokeranalytics.android.model.realm.CustomField): FilterSectionRow(customField.resId) { + override val name = this.customField::class.simpleName ?: throw PokerAnalyticsException.FilterElementUnknownName + } private enum class SelectionType { SINGLE, @@ -49,7 +55,18 @@ enum class FilterSectionRow(override val resId: Int?) : RowRepresentable { override val viewType: Int = RowViewType.HEADER_TITLE.ordinal - val allowMultiSelection: Boolean + open val name = this::class.simpleName ?: throw PokerAnalyticsException.FilterElementUnknownName + + override fun getDisplayName(context: Context): String { + when (this) { + is CustomField -> { + return customField.name + } + } + return name + } + + val allowMultiSelection: Boolean get() = (this.selectionType == SelectionType.MULTIPLE) val filterElements: List @@ -60,13 +77,13 @@ enum class FilterSectionRow(override val resId: Int?) : RowRepresentable { when (this@FilterSectionRow) { // General - CASH_TOURNAMENT -> Criteria.SessionTypes.queryConditions.mapFirstCondition() - LIVE_ONLINE -> Criteria.BankrollTypes.queryConditions.mapFirstCondition() - GAME -> Criteria.Games.queryConditions.mapFirstCondition() - LIMIT_TYPE -> Criteria.Limits.queryConditions.mapFirstCondition() - TABLE_SIZE -> Criteria.TableSizes.queryConditions.mapFirstCondition() + CashOrTournament -> Criteria.SessionTypes.queryConditions.mapFirstCondition() + LiveOrOnline -> Criteria.BankrollTypes.queryConditions.mapFirstCondition() + Game -> Criteria.Games.queryConditions.mapFirstCondition() + LimitType -> Criteria.Limits.queryConditions.mapFirstCondition() + TableSize -> Criteria.TableSizes.queryConditions.mapFirstCondition() // Date - DYNAMIC_DATE -> arrayListOf( + DynamicDate -> arrayListOf( QueryCondition.IsToday, QueryCondition.WasYesterday, QueryCondition.WasTodayAndYesterday, @@ -74,44 +91,63 @@ enum class FilterSectionRow(override val resId: Int?) : RowRepresentable { QueryCondition.DuringThisMonth, QueryCondition.DuringThisYear ) - FIXED_DATE -> arrayListOf(QueryCondition.StartedFromDate(), QueryCondition.EndedToDate()) - DURATION -> arrayListOf(QueryCondition.PastDay()) - WEEKDAYS_OR_WEEKEND -> arrayListOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd) - YEAR -> Criteria.Years.queryConditions.mapFirstCondition() - DAY_OF_WEEK -> Criteria.DaysOfWeek.queryConditions.mapFirstCondition() - MONTH_OF_YEAR -> Criteria.MonthsOfYear.queryConditions.mapFirstCondition() + FixedDate -> arrayListOf(QueryCondition.StartedFromDate(), QueryCondition.EndedToDate()) + Duration -> arrayListOf(QueryCondition.PastDay()) + WeekdayOrWeekend -> arrayListOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd) + Year -> Criteria.Years.queryConditions.mapFirstCondition() + DayOfWeek -> Criteria.DaysOfWeek.queryConditions.mapFirstCondition() + MonthOfYear -> Criteria.MonthsOfYear.queryConditions.mapFirstCondition() // Duration - SESSION_DURATION -> QueryCondition.moreOrLess() - TIME_FRAME_RANGE -> arrayListOf(QueryCondition.StartedFromTime(), QueryCondition.EndedToTime()) + SessionDuration -> QueryCondition.moreOrLess() + TimeFrameRange -> arrayListOf(QueryCondition.StartedFromTime(), QueryCondition.EndedToTime()) // Sessions - SESSIONS -> arrayListOf(QueryCondition.LastGame(), QueryCondition.LastSession()) + //Sessions -> arrayListOf(QueryCondition.LastGame(), QueryCondition.LastSession()) // Cash - BLIND -> Criteria.Blinds.queryConditions.mapFirstCondition() -// CASH_RE_BUY_COUNT -> QueryCondition.moreOrLess() + Blind -> Criteria.Blinds.queryConditions.mapFirstCondition() +// CashRebuyCount -> QueryCondition.moreOrLess() // Tournament - TOURNAMENT_TYPE -> Criteria.TournamentTypes.queryConditions.mapFirstCondition() -// COMPLETION_PERCENTAGE -> arrayListOf() - - TOURNAMENT_FINAL_POSITION -> QueryCondition.moreOrLess() - TOURNAMENT_NUMBER_OF_PLAYER -> QueryCondition.moreOrLess() - TOURNAMENT_ENTRY_FEE -> Criteria.TournamentFees.queryConditions.mapFirstCondition() - TOURNAMENT_NAME -> Criteria.TournamentNames.queryConditions.mapFirstCondition() - TOURNAMENT_FEATURE -> Criteria.TournamentFeatures.queryConditions.mapFirstCondition() - LOCATION -> Criteria.Locations.queryConditions.mapFirstCondition() - BANKROLL -> Criteria.Bankrolls.queryConditions.mapFirstCondition() - MULTI_TABLING -> QueryCondition.moreOrLess() - //NUMBER_OF_PLAYERS -> QueryCondition.moreOrLess() - NUMBER_OF_REBUY -> QueryCondition.moreOrLess() -// MULTI_PLAYER -> arrayListOf() - - VALUE -> arrayListOf().apply { + TournamentType -> Criteria.TournamentTypes.queryConditions.mapFirstCondition() +// CompletionPercentage -> arrayListOf() + + TournamentFinalPosition -> QueryCondition.moreOrLess() + TournamentNumberOfPlayer -> QueryCondition.moreOrLess() + TournamentEntryFee -> Criteria.TournamentFees.queryConditions.mapFirstCondition() + TournamentName -> Criteria.TournamentNames.queryConditions.mapFirstCondition() + TournamentFeature -> Criteria.TournamentFeatures.queryConditions.mapFirstCondition() + Location -> Criteria.Locations.queryConditions.mapFirstCondition() + Bankroll -> Criteria.Bankrolls.queryConditions.mapFirstCondition() + MultiTabling -> QueryCondition.moreOrLess() + //NumberOfPlayers -> QueryCondition.moreOrLess() + NumberOfRebuy -> QueryCondition.moreOrLess() +// MultiPlayer -> arrayListOf() + + Value -> arrayListOf().apply { addAll(QueryCondition.moreOrLess()) addAll(QueryCondition.moreOrLess()) } + TransactionType -> Criteria.TransactionTypes.queryConditions.mapFirstCondition() + is CustomField -> { + val cf = this@FilterSectionRow.customField + if (cf.isListType) { + Criteria.ListCustomFields(cf.id).queryConditions.mapFirstCondition() + } else if (cf.isAmountType) { + QueryCondition.moreOrLess().apply { + this.forEach { + it.customFieldId = cf.id + } + } + } else { + QueryCondition.moreOrLess().apply { + this.forEach { + it.customFieldId = cf.id + } + } + } + } else -> arrayListOf() }.apply { this.forEach { @@ -132,7 +168,7 @@ enum class FilterSectionRow(override val resId: Int?) : RowRepresentable { private val selectionType: SelectionType get() { return when (this) { - CASH_TOURNAMENT, DYNAMIC_DATE, LIVE_ONLINE, SESSIONS -> SelectionType.SINGLE + CashOrTournament, DynamicDate, LiveOrOnline, Sessions -> SelectionType.SINGLE else -> SelectionType.MULTIPLE } } @@ -140,9 +176,10 @@ enum class FilterSectionRow(override val resId: Int?) : RowRepresentable { val exclusiveWith: List? get() { return when (this) { - DYNAMIC_DATE -> arrayListOf(FIXED_DATE) - FIXED_DATE -> arrayListOf(DYNAMIC_DATE) + DynamicDate -> arrayListOf(FixedDate) + FixedDate -> arrayListOf(DynamicDate) else -> null } } + } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/GraphRow.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/GraphRow.kt index b877e42e..7af17e6b 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/GraphRow.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/GraphRow.kt @@ -1,12 +1,13 @@ package net.pokeranalytics.android.ui.view.rowrepresentable +import com.github.mikephil.charting.data.DataSet import net.pokeranalytics.android.calculus.Report import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType -class GraphRow(var report: Report, var stat: Stat, var title: String? = null) : RowRepresentable { +class GraphRow(var dataSet: DataSet<*>?, var title: String? = null, var report: Report? = null, var stat: Stat? = null) : RowRepresentable { override val viewType: Int get() = RowViewType.GRAPH.ordinal diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/ReportRow.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/ReportRow.kt index defbd5ae..a0da7590 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/ReportRow.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/ReportRow.kt @@ -14,7 +14,7 @@ enum class ReportRow : RowRepresentable { DAY_OF_WEEKS, GENERAL, LOCATIONS, -// NUMBER_OF_TABLES, + //NUMBER_OF_TABLES, TOURNAMENT_TYPES, GAME; @@ -39,7 +39,7 @@ enum class ReportRow : RowRepresentable { DAY_OF_WEEKS -> R.string.day_of_the_week GENERAL -> R.string.general LOCATIONS -> R.string.locations -// NUMBER_OF_TABLES -> R.string.number_of_tables + //NUMBER_OF_TABLES -> R.string.number_of_tables TOURNAMENT_TYPES -> R.string.tournament_type_complete GAME -> R.string.game } @@ -55,7 +55,7 @@ enum class ReportRow : RowRepresentable { DAY_OF_WEEKS -> listOf(Criteria.DaysOfWeek) GENERAL -> listOf(Criteria.SessionTypes, Criteria.BankrollTypes) LOCATIONS -> listOf(Criteria.Locations) -// NUMBER_OF_TABLES -> listOf() //TODO + //NUMBER_OF_TABLES -> listOf() //TODO TOURNAMENT_TYPES -> listOf(Criteria.TournamentTypes) GAME -> listOf(Criteria.Games) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SessionRow.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SessionRow.kt index 40a49efe..fd43faa8 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SessionRow.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SessionRow.kt @@ -43,7 +43,7 @@ enum class SessionRow : RowRepresentable { /** * Return the rows to display for the current session state */ - fun getRows(session: Session): ArrayList { + fun getRows(session: Session): List { when (session.type) { Session.Type.TOURNAMENT.ordinal -> { return when (session.getState()) { @@ -92,27 +92,16 @@ enum class SessionRow : RowRepresentable { return arrayListOf(GAME, BLINDS, LOCATION, BANKROLL, TABLE_SIZE, START_DATE, END_DATE) } SessionState.STARTED, SessionState.PAUSED, SessionState.FINISHED -> { - val liveBankroll = session.bankroll?.live ?: false - return if (liveBankroll) { - arrayListOf( - CASHED_OUT, - BUY_IN, - TIPS, - SeparatorRow(), - GAME, - BLINDS, - LOCATION, - BANKROLL, - TABLE_SIZE, - START_DATE, - END_DATE, - BREAK_TIME, - COMMENT - ) - } else { - arrayListOf( - NET_RESULT, - SeparatorRow(), + + val fields = mutableListOf() + when { + session.hasBuyin -> fields.addAll(listOf(CASHED_OUT, BUY_IN, TIPS)) + session.hasNetResult -> fields.add(NET_RESULT) + session.isLive -> fields.addAll(listOf(CASHED_OUT, BUY_IN, TIPS)) + else -> fields.add(NET_RESULT) + } + fields.add(SeparatorRow()) + fields.addAll(listOf( GAME, BLINDS, LOCATION, @@ -123,9 +112,9 @@ enum class SessionRow : RowRepresentable { BREAK_TIME, COMMENT ) - } + ) + return fields } - else -> return arrayListOf() } } } @@ -255,7 +244,7 @@ enum class SessionRow : RowRepresentable { GAME -> { val limit: Int? by map val defaultValue: Any? by map - val data: RealmResults<*>? by map + val data: RealmResults? by map arrayListOf( RowRepresentableEditDescriptor(limit), RowRepresentableEditDescriptor(defaultValue, data = data) @@ -269,7 +258,7 @@ enum class SessionRow : RowRepresentable { } BANKROLL, LOCATION, TOURNAMENT_FEATURE, TOURNAMENT_NAME -> { val defaultValue: Any? by map - val data: RealmResults<*>? by map + val data: RealmResults? by map arrayListOf( RowRepresentableEditDescriptor(defaultValue, data = data) ) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SettingRow.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SettingRow.kt index 018896b8..e3ab95ea 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SettingRow.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SettingRow.kt @@ -9,6 +9,7 @@ import net.pokeranalytics.android.ui.view.RowViewType enum class SettingRow : RowRepresentable { // About + SUBSCRIPTION, VERSION, RATE_APP, CONTACT_US, @@ -22,6 +23,7 @@ enum class SettingRow : RowRepresentable { CURRENCY, // Data management + CUSTOM_FIELD, BANKROLL, GAME, LOCATION, @@ -44,7 +46,7 @@ enum class SettingRow : RowRepresentable { val rows = ArrayList() rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.information)) - rows.addAll(arrayListOf(VERSION, RATE_APP, CONTACT_US, BUG_REPORT)) + rows.addAll(arrayListOf(SUBSCRIPTION, VERSION, RATE_APP, CONTACT_US, BUG_REPORT)) rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.follow_us)) rows.addAll(arrayListOf(FOLLOW_US)) @@ -58,8 +60,7 @@ enum class SettingRow : RowRepresentable { resId = R.string.data_management ) ) - rows.addAll(arrayListOf(BANKROLL, GAME, LOCATION, TOURNAMENT_NAME, TOURNAMENT_FEATURE)) -//, TRANSACTION, TRANSACTION_TYPE //TODO add them back + rows.addAll(arrayListOf(CUSTOM_FIELD, GAME, LOCATION, TOURNAMENT_NAME, TOURNAMENT_FEATURE, TRANSACTION_TYPE)) rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.terms)) rows.addAll(arrayListOf(PRIVACY_POLICY, TERMS_OF_USE, GDPR)) @@ -72,9 +73,10 @@ enum class SettingRow : RowRepresentable { override val resId: Int? get() { this.relatedResultsRepresentable?.let { - return it.resId + return it.pluralResId } ?: run { return when (this) { + SUBSCRIPTION -> R.string.subscription VERSION -> R.string.version RATE_APP -> R.string.releasenote_rating CONTACT_US -> R.string.contact @@ -94,7 +96,7 @@ enum class SettingRow : RowRepresentable { override val viewType: Int get() { return when (this) { - VERSION -> RowViewType.TITLE_VALUE.ordinal + VERSION, SUBSCRIPTION -> RowViewType.TITLE_VALUE.ordinal LANGUAGE, CURRENCY -> RowViewType.TITLE_VALUE_ARROW.ordinal FOLLOW_US -> RowViewType.ROW_FOLLOW_US.ordinal else -> RowViewType.TITLE_ARROW.ordinal @@ -104,6 +106,7 @@ enum class SettingRow : RowRepresentable { override val relatedResultsRepresentable: LiveData? get() { return when (this) { + CUSTOM_FIELD -> LiveData.CUSTOM_FIELD BANKROLL -> LiveData.BANKROLL GAME -> LiveData.GAME LOCATION -> LiveData.LOCATION diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/TransactionRow.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/TransactionRow.kt index 6dd25384..c1cb566a 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/TransactionRow.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/TransactionRow.kt @@ -14,8 +14,8 @@ enum class TransactionRow : RowRepresentable, DefaultEditDataSource { BANKROLL, TYPE, AMOUNT, - COMMENT, - DATE; + DATE, + COMMENT; override val resId: Int? get() { @@ -54,14 +54,14 @@ enum class TransactionRow : RowRepresentable, DefaultEditDataSource { return when (this) { BANKROLL -> { val defaultValue : Any? by map - val data : RealmResults<*>? by map + val data : RealmResults? by map arrayListOf( RowRepresentableEditDescriptor(defaultValue, data = data) ) } TYPE -> { val defaultValue : Any? by map - val data : RealmResults<*>? by map + val data : RealmResults? by map arrayListOf( RowRepresentableEditDescriptor(defaultValue, data = data) ) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/TransactionTypeRow.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/TransactionTypeRow.kt index 83707b5d..a54484b1 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/TransactionTypeRow.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/TransactionTypeRow.kt @@ -1,6 +1,35 @@ package net.pokeranalytics.android.ui.view.rowrepresentable +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.view.DefaultEditDataSource import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowViewType -enum class TransactionTypeRow : RowRepresentable, DefaultEditDataSource +enum class TransactionTypeRow : RowRepresentable, DefaultEditDataSource { + TRANSACTION_ADDITIVE; + + override val resId: Int? + get() { + return when (this) { + TRANSACTION_ADDITIVE -> R.string.additive + } + } + + override val viewType: Int + get() { + return when (this) { + TRANSACTION_ADDITIVE -> RowViewType.TITLE_SWITCH.ordinal + } + } + + override val bottomSheetType: BottomSheetType + get() { + return when (this) { + TRANSACTION_ADDITIVE -> BottomSheetType.NONE + } + } + + + +} diff --git a/app/src/main/java/net/pokeranalytics/android/util/Preferences.kt b/app/src/main/java/net/pokeranalytics/android/util/Preferences.kt index def5f091..6262f7c5 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/Preferences.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/Preferences.kt @@ -23,8 +23,11 @@ class Preferences { CURRENCY_CODE("CurrencyCode"), LOCALE_CODE("LocaleCode"), FIRST_LAUNCH("firstLaunch"), - STOP_SHOWING_DISCLAIMER("stopShowingDisclaimer") - + STOP_SHOWING_DISCLAIMER("stopShowingDisclaimer"), + ACTIVE_FILTER_ID("ActiveFilterId"), + LATEST_PURCHASE("latestPurchase"), + PATCH_BREAK("patchBreaks"), + PATCH_TRANSACTION_TYPES_NAMES("patchTransactionTypesNames") } companion object { @@ -36,7 +39,14 @@ class Preferences { editor.apply() } - fun getString(key: PreferenceKey, context: Context) : String? { + private fun removeKey(key: Keys, context: Context) { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + val editor = preferences.edit() + editor.remove(key.identifier) + editor.apply() + } + + fun getString(key: Keys, context: Context) : String? { val preferences = PreferenceManager.getDefaultSharedPreferences(context) return preferences.getString(key.identifier, null) } @@ -58,6 +68,18 @@ class Preferences { UserDefaults.setCurrencyValues(context) } + fun setActiveFilterId(filterId: String, context: Context) { + setString(Keys.ACTIVE_FILTER_ID, filterId, context) + } + + fun getActiveFilterId(context: Context) : String? { + return getString(Keys.ACTIVE_FILTER_ID, context) + } + + fun removeActiveFilterId(context: Context) { + removeKey(Keys.ACTIVE_FILTER_ID, context) + } + private fun getCurrencyCode(context: Context) : String? { return getString(Keys.CURRENCY_CODE, context) } @@ -92,6 +114,13 @@ class Preferences { return !getBoolean(Keys.STOP_SHOWING_DISCLAIMER, context) } + fun executeOnce(key: Keys, context: Context, executable: () -> Unit) { + if (!getBoolean(key, context)) { + executable.invoke() + setBoolean(key, true, context) + } + } + } } diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/TextFormat.kt b/app/src/main/java/net/pokeranalytics/android/util/TextFormat.kt similarity index 87% rename from app/src/main/java/net/pokeranalytics/android/calculus/TextFormat.kt rename to app/src/main/java/net/pokeranalytics/android/util/TextFormat.kt index a5d26c6c..5bea21f5 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/TextFormat.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/TextFormat.kt @@ -1,4 +1,4 @@ -package net.pokeranalytics.android.calculus +package net.pokeranalytics.android.util import android.content.Context import android.graphics.Color diff --git a/app/src/main/java/net/pokeranalytics/android/util/URL.kt b/app/src/main/java/net/pokeranalytics/android/util/URL.kt index 53bb173a..9a8c1fe3 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/URL.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/URL.kt @@ -18,7 +18,7 @@ enum class URL(var value: String) { // Support SUPPORT_EMAIL("support@pokeranalytics.net"), - // Currency Converter API + // CurrencyCode Converter API API_CURRENCY_CONVERTER("https://free.currencyconverterapi.com/api/v5/") } diff --git a/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt b/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt new file mode 100644 index 00000000..955febdc --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt @@ -0,0 +1,231 @@ +package net.pokeranalytics.android.util.billing + +import android.app.Activity +import android.content.Context +import com.android.billingclient.api.* +import net.pokeranalytics.android.R +import timber.log.Timber +import java.io.IOException +import java.util.* +import kotlin.collections.ArrayList + +enum class IAPProducts(var identifier: String) { + PRO("unlimited") +} + +interface PurchaseDelegate { + fun purchaseDidSucceed(purchase: Purchase) +} + +/** + * the AppGuard object is in charge of contacting the Billing services to retrieve products, + * initiating purchases and verifying transactions + * Requests performed with the BillingClient must be done while being connected. + * Use executeServiceRequest to ensure this + */ +object AppGuard : PurchasesUpdatedListener { + + private const val BASE_64_ENCODED_PUBLIC_KEY = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0VuL5bk2w0FNaZIwE3v2857ZxJlo0epIIsVJfVs7Pqh7zE1JN7uGBgZ/r2s5Rn3o0R1Ycqxp832kYg/B0FvlQJ3Ko6IZkoyfJQ3i1zuAcc7NLMxKUMJY8Mc0U6Go2bevjQ54WkvumIdAFWIlMjyuOOFcSZRZr8V7tlq0SYlenkuellQeHIq3V47M/0jlDrEbCFj59hsukN75eGIiafFAxBYO/8L/flkZLik8YyhV1uZTu+KziA0PsbIvXKyN+gCK9UmrscTyM4+hfmRgb74fro67UsEqq2OvmHFUhubPzCZDElOwPeauUDEGeQjJn43iUHZWIcSEVktVB9cFa/0JwIDAQAB" + + const val MAX_SESSIONS_BEFORE_REQUESTING_SUBSCRIPTION: Int = 10 + + /** + * The Billing Client making requests with Google Billing services + */ + private lateinit var billingClient: BillingClient + + /** + * Whether the billing client is available + */ + private var billingClientAvailable: Boolean = false + + /** + * Returns whether the user has the pro subscription + */ + var isProUser: Boolean = false + private set + + /** + * A delegate to notify when the purchase has succeeded + */ + private var purchaseDelegate: PurchaseDelegate? = null + + /** + * Initialization of AppGuard + * Connects to billing services and restores purchases + */ + fun load(context: Context) { + + billingClient = BillingClient.newBuilder(context).setListener(this).build() + + this.startConnection(Runnable { + this.updatePurchases() + }) + + } + + private fun startConnection(executeOnSuccess: Runnable) { + + billingClient.startConnection(object : BillingClientStateListener { + override fun onBillingSetupFinished(@BillingClient.BillingResponse billingResponseCode: Int) { + if (billingResponseCode == BillingClient.BillingResponse.OK) { + // The BillingClient is ready. You can query purchases here. + billingClientAvailable = true + executeOnSuccess.run() + } + } + + override fun onBillingServiceDisconnected() { + billingClientAvailable = false + } + }) + + } + + private fun executeServiceRequest(runnable: Runnable) { + + if (billingClientAvailable) { + runnable.run() + } else { + this.startConnection(runnable) + } + } + + fun requestPurchasesUpdate() { + this.executeServiceRequest(Runnable { + this.updatePurchases() + }) + } + + /** + * Update the state of subscriptions + * Restores or stops access to IAPs + */ + private fun updatePurchases() { + this.resetPurchases() + // Automatically checks for purchases (when switching devices for example) + val purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS) + if (purchasesResult != null) { + purchasesResult.purchasesList.forEach { + this.handlePurchase(it) + } + } + } + + /** + * Reset all purchases + * This is done before restoring in order to ensure that subscriptions stops + */ + private fun resetPurchases() { + this.isProUser = false + } + + /** + * Requests the product descriptions + */ + fun requestProducts(listener: SkuDetailsResponseListener): Boolean { + + if (this.billingClientAvailable) { + + val skuList = ArrayList() + skuList.add(IAPProducts.PRO.identifier) + val params = SkuDetailsParams.newBuilder() + params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS) + + this.executeServiceRequest(Runnable { + billingClient.querySkuDetailsAsync(params.build(), listener) + }) + + return true + } + return false + } + + /** + * Initiates purchase with the product [skuDetails] + */ + fun initiatePurchase(activity: Activity, skuDetails: SkuDetails, delegate: PurchaseDelegate) { + + this.purchaseDelegate = delegate + + val flowParams = BillingFlowParams.newBuilder() + .setSkuDetails(skuDetails) + .build() + + this.executeServiceRequest(Runnable { + val responseCode = billingClient.launchBillingFlow(activity, flowParams) + Timber.d("launchBillingFlow returned $responseCode") + }) + + } + + // PurchasesUpdatedListener + + /** + * Purchase callback + */ + override fun onPurchasesUpdated(responseCode: Int, purchases: MutableList?) { + + if (responseCode == BillingClient.BillingResponse.OK && purchases != null) { + for (purchase in purchases) { + handlePurchase(purchase) + } + } else if (responseCode == BillingClient.BillingResponse.USER_CANCELED) { + // Handle an error caused by a user cancelling the purchase flow. + } else { + // Handle any other error codes. + } + + } + + /** + * Method called when a [purchase] has been made + */ + private fun handlePurchase(purchase: Purchase) { + + if (this.verifyValidSignature(purchase.originalJson, purchase.signature)) { + when (purchase.sku) { + IAPProducts.PRO.identifier -> { + + + val date = Date(purchase.purchaseTime) + Timber.d("*** Auto renewing = ${purchase.isAutoRenewing}") + Timber.d("*** purchaseTime = ${date}") + + this.isProUser = true + this.purchaseDelegate?.let { + it.purchaseDidSucceed(purchase) + this.purchaseDelegate = null + } + } + else -> { + } + } + } else { + // invalid purchase + } + + } + + /** + * Verifies the validity of a purchase with its [signedData] and [signature] + */ + private fun verifyValidSignature(signedData: String, signature: String): Boolean { + return try { + Security.verifyPurchase(BASE_64_ENCODED_PUBLIC_KEY, signedData, signature) + } catch (e: IOException) { + Timber.d("Got an exception trying to validate a purchase: $e") + false + } + } + + /** + * Returns the subscription status of the user, using the [context] + */ + fun subscriptionStatus(context: Context): String { + val resId = if (this.isProUser) R.string.pro_sub_short_title else R.string.none + return context.getString(resId) + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/billing/Security.kt b/app/src/main/java/net/pokeranalytics/android/util/billing/Security.kt new file mode 100644 index 00000000..07676606 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/billing/Security.kt @@ -0,0 +1,127 @@ +package net.pokeranalytics.android.util.billing + +/* + * Copyright (c) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + + +import android.text.TextUtils +import android.util.Base64 +import com.android.billingclient.util.BillingHelper +import java.io.IOException +import java.security.* +import java.security.spec.InvalidKeySpecException +import java.security.spec.X509EncodedKeySpec + +/** + * Security-related methods. For a secure implementation, all of this code should be implemented on + * a server that communicates with the application on the device. + */ +object Security { + + private val TAG = "IABUtil/Security" + + private val KEY_FACTORY_ALGORITHM = "RSA" + private val SIGNATURE_ALGORITHM = "SHA1withRSA" + + /** + * Verifies that the data was signed with the given signature, and returns the verified + * purchase. + * @param base64PublicKey the base64-encoded public key to use for verifying. + * @param signedData the signed JSON string (signed, not encrypted) + * @param signature the signature for the data, signed with the private key + * @throws IOException if encoding algorithm is not supported or key specification + * is invalid + */ + @Throws(IOException::class) + fun verifyPurchase( + base64PublicKey: String, signedData: String, + signature: String + ): Boolean { + if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) + || TextUtils.isEmpty(signature) + ) { + BillingHelper.logWarn(TAG, "Purchase verification failed: missing data.") + return false + } + + val key = generatePublicKey(base64PublicKey) + return verify(key, signedData, signature) + } + + /** + * Generates a PublicKey instance from a string containing the Base64-encoded public key. + * + * @param encodedPublicKey Base64-encoded public key + * @throws IOException if encoding algorithm is not supported or key specification + * is invalid + */ + @Throws(IOException::class) + fun generatePublicKey(encodedPublicKey: String): PublicKey { + try { + val decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT) + val keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM) + return keyFactory.generatePublic(X509EncodedKeySpec(decodedKey)) + } catch (e: NoSuchAlgorithmException) { + // "RSA" is guaranteed to be available. + throw RuntimeException(e) + } catch (e: InvalidKeySpecException) { + val msg = "Invalid key specification: $e" + BillingHelper.logWarn(TAG, msg) + throw IOException(msg) + } + + } + + /** + * Verifies that the signature from the server matches the computed signature on the data. + * Returns true if the data is correctly signed. + * + * @param publicKey public key associated with the developer account + * @param signedData signed data from server + * @param signature server signature + * @return true if the data and signature match + */ + fun verify(publicKey: PublicKey, signedData: String, signature: String): Boolean { + val signatureBytes: ByteArray + try { + signatureBytes = Base64.decode(signature, Base64.DEFAULT) + } catch (e: IllegalArgumentException) { + BillingHelper.logWarn(TAG, "Base64 decoding failed.") + return false + } + + try { + val signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM) + signatureAlgorithm.initVerify(publicKey) + signatureAlgorithm.update(signedData.toByteArray()) + if (!signatureAlgorithm.verify(signatureBytes)) { + BillingHelper.logWarn(TAG, "Signature verification failed.") + return false + } + return true + } catch (e: NoSuchAlgorithmException) { + // "RSA" is guaranteed to be available. + throw RuntimeException(e) + } catch (e: InvalidKeyException) { + BillingHelper.logWarn(TAG, "Invalid key specification.") + } catch (e: SignatureException) { + BillingHelper.logWarn(TAG, "Signature exception.") + } + + return false + } +} diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt new file mode 100644 index 00000000..f4ef3174 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt @@ -0,0 +1,91 @@ +package net.pokeranalytics.android.util.csv + +import io.realm.Realm +import io.realm.RealmModel +import org.apache.commons.csv.CSVRecord + +/** + * The various sources of CSV + */ +enum class DataSource { + POKER_INCOME, + POKER_BANKROLL_TRACKER, + RUNGOOD +} + +/** + * A DataCSVDescriptor produces RealmModel instances for each row + */ +abstract class DataCSVDescriptor(source: DataSource, vararg elements: CSVField) : CSVDescriptor(source, *elements) { + + val realmModels = mutableListOf() + + abstract fun parseData(realm: Realm, record: CSVRecord): T? + + override fun parse(realm: Realm, record: CSVRecord) { + + val data = this.parseData(realm, record) + data?.let { + this.realmModels.add(it) + } + + } + +} + +/** + * A CSVDescriptor describes a CSV format by a source and a list of Fields + */ +abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField) { + + /** + * The CSVField list describing the CSV header format + */ + protected var fields: List = listOf() + /** + * The mapping of CSVField with their index in the CSV file + */ + protected var fieldMapping: MutableMap = mutableMapOf() + + init { + if (elements.size > 0) { + this.fields = elements.toList() + } + } + + companion object { + /** + * The list of all managed CSVDescriptors + */ + val all: List = + listOf(SessionCSVDescriptor.pokerIncomeCash, SessionCSVDescriptor.pokerBankrollTracker, SessionCSVDescriptor.runGoodCashGames, SessionCSVDescriptor.runGoodTournaments) + } + + /** + * Returns whether the [record] matches the CSVDescriptor + */ + fun matches(record: CSVRecord): Boolean { + + var count = 0 + val headers = record.toSet() + + this.fields.forEach { field -> + + val index = headers.indexOf(field.header) + this.fieldMapping[field] = index + + if (index >= 0) { + count++ + } + + } + + return count == this.fields.size + } + + /** + * Method called when iterating on a CSVRecord + */ + abstract fun parse(realm: Realm, record: CSVRecord) + +} diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt new file mode 100644 index 00000000..3e7e6659 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt @@ -0,0 +1,80 @@ +package net.pokeranalytics.android.util.csv + +import timber.log.Timber +import java.text.DateFormat +import java.text.NumberFormat +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +/** + * An interface used to provide a Number flavour to a CSV field + */ +interface NumberCSVField: TypedCSVField { + + val numberFormat: String? + + override fun parse(value: String) : Double? { + + if (value.isEmpty()) { + return null + } + + val formatter = NumberFormat.getInstance() + + return try { + formatter.parse(value).toDouble() + } catch (e: ParseException) { + Timber.d("Unparseable number: $value") + null + } + } +} + +interface DateCSVField : TypedCSVField { + + val dateFormat: String? + + override fun parse(value: String) : Date? { + + val formatter = if (dateFormat != null) SimpleDateFormat(dateFormat) else SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) + return try { + formatter.parse(value) + } catch (e: ParseException) { + Timber.d("Unparseable date: $value") + null + } + } + +} + +interface BlindCSVField : TypedCSVField> { + + override fun parse(value: String) : Pair? { + + this.callback?.let { cb -> + return cb(value) + } + + val strBlinds = value.split("/") + if (strBlinds.size == 2) { + val bb = strBlinds.last().toDouble() + val sb = strBlinds.first().toDouble() + return Pair(sb, bb) + } else { + Timber.d("Blinds could not be parsed: $value") + } + + return null + } + +} + +interface TypedCSVField : CSVField { + fun parse(value: String) : T? + var callback: ((String) -> T?)? +} + +interface CSVField { + val header: String +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt new file mode 100644 index 00000000..7a1bfb90 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt @@ -0,0 +1,158 @@ +package net.pokeranalytics.android.util.csv + +import io.realm.Realm +import org.apache.commons.csv.CSVFormat +import org.apache.commons.csv.CSVRecord +import timber.log.Timber +import java.io.FileReader +import java.io.InputStream +import java.io.InputStreamReader +import java.io.Reader + +class ImportException(message: String) : Exception(message) + +/** + * A CSVImporter is a class in charge of parsing a CSV file and processing it + * When starting the parsing of a file, the instance will search for a CSVDescriptor, which describes + * the format of a CSV file. + * When finding a descriptor, the CSVImporter then continue to parse the file and delegates the parsing of each row + * to the CSVDescriptor + */ +open class CSVImporter(istream: InputStream) { + + /** + * Number of commits required to commit a Realm transaction + */ + private val COMMIT_FREQUENCY = 100 + /** + * The number of column indicating a valid record + */ + private val VALID_RECORD_COLUMNS = 4 + /** + * The number of valid record to test for descriptor before throwing a File Format Exception + */ + private val VALID_RECORD_ATTEMPTS_BEFORE_THROWING_EXCEPTION = 4 + + /** + * The path of the CSV file + */ + private var path: String? = null + /** + * The InputStream containing a file content + */ + private var inputStream: InputStream? = istream + + /** + * The current number of attempts at finding a valid CSVDescriptor + */ + private var descriptorFindingAttempts = 0 + + /** + * Stores the descriptors found + */ + private var usedDescriptors: MutableList = mutableListOf() + /** + * The currently used CSVDescriptor for parsing + */ + private var currentDescriptor: CSVDescriptor? = null + + /** + * Constructs a CSVParser object and starts parsing the CSV + */ + fun start() { + + val realm = Realm.getDefaultInstance() + + var reader: Reader? = null + if (this.path != null) { + reader = FileReader(this.path) + } + if (this.inputStream != null) { + reader = InputStreamReader(this.inputStream) + } + + val parser = CSVFormat.DEFAULT.withAllowMissingColumnNames().parse(reader) + + Timber.d("Starting import...") + + realm.beginTransaction() + + parser.forEachIndexed { index, record -> + + Timber.d("line $index") + + if (this.currentDescriptor == null) { // find descriptor + this.currentDescriptor = this.findDescriptor(record) + + if (this.currentDescriptor == null) { + + if (record.size() >= VALID_RECORD_COLUMNS) { + this.descriptorFindingAttempts++ + } + if (this.descriptorFindingAttempts >= VALID_RECORD_ATTEMPTS_BEFORE_THROWING_EXCEPTION) { + realm.cancelTransaction() + throw ImportException("This type of file is not supported") + } + } + + } else { // parse + + val parsingIndex = index + 1 + if (parsingIndex % COMMIT_FREQUENCY == 0) { + Timber.d("****** committing at $parsingIndex sessions...") + realm.commitTransaction() + realm.beginTransaction() + } + + this.currentDescriptor?.let { + if (record.size() == 0) { + this.usedDescriptors.add(it) + this.currentDescriptor = null // reset descriptor when encountering an empty line (multiple descriptors can be found in a single file) + this.descriptorFindingAttempts = 0 + } else { + it.parse(realm, record) + } + } ?: run { + realm.cancelTransaction() + throw IllegalStateException("CSVDescriptor should never be null here") + } + + } + } + + realm.commitTransaction() + + Timber.d("Ending import...") + + realm.close() + } + + /** + * Search for a descriptor in the list of managed formats + */ + private fun findDescriptor(record: CSVRecord) : CSVDescriptor? { + + CSVDescriptor.all.forEach { descriptor -> + if (descriptor.matches(record)) { + this.currentDescriptor = descriptor + Timber.d("Identified source: ${descriptor.source}") + return descriptor + } + } + return null + } + + fun save(realm: Realm) { + + this.usedDescriptors.forEach { descriptor -> + + if (descriptor is DataCSVDescriptor<*>) { + realm.executeTransaction { + realm.copyToRealm(descriptor.realmModels) + } + } + } + + } + +} diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt new file mode 100644 index 00000000..d33e719f --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt @@ -0,0 +1,304 @@ +package net.pokeranalytics.android.util.csv + +import io.realm.Realm +import net.pokeranalytics.android.model.Limit +import net.pokeranalytics.android.model.TableSize +import net.pokeranalytics.android.model.TournamentType +import net.pokeranalytics.android.model.realm.Bankroll +import net.pokeranalytics.android.model.realm.Session +import net.pokeranalytics.android.model.utils.SessionUtils +import net.pokeranalytics.android.util.extensions.getOrCreate +import net.pokeranalytics.android.util.extensions.setHourMinutes +import org.apache.commons.csv.CSVRecord +import java.util.* + +/** + * The enumeration of Session fields + */ +sealed class SessionField { + + data class Start( + override var header: String, + override var callback: ((String) -> Date?)? = null, + override val dateFormat: String? = null + ) : DateCSVField + + data class StartTime( + override var header: String, + override var callback: ((String) -> Date?)? = null, + override val dateFormat: String? = null + ) : DateCSVField + + data class End( + override var header: String, + override var callback: ((String) -> Date?)? = null, + override val dateFormat: String? = null + ) : DateCSVField + + data class EndTime( + override var header: String, + override var callback: ((String) -> Date?)? = null, + override val dateFormat: String? = null + ) : DateCSVField + + data class Buyin( + override var header: String, + override var callback: ((String) -> Double?)? = null, + override val numberFormat: String? = null + ) : NumberCSVField + + data class NetResult( + override var header: String, + override var callback: ((String) -> Double?)? = null, + override val numberFormat: String? = null + ) : NumberCSVField + + data class CashedOut( + override var header: String, + override var callback: ((String) -> Double?)? = null, + override val numberFormat: String? = null + ) : NumberCSVField + + data class Break( + override var header: String, + override var callback: ((String) -> Double?)? = null, + override val numberFormat: String? = null + ) : NumberCSVField + + data class Tips( + override var header: String, + override var callback: ((String) -> Double?)? = null, + override val numberFormat: String? = null + ) : NumberCSVField + + data class SmallBlind( + override var header: String, + override var callback: ((String) -> Double?)? = null, + override val numberFormat: String? = null + ) : NumberCSVField + + data class BigBlind( + override var header: String, + override var callback: ((String) -> Double?)? = null, + override val numberFormat: String? = null + ) : NumberCSVField + + data class Blind(override var header: String, override var callback: ((String) -> Pair?)? = null) : BlindCSVField + data class Game(override var header: String) : CSVField + data class Location(override var header: String) : CSVField + data class LocationType(override var header: String) : CSVField + data class Bankroll(override var header: String) : CSVField + data class LimitType(override var header: String) : CSVField + data class Comment(override var header: String) : CSVField + data class SessionType(override var header: String) : CSVField + data class TableSize(override var header: String) : CSVField + data class CurrencyCode(override var header: String) : CSVField + data class TournamentName(override var header: String) : CSVField + data class TournamentType(override var header: String) : CSVField + + data class CurrencyRate( + override var header: String, + override var callback: ((String) -> Double?)? = null, + override val numberFormat: String? = null + ) : NumberCSVField + + data class TournamentPosition( + override var header: String, + override var callback: ((String) -> Double?)? = null, + override val numberFormat: String? = null + ) : NumberCSVField + + data class TournamentNumberOfPlayers( + override var header: String, + override var callback: ((String) -> Double?)? = null, + override val numberFormat: String? = null + ) : NumberCSVField +} + +/** + * A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects + */ +class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean, vararg elements: CSVField) : DataCSVDescriptor(source, *elements) { + + companion object { + val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor( + DataSource.POKER_INCOME, + false, + SessionField.Start("Start Time"), + SessionField.End("End Time"), + SessionField.Buyin("Buy In"), + SessionField.CashedOut("Cashed Out"), + SessionField.Break("Break Minutes"), + SessionField.LimitType("Limit Type"), + SessionField.Game("Game"), + SessionField.Bankroll("Bankroll"), + SessionField.Location("Location"), + SessionField.Location("Location Type"), + SessionField.Comment("Note"), + SessionField.Tips("Tips"), + SessionField.Blind("Stake") + ) + + val pokerBankrollTracker: CSVDescriptor = SessionCSVDescriptor( + DataSource.POKER_BANKROLL_TRACKER, + true, + SessionField.Start("starttime", dateFormat = "MM/dd/yyyy HH:mm"), + SessionField.End("endtime", dateFormat = "MM/dd/yyyy HH:mm"), + SessionField.SessionType("variant"), + SessionField.Buyin("buyin"), + SessionField.CashedOut("cashout"), + SessionField.Break("breakminutes"), + SessionField.LimitType("limit"), + SessionField.Game("game"), + SessionField.Bankroll("currency"), // same as currency code + SessionField.Location("location"), + SessionField.Comment("sessionnote"), + SessionField.Tips("expensesfromstack"), + SessionField.SmallBlind("smallblind"), + SessionField.BigBlind("bigblind"), + SessionField.TournamentNumberOfPlayers("player"), + SessionField.TournamentPosition("place"), + SessionField.TournamentName("mttname"), + SessionField.CurrencyCode("currency"), + SessionField.CurrencyRate("exchangerate"), + SessionField.TableSize("tablesize") + ) + + val runGoodTournaments: CSVDescriptor = SessionCSVDescriptor( + DataSource.RUNGOOD, + true, + SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"), + SessionField.StartTime("Start Time"), + SessionField.End("End Date"), + SessionField.EndTime("End Time"), + SessionField.Buyin("Total Buy-In"), + SessionField.CashedOut("Winnings"), + SessionField.NetResult("Profit"), + SessionField.Break("Break"), + SessionField.LimitType("Limit Type"), + SessionField.Game("Game"), + SessionField.Bankroll("Bankroll"), + SessionField.TableSize("Table Type"), + SessionField.Location("Location"), + SessionField.LocationType("Location Type"), + SessionField.Comment("Notes"), + SessionField.CurrencyCode("Currency"), + SessionField.TournamentName("Event Name"), + SessionField.TournamentNumberOfPlayers("Total Players"), + SessionField.TournamentPosition("Finished Place"), + SessionField.TournamentType("Single-Table/Multi-Table") + + ) + + val runGoodCashGames: CSVDescriptor = SessionCSVDescriptor( + DataSource.RUNGOOD, + false, + SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"), + SessionField.StartTime("Start Time", dateFormat = "HH:mm"), + SessionField.End("End Date", dateFormat = "dd/MM/yyyy"), + SessionField.EndTime("End Time", dateFormat = "HH:mm"), + SessionField.Buyin("Total Buy-In"), + SessionField.CashedOut("Cashed Out"), + SessionField.NetResult("Profit"), + SessionField.Break("Break"), + SessionField.LimitType("Limit Type"), + SessionField.Game("Game"), + SessionField.Bankroll("Bankroll"), + SessionField.TableSize("Table Type"), + SessionField.Location("Location"), + SessionField.LocationType("Location Type"), + SessionField.Comment("Notes"), + SessionField.CurrencyCode("Currency"), + SessionField.Blind("Stakes", callback = { value -> // $10/20 + value.drop(1) + val blinds = value.split("/") + if (blinds.size == 2) { + return@Blind Pair(blinds.first().toDouble(), blinds.last().toDouble()) + } else { + return@Blind null + } + }) + ) + + } + + /** + * Parses a [record] and return an optional Session + */ + override fun parseData(realm: Realm, record: CSVRecord): Session? { + + val session = Session.newInstance(realm, this.isTournament) + + var isLive = true + var bankrollName: String? = null + var currencyCode: String? = null + var currencyRate: Double? = null + + fields.forEach { field -> + + this.fieldMapping[field]?.let { index -> + + val value = record.get(index) + when (field) { + is SessionField.Start -> { + session.startDate = field.parse(value) + } + is SessionField.End -> { + session.endDate = field.parse(value) + } + is SessionField.StartTime -> { session.startDate?.setHourMinutes(value) } + is SessionField.EndTime -> { session.endDate?.setHourMinutes(value) } + is SessionField.Buyin -> session.result?.buyin = field.parse(value) + is SessionField.CashedOut -> session.result?.cashout = field.parse(value) + is SessionField.Tips -> session.result?.tips = field.parse(value) + is SessionField.Break -> { + field.parse(value)?.let { + session.breakDuration = it.toLong() * 60 * 1000 + } + } + is SessionField.Game -> session.game = realm.getOrCreate(value) + is SessionField.Location -> session.location = realm.getOrCreate(value) + is SessionField.Bankroll -> bankrollName = value + is SessionField.LimitType -> session.limit = Limit.getInstance(value)?.ordinal + is SessionField.Comment -> session.comment = value + is SessionField.Blind -> { // 1/2 + val blinds = field.parse(value) + session.cgSmallBlind = blinds?.first + session.cgBigBlind = blinds?.second + } + is SessionField.SmallBlind -> session.cgSmallBlind = field.parse(value) + is SessionField.BigBlind -> session.cgBigBlind = field.parse(value) + is SessionField.TableSize -> session.tableSize = TableSize.valueForLabel(value) + is SessionField.SessionType -> { + Session.Type.getValueFromString(value)?.let { type -> + session.type = type.ordinal + } + } + is SessionField.TournamentPosition -> session.result?.tournamentFinalPosition = field.parse(value)?.toInt() + is SessionField.TournamentName -> session.tournamentName = realm.getOrCreate(value) + is SessionField.TournamentType -> session.tournamentType = TournamentType.getValueForLabel(value)?.ordinal + is SessionField.TournamentNumberOfPlayers -> session.tournamentNumberOfPlayers = field.parse(value)?.toInt() + is SessionField.CurrencyCode -> currencyCode = value + is SessionField.CurrencyRate -> currencyRate = field.parse(value) + else -> { } + } + } + + } + + session.bankroll = Bankroll.getOrCreate(realm, bankrollName ?: "Import", isLive, currencyCode, currencyRate) + + val startDate = session.startDate + val endDate = session.endDate + val net = session.result?.net + + return if (startDate != null && endDate != null && net != null) { // valid session + val unique = SessionUtils.unicityCheck(realm, startDate, endDate, net) + if (unique) session else null + } else { // invalid session + null + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/enumerations/SavableEnumeration.kt b/app/src/main/java/net/pokeranalytics/android/util/enumerations/SavableEnumeration.kt new file mode 100644 index 00000000..a6914dbf --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/enumerations/SavableEnumeration.kt @@ -0,0 +1,23 @@ +package net.pokeranalytics.android.util.enumerations + +import net.pokeranalytics.android.exceptions.EnumIdentifierNotFoundException +import net.pokeranalytics.android.exceptions.MisconfiguredSavableEnumException + +interface IntSearchable { + + fun valuesInternal(): Array + + fun valueByIdentifier(identifier: Int) : T { + val values = this.valuesInternal().filter { it.uniqueIdentifier == identifier } + return when (values.size) { + 0 -> throw EnumIdentifierNotFoundException("Savable enumeration uniqueIdentifier $identifier not found") + 1 -> values.first() + else -> throw MisconfiguredSavableEnumException("Savable enumeration has multiple elements with uniqueIdentifier $identifier") + } + } + +} + +interface IntIdentifiable { + var uniqueIdentifier: Int +} diff --git a/app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt b/app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt index b60ba293..d65d8a09 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt @@ -7,49 +7,62 @@ import java.util.* // Calendar +// Return a double representing the hour / minute of a date from a calendar +fun Calendar.hourMinute(): Double { + return (this.get(Calendar.HOUR_OF_DAY) + this.get(Calendar.MINUTE).toDouble() / 60.0).roundOffDecimal() +} + + // Return if the calendar dates are in the same month fun Calendar.isSameMonth(calendar: Calendar): Boolean { - return calendar.get(Calendar.YEAR) == this.get(Calendar.YEAR) && - calendar.get(Calendar.MONTH) == this.get(Calendar.MONTH) + return calendar.get(Calendar.YEAR) == this.get(Calendar.YEAR) && + calendar.get(Calendar.MONTH) == this.get(Calendar.MONTH) } + // Return if the calendar dates are in the same day fun Calendar.isSameDay(calendar: Calendar): Boolean { - return calendar.get(Calendar.YEAR) == this.get(Calendar.YEAR) && - calendar.get(Calendar.MONTH) == this.get(Calendar.MONTH) && - calendar.get(Calendar.DAY_OF_MONTH) == this.get(Calendar.DAY_OF_MONTH) + return calendar.get(Calendar.YEAR) == this.get(Calendar.YEAR) && + calendar.get(Calendar.MONTH) == this.get(Calendar.MONTH) && + calendar.get(Calendar.DAY_OF_MONTH) == this.get(Calendar.DAY_OF_MONTH) } // Date // Return a short string of the date fun Date.shortDate(): String { - return DateFormat.getDateInstance(DateFormat.SHORT).format(this) + return DateFormat.getDateInstance(DateFormat.SHORT).format(this) } + // Return a short string of the date fun Date.mediumDate(): String { - return DateFormat.getDateInstance(DateFormat.MEDIUM).format(this) + return DateFormat.getDateInstance(DateFormat.MEDIUM).format(this) } + // Return a long string of the date fun Date.longDate(): String { - return DateFormat.getDateInstance(DateFormat.LONG).format(this) + return DateFormat.getDateInstance(DateFormat.LONG).format(this) } + // Return a short string of the date fun Date.fullDate(): String { - return DateFormat.getDateInstance(DateFormat.FULL).format(this) + return DateFormat.getDateInstance(DateFormat.FULL).format(this) } // Return a short string of the time fun Date.shortTime(): String { return DateFormat.getTimeInstance(DateFormat.SHORT).format(this) } + // Return a short string of the time fun Date.mediumTime(): String { return DateFormat.getTimeInstance(DateFormat.MEDIUM).format(this) } + // Return a long string of the time fun Date.longTime(): String { return DateFormat.getTimeInstance(DateFormat.LONG).format(this) } + // Return a short string of the time fun Date.fullTime(): String { return DateFormat.getTimeInstance(DateFormat.FULL).format(this) @@ -57,88 +70,120 @@ fun Date.fullTime(): String { // Return a short string of the date & time fun Date.shortDateTime(): String { - return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(this) + return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(this) } + // Return a medium string of the date & time fun Date.mediumDateTime(): String { - return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(this) + return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(this) } + // Return a long string of the date & time fun Date.longDateTime(): String { - return DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(this) + return DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(this) } + // Return the full string of the date & time fun Date.fullDateTime(): String { - return DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(this) + return DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(this) } + // Return the day number of the date -fun Date.getDayNumber() : String { - return SimpleDateFormat("dd", Locale.getDefault()).format(this) +fun Date.getDayNumber(): String { + return SimpleDateFormat("dd", Locale.getDefault()).format(this) } + // Return the 3 first letters of the date's day -fun Date.getShortDayName() : String { - return SimpleDateFormat("EEE", Locale.getDefault()).format(this) +fun Date.getShortDayName(): String { + return SimpleDateFormat("EEE", Locale.getDefault()).format(this) } + // Return the month of the date fun Date.getDateMonth(): String { return SimpleDateFormat("MMMM", Locale.getDefault()).format(this).capitalize() } + // Return the year of the date fun Date.getDateYear(): String { return SimpleDateFormat("yyyy", Locale.getDefault()).format(this).capitalize() } + // Return the month & year of the date fun Date.getMonthAndYear(): String { - return SimpleDateFormat("MMMM yyyy", Locale.getDefault()).format(this).capitalize() + return SimpleDateFormat("MMMM yyyy", Locale.getDefault()).format(this).capitalize() } // Return the netDuration between two dates -fun Date.getFormattedDuration(toDate: Date) : String { - val difference = (toDate.time - this.time).toInt() - val numOfDays = (difference / (1000 * 60 * 60 * 24)) - val hours = (difference / (1000 * 60 * 60)) - val minutes = (difference / (1000 * 60)) % 60 +fun Date.getFormattedDuration(toDate: Date): String { + val difference = (toDate.time - this.time).toInt() + val hours = (difference / (1000 * 60 * 60)) + val minutes = (difference / (1000 * 60)) % 60 - val hoursStr = "$hours" - val minutesStr = if (minutes < 10) "0$minutes" else "$minutes" + val hoursStr = "$hours" + val minutesStr = if (minutes < 10) "0$minutes" else "$minutes" - return "$hoursStr:$minutesStr" + return "$hoursStr:$minutesStr" } // Return the date of the beginning of the current date -fun Date.startOfDay() : Date { - val calendar = Calendar.getInstance() - calendar.time = this - calendar.set(Calendar.HOUR_OF_DAY, 0) - calendar.set(Calendar.MINUTE, 0) - calendar.set(Calendar.SECOND, 0) - calendar.set(Calendar.MILLISECOND, 0) - return calendar.time +fun Date.startOfDay(): Date { + val calendar = Calendar.getInstance() + calendar.time = this + calendar.set(Calendar.HOUR_OF_DAY, 0) + calendar.set(Calendar.MINUTE, 0) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MILLISECOND, 0) + return calendar.time } // Return the date of the end of the current date -fun Date.endOfDay() : Date { - val calendar = Calendar.getInstance() - calendar.time = this - calendar.set(Calendar.HOUR_OF_DAY, 23) - calendar.set(Calendar.MINUTE, 59) - calendar.set(Calendar.SECOND, 59) - calendar.set(Calendar.MILLISECOND, 999) - return calendar.time +fun Date.endOfDay(): Date { + val calendar = Calendar.getInstance() + calendar.time = this + calendar.set(Calendar.HOUR_OF_DAY, 23) + calendar.set(Calendar.MINUTE, 59) + calendar.set(Calendar.SECOND, 59) + calendar.set(Calendar.MILLISECOND, 999) + return calendar.time } // Return the date of the beginning of the current month -fun Date.startOfMonth() : Date { - val calendar = Calendar.getInstance() - calendar.time = this.startOfDay() - calendar.set(Calendar.DAY_OF_MONTH, 1) - return calendar.time +fun Date.startOfMonth(): Date { + val calendar = Calendar.getInstance() + calendar.time = this.startOfDay() + calendar.set(Calendar.DAY_OF_MONTH, 1) + return calendar.time } // Return the date of the beginning of the current year -fun Date.startOfYear() : Date { - val calendar = Calendar.getInstance() - calendar.time = this.startOfMonth() - calendar.set(Calendar.MONTH, 0) - return calendar.time -} \ No newline at end of file +fun Date.startOfYear(): Date { + val calendar = Calendar.getInstance() + calendar.time = this.startOfMonth() + calendar.set(Calendar.MONTH, 0) + return calendar.time +} + +// Return the number of seconds until the next minute +fun Date.getNextMinuteInseconds(): Int { + return (getNextMinuteInMilliseconds() / 1000).toInt() +} + +// Return the number of milliseconds until the next minute +fun Date.getNextMinuteInMilliseconds(): Long { + val calendar = Calendar.getInstance() + calendar.add(Calendar.MINUTE, 1) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MILLISECOND, 0) + return calendar.time.time - this.time +} + +fun Date.setHourMinutes(value: String, dateFormat: String = "HH:mm") { + val calendar1 = Calendar.getInstance() + calendar1.time = this + val calendar2 = Calendar.getInstance().apply { time = SimpleDateFormat(dateFormat, Locale.getDefault()).parse(value) } + calendar1.set(Calendar.HOUR_OF_DAY, calendar2.get(Calendar.HOUR_OF_DAY)) + calendar1.set(Calendar.MINUTE, calendar2.get(Calendar.MINUTE)) + calendar1.set(Calendar.SECOND, 0) + calendar1.set(Calendar.MILLISECOND, 0) + this.time = calendar1.time.time +} diff --git a/app/src/main/java/net/pokeranalytics/android/util/extensions/NumbersExtension.kt b/app/src/main/java/net/pokeranalytics/android/util/extensions/NumbersExtension.kt index c2578945..39a3f043 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/extensions/NumbersExtension.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/extensions/NumbersExtension.kt @@ -3,6 +3,7 @@ package net.pokeranalytics.android.util.extensions import android.content.Context import net.pokeranalytics.android.R import java.lang.Math.abs +import java.math.RoundingMode import java.text.DecimalFormat import java.text.NumberFormat import java.util.* @@ -36,6 +37,10 @@ fun Double.round(): String { return formatter.format(this) } +fun Double.roundOffDecimal(): Double { + return this.toBigDecimal().setScale(2, RoundingMode.CEILING).toDouble() +} + fun Double.formatted(): String { val format = NumberFormat.getNumberInstance() format.maximumFractionDigits = 2 diff --git a/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt b/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt new file mode 100644 index 00000000..aa15c8e6 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt @@ -0,0 +1,101 @@ +package net.pokeranalytics.android.util.extensions + +import io.realm.* +import io.realm.kotlin.where +import net.pokeranalytics.android.model.interfaces.CountableUsage +import net.pokeranalytics.android.model.interfaces.Identifiable +import net.pokeranalytics.android.model.interfaces.NameManageable +import net.pokeranalytics.android.model.realm.* +import net.pokeranalytics.android.ui.interfaces.FilterableType + +fun Realm.findById(clazz: Class, id: String) : T? { + return this.where(clazz).equalTo("id", id).findFirst() +} + +inline fun Realm.findById(id: String) : T? { + return this.findById(T::class.java, id) +} + +fun Realm.getOrCreate(clazz: Class, name: String) : T { + val instance = this.where(clazz).equalTo("name", name).findFirst() + return if (instance != null) { + instance + } else { + val newInstance = clazz.newInstance() + newInstance.name = name + this.copyToRealm(newInstance) + } +} + +inline fun Realm.getOrCreate(name: String) : T { + return this.getOrCreate(T::class.java, name) +} + +/** + * Returns all entities of the [clazz] sorted with their default sorting + * Set [editableOnly] to true to only receive entities that can be edited + */ +fun < T : RealmModel> Realm.sorted(clazz: Class, editableOnly: Boolean = false, filterableTypeUniqueIdentifier: Int? = null) : RealmResults { + val query = this.where(clazz) + when (clazz.kotlin) { + TransactionType::class -> { + if (editableOnly) { + query.equalTo("lock", false) + } + } + Filter::class -> { + filterableTypeUniqueIdentifier?.let { + query.equalTo("filterableTypeUniqueIdentifier", it) + } + } + } + + val items = query.findAll() + var sortField = arrayOf("name") + var resultSort = arrayOf(Sort.ASCENDING) + + when (items.firstOrNull()) { + is Transaction -> { + sortField = arrayOf("date") + resultSort = arrayOf(Sort.DESCENDING) + } + is Filter -> { + sortField = arrayOf("useCount", "name") + resultSort = arrayOf(Sort.DESCENDING, Sort.ASCENDING) + } + is CountableUsage -> { + this.updateUsageCount(clazz) + sortField = arrayOf("useCount") + resultSort = arrayOf(Sort.DESCENDING) + } + } + return items.sort(sortField, resultSort) +} + +/** + * Returns all entities of the [C] class sorted with their default sorting + */ +inline fun Realm.sorted(editableOnly: Boolean = false) : RealmResults { + return this.sorted(C::class.java, editableOnly) +} + +/** + * Updates the useCount variable of the CountableUsage entity + */ +fun Realm.updateUsageCount(clazz: Class) { + + val results = this.where(clazz).findAll() + this.executeTransaction { + results.forEach { countableUsage -> + + val countable = (countableUsage as CountableUsage) + val fieldName = when (clazz.kotlin) { + TournamentFeature::class -> "tournamentFeatures.id" + else -> "${clazz.simpleName.decapitalize()}.id" + } + val count = it.where().contains(fieldName, countable.id).count().toInt() + countable.useCount = count + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/color/chips_background_states.xml b/app/src/main/res/color/chips_background_states.xml index 7d57a042..bd398e18 100755 --- a/app/src/main/res/color/chips_background_states.xml +++ b/app/src/main/res/color/chips_background_states.xml @@ -2,4 +2,9 @@ + \ No newline at end of file diff --git a/app/src/main/res/color/chips_stroke_states.xml b/app/src/main/res/color/chips_stroke_states.xml new file mode 100755 index 00000000..f0863fee --- /dev/null +++ b/app/src/main/res/color/chips_stroke_states.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/chips_text_states.xml b/app/src/main/res/color/chips_text_states.xml new file mode 100755 index 00000000..8f2660fa --- /dev/null +++ b/app/src/main/res/color/chips_text_states.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_all_inclusive.xml b/app/src/main/res/drawable/ic_baseline_all_inclusive.xml new file mode 100644 index 00000000..c857b95d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_all_inclusive.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_email.xml b/app/src/main/res/drawable/ic_baseline_email.xml new file mode 100644 index 00000000..789a0188 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_email.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_save.xml b/app/src/main/res/drawable/ic_baseline_save.xml new file mode 100644 index 00000000..1a3cbf91 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_save.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_vpn_key.xml b/app/src/main/res/drawable/ic_baseline_vpn_key.xml new file mode 100644 index 00000000..39adc2cf --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_vpn_key.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_wifi_off.xml b/app/src/main/res/drawable/ic_baseline_wifi_off.xml new file mode 100644 index 00000000..e857159f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_wifi_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_list.xml b/app/src/main/res/drawable/ic_list.xml new file mode 100644 index 00000000..4c2fb883 --- /dev/null +++ b/app/src/main/res/drawable/ic_list.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_reorder.xml b/app/src/main/res/drawable/ic_reorder.xml new file mode 100644 index 00000000..abf6041a --- /dev/null +++ b/app/src/main/res/drawable/ic_reorder.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/rectangle.xml b/app/src/main/res/drawable/rectangle.xml new file mode 100644 index 00000000..d32283e5 --- /dev/null +++ b/app/src/main/res/drawable/rectangle.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw320dp/fragment_calendar.xml b/app/src/main/res/layout-sw320dp/fragment_calendar.xml index 7bb22310..e5673d6d 100644 --- a/app/src/main/res/layout-sw320dp/fragment_calendar.xml +++ b/app/src/main/res/layout-sw320dp/fragment_calendar.xml @@ -13,6 +13,15 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_billing.xml b/app/src/main/res/layout/activity_billing.xml new file mode 100644 index 00000000..08d1f651 --- /dev/null +++ b/app/src/main/res/layout/activity_billing.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_calendar_details.xml b/app/src/main/res/layout/activity_calendar_details.xml index 5085b07e..90873fe0 100644 --- a/app/src/main/res/layout/activity_calendar_details.xml +++ b/app/src/main/res/layout/activity_calendar_details.xml @@ -10,6 +10,6 @@ android:name="net.pokeranalytics.android.ui.fragment.CalendarDetailsFragment" android:layout_width="match_parent" android:layout_height="match_parent" - tools:layout="@layout/fragment_statistic_details" /> + tools:layout="@layout/fragment_progress_report" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml index 4f76f96f..dd080ad1 100644 --- a/app/src/main/res/layout/activity_home.xml +++ b/app/src/main/res/layout/activity_home.xml @@ -6,15 +6,6 @@ android:layout_height="match_parent" tools:context=".ui.activity.HomeActivity"> - - + app:layout_constraintTop_toTopOf="parent" /> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_new_data.xml b/app/src/main/res/layout/activity_new_data.xml new file mode 100644 index 00000000..e1cdc09b --- /dev/null +++ b/app/src/main/res/layout/activity_new_data.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_statistic_details.xml b/app/src/main/res/layout/activity_progress_report.xml similarity index 100% rename from app/src/main/res/layout/activity_statistic_details.xml rename to app/src/main/res/layout/activity_progress_report.xml diff --git a/app/src/main/res/layout/activity_report_creation.xml b/app/src/main/res/layout/activity_report_creation.xml new file mode 100644 index 00000000..c99960dd --- /dev/null +++ b/app/src/main/res/layout/activity_report_creation.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_table_report.xml b/app/src/main/res/layout/activity_table_report.xml new file mode 100644 index 00000000..918c64c0 --- /dev/null +++ b/app/src/main/res/layout/activity_table_report.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_sum.xml b/app/src/main/res/layout/bottom_sheet_sum.xml index 52f9a318..b5451b6a 100644 --- a/app/src/main/res/layout/bottom_sheet_sum.xml +++ b/app/src/main/res/layout/bottom_sheet_sum.xml @@ -15,12 +15,12 @@ android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" + android:text="+ 1000 $" app:layout_constraintEnd_toStartOf="@+id/button2" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - android:text="+ 1000 $" /> + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" /> + + + + + + diff --git a/app/src/main/res/layout/fragment_bankroll.xml b/app/src/main/res/layout/fragment_bankroll.xml index 0cf1c9b1..b4340482 100644 --- a/app/src/main/res/layout/fragment_bankroll.xml +++ b/app/src/main/res/layout/fragment_bankroll.xml @@ -1,6 +1,7 @@ @@ -53,4 +54,17 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bankroll_details.xml b/app/src/main/res/layout/fragment_bankroll_details.xml new file mode 100644 index 00000000..0cf1c9b1 --- /dev/null +++ b/app/src/main/res/layout/fragment_bankroll_details.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_calendar.xml b/app/src/main/res/layout/fragment_calendar.xml index 642db7b7..8675f607 100644 --- a/app/src/main/res/layout/fragment_calendar.xml +++ b/app/src/main/res/layout/fragment_calendar.xml @@ -14,6 +14,15 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_custom_view.xml b/app/src/main/res/layout/fragment_custom_view.xml new file mode 100644 index 00000000..e06f0ab9 --- /dev/null +++ b/app/src/main/res/layout/fragment_custom_view.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_data_list.xml b/app/src/main/res/layout/fragment_data_list.xml index f284300d..034a120d 100644 --- a/app/src/main/res/layout/fragment_data_list.xml +++ b/app/src/main/res/layout/fragment_data_list.xml @@ -2,7 +2,7 @@ @@ -82,7 +82,6 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_history.xml b/app/src/main/res/layout/fragment_feed.xml similarity index 56% rename from app/src/main/res/layout/fragment_history.xml rename to app/src/main/res/layout/fragment_feed.xml index 6177771c..9ee9dc14 100644 --- a/app/src/main/res/layout/fragment_history.xml +++ b/app/src/main/res/layout/fragment_feed.xml @@ -3,53 +3,71 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" + android:animateLayoutChanges="true" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.activity.HomeActivity"> - + app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + + + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/appBar" /> + app:layout_constraintTop_toBottomOf="@+id/selectedFilter" + tools:listitem="@layout/row_feed_session" /> + + + + + + + + - + - + - + - - - + - + - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_import.xml b/app/src/main/res/layout/fragment_import.xml new file mode 100644 index 00000000..efbb84d7 --- /dev/null +++ b/app/src/main/res/layout/fragment_import.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_more.xml b/app/src/main/res/layout/fragment_more.xml index aaa1d704..aecbe709 100644 --- a/app/src/main/res/layout/fragment_more.xml +++ b/app/src/main/res/layout/fragment_more.xml @@ -1,18 +1,37 @@ - + - + + + app:layout_constraintTop_toTopOf="parent" + app:title="@string/more" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_statistic_details.xml b/app/src/main/res/layout/fragment_progress_report.xml similarity index 100% rename from app/src/main/res/layout/fragment_statistic_details.xml rename to app/src/main/res/layout/fragment_progress_report.xml diff --git a/app/src/main/res/layout/fragment_report_creation.xml b/app/src/main/res/layout/fragment_report_creation.xml new file mode 100644 index 00000000..c890c38a --- /dev/null +++ b/app/src/main/res/layout/fragment_report_creation.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_reports.xml b/app/src/main/res/layout/fragment_reports.xml index aaa1d704..e9cbb72e 100644 --- a/app/src/main/res/layout/fragment_reports.xml +++ b/app/src/main/res/layout/fragment_reports.xml @@ -1,18 +1,50 @@ - + - + + + app:layout_constraintTop_toTopOf="parent" + app:title="@string/reports" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_screen_slide_page.xml b/app/src/main/res/layout/fragment_screen_slide_page.xml new file mode 100644 index 00000000..569f77bc --- /dev/null +++ b/app/src/main/res/layout/fragment_screen_slide_page.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_session.xml b/app/src/main/res/layout/fragment_session.xml index 872fc2f6..3c873d31 100644 --- a/app/src/main/res/layout/fragment_session.xml +++ b/app/src/main/res/layout/fragment_session.xml @@ -41,7 +41,8 @@ app:expandedTitleGravity="bottom" app:expandedTitleMarginStart="72dp" app:expandedTitleTextAppearance="@style/PokerAnalyticsTheme.Toolbar.ExpandedTitleAppearance" - app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"> + app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" + app:title=""> + tools:title="Session" /> diff --git a/app/src/main/res/layout/fragment_stats.xml b/app/src/main/res/layout/fragment_stats.xml index aaa1d704..ea43175c 100644 --- a/app/src/main/res/layout/fragment_stats.xml +++ b/app/src/main/res/layout/fragment_stats.xml @@ -1,18 +1,46 @@ - + - + + + app:layout_constraintTop_toTopOf="parent" + app:title="@string/stats" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_subscription.xml b/app/src/main/res/layout/fragment_subscription.xml new file mode 100644 index 00000000..7791cfb8 --- /dev/null +++ b/app/src/main/res/layout/fragment_subscription.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_table_report.xml b/app/src/main/res/layout/fragment_table_report.xml new file mode 100644 index 00000000..75b893ca --- /dev/null +++ b/app/src/main/res/layout/fragment_table_report.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_legend_color.xml b/app/src/main/res/layout/layout_legend_color.xml index 7fd49b54..d64a5857 100644 --- a/app/src/main/res/layout/layout_legend_color.xml +++ b/app/src/main/res/layout/layout_legend_color.xml @@ -3,8 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_height="wrap_content"> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/row_list.xml b/app/src/main/res/layout/row_list.xml new file mode 100644 index 00000000..f31fbcc6 --- /dev/null +++ b/app/src/main/res/layout/row_list.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/row_title_value_action.xml b/app/src/main/res/layout/row_title_value_action.xml index 4e26243f..8ca8162d 100644 --- a/app/src/main/res/layout/row_title_value_action.xml +++ b/app/src/main/res/layout/row_title_value_action.xml @@ -1,67 +1,79 @@ - + android:layout_height="48dp"> - + - + - + - + - + + + + + + + + + - \ No newline at end of file diff --git a/app/src/main/res/layout/view_selected_filter.xml b/app/src/main/res/layout/view_selected_filter.xml new file mode 100644 index 00000000..1ebccbed --- /dev/null +++ b/app/src/main/res/layout/view_selected_filter.xml @@ -0,0 +1,41 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/navigation_home.xml b/app/src/main/res/menu/navigation_home.xml index 1ec55274..7d41675a 100644 --- a/app/src/main/res/menu/navigation_home.xml +++ b/app/src/main/res/menu/navigation_home.xml @@ -5,17 +5,17 @@ + android:titleResId="@string/feed" /> + android:titleResId="@string/statIds" /> + android:titleResId="@string/services" /> --> diff --git a/app/src/main/res/menu/toolbar_bankroll_details.xml b/app/src/main/res/menu/toolbar_bankroll_details.xml new file mode 100644 index 00000000..2132026e --- /dev/null +++ b/app/src/main/res/menu/toolbar_bankroll_details.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar_bottom_sheet.xml b/app/src/main/res/menu/toolbar_bottom_sheet.xml index 836d374c..7ef0e761 100644 --- a/app/src/main/res/menu/toolbar_bottom_sheet.xml +++ b/app/src/main/res/menu/toolbar_bottom_sheet.xml @@ -2,11 +2,18 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar_report.xml b/app/src/main/res/menu/toolbar_report.xml new file mode 100644 index 00000000..07c1fabc --- /dev/null +++ b/app/src/main/res/menu/toolbar_report.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar_report_creation.xml b/app/src/main/res/menu/toolbar_report_creation.xml new file mode 100644 index 00000000..f39fa10a --- /dev/null +++ b/app/src/main/res/menu/toolbar_report_creation.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar_session.xml b/app/src/main/res/menu/toolbar_session.xml index 836a654e..80d24410 100644 --- a/app/src/main/res/menu/toolbar_session.xml +++ b/app/src/main/res/menu/toolbar_session.xml @@ -7,6 +7,12 @@ android:title="@string/stop" app:showAsAction="always" /> + + Lignes Valeur initiale Il faut au moins deux valeurs pour afficher ce rapport! + L\'objet est invalide + Les services de paiement sont indisponibles. Veuillez vérifier votre connexion internet et réessayez plus tard. + Devenez membre Pro + Passer Pro + d\'essai gratuit + Cette bankroll contient au moins une transaction empêchant la suppression. + + À partir de + Jusqu\'à + ère + ème + ème + ème + + Nouveau nom de tournoi + Nouvelle propriété de tournoi + Nouveau filtre + Mise-à-jour %s Adresse Suggestions de noms - %s effacés + Élément effacé La date de fin doit être après la date de début Sauvegarder Nom du tournoi Propriété de tournoi + Noms de tournoi + Propriétés de tournoi En attente Poker Analytics est un tracker de sessions. Il s\'agit pour l\'instant d\'une version allégée de la version iOS existante. Nous allons ajouter les fonctionnalités au fur et à mesure. L\'app fonctionnera avec un abonnement illimité de 29,99€ par an, mais nous la proposons gratuitement en attendant plus de fonctionnalités C\'est compris ! @@ -290,8 +310,8 @@ La fonctionnalité est désactivée. Vous pouvez en apprendre plus ou l\'acheter directement. Récupération du fichier sur iCloud… perdu - Perte inférieur à - Perte supérieur à + Perte inférieure à + Perte supérieure à m Pause(s) effectuée(s) Mail @@ -373,7 +393,7 @@ Vérifiez que la transaction possède un type et un montant Il faut choisir un nom! Types de transactions - Transaction + Transactions Autres Choisissez un code PIN pour\n protéger l\'accès à l\'application.\nUn code PIN ne peut pas être retrouvé.\nSoyez attentif lors de l\'activation\n de cette fonction. Code PIN @@ -654,8 +674,8 @@ BB pour 100 mains Ecart type en BB/100 Ecart type horaire - Poker Analytics est une app hors ligne avant tout.\n\nA un certain point, par le biais d\'une opération manuelle, il est probable que votre adresse email soit stockée, afin de nous permettre de vous contacter occasionnellement.\n\nSi vous ne le souhaitez pas, dites-le nous dans le mail suivant. - Poker Analytics est une app hors ligne avant tout.\n\nAujourd\'hui seule votre adresse email, récupérée si nous avons échangés des emails, peut-être stockée, nous permettant de vous contacter occasionnellement.\n\nA tout moment, si vous souhaitez que nous effacions les données que nous avons sur vous, veuillez nous envoyer une requête par mail. + Poker Analytics est une app hors ligne avant tout.\n\nÀ un certain point, par le biais d\'une opération manuelle, il est probable que votre adresse email soit stockée, afin de nous permettre de vous contacter occasionnellement.\n\nSi vous ne le souhaitez pas, dites-le nous dans le mail suivant. + Poker Analytics est une app hors ligne avant tout.\n\nAujourd\'hui seule votre adresse email, récupérée si nous avons échangé des emails, peut être stockée, nous permettant de vous contacter occasionnellement.\n\nÀ tout moment, si vous souhaitez que nous effacions les données que nous avons sur vous, veuillez nous envoyer une requête par email. Je suis d\'accord Je refuse RGPD diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index a8d685f0..ec3eb74a 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -30,6 +30,7 @@ #57815a #657d60 #9bae8d + #283227 #FF5F57 diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 00000000..6e5acd9b --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 73666f07..8d7265fb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,14 +11,42 @@ Initial Value Can\'t show because there is less than two values to display! The object you\'re trying to access is invalid + The billing services are unavailable at the moment. Please check your internet connection and retry later. + Upgrade to Pro + Go Pro + free trial + Unlimited + Track all your poker life by adding as many data as you want + Offline first + Poker Analytics is available at all times and the data is yours. Export it at any times. Note: You’re currently in charge of backups, but that will change soon! + Private + We do not own servers. We do not know anything about your wins and losses. + Support + We try to answer as quickly as we can, in english or french ! + Loading, please wait… + Select your type of report + Select one or more statistics + Select one or more comparison criteria + Select a filter or launch report + Launch Report + Progress + Save Report + Do you want to proceed with the file import? + Update %s + Comparison chart + The filter cannot be deleted because it is currently selected. + Custom field + The item is used in one or more transactions…Please delete the linked transactions first Address Naming suggestions - %s deleted + Data deleted The end date should be after the start date Save - Tournament Name - Tournament Feature + Tournament name + Tournament names + Tournament feature + Tournament features Pending Poker Analytics is a poker tracking app. We’re currently on our way to reproduce the iOS app and you’re currently using a lighter version. The app will work with a US$29.99 yearly subscription for an unlimited usage, but will be free until reaching the appropriate feature coverage. I understand @@ -26,6 +54,21 @@ A tournament feature with the same name already exists. A tournament name can not be empty. This name already exists. + One or more transactions are associated with this bankroll, please delete the linked transaction(s) first. + Transaction type + + From + To + st + nd + rd + th + + New tournament name + New tournament feature + New filter + + @@ -136,7 +179,7 @@ Current year Curve Custom - Custom Fields + Custom fields Import & Export Data management Table @@ -687,6 +730,7 @@ You need to name the transaction type Transaction types Transactions + Transaction Add a transaction An identical transaction already exists A transaction type with the same name already exists diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d58f7de3..eecdebee 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -11,6 +11,7 @@ @color/colorPrimary @color/white @color/white + true @style/PokerAnalyticsTheme.BottomNavigationView @style/PokerAnalyticsTheme.Toolbar @@ -23,6 +24,7 @@ + @@ -241,6 +243,17 @@ + + + + + + + + + + + + + + diff --git a/app/src/test/java/net/pokeranalytics/android/SavableEnumTest.kt b/app/src/test/java/net/pokeranalytics/android/SavableEnumTest.kt new file mode 100644 index 00000000..13f1e6e7 --- /dev/null +++ b/app/src/test/java/net/pokeranalytics/android/SavableEnumTest.kt @@ -0,0 +1,21 @@ +package net.pokeranalytics.android + +import net.pokeranalytics.android.calculus.Stat +import net.pokeranalytics.android.model.Criteria +import org.junit.Assert +import org.junit.Test + +class SavableEnumTest { + + @Test + fun testSavableEnumConfiguration() { + + val statIds = Stat.valuesInternal().map { it.uniqueIdentifier } + Assert.assertEquals(statIds.toSet().size, statIds.size) + + val criteriaIds = Criteria.valuesInternal().map { it.uniqueIdentifier } + Assert.assertEquals(criteriaIds.toSet().size, criteriaIds.size) + + } + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8a1c4fe4..0340e807 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.android.tools.build:gradle:3.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'io.realm:realm-gradle-plugin:5.8.0' classpath 'com.google.gms:google-services:4.2.0' diff --git a/gradle.properties b/gradle.properties index 23339e0d..84ada6d0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m +org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects @@ -19,3 +19,20 @@ android.useAndroidX=true android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official + +# Enable Gradle Daemon: Daemon keeps the instance of the gradle up and running +# in the background even after your build finishes +org.gradle.daemon=true + +# Enable Configure on demand +org.gradle.configureondemand=true + +# Enable parallel builds +org.gradle.parallel=true + +# Enable Build Cache +android.enableBuildCache=true + +# Enable simple gradle caching +org.gradle.caching=true +