From c2f79015bca514ac69d01477251dc4d15cf1e5a3 Mon Sep 17 00:00:00 2001 From: Aurelien Hubert Date: Fri, 7 Jun 2019 15:11:02 +0200 Subject: [PATCH] Improve Player details, work in progress --- app/build.gradle | 8 +- .../android/PokerAnalyticsApplication.kt | 4 +- .../migrations/PokerAnalyticsMigration.kt | 14 + .../android/model/realm/Player.kt | 10 + .../ui/activity/EditableDataActivity.kt | 1 + .../ui/activity/components/MediaActivity.kt | 215 ++++++++++ .../ui/fragment/data/PlayerDataFragment.kt | 138 +++++++ .../android/ui/view/RowViewType.kt | 52 ++- .../ui/view/rowrepresentable/PlayerRow.kt | 62 +++ .../pokeranalytics/android/util/ImageUtils.kt | 377 ++++++++++++++++++ .../main/res/drawable/circle_stroke_kaki.xml | 12 + app/src/main/res/layout/fragment_player.xml | 38 ++ app/src/main/res/layout/row_player_image.xml | 46 +++ app/src/main/res/values/styles.xml | 11 + 14 files changed, 975 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt create mode 100644 app/src/main/java/net/pokeranalytics/android/ui/fragment/data/PlayerDataFragment.kt create mode 100644 app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/PlayerRow.kt create mode 100755 app/src/main/java/net/pokeranalytics/android/util/ImageUtils.kt create mode 100644 app/src/main/res/drawable/circle_stroke_kaki.xml create mode 100644 app/src/main/res/layout/fragment_player.xml create mode 100644 app/src/main/res/layout/row_player_image.xml diff --git a/app/build.gradle b/app/build.gradle index 4304c29c..835eb79d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,7 +48,7 @@ android { def appName = "PokerAnalytics" def buildType = variant.variantData.variantConfiguration.buildType.name def newName - if (buildType == 'debug'){ + if (buildType == 'debug') { newName = "${appName}_${defaultConfig.versionName}(${defaultConfig.versionCode})_${formattedDate}_debug.apk" } else { newName = "${appName}_${defaultConfig.versionName}(${defaultConfig.versionCode})_${formattedDate}_release.apk" @@ -71,7 +71,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) // Kotlin - implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" @@ -90,6 +90,10 @@ dependencies { implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1' implementation 'com.google.code.gson:gson:2.8.5' + // Glide + implementation 'com.github.bumptech.glide:glide:4.9.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0' + // Places implementation 'com.google.android.libraries.places:places:1.1.0' diff --git a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt index 35ea4536..ba3e26ba 100644 --- a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt +++ b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt @@ -33,7 +33,7 @@ class PokerAnalyticsApplication : Application() { Realm.init(this) val realmConfiguration = RealmConfiguration.Builder() .name(Realm.DEFAULT_REALM_NAME) - .schemaVersion(6) + .schemaVersion(7) .migration(PokerAnalyticsMigration()) .initialData(Seed(this)) .build() @@ -60,7 +60,7 @@ class PokerAnalyticsApplication : Application() { if (BuildConfig.DEBUG) { Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}") -// this.createFakeSessions() + this.createFakeSessions() } Patcher.patchAll(this.applicationContext) diff --git a/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt b/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt index 996f6da0..c58204ac 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt @@ -142,8 +142,22 @@ class PokerAnalyticsMigration : RealmMigration { schema.get("Filter")?.addField("filterableTypeUniqueIdentifier", Integer::class.java) schema.get("Filter")?.addField("useCount", Int::class.java) schema.get("Filter")?.removeField("usageCount") + currentVersion++ } + + // Migrate to version 7 + + Timber.d("currentVersion: $currentVersion") + + if (currentVersion == 6) { + schema.get("Player")?.let { + it.addField("summary", String::class.java).setRequired("summary", true) + it.addField("color", Int::class.java).setNullable("color", true) + it.addField("picture", String::class.java) + } + } + } override fun equals(other: Any?): Boolean { diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt index a75bcc9f..9298641c 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt @@ -10,6 +10,7 @@ import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor +import net.pokeranalytics.android.ui.view.rowrepresentable.PlayerRow import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow import net.pokeranalytics.android.util.NULL_TEXT import java.util.* @@ -19,7 +20,9 @@ open class Player : RealmObject(), NameManageable, Deletable, StaticRowRepresent companion object { val rowRepresentation: List by lazy { val rows = ArrayList() + rows.add(PlayerRow.PLAYER_IMAGE) rows.add(SimpleRow.NAME) + rows.add(PlayerRow.SUMMARY) rows } } @@ -31,6 +34,12 @@ open class Player : RealmObject(), NameManageable, Deletable, StaticRowRepresent override var name: String = "" + // New fields + var summary: String = "" + var color: Int? = null + var picture: String? = null + + override fun isValidForDelete(realm: Realm): Boolean { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } @@ -64,6 +73,7 @@ open class Player : RealmObject(), NameManageable, Deletable, StaticRowRepresent override fun updateValue(value: Any?, row: RowRepresentable) { when (row) { SimpleRow.NAME -> this.name = value as String? ?: "" + PlayerRow.SUMMARY -> this.summary = value as String? ?: "" } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt index dacbc126..f9d7bf2a 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt @@ -65,6 +65,7 @@ class EditableDataActivity : PokerAnalyticsActivity() { LiveData.TRANSACTION.ordinal -> TransactionDataFragment() LiveData.CUSTOM_FIELD.ordinal -> CustomFieldDataFragment() LiveData.TRANSACTION_TYPE.ordinal -> TransactionTypeDataFragment() + LiveData.PLAYER.ordinal -> PlayerDataFragment() else -> EditableDataFragment() } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt new file mode 100644 index 00000000..76745e03 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt @@ -0,0 +1,215 @@ +package net.pokeranalytics.android.ui.activity.components + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.net.Uri +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import net.pokeranalytics.android.util.ImageUtils +import timber.log.Timber +import java.io.File +import java.util.* + + +open class MediaActivity : PokerAnalyticsActivity() { + + companion object { + const val SELECTED_CHOICE_TAKE_PICTURE = 10 + const val SELECTED_CHOICE_SELECT_PICTURE = 11 + const val REQUEST_CODE_TAKE_PICTURE = 100 + const val REQUEST_CODE_SELECT_PICTURE = 101 + const val PERMISSION_REQUEST_EXTERNAL_STORAGE = 201 + const val PERMISSION_REQUEST_CAMERA = 202 + } + + + // Data + private val outputFileUri: Uri? = null + private val maxSampleWidth = 1024 + private val maxSampleHeight = 1024 + private val resizeImage = true + private var tempFile: File? = null + private var mCurrentPhotoPath: String? = null + private var selectedChoice = -1 + private var multiplePictures = false + + override fun onDestroy() { + super.onDestroy() + if (tempFile != null) { + tempFile!!.delete() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (resultCode == Activity.RESULT_OK) { + if (requestCode == REQUEST_CODE_SELECT_PICTURE || requestCode == REQUEST_CODE_TAKE_PICTURE) { + + val filesList = ArrayList() + + GlobalScope.launch { + if (data?.clipData != null) { + data?.clipData?.let { clipData -> + try { + + GlobalScope.launch(Dispatchers.Main) { + isLoadingNewPhotos() + } + + for (i in 0 until clipData.itemCount) { + val item = clipData.getItemAt(i) + val uri = item.uri + val inputStream = contentResolver.openInputStream(uri) + val photoFile = ImageUtils.createTempImageFile(this@MediaActivity) + ImageUtils.copyInputStreamToFile(inputStream!!, photoFile) + filesList.add(photoFile) + } + + GlobalScope.launch(Dispatchers.Main) { + getPhotos(filesList) + } + + } catch (e: Exception) { + e.printStackTrace() + } + } + } else if (data?.data != null) { + data?.data?.let { uri -> + try { + + GlobalScope.launch(Dispatchers.Main) { + isLoadingNewPhotos() + } + + val inputStream = contentResolver.openInputStream(uri) + val photoFile = ImageUtils.createTempImageFile(this@MediaActivity) + ImageUtils.copyInputStreamToFile(inputStream!!, photoFile) + filesList.add(photoFile) + GlobalScope.launch(Dispatchers.Main) { + getPhotos(filesList) + } + + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + } + } + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + if (grantResults.isNotEmpty()) { + for (result in grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + //Toast.makeText(this, getString(R.string.photo_library_add_usage_description), Toast.LENGTH_SHORT).show() + selectedChoice = -1 + return + } + } + + when (selectedChoice) { + SELECTED_CHOICE_SELECT_PICTURE -> { + Timber.d("openImageGalleryIntent") + openImageGalleryIntent(multiplePictures) + } + } + } + selectedChoice = -1 + } + + /** + * Open the gallery intent + */ + fun openImageGalleryIntent(multiplePictures: Boolean) { + + this.multiplePictures = multiplePictures + + // Test if we have the permission + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + selectedChoice = SELECTED_CHOICE_SELECT_PICTURE + askForStoragePermission() + return + } + + this.multiplePictures = multiplePictures + + val galleryIntent = Intent() + galleryIntent.type = "image/*" + galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiplePictures) + galleryIntent.action = Intent.ACTION_GET_CONTENT + startActivityForResult(galleryIntent, REQUEST_CODE_SELECT_PICTURE) + } + + /** + * Ask for the external storage permission + */ + private fun askForStoragePermission() { + // Here, thisActivity is the current activity + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + PERMISSION_REQUEST_EXTERNAL_STORAGE) + } + } + + /** + * Ask for the acmera permission + */ + private fun askForCameraPermission() { + // Here, thisActivity is the current activity + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), + PERMISSION_REQUEST_CAMERA) + } + } + + /** + * Ask for camera and storage permission + */ + private fun askForCameraAndStoragePermissions() { + + val permissions = ArrayList() + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + permissions.add(Manifest.permission.CAMERA) + } + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + + if (permissions.size > 0) { + ActivityCompat.requestPermissions(this, permissions.toArray(arrayOfNulls(permissions.size)), PERMISSION_REQUEST_CAMERA) + } + } + + + /** + * Called when a bitmap is return + * + * @param bitmap the bitmap returned + */ + open fun getBitmapImage(file: File?, bitmap: Bitmap?) {} + + + /** + * Called when the user is adding new photos + */ + open fun isLoadingNewPhotos() {} + + /** + * Called when the user has selected photos + */ + open fun getPhotos(files: ArrayList) {} + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/PlayerDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/PlayerDataFragment.kt new file mode 100644 index 00000000..b35d5534 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/PlayerDataFragment.kt @@ -0,0 +1,138 @@ +package net.pokeranalytics.android.ui.fragment.data + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.realm.Player +import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource +import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource +import net.pokeranalytics.android.ui.extensions.toast +import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetFragment +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor +import net.pokeranalytics.android.ui.view.rowrepresentable.PlayerRow +import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow +import net.pokeranalytics.android.util.NULL_TEXT +import java.util.* + +/** + * Player data fragment + */ +class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource { + + private val player: Player + get() { + return this.item as Player + } + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_player, container, false) + } + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUI() + } + + override fun getDataSource(): RowRepresentableDataSource { + return this + } + + override fun adapterRows(): List? { + return player.adapterRows() + } + + override fun stringForRow(row: RowRepresentable): String { + return when (row) { + PlayerRow.PLAYER_IMAGE -> { + if (player.name.isNotEmpty()) { + val playerData = player.name.split(" ") + if (playerData.size > 1) { + playerData[0].first().toString() + playerData[1].first().toString() + } else if (player.name.length > 1) { + player.name.substring(0, 2) + } else { + player.name.substring(0, player.name.length) + } + } else { + NULL_TEXT + } + } + + SimpleRow.NAME -> if (player.name.isNotEmpty()) player.name else NULL_TEXT + PlayerRow.SUMMARY -> if (player.summary.isNotEmpty()) player.summary else NULL_TEXT + else -> super.stringForRow(row) + } + } + + override fun editDescriptors(row: RowRepresentable): ArrayList? { + return when (row) { + SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.player.name)) + PlayerRow.SUMMARY -> row.editingDescriptors(mapOf("defaultValue" to this.player.summary)) + else -> null + } + } + + override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + when(row) { + PlayerRow.PLAYER_IMAGE -> { + toast("Yo.") + } + PlayerRow.SUMMARY -> { + val data = editDescriptors(row) + BottomSheetFragment.create(fragmentManager, row, this, data, isClearable = false, isDeletable = true) + } + SimpleRow.NAME -> super.onRowSelected(position, row, fromAction) + + } + + } + + override fun onRowValueChanged(value: Any?, row: RowRepresentable) { + super.onRowValueChanged(value, row) + when(row) { + SimpleRow.NAME -> rowRepresentableAdapter.refreshRow(PlayerRow.PLAYER_IMAGE) + } + } + + override fun onRowDeleted(row: RowRepresentable) { + super.onRowDeleted(row) + + } + + /** + * Init UI + */ + private fun initUI() { + /* + customField.updateRowRepresentation() + bottomBar.translationY = 72f.px + bottomBar.visibility = View.VISIBLE + */ + + /* + addItem.setOnClickListener { + val customFieldEntry = player.addEntry() + rowRepresentableAdapter.notifyDataSetChanged() + onRowSelected(-1, customFieldEntry) + } + */ + + /* + updateUI() + rowRepresentableAdapter.notifyDataSetChanged() + + if (!this.deleteButtonShouldAppear) { + rowRepresentableForPosition(0)?.let { + onRowSelected(0, it) + } + } + */ + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt index 12ccaece..32292164 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt @@ -78,6 +78,7 @@ enum class RowViewType(private var layoutRes: Int) { // Custom row ROW_SESSION(R.layout.row_feed_session), ROW_TRANSACTION(R.layout.row_transaction), + ROW_PLAYER_IMAGE(R.layout.row_player_image), ROW_BUTTON(R.layout.row_button), ROW_FOLLOW_US(R.layout.row_follow_us), STATS(R.layout.row_stats_title_value), @@ -86,6 +87,7 @@ enum class RowViewType(private var layoutRes: Int) { LEGEND_DEFAULT(R.layout.row_legend_default), LIST(R.layout.row_list), + // Separator SEPARATOR(R.layout.row_separator); @@ -110,6 +112,8 @@ enum class RowViewType(private var layoutRes: Int) { // Row Transaction ROW_TRANSACTION -> RowTransactionViewHolder(layout) + ROW_PLAYER_IMAGE -> RowPlayerImageViewHolder(layout) + // Row Button ROW_BUTTON -> RowButtonViewHolder(layout) @@ -401,17 +405,17 @@ enum class RowViewType(private var layoutRes: Int) { override fun bind(position: Int, row: RowRepresentable, adapter: RowRepresentableAdapter) { if (row is BankrollReport) { - itemView.findViewById(R.id.stat1Name)?.let { + itemView.findViewById(R.id.stat1Name)?.let { it.text = itemView.context.getString(R.string.total) } - itemView.findViewById(R.id.stat1Value)?.let { + itemView.findViewById(R.id.stat1Value)?.let { val formattedStat = ComputedStat(Stat.NET_RESULT, row.total).format() it.setTextFormat(formattedStat, itemView.context) } - itemView.findViewById(R.id.stat2Name)?.let { + itemView.findViewById(R.id.stat2Name)?.let { it.text = itemView.context.getString(R.string.risk_of_ruin) } - itemView.findViewById(R.id.stat2Value)?.let { + itemView.findViewById(R.id.stat2Value)?.let { val riskOfRuin = row.riskOfRuin ?: 0.0 val formattedStat = ComputedStat(Stat.RISK_OF_RUIN, riskOfRuin).format() it.setTextFormat(formattedStat, itemView.context) @@ -419,7 +423,7 @@ enum class RowViewType(private var layoutRes: Int) { val listener = View.OnClickListener { adapter.delegate?.onRowSelected(position, row) } - itemView.findViewById(R.id.container)?.setOnClickListener(listener) + itemView.findViewById(R.id.container)?.setOnClickListener(listener) } } @@ -434,7 +438,7 @@ enum class RowViewType(private var layoutRes: Int) { if (row is CustomFieldRow) { - itemView.findViewById(R.id.chipGroup)?.let { chipGroup -> + itemView.findViewById(R.id.chipGroup)?.let { chipGroup -> chipGroup.removeAllViews() chipGroup.setOnCheckedChangeListener(null) @@ -477,9 +481,9 @@ enum class RowViewType(private var layoutRes: Int) { */ inner class RowButtonViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { override fun bind(position: Int, row: RowRepresentable, adapter: RowRepresentableAdapter) { - itemView.findViewById(R.id.title).text = row.localizedTitle(itemView.context) - itemView.findViewById(R.id.title).isVisible = !adapter.dataSource.boolForRow(row) - itemView.findViewById(R.id.progressBar).isVisible = adapter.dataSource.boolForRow(row) + itemView.findViewById(R.id.title)?.text = row.localizedTitle(itemView.context) + itemView.findViewById(R.id.title)?.isVisible = !adapter.dataSource.boolForRow(row) + itemView.findViewById(R.id.progressBar)?.isVisible = adapter.dataSource.boolForRow(row) val listener = View.OnClickListener { adapter.delegate?.onRowSelected(position, row) } @@ -513,6 +517,36 @@ enum class RowViewType(private var layoutRes: Int) { } } + /** + * Display a transaction view + */ + inner class RowPlayerImageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { + override fun bind(position: Int, row: RowRepresentable, adapter: RowRepresentableAdapter) { + + itemView.findViewById(R.id.playerInitial)?.let { textView -> + textView.text = adapter.dataSource.stringForRow(row) + } + + itemView.findViewById(R.id.playerImage)?.let { imageView -> + } + + itemView.findViewById(R.id.playerImageSelection)?.let { imageView -> + val listener = View.OnClickListener { + adapter.delegate?.onRowSelected(position, row) + } + imageView.setOnClickListener(listener) + } + + /* + itemView.transactionRow.setData(row as Transaction) + val listener = View.OnClickListener { + adapter.delegate?.onRowSelected(position, row) + } + itemView.transactionRow.setOnClickListener(listener) + */ + } + } + /** * Display a separator */ diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/PlayerRow.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/PlayerRow.kt new file mode 100644 index 00000000..e20fd7c1 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/PlayerRow.kt @@ -0,0 +1,62 @@ +package net.pokeranalytics.android.ui.view.rowrepresentable + +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor +import net.pokeranalytics.android.ui.view.RowViewType + +/** + * An enum managing the player rows + */ +enum class PlayerRow : RowRepresentable { + PLAYER_IMAGE, + SUMMARY; + + + companion object { + /** + * Return the report rows + */ + fun getRows(): ArrayList { + val rows = ArrayList() + rows.addAll(values()) + return rows + } + } + + override val resId: Int? + get() { + return when (this) { + PLAYER_IMAGE -> R.string.app_name + SUMMARY -> R.string.summary + } + } + + override val viewType: Int + get() { + return when (this) { + PLAYER_IMAGE -> RowViewType.ROW_PLAYER_IMAGE.ordinal + SUMMARY -> RowViewType.TITLE_VALUE.ordinal + } + } + + override fun editingDescriptors(map: Map): ArrayList? { + return when (this) { + SUMMARY -> { + val defaultValue: String? by map + arrayListOf(RowRepresentableEditDescriptor(defaultValue, R.string.summary)) + } + else -> super.editingDescriptors(map) + } + } + + override val bottomSheetType: BottomSheetType + get() { + return when (this) { + SUMMARY -> BottomSheetType.EDIT_TEXT_MULTI_LINES + else -> BottomSheetType.NONE + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/ImageUtils.kt b/app/src/main/java/net/pokeranalytics/android/util/ImageUtils.kt new file mode 100755 index 00000000..f4182e33 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/ImageUtils.kt @@ -0,0 +1,377 @@ +package net.pokeranalytics.android.util + + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.graphics.* +import android.graphics.Paint.FILTER_BITMAP_FLAG +import android.media.ExifInterface +import android.net.Uri +import android.os.Environment +import androidx.core.content.ContextCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import net.pokeranalytics.android.R +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.text.SimpleDateFormat +import java.util.* + + +object ImageUtils { + + /** + * Rotate a bitmap if it's necessary (depending of the EXIF data) + * Some devices don't rotate the picture but instead add the orientation + * value in the EXIF data. + * That's why we need sometimes to rotate by ourselves the bitmap + * + * @param src The file to check (for getting the Exif data) + * @param bitmap The bitmap to modify (if necessary) + * @return The bitmap in the correct orientation + */ + fun rotateBitmap(src: String, bitmap: Bitmap, updateFile: Boolean): Bitmap { + try { + val orientation = getExifOrientation(src) + + if (orientation == ExifInterface.ORIENTATION_NORMAL) { + return bitmap + } + + val matrix = Matrix() + when (orientation) { + ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f) + ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f) + ExifInterface.ORIENTATION_FLIP_VERTICAL -> { + matrix.setRotate(180f) + matrix.postScale(-1f, 1f) + } + ExifInterface.ORIENTATION_TRANSPOSE -> { + matrix.setRotate(90f) + matrix.postScale(-1f, 1f) + } + ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f) + ExifInterface.ORIENTATION_TRANSVERSE -> { + matrix.setRotate(-90f) + matrix.postScale(-1f, 1f) + } + ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f) + else -> return bitmap + } + + try { + val oriented = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) + bitmap.recycle() + if (updateFile) { + updateFile(src, oriented) + } + return oriented + } catch (e: OutOfMemoryError) { + e.printStackTrace() + return bitmap + } + + } catch (e: IOException) { + e.printStackTrace() + } + + return bitmap + } + + /** + * Get the Exif orientation value + * + * @param filePath The path of the file + * @return the orientation value + * @throws IOException + */ + @Throws(IOException::class) + private fun getExifOrientation(filePath: String): Int { + val exifInterface = ExifInterface(filePath) + return exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + } + + /** + * Save a bitmap into a file (& apply 90% compression) + * + * @param filePath Path of the file + * @param bitmap Bitmap to save + */ + fun updateFile(filePath: String, bitmap: Bitmap) { + var out: FileOutputStream? = null + try { + out = FileOutputStream(filePath) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out) + } catch (e: Exception) { + e.printStackTrace() + } finally { + try { + out?.close() + } catch (e: IOException) { + e.printStackTrace() + } + + } + } + + /** + * Resize a file with the given maximum width or height (and keep the ratio!) + * @param filePath String: File path + * @param bitmap Bitmap: Image + * @param maxWidth int: Max width + * @param maxHeight int: Max height + */ + fun resizeFile(filePath: String, bitmap: Bitmap, maxWidth: Int, maxHeight: Int) { + var bitmap = bitmap + + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(filePath, options) + val imageWidth = options.outWidth + val imageHeight = options.outHeight + + var newWidth: Int + var newHeight: Int + + if (imageWidth > imageHeight) { + newWidth = maxWidth + newHeight = imageHeight * maxWidth / imageWidth + if (newHeight > maxHeight) { + newHeight = maxHeight + newWidth = imageWidth * maxHeight / imageHeight + } + } else { + newHeight = maxHeight + newWidth = imageWidth * maxHeight / imageHeight + if (newWidth > maxWidth) { + newWidth = maxWidth + newHeight = imageHeight * maxWidth / imageWidth + } + } + + bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) + updateFile(filePath, bitmap) + } + + /** + * Create a unique temp image file name + * + * @return + * @throws IOException + */ + @Throws(IOException::class) + fun createTempImageFile(context: Context): File { + // Create an image file name + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val imageFileName = "JPEG_" + timeStamp + "_" + val storageDir = context.cacheDir + return File.createTempFile(imageFileName, ".jpg", storageDir) + } + + /** + * Create a unique image file name + * + * @return + * @throws IOException + */ + @Throws(IOException::class) + fun createImageFile(context: Context): File { + // Create an image file name + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val imageFileName = "JPEG_" + timeStamp + "_" + + val storage = ContextCompat.getExternalFilesDirs(context, Environment.DIRECTORY_PICTURES) + val storageDir = if (storage.isNotEmpty()) storage.first() else Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + + val appStorageDir = File(storageDir.path + "/" + context.getString(R.string.app_name)) + if (!appStorageDir.exists()) { + appStorageDir.mkdirs() + } + + return File.createTempFile(imageFileName, ".jpg", appStorageDir) + } + + /** + * Decode sample Bitmap + * + * @param filePath The bitmap file path + * @param reqWidth Max width required + * @param reqHeight Max height required + * @return The sampled bitmap + */ + fun decodeSampledBitmapFromFile(filePath: String, reqWidth: Int, reqHeight: Int): Bitmap { + + // First decode with inJustDecodeBounds=true to check dimensions + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(filePath, options) + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false + return BitmapFactory.decodeFile(filePath, options) + } + + /** + * Calculate the sample size + */ + fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, + reqHeight: Int): Int { + // Raw height and width of image + val height = options.outHeight + val width = options.outWidth + var inSampleSize = 1 + + if (height > reqHeight || width > reqWidth) { + + val halfHeight = height / 2 + val halfWidth = width / 2 + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while (halfHeight / inSampleSize > reqHeight && halfWidth / inSampleSize > reqWidth) { + inSampleSize *= 2 + } + } + + return inSampleSize + } + + /** + * Copy an input stream inside a file + * + * @param in Input Stream + * @param file Destination file + */ + fun copyInputStreamToFile(inputStream: InputStream, file: File) { + try { + val out = FileOutputStream(file) + val buf = ByteArray(4096) + var len = 0 + while ({ len = inputStream.read(buf); len }() > 0) { + out.write(buf, 0, len) + } + + out.close() + inputStream.close() + } catch (e: Exception) { + e.printStackTrace() + } + + } + + /** + * Update the gallery with the current file + * + * @param context Context + * @param filePath The file to add + */ + fun updateGallery(context: Context, filePath: String) { + val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) + val f = File(filePath) + val contentUri = Uri.fromFile(f) + mediaScanIntent.data = contentUri + context.sendBroadcast(mediaScanIntent) + } + + /** + * Save the bitmap in a file + */ + fun saveBitmapInFile(context: Context, bitmap: Bitmap, filename: String, action: (filePath: String) -> Unit) { + + GlobalScope.launch { + + val outputFile = File(context.filesDir, filename) + + var out: FileOutputStream? = null + try { + out = FileOutputStream(outputFile.absolutePath) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out) // bmp is your Bitmap instance + } catch (e: Exception) { + e.printStackTrace() + } finally { + try { + if (out != null) { + out.close() + } + } catch (e: IOException) { + e.printStackTrace() + } + } + + GlobalScope.launch(Dispatchers.Main) { + Timber.d("Save file here: ${outputFile.absolutePath}") + action(outputFile.absolutePath) + } + } + + } + + /** + * Bitmap resizer + */ + fun bitmapResizer(bitmap: Bitmap, newWidth: Int, newHeight: Int): Bitmap { + val scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888) + + val ratioX = newWidth / bitmap.width.toFloat() + val ratioY = newHeight / bitmap.height.toFloat() + val middleX = newWidth / 2.0f + val middleY = newHeight / 2.0f + + val scaleMatrix = Matrix() + scaleMatrix.setScale(ratioX, ratioY, middleX, middleY) + + val canvas = Canvas(scaledBitmap) + canvas.matrix = scaleMatrix + canvas.drawBitmap(bitmap, middleX - bitmap.width / 2, middleY - bitmap.height / 2, Paint(FILTER_BITMAP_FLAG)) + + return scaledBitmap + + } + + /** + * Export a bitmap + */ + private fun exportFile(context: Activity, bitmap: Bitmap) { + /* + val outputFile = File.createTempFile("test_export", ".jpg", context.cacheDir) + + var out: FileOutputStream? = null + try { + out = FileOutputStream(outputFile.absolutePath) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out) + } catch (e: Exception) { + e.printStackTrace() + } finally { + try { + if (out != null) { + out.close() + } + } catch (e: IOException) { + e.printStackTrace() + } + } + + val uri = FileProvider.getUriForFile(context, + context.packageName + ".provider", outputFile) + + val shareIntent = ShareCompat.IntentBuilder.from(context) + .setType("image/jpg") + .setSubject(context.getString(R.string.share_file_name)) + .setStream(uri) + .setChooserTitle(context.getString(R.string.share_title)) + .createChooserIntent() + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + + context.startActivity(shareIntent) + */ + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_stroke_kaki.xml b/app/src/main/res/drawable/circle_stroke_kaki.xml new file mode 100644 index 00000000..547635d3 --- /dev/null +++ b/app/src/main/res/drawable/circle_stroke_kaki.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_player.xml b/app/src/main/res/layout/fragment_player.xml new file mode 100644 index 00000000..a1ef7ddb --- /dev/null +++ b/app/src/main/res/layout/fragment_player.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/row_player_image.xml b/app/src/main/res/layout/row_player_image.xml new file mode 100644 index 00000000..ad2c0a78 --- /dev/null +++ b/app/src/main/res/layout/row_player_image.xml @@ -0,0 +1,46 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index eecdebee..3669bc0f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -240,6 +240,17 @@ 16sp + + +