diff --git a/app/build.gradle b/app/build.gradle index c6e86fbc..284725a3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,7 +33,7 @@ android { applicationId "net.pokeranalytics.android" minSdkVersion 23 targetSdkVersion 28 - versionCode 80 + versionCode 85 versionName "3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } 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 e9a426b0..8fc72845 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt @@ -5,9 +5,9 @@ import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType -enum class TournamentType : RowRepresentable { - MTT, - SNG; +enum class TournamentType(val label: String) : RowRepresentable { + MTT("MTT"), + SNG("SNG"); companion object { val all : List @@ -17,8 +17,8 @@ enum class TournamentType : RowRepresentable { fun getValueForLabel(label: String) : TournamentType? { return when (label) { - "Single-Table" -> SNG - "Multi-Table" -> MTT + SNG.label, "Single-Table" -> SNG + MTT.label, "Multi-Table" -> MTT else -> null } } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt index 30a68ef6..0371c160 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt @@ -23,12 +23,29 @@ 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 timber.log.Timber import java.util.* import kotlin.collections.ArrayList open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable { + companion object { + + fun getOrCreate(realm: Realm, name: String, type: Int): CustomField { + val cf = realm.where(CustomField::class.java).equalTo("name", name).findFirst() + return if (cf != null) { + cf + } else { + val customField = CustomField() + customField.name = name + customField.type = type + realm.copyToRealm(customField) + } + } + + } + @Ignore override val realmObjectClass: Class = CustomField::class.java @@ -282,6 +299,19 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa this.entriesToDelete.clear() } + fun getOrCreateEntry(realm: Realm, value: String): CustomFieldEntry { + this.entries.find { it.value == value }?.let { + Timber.d("L>> get") + return it + } ?: run { + Timber.d("L>> create") + val entry = realm.copyToRealm(CustomFieldEntry()) + entry.value = value + this.entries.add(entry) + return entry + } + } + /** * Clean the entries if the type is not a list & remove the deleted entries from realm */ @@ -308,7 +338,7 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa val criteria: Criteria get() { return when (this.type) { - CustomField.Type.LIST.uniqueIdentifier -> Criteria.ListCustomFields(this.id) + Type.LIST.uniqueIdentifier -> Criteria.ListCustomFields(this.id) else -> Criteria.ValueCustomFields(this.id) } } 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 5d5830b0..ab747e4e 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 @@ -50,9 +50,9 @@ typealias BB = Double open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDataSource, RowRepresentable, Timed, TimeFilterable, Filterable, DatedBankrollGraphEntry { - enum class Type { - CASH_GAME, - TOURNAMENT; + enum class Type(val value: String) { + CASH_GAME("Cash Game"), + TOURNAMENT("Tournament"); companion object { 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 cafa780e..9d48fd6a 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt @@ -53,7 +53,7 @@ open class TournamentFeature : RealmObject(), NameManageable, StaticRowRepresent } override fun adapterRows(): List? { - return TournamentFeature.rowRepresentation + return rowRepresentation } override fun charSequenceForRow( 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 de2fdebc..f33aad66 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt @@ -30,13 +30,14 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo companion object { - fun newInstance(realm: Realm, bankroll: Bankroll, date: Date? = null, type: TransactionType, amount: Double): Transaction { + fun newInstance(realm: Realm, bankroll: Bankroll, date: Date? = null, type: TransactionType, amount: Double, comment: String? = null): Transaction { val transaction = realm.copyToRealm(Transaction()) transaction.date = date ?: Date() transaction.amount = amount transaction.type = type transaction.bankroll = bankroll + transaction.comment = comment ?: "" return transaction } @@ -174,5 +175,4 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo 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 f557add5..7d1454ba 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt @@ -68,6 +68,18 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab throw PAIllegalStateException("Transaction type ${value.name} should exist in database!") } + fun getOrCreate(realm: Realm, name: String, additive: Boolean): TransactionType { + val type = realm.where(TransactionType::class.java).equalTo("name", name).findFirst() + return if (type != null) { + type + } else { + val transactionType = TransactionType() + transactionType.name = name + transactionType.additive = additive + realm.copyToRealm(transactionType) + } + } + } @Ignore 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 index ef7ef3e7..491b399f 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.TextView import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_import.* import kotlinx.coroutines.Dispatchers @@ -14,9 +15,7 @@ import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.util.csv.CSVImporter import net.pokeranalytics.android.util.csv.ImportDelegate -import net.pokeranalytics.android.util.csv.ImportException import timber.log.Timber -import java.io.IOException import java.io.InputStream import java.text.NumberFormat import java.util.* @@ -76,7 +75,7 @@ class ImportFragment : RealmFragment(), ImportDelegate { this.importer = CSVImporter(inputStream) this.importer.delegate = this - var error = false + var exception: Exception? = null GlobalScope.launch(coroutineContext) { @@ -86,11 +85,8 @@ class ImportFragment : RealmFragment(), ImportDelegate { try { importer.start() - } catch (e: ImportException) { -// shouldDismissActivity = true - error = true - } catch (e: IOException) { - error = true + } catch (e: Exception) { + exception = e } val e = Date() val duration = (e.time - s.time) / 1000.0 @@ -99,12 +95,18 @@ class ImportFragment : RealmFragment(), ImportDelegate { } test.await() - if (error && view != null) { - Snackbar.make(view!!, R.string.import_error, Snackbar.LENGTH_INDEFINITE).show() + val exceptionMessage = exception?.message + if (exceptionMessage != null && view != null) { + val message = exceptionMessage + ". " + requireContext().getString(R.string.import_error) + val snackBar = Snackbar.make(view!!, message, Snackbar.LENGTH_INDEFINITE) + snackBar.setAction(R.string.ok) { + snackBar.dismiss() + } + val textView = snackBar.view.findViewById(com.google.android.material.R.id.snackbar_text) + textView.maxLines = 5 + snackBar.show() } - - // if (shouldDismissActivity) { // // activity?.let { 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 0b41bc10..061e9891 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 @@ -9,6 +9,8 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast +import androidx.core.content.FileProvider import androidx.recyclerview.widget.LinearLayoutManager import io.realm.Realm import kotlinx.android.synthetic.main.fragment_settings.* @@ -17,6 +19,7 @@ import net.pokeranalytics.android.R import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.realm.Currency 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.RowRepresentableAdapter @@ -25,20 +28,25 @@ import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.extensions.openContactMail import net.pokeranalytics.android.ui.extensions.openPlayStorePage import net.pokeranalytics.android.ui.extensions.openUrl -import net.pokeranalytics.android.ui.fragment.components.BaseFragment +import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.modules.datalist.DataListActivity import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.rowrepresentable.SettingRow +import net.pokeranalytics.android.util.FileUtils import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.URL import net.pokeranalytics.android.util.UserDefaults import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.IAPProducts +import net.pokeranalytics.android.util.csv.ProductCSVDescriptors +import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted import timber.log.Timber +import java.io.File +import java.io.IOException import java.util.* -class SettingsFragment : BaseFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource { +class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource { companion object { @@ -64,6 +72,7 @@ class SettingsFragment : BaseFragment(), RowRepresentableDelegate, StaticRowRepr private lateinit var settingsAdapterRow: RowRepresentableAdapter override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) return inflater.inflate(R.layout.fragment_settings, container, false) } @@ -134,6 +143,8 @@ class SettingsFragment : BaseFragment(), RowRepresentableDelegate, StaticRowRepr 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, RequestCode.CURRENCY.value) + SettingRow.EXPORT_CSV_SESSIONS -> this.sessionsCSVExport() + SettingRow.EXPORT_CSV_TRANSACTIONS -> this.transactionsCSVExport() SettingRow.FOLLOW_US -> { when (position) { 0 -> parentActivity?.openUrl(URL.BLOG.value) @@ -154,6 +165,7 @@ class SettingsFragment : BaseFragment(), RowRepresentableDelegate, StaticRowRepr } } + /** * Init UI */ @@ -205,4 +217,49 @@ class SettingsFragment : BaseFragment(), RowRepresentableDelegate, StaticRowRepr } } + private fun transactionsCSVExport() { + val transactions = getRealm().where(Transaction::class.java).findAll().sort("date") + val csv = ProductCSVDescriptors.pokerAnalyticsAndroidTransactions.toCSV(transactions) + this.shareCSV(csv, "Transactions") + } + + private fun sessionsCSVExport() { + val sessions = getRealm().where(Session::class.java).findAll().sort("startDate") + val csv = ProductCSVDescriptors.pokerAnalyticsAndroid.toCSV(sessions) + this.shareCSV(csv, "Sessions") + } + + private fun shareCSV(content: String, dataType: String) { + + try { + val fileName = "${dataType.toLowerCase()}_${Date().dateTimeFileFormatted}.csv" + FileUtils.writeFileToFilesDir(content, fileName, requireContext()) + this.shareFile(fileName, "Poker Analytics Export", "CSV $dataType") + } catch (e: IOException) { + Toast.makeText(requireContext(), "File write failed: ${e.message}", Toast.LENGTH_LONG).show() + } + + } + + private fun shareFile(filePath: String, subject: String, body: String) { + + val intent = Intent(Intent.ACTION_SEND) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + + val sharedFile = File(requireContext().filesDir, filePath) + + val uri = FileProvider.getUriForFile(requireContext(), "net.pokeranalytics.android.fileprovider", sharedFile) + + if (sharedFile.exists()) { + intent.type = "application/csv" + intent.putExtra(Intent.EXTRA_STREAM, uri) + intent.putExtra(Intent.EXTRA_SUBJECT, subject) + intent.putExtra(Intent.EXTRA_TEXT, body) + startActivity(Intent.createChooser(intent, "Share File")) + } else { + Timber.d("File located at $filePath does not exists") + } + + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt index ed56a377..5aa4d1ae 100644 --- 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 @@ -135,7 +135,7 @@ class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDa override fun boolForRow(row: RowRepresentable): Boolean { return when (row) { CustomFieldRow.COPY_ON_DUPLICATE -> customField.duplicateValue - CustomFieldRow.TYPE -> isUpdating +// CustomFieldRow.TYPE -> isUpdating // very weird else -> super.boolForRow(row) } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionDataFragment.kt index a67d4e9f..65e96639 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/TransactionDataFragment.kt @@ -1,8 +1,6 @@ package net.pokeranalytics.android.ui.fragment.data import android.content.Context -import android.os.Bundle -import android.view.View import io.realm.kotlin.where import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -22,6 +20,7 @@ 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 kotlin.math.abs /** * Custom EditableDataFragment to manage the Transaction data @@ -55,7 +54,7 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa } override fun adapterRows(): List? { - return transaction.adapterRows() + return this.transaction.adapterRows() } override fun charSequenceForRow( @@ -66,7 +65,7 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa return when (row) { TransactionRow.BANKROLL -> this.transaction.bankroll?.name ?: NULL_TEXT TransactionRow.TYPE -> this.transaction.type?.name ?: NULL_TEXT - TransactionRow.AMOUNT -> if (this.transaction.amount != 0.0) this.transaction.amount.round() else NULL_TEXT + TransactionRow.AMOUNT -> if (this.transaction.amount != 0.0) abs(this.transaction.amount).round() else NULL_TEXT TransactionRow.COMMENT -> if (this.transaction.comment.isNotEmpty()) this.transaction.comment else NULL_TEXT TransactionRow.DATE -> this.transaction.date.shortDate() else -> super.charSequenceForRow(row, context, 0) @@ -87,7 +86,7 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa "data" to getRealm().sorted() ) ) - TransactionRow.AMOUNT -> row.editingDescriptors(mapOf("defaultValue" to (if (this.transaction.amount != 0.0) this.transaction.amount.round() else ""))) + TransactionRow.AMOUNT -> row.editingDescriptors(mapOf("defaultValue" to (if (this.transaction.amount != 0.0) abs(this.transaction.amount).round() else ""))) TransactionRow.COMMENT -> row.editingDescriptors(mapOf("defaultValue" to this.transaction.comment)) else -> super.editDescriptors(row) } @@ -109,12 +108,19 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa override fun onRowValueChanged(value: Any?, row: RowRepresentable) { super.onRowValueChanged(value, row) - rowRepresentableAdapter.refreshRow(row) + this.rowRepresentableAdapter.refreshRow(row) + this.selectNextRow(row) + } + + /*** + * Selects the next row to ease the data capture + */ + private fun selectNextRow(currentRow: RowRepresentable) { - if (model.primaryKey == null) { // automatically change the row for new data + if (this.model.primaryKey == null) { // automatically change the row for new data GlobalScope.launch(Dispatchers.Main) { delay(200) - when (row) { + when (currentRow) { TransactionRow.BANKROLL -> onRowSelected(0, TransactionRow.TYPE) TransactionRow.TYPE -> onRowSelected(0, TransactionRow.AMOUNT) // TransactionRow.AMOUNT -> onRowSelected(0, TransactionRow.DATE) @@ -122,13 +128,14 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa } } } + } override fun willSaveData() { super.willSaveData() val additive = this.transaction.type?.additive ?: true if (!additive) { - this.transaction.amount *= -1 + this.transaction.amount = abs(this.transaction.amount) * -1 } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/graph/GraphExtensions.kt b/app/src/main/java/net/pokeranalytics/android/ui/graph/GraphExtensions.kt index b93cc841..63193351 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/graph/GraphExtensions.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/graph/GraphExtensions.kt @@ -35,7 +35,7 @@ fun BarLineChartBase<*>.setStyle( this.xAxis.granularity = 1.0f this.xAxis.textColor = ContextCompat.getColor(context, R.color.chart_default) - try { + try { // can crash for unknown reasons, same below for Y axis val font = ResourcesCompat.getFont(context, R.font.roboto_medium) this.xAxis.typeface = font } catch (e: Resources.NotFoundException) { @@ -47,12 +47,8 @@ fun BarLineChartBase<*>.setStyle( this.xAxis.isEnabled = true when (this) { - is BarChart -> { - this.xAxis.setDrawLabels(false) - } - else -> { - this.xAxis.setDrawLabels(true) - } + is BarChart -> this.xAxis.setDrawLabels(false) + else -> this.xAxis.setDrawLabels(true) } // Y Axis @@ -67,7 +63,14 @@ fun BarLineChartBase<*>.setStyle( this.axisLeft.granularity = 1.0f this.axisLeft.textColor = ContextCompat.getColor(context, R.color.chart_default) - this.axisLeft.typeface = ResourcesCompat.getFont(context, R.font.roboto_medium) + + try { + val font = ResourcesCompat.getFont(context, R.font.roboto_medium) + this.axisLeft.typeface = font + } catch (e: Resources.NotFoundException) { + Crashlytics.log(e.message) + } + this.axisLeft.labelCount = if (small) 1 else 7 // @todo not great if interval is [0..2] for number of records as we get decimals this.axisLeft.textSize = 12f diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionFragment.kt index 2053d20b..c9235d49 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionFragment.kt @@ -236,17 +236,18 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate { } override fun onRowValueChanged(value: Any?, row: RowRepresentable) { - sessionHasBeenUserCustomized = true + this.sessionHasBeenUserCustomized = true try { - currentSession.updateValue(value, row) + this.currentSession.updateValue(value, row) } catch (e: PAIllegalStateException) { Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() return } - sessionAdapter.refreshRow(row) + this.sessionAdapter.refreshRow(row) when (row) { - SessionRow.CASHED_OUT, SessionRow.PRIZE, SessionRow.NET_RESULT, SessionRow.BUY_IN, SessionRow.TIPS, - SessionRow.START_DATE, SessionRow.END_DATE, SessionRow.BANKROLL, SessionRow.BREAK_TIME -> updateSessionUI() + SessionRow.CASHED_OUT, SessionRow.PRIZE, SessionRow.NET_RESULT, + SessionRow.BUY_IN, SessionRow.TIPS, SessionRow.START_DATE, + SessionRow.END_DATE, SessionRow.BANKROLL, SessionRow.BREAK_TIME -> updateSessionUI() } } 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 00420032..7b99d734 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 @@ -29,6 +29,10 @@ enum class SettingRow : RowRepresentable { LANGUAGE, CURRENCY, + // Export + EXPORT_CSV_SESSIONS, + EXPORT_CSV_TRANSACTIONS, + // Data management CUSTOM_FIELD, BANKROLL, @@ -64,6 +68,9 @@ enum class SettingRow : RowRepresentable { rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.preferences)) rows.addAll(arrayListOf(CURRENCY)) + rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.export)) + rows.addAll(arrayListOf(EXPORT_CSV_SESSIONS, EXPORT_CSV_TRANSACTIONS)) + rows.add( CustomizableRowRepresentable( customViewType = RowViewType.HEADER_TITLE, @@ -99,6 +106,8 @@ enum class SettingRow : RowRepresentable { FOLLOW_US -> R.string.follow_us LANGUAGE -> R.string.language CURRENCY -> R.string.currency + EXPORT_CSV_SESSIONS -> R.string.sessions_csv + EXPORT_CSV_TRANSACTIONS -> R.string.transactions_csv GDPR -> R.string.gdpr POKER_RUMBLE -> R.string.poker_rumble DISCORD -> R.string.join_discord 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 c1cb566a..456cb5b8 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 @@ -73,7 +73,6 @@ enum class TransactionRow : RowRepresentable, DefaultEditDataSource { defaultValue, inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL - or InputType.TYPE_NUMBER_FLAG_SIGNED )) } COMMENT -> { diff --git a/app/src/main/java/net/pokeranalytics/android/util/FileUtils.kt b/app/src/main/java/net/pokeranalytics/android/util/FileUtils.kt new file mode 100644 index 00000000..666bf06b --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/FileUtils.kt @@ -0,0 +1,32 @@ +package net.pokeranalytics.android.util + +import android.content.Context +import timber.log.Timber +import java.io.BufferedWriter +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStreamWriter + +class FileUtils { + + companion object{ + + /*** + * Writes a [string] into a file named [fileName], using a [context] + * Should be surrounded by a try/catch IOException + */ + fun writeFileToFilesDir(string: String, fileName: String, context: Context) { + + Timber.d("Writing to: $fileName ...\n$string") + + val file = File(context.filesDir, fileName) + + val fileOutputStream = FileOutputStream(file) + fileOutputStream.write(string.toByteArray()) + fileOutputStream.close() + + } + + } + +} \ No newline at end of file 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 d6d6d580..a24dfaaf 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/Preferences.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/Preferences.kt @@ -14,22 +14,13 @@ import java.util.* class Preferences { interface PreferenceKey { - var identifier: String - } - - enum class DBPatch(var key: String) : PreferenceKey { - LONE_COMPUTABLE_RESULTS("loneComputableResult"); - - override var identifier: String = "" - get() { - return "dbpatch." + this.key - } + val identifier: String } enum class Keys(override var identifier: String) : PreferenceKey { CURRENCY_CODE("CurrencyCode"), LOCALE_CODE("LocaleCode"), - FIRST_LAUNCH("firstLaunch"), +// FIRST_LAUNCH("firstLaunch"), STOP_SHOWING_DISCLAIMER("stopShowingDisclaimer"), STOP_SHOWING_DUPLICATE("stopShowingDuplicate"), STOP_SHOWING_DISCORD("stopShowingDiscord"), @@ -99,26 +90,26 @@ class Preferences { editor.apply() } - private fun removeKey(key: Keys, context: Context) { - val preferences = PreferenceManager.getDefaultSharedPreferences(context) - val editor = preferences.edit() - editor.remove(key.identifier) - editor.apply() - } +// 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) } - fun setBoolean(key: PreferenceKey, value: Boolean, context: Context) { + private fun setBoolean(key: PreferenceKey, value: Boolean, context: Context) { val preferences = PreferenceManager.getDefaultSharedPreferences(context) val editor = preferences.edit() editor.putBoolean(key.identifier, value) editor.apply() } - fun getBoolean( + private fun getBoolean( key: PreferenceKey, context: Context, defaultValue: Boolean? = false @@ -140,15 +131,15 @@ class Preferences { return getString(Keys.ACTIVE_FILTER_ID, context) } - fun removeActiveFilterId(context: Context) { - removeKey(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) } - fun getCurrencyLocale(context: Context): Locale? { + private fun getCurrencyLocale(context: Context): Locale? { getCurrencyCode(context)?.let { currencyCode -> UserDefaults.availableCurrencyLocales.filter { Currency.getInstance(it).currencyCode == currencyCode diff --git a/app/src/main/java/net/pokeranalytics/android/util/TextFormat.kt b/app/src/main/java/net/pokeranalytics/android/util/TextFormat.kt index 5bea21f5..1522f9ee 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/TextFormat.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/TextFormat.kt @@ -3,6 +3,19 @@ package net.pokeranalytics.android.util import android.content.Context import android.graphics.Color import androidx.core.content.ContextCompat +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols + +object DotFormatSymbols : DecimalFormatSymbols() { + init { + this.decimalSeparator = '.' + } +} +object CSVNumberFormat : DecimalFormat("#.######", DotFormatSymbols) { + init { + this.isGroupingUsed = false + } +} class TextFormat(var text: String, var color: Int? = null) { 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 index 8a27f7d1..02f707b9 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt @@ -64,7 +64,7 @@ object AppGuard : PurchasesUpdatedListener { if (this.endOfUse != null) return true return if (BuildConfig.DEBUG) { - true //false //true + false //true } else { this._isProUser } 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 index df529a68..5585ef44 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt @@ -16,7 +16,7 @@ enum class DataSource { POKER_ANALYTICS, POKER_INCOME, POKER_BANKROLL_TRACKER, - RUNGOOD, + RUN_GOOD, POKER_AGENT } @@ -28,7 +28,7 @@ abstract class DataCSVDescriptor(source: DataSource, vararg el /** * List of Realm object identificators */ - val realmModelIds = mutableListOf() + private val realmModelIds = mutableListOf() abstract fun parseData(realm: Realm, record: CSVRecord): T? @@ -75,6 +75,35 @@ abstract class DataCSVDescriptor(source: DataSource, vararg el this.realmModelIds.clear() } + open fun prepareCSVExport() { + + } + + fun toCSV(dataSequence: List): String { + + prepareCSVExport() + + val lines = mutableListOf() + lines.add(this.csvHeaders) + + dataSequence.forEach { data -> + + val line = mutableListOf() + this.fields.forEach { field -> + val string = this.toCSV(data, field) + line.add(string ?: "") + } + lines.add(line.joinToString(",")) + } + return lines.joinToString("\n") + } + + protected open fun toCSV(data: T, field: CSVField): String? { + return null + } + +// abstract fun + } /** @@ -85,7 +114,13 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField) /** * The CSVField list describing the CSV header format */ - protected var fields: List = listOf() + protected var fields: MutableList = mutableListOf() + + /** + * A list of dynamic CSVField, described in the CSV header + */ +// protected var dynamicFields: MutableList = mutableListOf() + /** * The mapping of CSVField with their index in the CSV file */ @@ -93,7 +128,7 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField) init { if (elements.isNotEmpty()) { - this.fields = elements.toList() + this.fields = elements.toMutableList() } } @@ -131,4 +166,21 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField) return count >= mandatoryFields.size } + /*** + * Method called when the descriptor has matched a record and has been identified + * as able to parse the file + */ + open fun hasMatched(realm: Realm, record: CSVRecord) { + + } + + protected val csvHeaders: String + get() { + val headers = mutableListOf() + this.fields.forEach { + headers.add(it.header) + } + return headers.joinToString(",") + } + } 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 index a28c7a16..e56c2eda 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt @@ -1,5 +1,8 @@ package net.pokeranalytics.android.util.csv +import net.pokeranalytics.android.model.realm.CustomField +import net.pokeranalytics.android.model.realm.TournamentFeature +import net.pokeranalytics.android.util.CSVNumberFormat import timber.log.Timber import java.text.DateFormat import java.text.NumberFormat @@ -12,8 +15,6 @@ import java.util.* */ interface NumberCSVField: TypedCSVField { - val numberFormat: String? - companion object { fun defaultParse(value: String) : Double? { @@ -22,10 +23,10 @@ interface NumberCSVField: TypedCSVField { return null } - val formatter = NumberFormat.getInstance(Locale.US) +// val formatter = NumberFormat.getInstance(Locale.US) return try { - formatter.parse(value).toDouble() + CSVNumberFormat.parse(value).toDouble() } catch (e: ParseException) { Timber.d("Field > Unparseable number: $value") null @@ -43,12 +44,20 @@ interface NumberCSVField: TypedCSVField { return it(value) } - val formatter = NumberFormat.getInstance(Locale.US) +// val formatter = NumberFormat.getInstance(Locale.US) return try { - formatter.parse(value).toDouble() + CSVNumberFormat.parse(value).toDouble() } catch (e: ParseException) { - Timber.d("Field ${header} > Unparseable number: $value") + Timber.d("Field $header > Unparseable number: $value") + null + } + } + + override fun format(data: Double?): String? { + return if (data != null) { + CSVNumberFormat.format(data) + } else { null } } @@ -63,25 +72,20 @@ interface IntCSVField: TypedCSVField { } return try { - NumberFormat.getInstance().parse(value).toInt() + CSVNumberFormat.parse(value).toInt() } catch (e: ParseException) { - Timber.d("Field ${header} > Unparseable number: $value") + Timber.d("Field $header > Unparseable number: $value") null } } -} - - -interface DataCSVField : TypedCSVField { - override fun parse(value: String): T? { - - this.callback?.let { - return it(value) + override fun format(data: Int?): String? { + return if (data != null) { + CSVNumberFormat.format(data) + } else { + null } - return null } - } interface DateCSVField : TypedCSVField { @@ -99,6 +103,29 @@ interface DateCSVField : TypedCSVField { } } + override fun format(data: Date?): String? { + return if (data != null) { + val formatter = if (dateFormat != null) SimpleDateFormat(dateFormat) else SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) + formatter.format(data) + } else { + null + } + } +} + +interface TournamentFeaturesCSVField : TypedCSVField> { + + override fun parse(value: String): List? { + this.callback?.let { + return it(value) + } + return null + } + + override fun format(data: List?): String? { + return data?.joinToString(CSVField.separator) { it.name } + } + } interface BlindCSVField : TypedCSVField> { @@ -121,23 +148,52 @@ interface BlindCSVField : TypedCSVField> { return null } + override fun format(data: Pair?): String? { + data?.let { + val sb = CSVNumberFormat.format(data.first) + val bb = CSVNumberFormat.format(data.second) + return "$sb/$bb" + } ?: run { + return null + } + + } + } interface BooleanCSVField : TypedCSVField { override fun parse(value: String): Boolean? { return value == "1" } + override fun format(data: Boolean?): String { + return if (data != null && data) "1" else "0" + } +} + +interface CustomFieldCSVField : CSVField { + var customField: CustomField + + companion object { + val separator: String = "::CF" + } } interface TypedCSVField : CSVField { fun parse(value: String) : T? + fun format(data: T?): String? var callback: ((String) -> T?)? } interface CSVField { + + companion object { + const val separator = "|" + } + val header: String val optional: Boolean get() { return false } -} \ 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 index 05b37957..f924f944 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt @@ -112,6 +112,7 @@ open class CSVImporter(istream: InputStream) { if (this.currentDescriptor == null) { // find descriptor this.currentDescriptor = this.findDescriptor(record) + this.currentDescriptor?.hasMatched(realm, record) if (this.currentDescriptor == null) { @@ -120,6 +121,7 @@ open class CSVImporter(istream: InputStream) { } if (this.descriptorFindingAttempts >= VALID_RECORD_ATTEMPTS_BEFORE_THROWING_EXCEPTION) { realm.cancelTransaction() + realm.close() throw ImportException("This type of file is not supported") } } @@ -150,6 +152,7 @@ open class CSVImporter(istream: InputStream) { } } ?: run { realm.cancelTransaction() + realm.close() throw ImportException("CSVDescriptor should never be null here") } diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt new file mode 100644 index 00000000..e0befb16 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt @@ -0,0 +1,272 @@ +package net.pokeranalytics.android.util.csv + +import net.pokeranalytics.android.model.interfaces.Identifiable +import io.realm.Realm +import net.pokeranalytics.android.exceptions.PAIllegalStateException +import net.pokeranalytics.android.model.Limit +import net.pokeranalytics.android.model.TableSize +import net.pokeranalytics.android.model.TournamentType +import net.pokeranalytics.android.model.realm.* +import net.pokeranalytics.android.model.utils.DataUtils +import net.pokeranalytics.android.util.extensions.getOrCreate +import net.pokeranalytics.android.util.extensions.setHourMinutes +import org.apache.commons.csv.CSVRecord +import timber.log.Timber +import java.util.* + +abstract class PACSVDescriptor(source: DataSource, + private var isTournament: Boolean?, + vararg elements: CSVField) + : DataCSVDescriptor(source, *elements) { + + private var sameDaySessionCount: Int = 0 + private var currentDay: String = "" + private var startInSeconds: Double = 20 * 3600.0 + + /** + * Parses a [record] and return an optional Session + */ + protected fun parseSession(realm: Realm, record: CSVRecord): Session? { + + val isTournament = isTournament ?: false + val session = Session.newInstance(realm, isTournament, managed = false) + + var startDate: Date? = null + var endDate: Date? = null + + var isLive = true + var bankrollName = "" + var currencyCode: String? = null + var currencyRate: Double? = null + var additionalBuyins = 0.0 // rebuy + addon + + var stackingIn: Double? = null + var stackingOut: Double? = null + + this.fields.forEach { field -> + + this.fieldMapping[field]?.let { index -> + + val value = record.get(index) + when (field) { + is SessionField.Start -> { + startDate = field.parse(value) + if (source == DataSource.POKER_AGENT) { + if (currentDay == value) { + sameDaySessionCount++ + } else { + sameDaySessionCount = 0 + } + currentDay = value + } else {} + } + is SessionField.End -> { + endDate = field.parse(value) + } + is SessionField.StartTime -> { + startDate?.setHourMinutes(value) + } + is SessionField.EndTime -> { + endDate?.setHourMinutes(value) + } + is SessionField.Duration -> { + val hoursDuration = field.parse(value) ?: throw PAIllegalStateException("null duration") + + if (startDate != null) { + if (field.randomTime) { + if (sameDaySessionCount == 0) { + startInSeconds = 20 * 3600.0 + } else { + startInSeconds -= hoursDuration * 3600.0 + } + + if (startInSeconds < 0) { + startInSeconds = 20 * 3600.0 +// throw PAIllegalStateException("negative start: $startDate, start = $startInSeconds, net = ${session.result?.netResult}") + } + + val hour = (startInSeconds / 3600.0).toInt() + val minutes = ((startInSeconds - hour * 3600.0) / 60.0).toInt() + val formattedTime = "$hour:$minutes" + startDate?.setHourMinutes(formattedTime) + } + + val seconds = (hoursDuration * 3600.0).toInt() + val calendar = Calendar.getInstance() + calendar.time = startDate + calendar.add(Calendar.SECOND, seconds) + endDate = calendar.time + + } else { + throw PAIllegalStateException("start date ($startDate) + hoursDuration ($hoursDuration) required") + } + + } + is SessionField.Buyin -> { + val buyin = field.parse(value) + session.result?.buyin = buyin + if (session.type == Session.Type.TOURNAMENT.ordinal) { + session.tournamentEntryFee = buyin + } else {} + } + is SessionField.CashedOut -> session.result?.cashout = field.parse(value) + is SessionField.NetResult -> session.result?.netResult = field.parse(value) + is SessionField.SessionType -> { + Session.Type.getValueFromString(value)?.let { type -> + session.type = type.ordinal + } + } + is SessionField.Live -> isLive = field.parse(value) ?: false + is SessionField.NumberOfTables -> session.numberOfTables = field.parse(value) ?: 1 + is SessionField.Addon -> additionalBuyins += field.parse(value) ?: 0.0 + is SessionField.Rebuy -> additionalBuyins += field.parse(value) ?: 0.0 + is SessionField.Tips -> session.result?.tips = field.parse(value) + is SessionField.Break -> { + field.parse(value)?.let { + session.breakDuration = it.toLong() + } + } + is SessionField.LimitAndGame -> { + if (value.isNotEmpty()) { + var limitAndGame = value + for (someLimit in Limit.values()) { + if (value.startsWith(someLimit.longName)) { + session.limit = someLimit.ordinal + limitAndGame = limitAndGame.removePrefix(someLimit.longName) + break + } + } + session.game = realm.getOrCreate(limitAndGame.trim()) + + } else {} + } + is SessionField.Game -> { + if (value.isNotEmpty()) { + session.game = realm.getOrCreate(value) + } else {} + } + is SessionField.Location -> { + val trimmedValue = value.trim() + if (trimmedValue.isNotEmpty()) { + session.location = realm.getOrCreate(trimmedValue) + } else {} + } + 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 -> { + val sb = field.parse(value) + if (sb != null && sb > 0.0) { + session.cgSmallBlind = sb + } else {} + } + is SessionField.BigBlind -> { + val bb = field.parse(value) + if (bb != null && bb > 0.0) { + session.cgBigBlind = bb + } else {} + } + is SessionField.TableSize -> session.tableSize = TableSize.valueForLabel(value) + is SessionField.TournamentPosition -> session.result?.tournamentFinalPosition = + field.parse(value) + is SessionField.TournamentName -> { + if (value.isNotEmpty()) { + session.tournamentName = realm.getOrCreate(value) + } else {} + } + is SessionField.TournamentTypeName -> session.tournamentType = + TournamentType.getValueForLabel(value)?.ordinal + is SessionField.TournamentNumberOfPlayers -> session.tournamentNumberOfPlayers = + field.parse(value) + is SessionField.TournamentEntryFee -> session.tournamentEntryFee = field.parse(value) + is SessionField.TournamentType -> session.tournamentType = field.parse(value) + is SessionField.TournamentFeatures -> { + value.split(CSVField.separator).forEach { featureName -> + val tournamentFeature: TournamentFeature = realm.getOrCreate(featureName) + session.tournamentFeatures.add(tournamentFeature) + } + } + is SessionField.CurrencyCode -> currencyCode = value + is SessionField.CurrencyRate -> currencyRate = field.parse(value) + is SessionField.StackingIn -> { + stackingIn = field.parse(value) + } + is SessionField.StackingOut -> { + stackingOut = field.parse(value) + } + is SessionField.ListCustomField -> { + val entry = field.customField.getOrCreateEntry(realm, value) + session.customFieldEntries.add(entry) + } + is SessionField.NumberCustomField -> { + val customField = field.customField + + field.parse(value)?.let { number -> + Timber.d("N>> create: $number") + val entry = realm.copyToRealm(CustomFieldEntry()) + entry.numericValue = number + + customField.entries.add(entry) + session.customFieldEntries.add(entry) + } ?: run { + Timber.w("failed parse of numeric value: $value") + } + + } + else -> { + } + } + } + + } + + if (bankrollName.isEmpty()) { + bankrollName = "Import" + } + + val bankroll = Bankroll.getOrCreate(realm, bankrollName, isLive, currencyCode, currencyRate) + session.bankroll = bankroll + + session.result?.buyin?.let { + session.result?.buyin = it + additionalBuyins + } + val net = session.result?.net + + if (startDate != null && endDate != null && net != null) { // valid session + // session already in realm, we'd love not put it in Realm before doing the check + val count = DataUtils.sessionCount(realm, startDate!!, endDate!!, net) + if (count == 0) { + + val managedSession = realm.copyToRealm(session) + managedSession.startDate = startDate + managedSession.endDate = endDate + + if (stackingIn != null && stackingIn != 0.0) { + val type = TransactionType.getByValue(TransactionType.Value.STACKING_INCOMING, realm) + val transaction = Transaction.newInstance(realm, bankroll, startDate, type, stackingIn!!) + this.addAdditionallyCreatedIdentifiable(transaction) + } + + if (stackingOut != null && stackingOut != 0.0) { + val type = TransactionType.getByValue(TransactionType.Value.STACKING_OUTGOING, realm) + val transaction = Transaction.newInstance(realm, bankroll, startDate, type, stackingOut!!) + this.addAdditionallyCreatedIdentifiable(transaction) + } + + return managedSession + } else { + Timber.d("Session already exists(count=$count): sd=$startDate, ed=$endDate, net=$net") + } + } else { + Timber.d("Can't import session: sd=$startDate, ed=$endDate, net=$net") + } + + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt index ffc006d0..3a137468 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt @@ -16,7 +16,9 @@ class ProductCSVDescriptors { pokerBankrollTracker, runGoodCashGames, runGoodTournaments, - pokerAnalyticsiOS + pokerAnalyticsiOS, + pokerAnalyticsAndroid, + pokerAnalyticsAndroidTransactions ) private val pokerAgent: CSVDescriptor @@ -57,7 +59,7 @@ class ProductCSVDescriptors { private val pokerBankrollTracker: CSVDescriptor get() { - return SessionCSVDescriptor( + return SessionTransactionCSVDescriptor( DataSource.POKER_BANKROLL_TRACKER, true, SessionField.Start("starttime", dateFormat = "MM/dd/yy HH:mm"), @@ -90,7 +92,7 @@ class ProductCSVDescriptors { private val runGoodTournaments: CSVDescriptor get() { return SessionCSVDescriptor( - DataSource.RUNGOOD, + DataSource.RUN_GOOD, true, SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"), SessionField.StartTime("Start Time"), @@ -111,7 +113,7 @@ class ProductCSVDescriptors { SessionField.TournamentName("Event Name"), SessionField.TournamentNumberOfPlayers("Total Players"), SessionField.TournamentPosition("Finished Place"), - SessionField.TournamentType("Single-Table/Multi-Table") + SessionField.TournamentTypeName("Single-Table/Multi-Table") ) } @@ -121,7 +123,7 @@ class ProductCSVDescriptors { get() { return SessionCSVDescriptor( - DataSource.RUNGOOD, + DataSource.RUN_GOOD, false, SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"), SessionField.StartTime("Start Time", dateFormat = "HH:mm"), @@ -155,7 +157,7 @@ class ProductCSVDescriptors { ) } - private val pokerAnalyticsiOS: CSVDescriptor + val pokerAnalyticsiOS: SessionCSVDescriptor get() { return SessionCSVDescriptor( DataSource.POKER_ANALYTICS, @@ -179,7 +181,7 @@ class ProductCSVDescriptors { SessionField.CurrencyRate("Currency Rate"), SessionField.SmallBlind("Small Blind"), SessionField.BigBlind("Big Blind"), - SessionField.TournamentType("Tournament Type"), + SessionField.TournamentTypeName("Tournament Type"), SessionField.TournamentEntryFee("Entry fee"), SessionField.TournamentNumberOfPlayers("Number of players"), SessionField.TournamentPrizePool("Prize Pool"), @@ -188,6 +190,54 @@ class ProductCSVDescriptors { ) } + val pokerAnalyticsAndroidTransactions: TransactionCSVDescriptor + get() { + return TransactionCSVDescriptor( + DataSource.POKER_ANALYTICS, + TrField.TransactionDate("Date", dateFormat = "MM/dd/yy HH:mm:ss"), + TrField.Amount("Amount"), + TrField.TransactionType("Type"), + TrField.BankrollName("Bankroll"), + TrField.Live("Live"), + TrField.CurrencyCode("Currency Code"), + TrField.CurrencyRate("Currency Rate"), + TrField.Comment("Comment") + ) + } + + val pokerAnalyticsAndroid: SessionCSVDescriptor + get() { + return SessionCSVDescriptor( + DataSource.POKER_ANALYTICS, + true, + SessionField.Start("Start Date", dateFormat = "MM/dd/yy HH:mm:ss"), + SessionField.End("End Date", dateFormat = "MM/dd/yy HH:mm:ss"), + SessionField.Break("Break"), + SessionField.SessionType("Type"), + SessionField.Live("Live"), + SessionField.NumberOfTables("Tables"), + SessionField.Buyin("Buyin"), + SessionField.CashedOut("Cashed Out"), + SessionField.NetResult("Online Net"), + SessionField.Tips("Tips"), + SessionField.LimitType("Limit"), + SessionField.Game("Game"), + SessionField.TableSize("Table Size"), + SessionField.Location("Location"), + SessionField.Bankroll("Bankroll"), + SessionField.CurrencyCode("Currency Code"), + SessionField.CurrencyRate("Currency Rate"), + SessionField.SmallBlind("Small Blind"), + SessionField.BigBlind("Big Blind"), + SessionField.TournamentTypeName("Tournament Type"), + SessionField.TournamentName("Tournament Name"), + SessionField.TournamentEntryFee("Entry fee"), + SessionField.TournamentNumberOfPlayers("Number of players"), + SessionField.TournamentFeatures("Tournament Features"), + SessionField.TournamentPosition("Position"), + SessionField.Comment("Comment") + ) + } } } \ No newline at end of file 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 index c3c1907e..3e4b49fd 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt @@ -3,349 +3,127 @@ package net.pokeranalytics.android.util.csv import io.realm.Realm import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.Limit -import net.pokeranalytics.android.model.TableSize import net.pokeranalytics.android.model.TournamentType -import net.pokeranalytics.android.model.interfaces.Identifiable -import net.pokeranalytics.android.model.realm.Bankroll +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.model.realm.TransactionType -import net.pokeranalytics.android.model.utils.DataUtils -import net.pokeranalytics.android.util.extensions.getOrCreate -import net.pokeranalytics.android.util.extensions.setHourMinutes import org.apache.commons.csv.CSVRecord import timber.log.Timber -import java.util.* /** * A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects */ -class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean?, vararg elements: CSVField) : - DataCSVDescriptor(source, *elements) { +class SessionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg elements: CSVField) : + PACSVDescriptor(source, isTournament, *elements) { - private enum class DataType { - TRANSACTION, - SESSION; + override fun prepareCSVExport() { - companion object { - - fun valueForString(type: String): DataType? { - return when (type) { - "Deposit/Payout" -> TRANSACTION - "Cash Game", "Tournament" -> SESSION - else -> null + val realm = Realm.getDefaultInstance() + realm.where(CustomField::class.java).findAll().sort("name").forEach { customField -> + val header = customField.name + CustomFieldCSVField.separator + customField.type + val f = when (customField.type) { + CustomField.Type.LIST.uniqueIdentifier -> { + SessionField.ListCustomField(header, customField) + } + else -> { + SessionField.NumberCustomField(header, customField) } } + this.fields.add(f) } + realm.close() } - /** - * Parses a [record] and return an optional Session - */ - override fun parseData(realm: Realm, record: CSVRecord): Identifiable? { - - var dataType: DataType? = null - val typeField = fields.firstOrNull { it is SessionField.SessionType } - typeField?.let { field -> - this.fieldMapping[field]?.let { index -> - val typeValue = record.get(index) - dataType = DataType.valueForString(typeValue) - } - } - - return when (dataType) { - DataType.TRANSACTION -> parseTransaction(realm, record) - else -> parseSession(realm, record) - } - + override fun parseData(realm: Realm, record: CSVRecord): Session? { + return this.parseSession(realm, record) } - private fun parseTransaction(realm: Realm, record: CSVRecord): Transaction? { - - var date: Date? = null - var type: TransactionType? = null - var currencyCode: String? = null - var currencyRate: Double? = null - - // Poker Bankroll Tracker specifics - var buyin: Double? = null - var cashedOut: Double? = null - - fields.forEach { field -> - - val index = this.fieldMapping[field] - if (index != null) { - val value = record.get(index) - when (field) { - is SessionField.Start -> { - date = field.parse(value) - } - is SessionField.Buyin -> buyin = field.parse(value) - is SessionField.CashedOut -> cashedOut = field.parse(value) - is SessionField.CurrencyCode -> currencyCode = value - is SessionField.CurrencyRate -> currencyRate = field.parse(value) - else -> { - } + override fun toCSV(data: Session, field: CSVField): String? { + + return when (field) { + is SessionField.Start -> field.format(data.startDate) + is SessionField.End -> field.format(data.endDate) + is SessionField.Break -> field.format(data.breakDuration.toDouble()) + is SessionField.SessionType -> Session.Type.values()[data.type].value + is SessionField.Live -> field.format(data.isLive) + is SessionField.NumberOfTables -> field.format(data.numberOfTables) + is SessionField.Buyin -> field.format(data.result?.buyin) + is SessionField.CashedOut -> field.format(data.result?.cashout) + is SessionField.NetResult -> field.format(data.result?.netResult) + is SessionField.Tips -> field.format(data.result?.tips) + is SessionField.LimitType -> { + data.limit?.let { limit -> + Limit.values()[limit].longName } } - } - - val amount = if (buyin != null && buyin!! > 0) { - type = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm) - buyin!! * -1 - } else if (cashedOut != null && cashedOut!! > 0) { - type = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm) - cashedOut - } else { - null - } - - if (date != null && amount != null && type != null && currencyCode != null) { - - if (DataUtils.transactionUnicityCheck(realm, date!!, amount, type)) { - - val bankroll = Bankroll.getOrCreate( - realm, - currencyCode!!, - currencyCode = currencyCode!!, - currencyRate = currencyRate - ) - return Transaction.newInstance(realm, bankroll, date!!, type, amount) - } else { - Timber.d("Transaction already exists") + is SessionField.Game -> data.game?.name + is SessionField.TableSize -> data.tableSize?.toString() + is SessionField.Location -> data.location?.name + is SessionField.Bankroll -> data.bankroll?.name + is SessionField.CurrencyCode -> data.bankroll?.currency?.code + is SessionField.CurrencyRate -> field.format(data.bankroll?.currency?.rate) + is SessionField.SmallBlind -> field.format(data.cgSmallBlind) + is SessionField.BigBlind -> field.format(data.cgBigBlind) + is SessionField.TournamentType -> field.format(data.tournamentType) + is SessionField.TournamentTypeName -> { + data.tournamentType?.let { tt -> + TournamentType.values()[tt].label + } + } + is SessionField.TournamentName -> data.tournamentName?.name + is SessionField.TournamentFeatures -> field.format(data.tournamentFeatures) + is SessionField.TournamentEntryFee -> field.format(data.tournamentEntryFee) + is SessionField.TournamentNumberOfPlayers -> field.format(data.tournamentNumberOfPlayers) + is SessionField.TournamentPosition -> field.format(data.result?.tournamentFinalPosition) + is SessionField.Comment -> data.comment + is SessionField.NumberCustomField -> { + val entry = data.customFieldEntries.find { it.customField?.id == field.customField.id } + field.format(entry?.numericValue) + } + is SessionField.ListCustomField -> { + val entry = data.customFieldEntries.find { it.customField?.id == field.customField.id } + entry?.value } - } else { - Timber.d("Can't import transaction: date=$date, amount=$amount, type=${type?.name}") + else -> null } - return null } - private var sameDaySessionCount: Int = 0 - private var currentday: String = "" - private var startInSeconds: Double = 20 * 3600.0 - - private fun parseSession(realm: Realm, record: CSVRecord): Session? { - - val isTournament = isTournament ?: false - val session = Session.newInstance(realm, isTournament, managed = false) + override fun hasMatched(realm: Realm, record: CSVRecord) { + super.hasMatched(realm, record) - var startDate: Date? = null - var endDate: Date? = null + // identify if the headers has custom fields headers: + // create the custom fields if not existing + // adds the field to the dynamic list + val headers = record.toSet() + headers.filter { it.contains(CustomFieldCSVField.separator) }.forEach { header -> - var isLive = true - var bankrollName = "" - var currencyCode: String? = null - var currencyRate: Double? = null - var additionalBuyins = 0.0 // rebuy + addon - - var stackingIn: Double? = null - var stackingOut: Double? = null - - fields.forEach { field -> - - this.fieldMapping[field]?.let { index -> - - val value = record.get(index) - when (field) { - is SessionField.Start -> { - startDate = field.parse(value) - if (source == DataSource.POKER_AGENT) { - if (currentday == value) { - sameDaySessionCount++ - } else { - sameDaySessionCount = 0 - } - currentday = value - } else {} - } - is SessionField.End -> { - endDate = field.parse(value) - } - is SessionField.StartTime -> { - startDate?.setHourMinutes(value) - } - is SessionField.EndTime -> { - endDate?.setHourMinutes(value) - } - is SessionField.Duration -> { - val hoursDuration = field.parse(value) ?: throw PAIllegalStateException("null duration") - - if (startDate != null) { - if (field.randomTime) { - if (sameDaySessionCount == 0) { - startInSeconds = 20 * 3600.0 - } else { - startInSeconds -= hoursDuration * 3600.0 - } - - if (startInSeconds < 0) { - startInSeconds = 20 * 3600.0 -// throw PAIllegalStateException("negative start: $startDate, start = $startInSeconds, net = ${session.result?.netResult}") - } - - val hour = (startInSeconds / 3600.0).toInt() - val minutes = ((startInSeconds - hour * 3600.0) / 60.0).toInt() - val formattedTime = "$hour:$minutes" - startDate?.setHourMinutes(formattedTime) - } - - val seconds = (hoursDuration * 3600.0).toInt() - val calendar = Calendar.getInstance() - calendar.time = startDate - calendar.add(Calendar.SECOND, seconds) - endDate = calendar.time - - } else { - throw PAIllegalStateException("start date ($startDate) + hoursDuration ($hoursDuration) required") - } - - } - is SessionField.Buyin -> { - val buyin = field.parse(value) - session.result?.buyin = buyin - if (session.type == Session.Type.TOURNAMENT.ordinal) { - session.tournamentEntryFee = buyin - } else { - } - } - is SessionField.CashedOut -> session.result?.cashout = field.parse(value) - is SessionField.NetResult -> session.result?.netResult = field.parse(value) - is SessionField.SessionType -> { - Session.Type.getValueFromString(value)?.let { type -> - session.type = type.ordinal - } - } - is SessionField.Live -> isLive = field.parse(value) ?: false - is SessionField.NumberOfTables -> session.numberOfTables = field.parse(value) ?: 1 - is SessionField.Addon -> additionalBuyins += field.parse(value) ?: 0.0 - is SessionField.Rebuy -> additionalBuyins += field.parse(value) ?: 0.0 - is SessionField.Tips -> session.result?.tips = field.parse(value) - is SessionField.Break -> { - field.parse(value)?.let { - session.breakDuration = it.toLong() - } - } - is SessionField.LimitAndGame -> { - if (value.isNotEmpty()) { - var limitAndGame = value - for (someLimit in Limit.values()) { - if (value.startsWith(someLimit.longName)) { - session.limit = someLimit.ordinal - limitAndGame = limitAndGame.removePrefix(someLimit.longName) - break - } - } - session.game = realm.getOrCreate(limitAndGame.trim()) - - } else { - } - } - is SessionField.Game -> { - if (value.isNotEmpty()) { - session.game = realm.getOrCreate(value) - } else { - } - } - is SessionField.Location -> { - val trimmedValue = value.trim() - if (trimmedValue.isNotEmpty()) { - session.location = realm.getOrCreate(trimmedValue) - } else { - } - } - 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 -> { - val sb = field.parse(value) - if (sb != null && sb > 0.0) { - session.cgSmallBlind = sb - } else {} - } - is SessionField.BigBlind -> { - val bb = field.parse(value) - if (bb != null && bb > 0.0) { - session.cgBigBlind = bb - } else {} - } - is SessionField.TableSize -> session.tableSize = TableSize.valueForLabel(value) - is SessionField.TournamentPosition -> session.result?.tournamentFinalPosition = - field.parse(value)?.toInt() - is SessionField.TournamentName -> { - if (value.isNotEmpty()) { - session.tournamentName = realm.getOrCreate(value) - } else { - } - } - is SessionField.TournamentType -> session.tournamentType = - TournamentType.getValueForLabel(value)?.ordinal - is SessionField.TournamentNumberOfPlayers -> session.tournamentNumberOfPlayers = - field.parse(value)?.toInt() - is SessionField.TournamentEntryFee -> session.tournamentEntryFee = field.parse(value) - is SessionField.CurrencyCode -> currencyCode = value - is SessionField.CurrencyRate -> currencyRate = field.parse(value) - is SessionField.StackingIn -> { - stackingIn = field.parse(value) - } - is SessionField.StackingOut -> { - stackingOut = field.parse(value) - } - else -> { - } - } + val cfProperties = header.split(CustomFieldCSVField.separator) + if (cfProperties.size != 2) { + throw PAIllegalStateException("A custom field header is wrongly formed: $header") } - } - - if (bankrollName.isEmpty()) { - bankrollName = "Import" - } - - val bankroll = Bankroll.getOrCreate(realm, bankrollName, isLive, currencyCode, currencyRate) - session.bankroll = bankroll - - session.result?.buyin?.let { - session.result?.buyin = it + additionalBuyins - } - val net = session.result?.net + val name = cfProperties.first() + val type = cfProperties.last().toInt() + val customField = CustomField.getOrCreate(realm, name, type) - if (startDate != null && endDate != null && net != null) { // valid session - // session already in realm, we'd love not put it in Realm before doing the check - val count = DataUtils.sessionCount(realm, startDate!!, endDate!!, net) - if (count == 0) { - - val managedSession = realm.copyToRealm(session) - managedSession.startDate = startDate - managedSession.endDate = endDate - - if (stackingIn != null && stackingIn != 0.0) { - val type = TransactionType.getByValue(TransactionType.Value.STACKING_INCOMING, realm) - val transaction = Transaction.newInstance(realm, bankroll, startDate, type, stackingIn!!) - this.addAdditionallyCreatedIdentifiable(transaction) + val field = when (customField.type) { + CustomField.Type.LIST.uniqueIdentifier -> { + SessionField.ListCustomField(header, customField) } - - if (stackingOut != null && stackingOut != 0.0) { - val type = TransactionType.getByValue(TransactionType.Value.STACKING_OUTGOING, realm) - val transaction = Transaction.newInstance(realm, bankroll, startDate, type, stackingOut!!) - this.addAdditionallyCreatedIdentifiable(transaction) + else -> { + SessionField.NumberCustomField(header, customField) } + } - return managedSession - } else { - Timber.d("Session already exists(count=$count): sd=$startDate, ed=$endDate, net=$net") + val index = headers.indexOf(header) // get index in the record + if (index >= 0) { + this.fieldMapping[field] = index } - } else { - Timber.d("Can't import session: sd=$startDate, ed=$endDate, net=$net") + this.fields.add(field) + } - return null } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt index db4d316a..842c92c4 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt @@ -1,14 +1,39 @@ package net.pokeranalytics.android.util.csv import net.pokeranalytics.android.exceptions.PAIllegalStateException +import net.pokeranalytics.android.model.realm.CustomField +import net.pokeranalytics.android.model.realm.TournamentFeature import java.util.* -sealed class TransactionField { +sealed class TrField { - data class TransactionType( + data class TransactionDate( override var header: String, - override var callback: ((String) -> net.pokeranalytics.android.model.realm.TransactionType?)? = null - ) : DataCSVField + override var callback: ((String) -> Date?)? = null, + override val dateFormat: String? = null + ) : DateCSVField + + data class Amount( + override var header: String, + override var callback: ((String) -> Double?)? = null + ) : NumberCSVField + + data class BankrollName(override var header: String) : CSVField + + data class Live( + override var header: String, + override var callback: ((String) -> Boolean?)? = null + ) : BooleanCSVField + + data class CurrencyCode(override var header: String) : CSVField + + data class CurrencyRate( + override var header: String, + override var callback: ((String) -> Double?)? = null + ) : NumberCSVField + + data class TransactionType(override var header: String) : CSVField + data class Comment(override var header: String) : CSVField } @@ -44,96 +69,91 @@ sealed class SessionField { data class Duration( override var header: String, override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null, val randomTime: Boolean = false ) : NumberCSVField data class Buyin( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class NetResult( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class CashedOut( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class Break( override var header: String, var unit: Int = Calendar.MINUTE, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField { - override fun parse(value: String): Double? { + private val multiplier: Int + get() { + return when (unit) { + Calendar.HOUR -> 3600 * 1000 + Calendar.MINUTE -> 60 * 1000 + Calendar.SECOND -> 1000 + else -> throw PAIllegalStateException("Unmanaged time unit: $unit") + } + } + override fun parse(value: String): Double? { this.callback?.let { return it(value) } - val v = NumberCSVField.defaultParse(value) - val multiplier = when (unit) { - Calendar.HOUR -> 3600 * 1000 - Calendar.MINUTE -> 60 * 1000 - Calendar.SECOND -> 1000 - else -> throw PAIllegalStateException("Unmanaged time unit: $unit") - } - return v?.times(multiplier) + return v?.times(this.multiplier) + } + + override fun format(data: Double?): String? { + return super.format(data?.div(multiplier)) } } data class Tips( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class SmallBlind( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class BigBlind( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class Rebuy( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class Addon( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class StackingIn( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class StackingOut( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField - data class Blind(override var header: String, override var callback: ((String) -> Pair?)? = null) : - BlindCSVField + data class Blind(override var header: String, + override var callback: ((String) -> Pair?)? = null + ) : BlindCSVField data class Live( override var header: String, @@ -156,36 +176,51 @@ sealed class SessionField { 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 TournamentTypeName(override var header: String) : CSVField + + data class TournamentFeatures(override var header: String, + override var callback: ((String) -> List?)? = null + ) : TournamentFeaturesCSVField data class CurrencyRate( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class TournamentPosition( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null - ) : NumberCSVField + override var callback: ((String) -> Int?)? = null + ) : IntCSVField data class TournamentNumberOfPlayers( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null - ) : NumberCSVField + override var callback: ((String) -> Int?)? = null + ) : IntCSVField + + data class TournamentType( + override var header: String, + override var callback: ((String) -> Int?)? = null + ) : IntCSVField data class TournamentEntryFee( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class TournamentPrizePool( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField + data class NumberCustomField( + override val header: String, + override var customField: CustomField, + override var callback: ((String) -> Double?)? = null + ) : CustomFieldCSVField, NumberCSVField + + data class ListCustomField( + override val header: String, + override var customField: CustomField + ) : CustomFieldCSVField + } diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt new file mode 100644 index 00000000..e71a1585 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt @@ -0,0 +1,118 @@ +package net.pokeranalytics.android.util.csv + +import io.realm.Realm +import net.pokeranalytics.android.model.interfaces.Identifiable +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.model.utils.DataUtils +import org.apache.commons.csv.CSVRecord +import timber.log.Timber +import java.util.* + +/** + * A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects + */ +class SessionTransactionCSVDescriptor(source: DataSource, private var isTournament: Boolean?, vararg elements: CSVField) : + PACSVDescriptor(source, isTournament, *elements) { + + private enum class DataType { + TRANSACTION, + SESSION; + + companion object { + + fun valueForString(type: String): DataType? { + return when (type) { + "Deposit/Payout" -> TRANSACTION + "Cash Game", "Tournament" -> SESSION + else -> null + } + } + } + + } + + /** + * Parses a [record] and return an optional Session + */ + override fun parseData(realm: Realm, record: CSVRecord): Identifiable? { + + var dataType: DataType? = null + val typeField = fields.firstOrNull { it is SessionField.SessionType } + typeField?.let { field -> + this.fieldMapping[field]?.let { index -> + val typeValue = record.get(index) + dataType = DataType.valueForString(typeValue) + } + } + + return when (dataType) { + DataType.TRANSACTION -> parseTransaction(realm, record) + else -> parseSession(realm, record) + } + + } + + private fun parseTransaction(realm: Realm, record: CSVRecord): Transaction? { + + var date: Date? = null + var type: TransactionType? = null + var currencyCode: String? = null + var currencyRate: Double? = null + + // Poker Bankroll Tracker specifics + var buyin: Double? = null + var cashedOut: Double? = null + + this.fields.forEach { field -> + + val index = this.fieldMapping[field] + if (index != null) { + val value = record.get(index) + when (field) { + is SessionField.Start -> { + date = field.parse(value) + } + is SessionField.Buyin -> buyin = field.parse(value) + is SessionField.CashedOut -> cashedOut = field.parse(value) + is SessionField.CurrencyCode -> currencyCode = value + is SessionField.CurrencyRate -> currencyRate = field.parse(value) + else -> { + } + } + } + } + + val amount = if (buyin != null && buyin!! > 0) { + type = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm) + buyin!! * -1 + } else if (cashedOut != null && cashedOut!! > 0) { + type = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm) + cashedOut + } else { + null + } + + if (date != null && amount != null && type != null && currencyCode != null) { + + if (DataUtils.transactionUnicityCheck(realm, date!!, amount, type)) { + + val bankroll = Bankroll.getOrCreate( + realm, + currencyCode!!, + currencyCode = currencyCode!!, + currencyRate = currencyRate + ) + return Transaction.newInstance(realm, bankroll, date!!, type, amount) + } else { + Timber.d("Transaction already exists") + } + } else { + Timber.d("Can't import transaction: date=$date, amount=$amount, type=${type?.name}") + } + + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/TransactionCSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/TransactionCSVDescriptor.kt new file mode 100644 index 00000000..3b2db42d --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/TransactionCSVDescriptor.kt @@ -0,0 +1,78 @@ +package net.pokeranalytics.android.util.csv + +import io.realm.Realm +import net.pokeranalytics.android.exceptions.PAIllegalStateException +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.model.utils.DataUtils +import org.apache.commons.csv.CSVRecord +import timber.log.Timber +import java.util.* + +class TransactionCSVDescriptor(source: DataSource, vararg elements: CSVField) : + DataCSVDescriptor(source, *elements) { + + override fun parseData(realm: Realm, record: CSVRecord): Transaction? { + + var date: Date? = null + var typeName: String? = null + var bankrollName: String? = null + var amount: Double? = null + var comment: String? = null + var live = false + var currencyCode: String? = null + var currencyRate: Double? = null + + for (field in this.fields) { + + val index = this.fieldMapping[field] + if (index != null) { + val value = record.get(index) + when (field) { + is TrField.TransactionDate -> date = field.parse(value) + is TrField.TransactionType -> typeName = value + is TrField.Amount -> amount = field.parse(value) + is TrField.BankrollName -> bankrollName = value + is TrField.Live -> live = field.parse(value) ?: true + is TrField.CurrencyCode -> currencyCode = value + is TrField.CurrencyRate -> currencyRate = field.parse(value) + is TrField.Comment -> comment = value + } + } + } + + if (date != null && amount != null && typeName != null && bankrollName != null) { + + val type = TransactionType.getOrCreate(realm, typeName, amount > 0) + + if (DataUtils.transactionUnicityCheck(realm, date, amount, type)) { + val bankroll = Bankroll.getOrCreate(realm, bankrollName, live, currencyCode, currencyRate) + return Transaction.newInstance(realm, bankroll, date, type, amount, comment) + } else { + Timber.d("Transaction already exists") + } + } else { + Timber.d("Can't import transaction: date=$date, amount=$amount, type=$typeName") + } + + return null + } + + override fun toCSV(data: Transaction, field: CSVField): String? { + + return when(field) { + is TrField.TransactionDate -> field.format(data.date) + is TrField.TransactionType -> data.type?.name + is TrField.Amount -> field.format(data.amount) + is TrField.BankrollName -> data.bankroll?.name + is TrField.Live -> field.format(data.bankroll?.live) + is TrField.CurrencyCode -> data.bankroll?.currency?.code + is TrField.CurrencyRate -> field.format(data.bankroll?.currency?.rate) + is TrField.Comment -> data.comment + else -> throw PAIllegalStateException("unmanaged field: $field") + } + + } + +} \ No newline at end of file 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 d153943e..490b750a 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 @@ -117,6 +117,11 @@ fun Date.getMonthAndYear(): String { fun Date.getDayMonthYear(): String { return SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format(this).capitalize() } +// Returns a file friendly date time string +val Date.dateTimeFileFormatted: String + get() { + return SimpleDateFormat("yy_MM_dd_hh_mm_ss", Locale.getDefault()).format(this) + } // Return the netDuration between two dates fun Date.getFormattedDuration(toDate: Date): String { diff --git a/app/src/main/res/layout/fragment_subscription.xml b/app/src/main/res/layout/fragment_subscription.xml index e281aa06..8549087f 100644 --- a/app/src/main/res/layout/fragment_subscription.xml +++ b/app/src/main/res/layout/fragment_subscription.xml @@ -66,16 +66,15 @@ + app:layout_constraintVertical_bias="0.5" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c081dec1..319569f9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,11 +1,19 @@ Poker Analytics - Bitte legen Sie ein Startdatum für die Sitzung fest - Hour + Stunde Minute - More - Lines + Mehr + Linien + Unbegrenzt + Unterstützung + Importiert + Sparen + Steht aus + Von + Zu + + Bitte legen Sie ein Startdatum für die Sitzung fest Anfangswert Kann nicht gezeigt werden, weil weniger als zwei Werte anzuzeigen sind! Das Objekt, auf das Sie zugreifen wollen, ist ungültig. @@ -14,13 +22,11 @@ Upgrade auf Pro Pro werden Gratisversion - Unlimited Verfolgen Sie Ihr gesamtes Poker-Leben und ergänzen Sie so viele Daten wie gewünscht Zuerst offline Poker Analytics ist jederzeit verfügbar und die Daten gehören Ihnen. Hinweis: Wir werden bald Exportfunktionen hinzufügen und derzeit sind Sie verantwortlich für Sicherungen. Vielen Dank für Ihre Geduld! Privat Wir sind nicht Eigentümer von Servern. Wir wissen nichts über Ihre Gewinne und Verluste. - Support Wir versuchen so schnell wie möglich zu antworten, auf Englisch oder Französisch! Wird geladen, bitte warten… Wählen Sie Ihren Berichtstyp @@ -36,7 +42,6 @@ Der Filter kann nicht gelöscht werden, weil er derzeit ausgewählt ist. Benutzerdefiniertes Feld Dieser Posten wird derzeit in einer oder mehreren Transaktionen genutzt…Bitte löschen Sie zuerst die verknüpften Transaktionen. - Imported Sie haben die maximale Anzahl von Gratis-Sitzungen erreicht. Bitte abonnieren Sie für eine unbegrenzte Nutzung und zögern Sie nicht, uns mehr über Ihre derzeitige Nutzererfahrung zu sagen! Eingehender Einsatz Ausgehender Einsatz @@ -47,12 +52,10 @@ Namensvorschläge Daten gelöscht Das Enddatum sollte nach dem Startdatum liegen - Save Turnierbezeichnung Turnierbezeichnungen Turniermerkmal Turniermerkmale - Pending Poker Analytics ist eine Poker-Tracking-App.\nSie können die App über ein Jahresabonnement für unbegrenzte Nutzung erwerben, aber Sie erhalten auch 10 Sitzungen + eine Gratisversion, um die App auszuprobieren. Ich verstehe Sie müssen diesem Turniermerkmal einen Namen geben @@ -61,8 +64,6 @@ Diese Bezeichnung existiert bereits. Eine oder mehrere Transaktionen sind mit dieser Finanzierung verknüpft. Bitte löschen Sie zuerst die verknüpften Transaktion(en). Transaktionstyp - From - To . . . @@ -184,7 +185,7 @@ Die eingegrenzte Suche ergab keine Ergebnisse Bestätigung Kontakt - CSV (Importvorgang fehlgeschlagen) + CSV One of the lines in your CSV file is not well formed. Please check your file or contact the support. Währung Aktueller Monat diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 96bc751d..ebeb7ded 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -2,10 +2,6 @@ Poker Analytics Establece una fecha de inicio para la sesión - Hour - Minute - More - Lines Valor inicial No se puede mostrar porque hay menos de dos valores para mostrar El objeto al que intentas acceder no es válido @@ -14,13 +10,11 @@ Actualizar a la versión Pro Avanzar a Pro Prueba gratuita - Unlimited Rastrea toda tu vida de póker agregando tantos datos como quieras Desconectado primero Poker Analytics está disponible en todo momento y los datos son tuyos. Nota: pronto agregaremos capacidad de exportación y por ahora tú estás a cargo de las copias de seguridad. ¡Gracias por tu paciencia! Privado No poseemos servidores. No sabemos nada sobre tus victorias y derrotas. - Support Intentamos responder lo más rápido posible en inglés o francés. Cargando, espera… Selecciona tu tipo de reporte @@ -37,7 +31,6 @@ Campo personalizado El artículo se utiliza en una o más transacciones.… Primero elimina las transacciones vinculadas - Imported Has alcanzado el número máximo de sesiones gratuitas. ¡Suscríbete para un uso ilimitado y no olvides contarnos tu propia experiencia! Apuesta entrante Apuesta saliente @@ -50,12 +43,10 @@ Una suscripción se renueva automáticamente a menos que se cancele\n• Puedes Sugerencias de nombres Datos borrados La fecha de finalización debe ser posterior a la fecha de inicio - Save Nombre del torneo Nombres de torneos Función del torneo Funciones de torneos - Pending Poker Analytics es una aplicación de seguimiento de póker.\n La aplicación funciona con una suscripción anual para uso ilimitado, pero obtienes 10 sesiones, además de una prueba gratuita para probar la aplicación. Comprendo @@ -65,8 +56,6 @@ La aplicación funciona con una suscripción anual para uso ilimitado, pero obti Ya existe este nombre Una o más transacciones están asociadas con este fondo, elimina primero la o las transacciones vinculadas. Tipo de transacción - From - To @@ -88,6 +77,17 @@ La aplicación funciona con una suscripción anual para uso ilimitado, pero obti Flop Turn River + Hora + Minuto + Más + Líneas + Ilimitado + Apoyo + Importado + Salvar + Pendiente + Desde + A hace %s %s no tiene acceso a tus contactos y no puede encontrar el nombre de tus amigos existentes. Puedes otorgar acceso a través de las preferencias del iPhone para evitar nombres de \'usuario desconocido\'. @@ -188,7 +188,7 @@ La aplicación funciona con una suscripción anual para uso ilimitado, pero obti Los filtros aún no dieron ningún resultado. Confirmación Contáctanos - CSV (no se puede importar) + CSV Una de las líneas en tu archivo CSV no está bien formada. Verifica el archivo o contacta al soporte. Divisa Mes en curso diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index bb5a6cf7..01046ca8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -10,7 +10,6 @@ Heure Minute Autre - Jeu Lignes Valeur initiale Il faut au moins deux valeurs pour afficher ce rapport! @@ -56,7 +55,7 @@ Mise-à-jour %s - Adresse + Suggestions de noms Élément effacé La date de fin doit être après la date de début @@ -173,7 +172,7 @@ Le filtre ne retourne aucun résultat Confirmation Contactez-nous - CSV (ne peut être importé) + CSV Une ligne de votre fichier CSV est mal formée. Veuillez vérifier votre fichier ou contacter le support. Devise Ce mois-ci @@ -670,7 +669,7 @@ Semi-Pro Pro Conditions de l\'abonnement - Conditions d’utilisations concernant l’abonnement:\n- Le paiement sera facturé sur votre compte iTunes.\n- L’abonnement est renouvelé automatiquement chaque année, à moins d’avoir été désactivé au moins 24 heures avant la fin de la période de l’abonnement.\n- L’abonnement peut être géré par l’utilisateur et désactivé en allant dans les réglages de son compte après s’être abonné.\n- Le compte sera facturé pour le renouvellement de l\'abonnement dans les 24 heures précédent la fin de la période d’abonnement.\n- Un abonnement en cours ne peut être annulé.\n- Toute partie inutilisée de l\'offre gratuite, si souscrite, sera abandonnée lorsque l\'utilisateur s\'abonnera, dans les cas applicables + Conditions d’utilisations:\n• Votre compte ne sera pas prélevé pendant toute la durée de l\'essai gratuit\n• A la fin de l\'essai gratuit, vous serez automatiquement prélevé du montant total de l\'abonnement annuel\n• Un abonnement est automatiquement renouvelé sauf si annulé\n• Votre abonnement peut-être géré en allant dans le tab \"Autre\" de l\'app, puis \"Abonnement\" Politique de confidentialité Nous sommes désolé, mais il y a problème…Il est possible qu\'iCloud synchronise vos données. Veuillez réessayer plus tard. Pouvez-vous nous envoyer un rapport expliquant l\'état de l\'app pour nous aider à résoudre le problème? Merci! Appuyez et maintenez sur une session pour la dupliquer diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 99e52e12..3b0eb55e 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -2,10 +2,6 @@ Poker Analytics कृपया सत्र के लिए एक आरंभ तारीख सेट करें - Hour - Minute - More - Lines आरंभिक मान प्रदर्शित नहीं कर सकते क्योंकि डिसप्ले में मान दो से कम हैं! उद्देश्य जिसका आप प्रयास कर रहे हैं वह अमान्य है @@ -14,13 +10,11 @@ प्रो में अपग्रेड करें प्रो हो जाएं मुफ्त ट्रायल - Unlimited आप जितना चाहें उतना डेटा जोड़कर आपके समस्त पोकर जीवन को ट्रेक करें ऑफलाइन पहले Poker Analytics हर समय उपलब्ध है और डेटा आपका है. नोट: हम जल्द ही एक्सपोर्टिंग क्षमताएं जोड़ेंगे और वर्तमान में आप बैकअप के इंचार्ज हैं. आपके धैर्य के लिए धन्यवाद! निजी हमारे पास सर्वर नहीं हैं. हम आपकी जीत और हारों के बारे में कुछ भी नहीं जानते हैं. - Support हम जितना जल्दी संभव हो, इंग्लिश या फ्रेंच में जवाब देने का प्रयास करते हैं! लोड हो रहा है, कृपया प्रतीक्षा करें… आपकी रिपोर्ट का प्रकार चुनें @@ -36,7 +30,6 @@ फिल्टर हटाया नहीं जा सकता क्योंकि वर्तमान में इसे चुना हुआ है. कस्टम फील्ड यह आयटम एक या अधिक लेनदेन में उपयोग की गई है…कृपया पहले लिंक हुए लेनदेन हटाएं - Imported आप मुफ्त सत्रों की अधिकतम सीमा तक पहुँच गए हैं. असीमित उपयोग के लिए कृपया सदस्यता लें और हमें निस्संकोच बताएं कि आपको अपने वर्तमान अनुभव के साथ कैसा महसूस होता है! दाँव इनकमिंग दाँव आउटगोइंग @@ -47,12 +40,10 @@ नामकरण सुझाव डेटा हटाया गया समाप्ति तारीख आरंभ तारीख के बाद होनी चाहिए - Save टूर्नामेंट का नाम टूर्नामेंट के नाम टूर्नामेंट की विशेषता टूर्नामेंट की विशेषताएं - Pending Poker Analytics एक पोकर ट्रेकिंग एप्प है.\nएप्प वार्षिक सदस्यता के साथ एक असीमित उपयोग के रूप में कार्य करता है, लेकिन आपको एप्प का परीक्षण करने के लिए 10 सत्र + एक मुफ्त ट्रायल मिलते हैं. मैं समझ गया/गई आपको इस टूर्नामेंट व‍िशेषता के लिए एक नाम देने की जरूरत है @@ -61,8 +52,6 @@ यह नाम पहले ही मौजूद है. इस बैंकरोल के साथ एक या अधिक लेनदेन जुड़े हैं, कृपया पहले लिंक हुए लेनदेन (नों) को हटाएं. लेनदेन का प्रकार - From - To st nd rd @@ -84,6 +73,17 @@ Flop Turn River + घंटा + मिनट + अधिक + पंक्तियां + असीमित + सहयोग + आयातित + सहेजें + विचाराधीन + से + सेवा %s पहले %s की आपके संपर्कों तक पहुँच नहीं है और आपके मौजुदा दोस्तों के नामों को पुन: प्राप्त नहीं कर सकता है. किसी \'अज्ञात उपयोगकर्ता\' से बचाव के लिए आपको iPhone प्राथमिकताएं द्वारा पहुँच प्रदान करनी होगी. @@ -184,7 +184,7 @@ फिल्टर से कोई परिणाम नहीं मिलेे पुष्टीकरण हमें संपर्क करें - CSV (इम्पोर्ट नहीं किया जा सकता) + CSV आपकी CSV फाइल में लाइनों से एक अच्छी तरह फार्मेट नहीं की गई है. कृपया अपनी फाइल जाँचे या सहायता टीम से संपर्क करें. करेंसी वर्तमान महीना diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6023b4a7..af9d9b0f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2,10 +2,6 @@ Poker Analytics Imposta una data di inizio per la sessione - Hour - Minute - More - Lines Valore iniziale Non mostrabile, perché ci sono meno di due valori da visualizzare! L\'oggetto a cui stai cercando di accedere non è valido @@ -14,13 +10,11 @@ Passa a Pro Passa a Pro prova gratuita - Unlimited Segui tutta la tua vita da pokerista aggiungendo tutti i dati desiderati Prima offline Poker Analytics è disponibile sempre e i dati sono tuoi. Nota: presto aggiungeremo la funzione di esportazione e al momento spetta a te fare i backup. Grazie per la pazienza! Privato Non siamo proprietari dei server. Non abbiamo alcuna informazione sulle tue vincite né sulle tue perdite. - Support Cerchiamo di rispondere il prima possibile, in inglese o francese! Caricamento in corso, attendere… Seleziona il tuo tipo di report @@ -36,7 +30,6 @@ Il filtro non può essere eliminato, perché è al momento selezionato. Campo personalizzato La voce è utilizzata in una o più transazioni…Prima elimina le transazioni collegate - Imported Hai raggiunto il limite massimo di sessioni gratuite. Abbonati per avere l\'uso illimitato, e non esitare a contattarci per farci sapere come ti trovi con la versione illimitata! Staking in entrata Staking in uscita @@ -47,12 +40,10 @@ Suggerimenti di nomi Dati eliminati La data di fine deve essere successiva alla data di inizio - Save Nome torneo Nomi torneo Caratteristica torneo Caratteristiche torneo - Pending Poker Analytics è un\'app per monitorare le proprie partite a poker.\nL\'app funziona con un abbonamento annuale per uso illimitato, ma ogni utente riceve 10 sessioni + una prova gratuita per testare l\'app. Ho compreso Assegna un nome a questa caratteristica di torneo @@ -61,8 +52,6 @@ Questo nome esiste già. Una o più transazioni sono associate a questa posta di gioco. Prima elimina una o più transazioni associate. Tipo di transazione - From - To ° ° ° @@ -84,6 +73,17 @@ Flop Turn River + Ora + Minuto + Di Più + Linee + Illimitato + Supporto + Importato + Salva + In attesa di + A partire dal + Per %s fa %s non ha accesso ai tuoi contatti e non può recuperare il nome dei tuoi amici esistenti. Potresti consentire l\'accesso attraverso le preferenze di iPhone per evitare qualsiasi nome di \"utente sconosciuto\". @@ -184,7 +184,7 @@ Il filtraggio non ha dato alcun risultato Conferma Contattaci - CSV (non si può importare) + CSV Una delle linee nel tuo file CSV non è ben formata. Controlla il tuo file o contatta l\'assistenza. Valuta Mese corrente diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 52b7b226..3afb44b9 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -2,10 +2,6 @@ Poker Analytics セッションの開始日を設定してください - Hour - Minute - More - Lines 初期値 表示できません。表示するには最低2つの値が必要です。 アクセスしようとしているオブジェクトは無効です @@ -14,13 +10,11 @@ プロにアップグレードする プロになる 無料お試し - Unlimited お好きなだけデータを追加し、ポーカーライフをトラッキングしましょう オフラインファースト Poker Analyticsはいつでもご利用できます。留意点:近々エクスポート機能が追加されます。現在はバックアップ機能をご使用ください。ご理解のほど、お願い申し上げます。 プライベート 私達はサーバーを所有しておらず、ユーザー様の勝敗については何も把握しておりません。 - Support 出来るかぎり早く英語またはフランス語でご返信するよう努めます。 ロード中です。しばらくお待ちください… 報告の種類を選んでください @@ -36,7 +30,6 @@ フィルターは現在選択中のため削除できません。 カスタムフィールド アイテムは一つまたは複数の取引で使用中です。…まずは該当する取引を削除してください。 - Imported 無料セッションの利用限度に達しました。無制限利用のサブスクリプションを購 入し、現在の感想をお聞かせください。 ステーキングを受け取っています @@ -50,12 +43,10 @@ 名前の候補 データは削除されました 終了日は開始日以降を選んでください - Save トーナメント名 トーナメント名 トーナメントの機能 トーナメントの機能 - Pending Poker Analyticsはポーカーのトラッキングアプリです。\nアプリは年間サブスクリプションで無制限でご利用できますが、アプリのお試し用に10のセッションと無料期間があります。 分かりました このトーナメントの機能に名前をつけてください @@ -65,8 +56,6 @@ このバンクロールは一つまたは複数の取引に賭けられています。まずは該当す る取引を削除してください。 取引の種類 - From - To st nd rd @@ -88,6 +77,17 @@ Flop Turn River + + + もっと + + 無制限 + サポート + 輸入 + セーブ + 保留中 + から + %s 前 %s では、あなたの連絡先にアクセスできないため、既存のお友達の名前を取得できません。「不明なユーザー」名を回避するには、iPhone のユーザー設定からアクセス権限を付与してください。 @@ -188,7 +188,7 @@ フィルタリングでは結果が戻されませんでした 確認 ご連絡ください - CSV (インポートできません) + CSV CSV ファイルのいずれかの行が正しく形成されていません。ファイルをご確認いただくが、サポートまでお問い合わせください。 通貨 今月 diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index c5fcb6dc..cd20e32d 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -2,10 +2,6 @@ Poker Analytics Defina uma data de início para a sessão - Hour - Minute - More - Lines Valor inicial Não é possível exibir porque há menos de dois valores para exibir! O objeto que você está tentando acessar é inválido @@ -14,13 +10,11 @@ Fazer upgrade para Pro Seja Pro avaliação gratuita - Unlimited Monitore toda a sua vida no pôquer com quantos dados você quiser Offline primeiro O Poker Analytics está disponível sempre e os dados são seus. Nota: Adicionaremos a capacidade de exportar em breve e você é responsável por seus backups no momento. Agradecemos a paciência! Privado Não temos servidores. Não sabemos nada sobre suas vitórias e derrotas. - Support Tentamos responder o mais rápido possível, em inglês ou francês! Carregando, aguarde… Selecione seu tipo de relatório @@ -36,7 +30,6 @@ O filtro não pode ser excluído porque está selecionado no momento. Campo personalizado O item é usado em uma ou mais transações…Exclua as transações vinculadas primeiro - Imported Você atingiu o número máximo de sessões gratuitas. Assine para usar sem limites e não deixe de nos contar o que você está achando até agora da experiência! Staking chegando Staking saindo @@ -47,12 +40,10 @@ Sugestões de nome Dados excluídos A data de término deve ser depois da data de início - Save Nome do torneio Nomes dos torneios Função do torneio Funções do torneio - Pending O Poker Analytics é um app de monitoramento.\nO app funciona com uma assinatura anual com uso ilimitado, mas você tem 10 sessões + teste grátis para testar o app. Entendi Você precisa dar um nome para esta função do torneio @@ -61,8 +52,6 @@ Este nome já existe. Uma ou mais transações associadas com esta banca. Exclua as transações vinculadas primeiro. Tipo de transação - From - To º º º @@ -84,6 +73,17 @@ Flop Turn River + Hora + Minuto + Mais + Linhas + Ilimitado + Apoio, suporte + Importado + Salve + Pendente + De + Para %s atrás O %s não possui acesso à lista de contatos e não pode obter os nomes dos seus amigos. Você pode permitir o acesso nos ajustes dos iPhone para evitar exibir \'usuário desconhecido\' no lugar do nome. @@ -184,7 +184,7 @@ O filtro não apresentou nenhum resultado Confirmação Contate-nos - CSV (Não pode ser importado) + CSV Uma das linhas do seu arquivo CSV não está no formato correto. Por favor, verifique o seu arquivo ou contate o suporte. Moeda Mês atual diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bbc232ca..dac794d9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -2,10 +2,6 @@ Poker Analytics Пожалуйста, установите дату начала сессии - Hour - Minute - More - Lines Начальное значение Невозможно показать, потому что для отображения меньше двух значений! Объект, к которому пытаются получить доступ, недействителен. @@ -14,13 +10,11 @@ Повысить категорию до Pro Перейти на Pro бесплатное опробование - Unlimited Отслеживайте всю свою жизнь в покере, добавляя столько данных, сколько хотите Сначала автономно Poker Analytics доступно в любое время, а данные - ваши. Примечание: вскоре мы добавим возможности экспорта, но в настоящее время вы отвечаете за резервное копирование. Спасибо за ваше терпение! Частное Мы не владеем серверами. Мы ничего не знаем о ваших выигрышах и проигрышах. - Support Мы стараемся ответить как можно быстрее, по-английски или по-французски! Загрузка, пожалуйста, подождите… Выберите свой тип отчёта @@ -36,7 +30,6 @@ Фильтр не может быть удален, так как он выбран в данный момент. Пользовательское поле Элемент используется в одной или нескольких транзакциях…Пожалуйста, сначала удалите связанные транзакции - Imported Вы достигли максимального количества бесплатных сессий. Пожалуйста, подпишитесь на неограниченное использование и без колебаний сообщайте нам, что вы думаете о своих текущих впечатлениях! Ставка прибывает Ставка убывает @@ -47,12 +40,10 @@ Предложения по наименованию Данные удалены Дата окончания должна быть после даты начала - Save Название турнира Названия турниров Функция турнира Функции турниров - Pending Poker Analytics - это приложение для отслеживания покера.\nПриложение работает по годовой подписке на неограниченное использование, но вы получаете 10 сессий + бесплатную пробную версию для тестирования приложения. Вас понял(а) Нужно дать название этой турнирной функции @@ -61,8 +52,6 @@ Это название уже существует. С этой финансовой операцией связана одна или несколько транзакций, пожалуйста, сначала удалите связанную(ые) транзакцию(ы). Тип транзакции - From - To @@ -84,6 +73,17 @@ Flop Turn River + Час + минут + Больше + линии + неограниченный + Поддержка + импортный + Сохранить + В ожидании + От + к %s назад %s не имеет доступа к адресной книге и не может определить имена выбранных вами друзей. Предоставьте доступ к контактам в настройках телефона чтобы исключить имена \'неизвестный пользователь\' @@ -184,7 +184,7 @@ Не найдено результатов после фильтрации Подтверждение Напишите нам - CSV (Импорт невозможен) + CSV Одна из строк в вашем файле CSV отформатирована некорректно. Пожалуйста проверьте ваш файл или свяжитесь со службой поддержки. Валюта Текущий месяц diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index d76476a7..692b3b7a 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -2,10 +2,6 @@ Poker Analytics 请设置该进程的开始日期 - Hour - Minute - More - Lines 初始值 不能显示,因为要显示的值少于两个! 您试图访问的对象无效 @@ -14,13 +10,11 @@ 升级到专业版 获取专业版 免费试用 - Unlimited 通过添加尽可能多的数据来追踪您的所有扑克游戏 先离线 Poker Analytics随时可用且该数据属于你。注意:我们将很快添加导出功能,您目前负责备份。谢谢你的耐心! 私人 我们没有服务器。我们对你的输赢一无所知。 - Support 我们尽快用英语或法语回答! 正在加载,请等待… 选择您的报告类型 @@ -36,7 +30,6 @@ 无法删除该筛选条件,因为它目前已被选取。 自定义字段 该项目用于一个或多个交易…请先删除链接的交易 - Imported 您已经达到了免费进程的最大数量。请订阅无限制使用,不要犹豫,告诉我们您对目前的体验感觉如何! 接收的赌注 发出的赌注 @@ -47,12 +40,10 @@ 命名建议 数据已删除 结束日期应在开始日期后 - Save 锦标赛名称 锦标赛名称 锦标赛功能 锦标赛功能 - Pending Poker Analytics是一款扑克追踪应用。该应用可包年订阅获取无限制使用,但你可以获得10次进程+1次免费试用来测试本应用。 我理解 您需要为此锦标赛功能指定一个名称 @@ -61,8 +52,6 @@ 此名称已存在。 一个或多个交易与此资金相关,请先删除相关交易。 交易类型 - From - To . . . @@ -83,6 +72,17 @@ Flop Turn River + 小时 + 分钟 + 更多 + 线数 + 无限 + 支持 + 进口的 + 保存 + 待定 + + %s前 %s不能访问你的联系人且不能检索你的现有好友姓名。通过iPhone首选项可授权防止任何\'陌生用户\'名访问。 @@ -169,7 +169,7 @@ 比较 确认 联系我们 - CSV(无法导入) + CSV CSV文件中的线之一不合适。请核查你的文件或联系支持团队。 货币 本月 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 16bafeb8..062d6e62 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,7 +11,6 @@ Hour Minute More - Variant Lines Initial Value Can\'t show because there is less than two values to display! @@ -45,11 +44,12 @@ The item is used in one or more transactions…Please delete the linked transactions first Imported You\'ve reached the maximum number of free sessions. Please subscribe for unlimited use and don\'t hesitate to tell us how you feel about your current experience! - Stacking incoming - Stacking outgoing + Staking incoming + Staking outgoing There has been an issue with the import. Please check out your file or contact the support! + Subscription terms of use:\n• When subscribing, the free trial prevents you from being charged until the period ends\n• At the end of the free trial, you will be automatically charged the yearly subscription amount\n• A subscription automatically renews unless canceled\n• You can manage your subscription by going in the app "More" tab, then "Subscription" + Show full screen - Address Naming suggestions Data deleted The end date should be after the start date @@ -67,7 +67,6 @@ 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 @@ -79,8 +78,6 @@ New tournament feature New filter - - %s ago @@ -182,7 +179,7 @@ Filtering did not yield any results Confirmation Contact us - CSV (Cannot be imported) + CSV One of the lines in your CSV file is not well formed. Please check your file or contact the support. Currency Current month @@ -666,7 +663,6 @@ Semi-Pro Pro Subscription terms - Subscription terms of use:\n• Payment will be charged to iTunes Account at confirmation of purchase\n• Subscription automatically renews unless auto-renew is turned off at least 24-hours before the end of the current period\n• Account will be charged for renewal within 24-hours prior to the end of the current period, and identify the cost of the renewal\n• Subscriptions may be managed by the user and auto-renewal may be turned off by going to the user\'s Account Settings after purchase\n• Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication, where applicable Privacy policy We\'re truly sorry, but something is wrong here…You may be waiting for iCloud sync. Please wait and retry later. Would you mind sending us a report explaining your current state to help us solve this issue? Tap and hold on a session to duplicate it! @@ -762,7 +758,6 @@ Do you really want to delete this game? Comments Big Blind Ante - Show full screen @@ -778,6 +773,8 @@ Join us on Discord! We\'ve opened our Discord channel! Come to hang out, talk about poker or about the app! Good for you! + Sessions (CSV) + Transactions (CSV) posts posts diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml index 43873cba..bfe997bf 100644 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -1,11 +1,10 @@ - - + + + + + \ No newline at end of file diff --git a/app/src/test/java/net/pokeranalytics/android/BasicUnitTest.kt b/app/src/test/java/net/pokeranalytics/android/BasicUnitTest.kt index fcac5be9..04f2d1fe 100644 --- a/app/src/test/java/net/pokeranalytics/android/BasicUnitTest.kt +++ b/app/src/test/java/net/pokeranalytics/android/BasicUnitTest.kt @@ -1,5 +1,6 @@ package net.pokeranalytics.android +import net.pokeranalytics.android.util.CSVNumberFormat import net.pokeranalytics.android.util.Parser import net.pokeranalytics.android.util.extensions.formatted import net.pokeranalytics.android.util.extensions.kmbFormatted @@ -56,4 +57,13 @@ class BasicUnitTest : RealmUnitTest() { } + @Test + fun testCSVFormatter() { + val str1 = CSVNumberFormat.format(1111.2567) + Assert.assertEquals("1111.2567", str1) + + val str2 = CSVNumberFormat.format(1000) + Assert.assertEquals("1000", str2) + } + } diff --git a/app/standard/release/output.json b/app/standard/release/output.json index 6b70345c..c113850d 100644 --- a/app/standard/release/output.json +++ b/app/standard/release/output.json @@ -1 +1,5 @@ -[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":79,"versionName":"3.0","enabled":true,"outputFile":"PokerAnalytics_3.0(79)_200428_1456_release.apk","fullName":"standardRelease","baseName":"standard-release","dirName":""},"path":"PokerAnalytics_3.0(79)_200428_1456_release.apk","properties":{}}] \ No newline at end of file +<<<<<<< HEAD +[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":79,"versionName":"3.0","enabled":true,"outputFile":"PokerAnalytics_3.0(79)_200428_1456_release.apk","fullName":"standardRelease","baseName":"standard-release","dirName":""},"path":"PokerAnalytics_3.0(79)_200428_1456_release.apk","properties":{}}] +======= +[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":84,"versionName":"2.4.3","enabled":true,"outputFile":"PokerAnalytics_2.4.3(84)_200526_1106_release.apk","fullName":"standardRelease","baseName":"standard-release","dirName":""},"path":"PokerAnalytics_2.4.3(84)_200526_1106_release.apk","properties":{}}] +>>>>>>> master diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bd2ca7b0..3b80afcf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Wed Mar 04 11:24:53 CET 2020 +#Wed May 13 12:21:50 CEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME