From 6f539a277f439c9f312973d620fa49c5773df360 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 16 Oct 2019 12:22:31 +0200 Subject: [PATCH] Adds Player feature --- app/src/main/AndroidManifest.xml | 356 +++++++++-------- .../android/PokerAnalyticsApplication.kt | 2 +- .../android/exceptions/Exceptions.kt | 1 + .../pokeranalytics/android/model/LiveData.kt | 7 +- .../migrations/PokerAnalyticsMigration.kt | 19 + .../android/model/realm/Comment.kt | 78 ++++ .../android/model/realm/Player.kt | 184 ++++++++- .../ui/activity/ColorPickerActivity.kt | 77 ++++ .../ui/activity/EditableDataActivity.kt | 38 +- .../ui/activity/components/MediaActivity.kt | 268 +++++++++++++ .../android/ui/extensions/UIExtensions.kt | 10 + .../android/ui/fragment/DataListFragment.kt | 54 ++- .../android/ui/fragment/SettingsFragment.kt | 2 + .../components/PokerAnalyticsFragment.kt | 11 + .../ui/fragment/data/PlayerDataFragment.kt | 218 ++++++++++ .../android/ui/view/PlayerImageView.kt | 144 +++++++ .../android/ui/view/RowRepresentable.kt | 3 + .../android/ui/view/RowViewType.kt | 54 ++- .../ui/view/rowrepresentable/PlayerRow.kt | 69 ++++ .../ui/view/rowrepresentable/SettingRow.kt | 7 +- .../pokeranalytics/android/util/ImageUtils.kt | 377 ++++++++++++++++++ .../util/extensions/RealmExtensions.kt | 28 +- .../drawable-xxhdpi/circle_player_color_1.xml | 15 + .../drawable-xxhdpi/circle_player_color_2.xml | 11 + .../drawable-xxhdpi/circle_player_color_3.xml | 11 + .../drawable-xxhdpi/circle_player_color_4.xml | 11 + .../drawable-xxhdpi/circle_player_color_5.xml | 11 + .../drawable-xxhdpi/circle_player_color_6.xml | 11 + .../drawable-xxhdpi/circle_player_color_7.xml | 11 + .../drawable-xxhdpi/circle_player_color_8.xml | 11 + .../drawable-xxhdpi/circle_player_color_9.xml | 11 + .../drawable-xxhdpi/circle_stroke_kaki.xml | 12 + .../res/drawable-xxhdpi/ic_outline_people.xml | 9 + .../res/drawable-xxhdpi/ic_outline_search.xml | 9 + .../main/res/layout/activity_color_picker.xml | 136 +++++++ app/src/main/res/layout/fragment_player.xml | 80 ++++ app/src/main/res/layout/row_content.xml | 47 +++ .../main/res/layout/row_header_subtitle.xml | 18 + app/src/main/res/layout/row_player.xml | 91 +++++ app/src/main/res/layout/row_player_image.xml | 20 + .../main/res/layout/row_title_subtitle.xml | 47 +++ app/src/main/res/layout/view_player_image.xml | 55 +++ app/src/main/res/menu/toolbar_data_list.xml | 13 + app/src/main/res/values/colors.xml | 11 + app/src/main/res/values/dimens.xml | 4 + app/src/main/res/values/styles.xml | 29 ++ app/src/main/res/xml/provider_paths.xml | 11 +- 47 files changed, 2494 insertions(+), 208 deletions(-) create mode 100644 app/src/main/java/net/pokeranalytics/android/model/realm/Comment.kt create mode 100644 app/src/main/java/net/pokeranalytics/android/ui/activity/ColorPickerActivity.kt 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/PlayerImageView.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-xxhdpi/circle_player_color_1.xml create mode 100644 app/src/main/res/drawable-xxhdpi/circle_player_color_2.xml create mode 100644 app/src/main/res/drawable-xxhdpi/circle_player_color_3.xml create mode 100644 app/src/main/res/drawable-xxhdpi/circle_player_color_4.xml create mode 100644 app/src/main/res/drawable-xxhdpi/circle_player_color_5.xml create mode 100644 app/src/main/res/drawable-xxhdpi/circle_player_color_6.xml create mode 100644 app/src/main/res/drawable-xxhdpi/circle_player_color_7.xml create mode 100644 app/src/main/res/drawable-xxhdpi/circle_player_color_8.xml create mode 100644 app/src/main/res/drawable-xxhdpi/circle_player_color_9.xml create mode 100644 app/src/main/res/drawable-xxhdpi/circle_stroke_kaki.xml create mode 100644 app/src/main/res/drawable-xxhdpi/ic_outline_people.xml create mode 100644 app/src/main/res/drawable-xxhdpi/ic_outline_search.xml create mode 100644 app/src/main/res/layout/activity_color_picker.xml create mode 100644 app/src/main/res/layout/fragment_player.xml create mode 100644 app/src/main/res/layout/row_content.xml create mode 100644 app/src/main/res/layout/row_header_subtitle.xml create mode 100644 app/src/main/res/layout/row_player.xml create mode 100644 app/src/main/res/layout/row_player_image.xml create mode 100644 app/src/main/res/layout/row_title_subtitle.xml create mode 100644 app/src/main/res/layout/view_player_image.xml create mode 100644 app/src/main/res/menu/toolbar_data_list.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7efe637b..11763e49 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,179 +1,185 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:tools="http://schemas.android.com/tools" + package="net.pokeranalytics.android"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt index e5f41fd5..f3601912 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(7) + .schemaVersion(8) .migration(PokerAnalyticsMigration()) .initialData(Seed(this)) .build() diff --git a/app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt b/app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt index b8358605..b12a9342 100644 --- a/app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt +++ b/app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt @@ -25,4 +25,5 @@ sealed class PokerAnalyticsException(message: String) : Exception(message) { data class QueryValueMapMissingKeys(val missingKeys: List) : PokerAnalyticsException(message = "valueMap does not contain $missingKeys") data class UnknownQueryTypeForRow(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "no queryWith type for $filterElementRow") data class MissingFieldNameForQueryCondition(val name: String) : PokerAnalyticsException(message = "Missing fieldname for QueryCondition ${name}") + object InputFragmentException : PokerAnalyticsException(message = "RowEditableDelegate must be a Fragment") } diff --git a/app/src/main/java/net/pokeranalytics/android/model/LiveData.kt b/app/src/main/java/net/pokeranalytics/android/model/LiveData.kt index ba7658ff..322ea5c5 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/LiveData.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/LiveData.kt @@ -21,7 +21,8 @@ enum class LiveData : Localizable { TRANSACTION_TYPE, FILTER, CUSTOM_FIELD, - REPORT_SETUP; + REPORT_SETUP, + PLAYER; var subType:Int? = null @@ -38,6 +39,7 @@ enum class LiveData : Localizable { FILTER -> Filter::class.java CUSTOM_FIELD -> CustomField::class.java REPORT_SETUP -> ReportSetup::class.java + PLAYER -> Player::class.java } } @@ -78,6 +80,7 @@ enum class LiveData : Localizable { FILTER -> R.string.filter CUSTOM_FIELD -> R.string.custom_field REPORT_SETUP -> R.string.custom + PLAYER -> R.string.player } } @@ -94,6 +97,7 @@ enum class LiveData : Localizable { FILTER -> R.string.filters CUSTOM_FIELD -> R.string.custom_fields REPORT_SETUP -> R.string.custom + PLAYER -> R.string.players } } @@ -110,6 +114,7 @@ enum class LiveData : Localizable { FILTER -> R.string.new_filter CUSTOM_FIELD -> R.string.new_custom_field REPORT_SETUP -> R.string.new_report + PLAYER -> R.string.new_friend } } 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 687fff28..3ca6d48b 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt @@ -2,7 +2,9 @@ package net.pokeranalytics.android.model.migrations import io.realm.DynamicRealm import io.realm.RealmMigration +import net.pokeranalytics.android.model.realm.Comment import timber.log.Timber +import java.util.* class PokerAnalyticsMigration : RealmMigration { @@ -151,6 +153,23 @@ class PokerAnalyticsMigration : RealmMigration { schema.get("TransactionType")?.addField("useCount", Int::class.java) currentVersion++ } + + // Migrate to version 8 + if (currentVersion == 7) { + schema.create("Comment")?.let { + it.addField("id", String::class.java).setRequired("id", true) + it.addField("content", String::class.java) + it.addField("date", Date::class.java) + } + + 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) + it.addRealmListField("comments", Comment::class.java) + } + currentVersion++ + } } override fun equals(other: Any?): Boolean { diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Comment.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Comment.kt new file mode 100644 index 00000000..0e94c26e --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Comment.kt @@ -0,0 +1,78 @@ +package net.pokeranalytics.android.model.realm + +import android.content.Context +import io.realm.Realm +import io.realm.RealmObject +import io.realm.annotations.Ignore +import io.realm.annotations.PrimaryKey +import net.pokeranalytics.android.R +import net.pokeranalytics.android.exceptions.ModelException +import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus +import net.pokeranalytics.android.model.interfaces.Identifiable +import net.pokeranalytics.android.model.interfaces.Manageable +import net.pokeranalytics.android.model.interfaces.SaveValidityStatus +import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowViewType +import net.pokeranalytics.android.util.NULL_TEXT +import java.util.* + +open class Comment : RealmObject(), Manageable, RowRepresentable { + + @PrimaryKey + override var id = UUID.randomUUID().toString() + var content: String = "" + var date: Date = Date() + + @Ignore + override val realmObjectClass: Class = Comment::class.java + + @Ignore + override val viewType: Int = RowViewType.CONTENT.ordinal + + @Ignore + override val bottomSheetType: BottomSheetType = BottomSheetType.EDIT_TEXT_MULTI_LINES + +// @Ignore +// override val valueCanBeClearedWhenEditing: Boolean = false + + override fun localizedTitle(context: Context): String { + return context.getString(R.string.comment) + } + + override fun getDisplayName(context: Context): String { + return if (content.isNotEmpty()) content else NULL_TEXT + } + +// override fun startEditing(dataSource: Any?, parent: Fragment?) { +// if (parent == null) return +// if (parent !is RowRepresentableDelegate) return +// val data = RowEditableDataSource() +// data.append(this.content, R.string.value) +// InputFragment.buildAndShow(this, parent, data, isDeletable = true) +// } + + override fun updateValue(value: Any?, row: RowRepresentable) { + this.content = value as String? ?: "" + } + + override fun isValidForSave(): Boolean { + return true + } + + override fun alreadyExists(realm: Realm): Boolean { + return realm.where(this::class.java).notEqualTo("id", this.id).findAll().isNotEmpty() + } + + override fun getFailedSaveMessage(status: SaveValidityStatus): Int { + throw ModelException("${this::class.java} getFailedSaveMessage for $status not handled") + } + + override fun isValidForDelete(realm: Realm): Boolean { + return true + } + + override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int { + return R.string.cf_entry_delete_popup_message + } +} 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 7323e151..267e2cb0 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 @@ -1,15 +1,191 @@ package net.pokeranalytics.android.model.realm +import android.content.Context +import io.realm.Realm +import io.realm.RealmList import io.realm.RealmObject +import io.realm.annotations.Ignore import io.realm.annotations.PrimaryKey +import io.realm.kotlin.where +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.interfaces.* +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.RowViewType +import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable +import net.pokeranalytics.android.ui.view.rowrepresentable.PlayerRow +import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRow +import net.pokeranalytics.android.util.NULL_TEXT +import net.pokeranalytics.android.util.extensions.isSameDay +import net.pokeranalytics.android.util.extensions.mediumDate import java.util.* +import kotlin.collections.ArrayList -open class Player : RealmObject() { +open class Player : RealmObject(), NameManageable, Deletable, StaticRowRepresentableDataSource, RowRepresentable { @PrimaryKey - var id = UUID.randomUUID().toString() + override var id = UUID.randomUUID().toString() // The name of the player - var name: String = "" + override var name: String = "" - } \ No newline at end of file + // New fields + var summary: String = "" + var color: Int? = null + var picture: String? = null + var comments: RealmList = RealmList() + + @Ignore + override val realmObjectClass: Class = Player::class.java + + @Ignore + override val viewType: Int = RowViewType.ROW_PLAYER.ordinal + + @Ignore + private var rowRepresentation: List = mutableListOf() + + @Ignore + private var commentsToDelete: ArrayList = ArrayList() + + + override fun isValidForDelete(realm: Realm): Boolean { + //TODO + return true + } + + override fun getFailedSaveMessage(status: SaveValidityStatus): Int { + return when(status) { + SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_user_error + else -> super.getFailedSaveMessage(status) + } + } + + override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int { + //TODO + return R.string.relationship_error + } + + override fun adapterRows(): List? { + return rowRepresentation + } + + override fun getDisplayName(context: Context): String { + return this.name + } + + override fun stringForRow(row: RowRepresentable): String { + return when (row) { + PlayerRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT + else -> return super.stringForRow(row) + } + } + + override fun updateValue(value: Any?, row: RowRepresentable) { + when (row) { + PlayerRow.NAME -> this.name = value as String? ?: "" + PlayerRow.SUMMARY -> this.summary = value as String? ?: "" + PlayerRow.IMAGE -> this.picture = value as String? ?: "" + } + } + + + /** + * Update the row representation + */ + private fun updatedRowRepresentationForCurrentState(): List { + val rows = ArrayList() + rows.add(PlayerRow.IMAGE) + rows.add(PlayerRow.NAME) + rows.add(PlayerRow.SUMMARY) + + if (comments.size > 0) { + // Adds Comments section + rows.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, R.string.comments)) + + val currentCommentCalendar = Calendar.getInstance() + val currentDateCalendar = Calendar.getInstance() + + val commentsToDisplay = ArrayList() + commentsToDisplay.addAll(comments) + commentsToDisplay.sortByDescending { it.date } + + commentsToDisplay.forEachIndexed { index, comment -> + currentCommentCalendar.time = comment.date + + if (!currentCommentCalendar.isSameDay(currentDateCalendar) || index == 0) { + currentDateCalendar.time = currentCommentCalendar.time + // Adds day sub section + rows.add(CustomizableRowRepresentable(RowViewType.HEADER_SUBTITLE, title = currentDateCalendar.time.mediumDate())) + } + + // Adds comment + rows.add(comment) + } + + rows.add(SeparatorRow()) + } + + return rows + } + + /** + * Return if the player has a picture + */ + fun hasPicture(): Boolean { + return picture != null && picture?.isNotEmpty() == true + } + + /** + * Update row representation + */ + fun updateRowRepresentation() { + this.rowRepresentation = this.updatedRowRepresentationForCurrentState() + } + + + /** + * Add an entry + */ + fun addComment(): Comment { + val entry = Comment() + this.comments.add(entry) + updateRowRepresentation() + return entry + } + + /** + * Delete an entry + */ + fun deleteComment(comment: Comment) { + commentsToDelete.add(comment) + this.comments.remove(comment) + updateRowRepresentation() + } + + /** + * Clean up deleted entries + */ + fun cleanupComments() { // called when saving the custom field + val realm = Realm.getDefaultInstance() + realm.executeTransaction { + this.commentsToDelete.forEach { // entries are out of realm + realm.where().equalTo("id", it.id).findFirst()?.deleteFromRealm() + } + } + realm.close() + this.commentsToDelete.clear() + } + + override fun editDescriptors(row: RowRepresentable): ArrayList? { + + when (row) { + PlayerRow.NAME -> return row.editingDescriptors(mapOf("defaultValue" to this.name)) + PlayerRow.SUMMARY -> return row.editingDescriptors(mapOf("defaultValue" to this.summary)) + } + + return null + } + + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/ColorPickerActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/ColorPickerActivity.kt new file mode 100644 index 00000000..ce8faf29 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/ColorPickerActivity.kt @@ -0,0 +1,77 @@ +package net.pokeranalytics.android.ui.activity + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import kotlinx.android.synthetic.main.activity_color_picker.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity + +class ColorPickerActivity : PokerAnalyticsActivity() { + + companion object { + + const val INTENT_COLOR = "INTENT_COLOR" + + fun newInstance(context: Context) { + val intent = Intent(context, ColorPickerActivity::class.java) + context.startActivity(intent) + } + + /** + * Create a new instance for result + */ + fun newInstanceForResult(fragment: Fragment, requestCode: Int) { + val intent = Intent(fragment.requireContext(), ColorPickerActivity::class.java) + fragment.startActivityForResult(intent, requestCode) + } + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_color_picker) + + initUI() + } + + /** + * Init UI + */ + private fun initUI() { + color1.setOnClickListener { manageSelectedColor(it) } + color2.setOnClickListener { manageSelectedColor(it) } + color3.setOnClickListener { manageSelectedColor(it) } + color4.setOnClickListener { manageSelectedColor(it) } + color5.setOnClickListener { manageSelectedColor(it) } + color6.setOnClickListener { manageSelectedColor(it) } + color7.setOnClickListener { manageSelectedColor(it) } + color8.setOnClickListener { manageSelectedColor(it) } + color9.setOnClickListener { manageSelectedColor(it) } + } + + private fun manageSelectedColor(view: View) { + + val color = when(view) { + color1 -> getColor(R.color.player_color_1) + color2 -> getColor(R.color.player_color_2) + color3 -> getColor(R.color.player_color_3) + color4 -> getColor(R.color.player_color_4) + color5 -> getColor(R.color.player_color_5) + color6 -> getColor(R.color.player_color_6) + color7 -> getColor(R.color.player_color_7) + color8 -> getColor(R.color.player_color_8) + color9 -> getColor(R.color.player_color_9) + else -> getColor(R.color.player_color_1) + } + + val intent = Intent() + intent.putExtra(INTENT_COLOR, color) + setResult(Activity.RESULT_OK, intent) + finish() + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt index 1ea76543..c540be6b 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 @@ -4,13 +4,14 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.fragment.app.Fragment -import com.crashlytics.android.Crashlytics import net.pokeranalytics.android.R import net.pokeranalytics.android.model.LiveData -import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +import net.pokeranalytics.android.ui.activity.components.MediaActivity import net.pokeranalytics.android.ui.fragment.data.* +import java.io.File +import java.util.* -class EditableDataActivity : PokerAnalyticsActivity() { +class EditableDataActivity : MediaActivity() { enum class IntentKey(val keyName: String) { DATA_TYPE("DATA_TYPE"), @@ -44,6 +45,8 @@ class EditableDataActivity : PokerAnalyticsActivity() { } + private var currentFragment: EditableDataFragment? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_editable_data) @@ -58,20 +61,35 @@ class EditableDataActivity : PokerAnalyticsActivity() { val dataType = intent.getIntExtra(IntentKey.DATA_TYPE.keyName, 0) val primaryKey = intent.getStringExtra(IntentKey.PRIMARY_KEY.keyName) - val fragmentManager = supportFragmentManager - val fragmentTransaction = fragmentManager.beginTransaction() - val fragment: EditableDataFragment = when (dataType) { + currentFragment = when (dataType) { LiveData.BANKROLL.ordinal -> BankrollDataFragment() LiveData.LOCATION.ordinal -> LocationDataFragment() LiveData.TRANSACTION.ordinal -> TransactionDataFragment() LiveData.CUSTOM_FIELD.ordinal -> CustomFieldDataFragment() LiveData.TRANSACTION_TYPE.ordinal -> TransactionTypeDataFragment() + LiveData.PLAYER.ordinal -> PlayerDataFragment() else -> EditableDataFragment() } - Crashlytics.log("creating EditableDataFragment with dataType = $dataType") - fragment.setData(dataType, primaryKey) - fragmentTransaction.add(R.id.container, fragment) - fragmentTransaction.commit() + + currentFragment?.let { fragment -> + val fragmentManager = supportFragmentManager + val fragmentTransaction = fragmentManager.beginTransaction() + fragmentTransaction.add(R.id.container, fragment) + fragmentTransaction.commit() + + fragment.setData(dataType, primaryKey) + } + + } + + override fun isLoadingNewPictures() { + super.isLoadingNewPictures() + currentFragment?.isLoadingNewPhotos() + } + + override fun getPictures(files: ArrayList) { + super.getPictures(files) + currentFragment?.getPhotos(files) } } \ No newline at end of file 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..3a22539d --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt @@ -0,0 +1,268 @@ +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.provider.MediaStore +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +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.io.IOException +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 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 (tempFile != null) { + tempFile?.let { + GlobalScope.launch(Dispatchers.Main) { + filesList.add(it) + getPictures(filesList) + } + } + } else if (data?.clipData != null) { + data?.clipData?.let { clipData -> + try { + + GlobalScope.launch(Dispatchers.Main) { + isLoadingNewPictures() + } + + 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) { + getPictures(filesList) + } + + } catch (e: Exception) { + e.printStackTrace() + } + } + } else if (data?.data != null) { + data?.data?.let { uri -> + try { + + GlobalScope.launch(Dispatchers.Main) { + isLoadingNewPictures() + } + + val inputStream = contentResolver.openInputStream(uri) + val photoFile = ImageUtils.createTempImageFile(this@MediaActivity) + ImageUtils.copyInputStreamToFile(inputStream!!, photoFile) + filesList.add(photoFile) + GlobalScope.launch(Dispatchers.Main) { + getPictures(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_TAKE_PICTURE -> { + openImageCaptureIntent(multiplePictures) + } + SELECTED_CHOICE_SELECT_PICTURE -> { + openImageGalleryIntent(multiplePictures) + } + } + } + selectedChoice = -1 + } + + + /** + * Open the Camera Intent + */ + fun openImageCaptureIntent(multiplePictures: Boolean) { + + tempFile = null + + this.mCurrentPhotoPath = null + this.multiplePictures = multiplePictures + + // Test if we have the permission + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + selectedChoice = SELECTED_CHOICE_TAKE_PICTURE + askForStoragePermission() + return + } + + val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + // Ensure that there's a camera activity to handle the intent + if (takePictureIntent.resolveActivity(packageManager) != null) { + // Create the File where the photo should go + try { + tempFile = ImageUtils.createImageFile(this) + mCurrentPhotoPath = "file:" + tempFile?.absolutePath + } catch (ex: IOException) { + // Error occurred while creating the File + } + + // Continue only if the File was successfully created + if (tempFile != null) { + Timber.d("tempFile: ${tempFile?.absolutePath}") + val photoURI = FileProvider.getUriForFile( + this, + applicationContext.packageName + ".fileprovider", tempFile!! + ) + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI) + startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PICTURE) + } + } + } + + + /** + * Open the gallery intent + */ + fun openImageGalleryIntent(multiplePictures: Boolean) { + + tempFile = null + + 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 isLoadingNewPictures() {} + + /** + * Called when the user has selected photos + */ + open fun getPictures(files: ArrayList) {} + + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt b/app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt index 525741b6..33af950f 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt @@ -7,9 +7,11 @@ import android.content.res.Resources import android.net.Uri import android.util.TypedValue import android.view.View +import android.widget.LinearLayout import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.AppCompatTextView +import androidx.appcompat.widget.SearchView import androidx.browser.customtabs.CustomTabsIntent import androidx.core.content.ContextCompat import androidx.core.content.FileProvider @@ -160,4 +162,12 @@ fun View.showWithAnimation() { fun View.addCircleRipple() = with(TypedValue()) { context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, this, true) setBackgroundResource(resourceId) +} + +fun SearchView.removeMargins() { + val searchEditFrame = findViewById(R.id.search_edit_frame) + val layoutParams = searchEditFrame?.layoutParams as LinearLayout.LayoutParams? + layoutParams?.leftMargin = 0 + layoutParams?.rightMargin = 0 + searchEditFrame?.layoutParams = layoutParams } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/DataListFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/DataListFragment.kt index 13c4a9fc..722b9dac 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/DataListFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/DataListFragment.kt @@ -3,9 +3,8 @@ package net.pokeranalytics.android.ui.fragment import android.app.Activity import android.content.Intent import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.view.* +import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -23,10 +22,12 @@ import net.pokeranalytics.android.ui.activity.FiltersActivity import net.pokeranalytics.android.ui.adapter.LiveRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate +import net.pokeranalytics.android.ui.extensions.removeMargins import net.pokeranalytics.android.ui.fragment.components.DeletableItemFragment import net.pokeranalytics.android.ui.helpers.SwipeToDeleteCallback import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType +import net.pokeranalytics.android.util.extensions.find import net.pokeranalytics.android.util.extensions.sorted @@ -40,6 +41,15 @@ open class DataListFragment : DeletableItemFragment(), LiveRowRepresentableDataS private lateinit var dataType: LiveData private lateinit var items: RealmResults + private var dataListMenu: Menu? = null + private var searchView: SearchView? = null + + var isSearchable: Boolean = false + set(value) { + field = value + val searchMenuItem = dataListMenu?.findItem(R.id.action_search) + searchMenuItem?.isVisible = value + } /** * Set fragment data @@ -51,6 +61,11 @@ open class DataListFragment : DeletableItemFragment(), LiveRowRepresentableDataS setToolbarTitle(this.dataType.pluralLocalizedTitle(requireContext())) this.items = this.retrieveItems(getRealm()) + + this.isSearchable = when (this.dataType) { + LiveData.PLAYER, LiveData.LOCATION -> true + else -> false + } } open fun retrieveItems(realm: Realm): RealmResults { @@ -114,6 +129,31 @@ open class DataListFragment : DeletableItemFragment(), LiveRowRepresentableDataS this.recyclerView?.adapter?.notifyDataSetChanged() } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + + menu.clear() + inflater.inflate(R.menu.toolbar_data_list, menu) + this.dataListMenu = menu + + val searchMenuItem = menu.findItem(R.id.action_search) + searchMenuItem.isVisible = isSearchable + + searchView = searchMenuItem.actionView as SearchView? + searchView?.removeMargins() + searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + filterItemsWithSearch(newText) + return false + } + }) + + super.onCreateOptionsMenu(menu, inflater) + } + override fun rowRepresentableForPosition(position: Int): RowRepresentable? { return this.items[position] as RowRepresentable } @@ -150,4 +190,12 @@ open class DataListFragment : DeletableItemFragment(), LiveRowRepresentableDataS this.addButton.isVisible = showAddButton } + /** + * Filter the items list with the given search content + */ + private fun filterItemsWithSearch(searchContent: String?) { + this.items = getRealm().find(this.identifiableClass, searchContent) + dataListAdapter.notifyDataSetChanged() + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt index 1f679742..7f5d3c08 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 @@ -13,6 +13,7 @@ import io.realm.Realm import kotlinx.android.synthetic.main.fragment_settings.* import net.pokeranalytics.android.BuildConfig 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.ui.activity.* @@ -115,6 +116,7 @@ class SettingsFragment : PokerAnalyticsFragment(), RowRepresentableDelegate, Sta when (row) { SettingRow.BANKROLL_REPORT -> BankrollActivity.newInstance(requireContext()) SettingRow.TOP_10 -> Top10Activity.newInstance(requireContext()) + SettingRow.PLAYERS -> DataListActivity.newInstance(requireContext(), LiveData.PLAYER.ordinal) SettingRow.SUBSCRIPTION -> { if (!AppGuard.isProUser) { BillingActivity.newInstanceForResult(this, false) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/PokerAnalyticsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/PokerAnalyticsFragment.kt index 936cb56f..fa1a95cc 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/PokerAnalyticsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/PokerAnalyticsFragment.kt @@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment import com.crashlytics.android.Crashlytics import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +import java.io.File open class PokerAnalyticsFragment : Fragment() { @@ -99,4 +100,14 @@ open class PokerAnalyticsFragment : Fragment() { parentActivity?.supportActionBar?.setDisplayHomeAsUpEnabled(enabled) } + /** + * 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..fece704e --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/PlayerDataFragment.kt @@ -0,0 +1,218 @@ +package net.pokeranalytics.android.ui.fragment.data + +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import kotlinx.android.synthetic.main.fragment_player.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.realm.Comment +import net.pokeranalytics.android.model.realm.Player +import net.pokeranalytics.android.ui.activity.ColorPickerActivity +import net.pokeranalytics.android.ui.activity.components.MediaActivity +import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource +import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource +import net.pokeranalytics.android.ui.extensions.showAlertDialog +import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetFragment +import net.pokeranalytics.android.ui.view.RowRepresentable +import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor +import net.pokeranalytics.android.ui.view.RowViewType +import net.pokeranalytics.android.ui.view.rowrepresentable.PlayerRow +import net.pokeranalytics.android.util.NULL_TEXT +import java.io.File + +/** + * Player data fragment + */ +class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource { + + companion object { + const val REQUEST_CODE_PICK_COLOR = 1000 + } + + private val player: Player + get() { + return this.item as Player + } + + private var mediaActivity: MediaActivity? = null + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + shouldOpenKeyboard = false + return inflater.inflate(R.layout.fragment_player, container, false) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == REQUEST_CODE_PICK_COLOR && resultCode == RESULT_OK && data?.hasExtra(ColorPickerActivity.INTENT_COLOR) == true) { + val color = data.getIntExtra(ColorPickerActivity.INTENT_COLOR, Color.TRANSPARENT) + player.color = if (color != Color.TRANSPARENT) color else null + rowRepresentableAdapter.refreshRow(PlayerRow.IMAGE) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUI() + } + + /** + * Init UI + */ + private fun initUI() { + mediaActivity = parentActivity as MediaActivity? + + player.updateRowRepresentation() + + if (!deleteButtonShouldAppear) { + onRowSelected(0, PlayerRow.NAME) + } + + addComment.setOnClickListener { + val comment = player.addComment() + rowRepresentableAdapter.notifyDataSetChanged() + onRowSelected(-1, comment) + } + } + + override fun getPhotos(files: ArrayList) { + super.getPhotos(files) + files.firstOrNull()?.let { picture -> + player.updateValue(picture.absolutePath, PlayerRow.IMAGE) + rowRepresentableAdapter.refreshRow(PlayerRow.IMAGE) + } + } + + override fun getDataSource(): RowRepresentableDataSource { + return this + } + + override fun adapterRows(): List? { + return player.adapterRows() + } + + override fun viewTypeForPosition(position: Int): Int { + return when (position) { + 0 -> RowViewType.ROW_PLAYER_IMAGE.ordinal + else -> super.viewTypeForPosition(position) + } + } + + override fun rowRepresentableForPosition(position: Int): RowRepresentable? { + return when (position) { + 0 -> player + else -> super.rowRepresentableForPosition(position) + } + } + + override fun stringForRow(row: RowRepresentable): String { + return when (row) { + PlayerRow.NAME -> if (player.name.isNotEmpty()) player.name else NULL_TEXT + PlayerRow.SUMMARY -> if (player.summary.isNotEmpty()) player.summary else NULL_TEXT + else -> super.stringForRow(row) + } + } + + override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { + when (row) { + PlayerRow.IMAGE -> openPictureDialog() + is Comment -> { + val data = arrayListOf(RowRepresentableEditDescriptor(row.content)) + BottomSheetFragment.create(fragmentManager, row, this, data, isClearable = false, isDeletable = true) + } + else -> super.onRowSelected(position, row, fromAction) + } + } + + override fun onRowValueChanged(value: Any?, row: RowRepresentable) { + when (row) { + is Comment -> { + row.updateValue(value, row) + player.updateRowRepresentation() + rowRepresentableAdapter.notifyDataSetChanged() + } + else -> { + super.onRowValueChanged(value, row) + if (row == PlayerRow.NAME) { + rowRepresentableAdapter.refreshRow(PlayerRow.IMAGE) + + } + } + } + } + + override fun onRowDeleted(row: RowRepresentable) { + super.onRowDeleted(row) + when (row) { + is Comment -> { + if (row.isValidForDelete(getRealm())) { + GlobalScope.launch(Dispatchers.Main) { + delay(300) + showAlertDialog(requireContext(), message = R.string.are_you_sure_you_want_to_delete, showCancelButton = true, positiveAction = { + player.deleteComment(row) + rowRepresentableAdapter.notifyDataSetChanged() + }) + } + } + } + } + } + + /** + * Open picture dialog + */ + private fun openPictureDialog() { + + val builder = AlertDialog.Builder(requireContext()) + + val placesArray = ArrayList() + placesArray.add(getString(R.string.take_a_picture)) + placesArray.add(getString(R.string.library)) + placesArray.add(getString(R.string.select_a_color)) + + if (player.hasPicture()) { + placesArray.add(getString(R.string.remove_picture)) + } + + builder.setItems(placesArray.toTypedArray()) { _, which -> + when (placesArray[which]) { + getString(R.string.take_a_picture) -> mediaActivity?.openImageCaptureIntent(false) + getString(R.string.library) -> mediaActivity?.openImageGalleryIntent(false) + getString(R.string.select_a_color) -> { + ColorPickerActivity.newInstanceForResult(this, REQUEST_CODE_PICK_COLOR) + } + getString(R.string.remove_picture) -> { + player.updateValue(null, PlayerRow.IMAGE) + rowRepresentableAdapter.refreshRow(PlayerRow.IMAGE) + } + } + } + builder.show() + } + + + override fun onDataSaved() { + super.onDataSaved() + player.cleanupComments() + } + + override fun editDescriptors(row: RowRepresentable): ArrayList? { + when (row) { + PlayerRow.NAME -> return arrayListOf(RowRepresentableEditDescriptor(this.player.name, R.string.name)) + PlayerRow.SUMMARY -> return arrayListOf(RowRepresentableEditDescriptor(this.player.summary, R.string.summary)) + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/PlayerImageView.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/PlayerImageView.kt new file mode 100644 index 00000000..7d732ca2 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/PlayerImageView.kt @@ -0,0 +1,144 @@ +package net.pokeranalytics.android.ui.view + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.util.AttributeSet +import android.util.TypedValue +import android.view.LayoutInflater +import android.widget.FrameLayout +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat.getColor +import androidx.core.content.res.ResourcesCompat +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade +import com.bumptech.glide.request.RequestOptions +import kotlinx.android.synthetic.main.view_player_image.view.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.model.realm.Player +import net.pokeranalytics.android.ui.extensions.px + + +/** + * Display a row session + */ +class PlayerImageView : FrameLayout { + + enum class Size { + NORMAL, + SMALL; + + fun getFontSize(): Float { + return when (this) { + NORMAL -> 32f + SMALL -> 16f + } + } + + fun getStrokeSize() : Int { + return when (this) { + NORMAL -> 4.px + SMALL -> 2.px + } + } + } + + private lateinit var playerImageView: ConstraintLayout + + private var onImageClickListener: OnClickListener? = null + + /** + * Constructors + */ + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + init() + } + + /** + * Init + */ + private fun init() { + val layoutInflater = LayoutInflater.from(context) + playerImageView = layoutInflater.inflate(R.layout.view_player_image, this, false) as ConstraintLayout + val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + addView(playerImageView, layoutParams) + } + + /** + * Set the session data to the view + */ + fun setPlayer(player: Player, size: Size = Size.NORMAL, isClickable: Boolean = true) { + + // Initial + val playerInitial = 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 + } + + // Picture + if (player.hasPicture()) { + + playerImageView.playerInitial.text = "" + + Glide.with(this) + .load(player.picture) + .apply(RequestOptions().circleCrop()) + .transition(withCrossFade()) + .into(playerImageView.playerImage) + + } else { + playerImageView.playerStroke.background = ResourcesCompat.getDrawable(resources, R.drawable.circle_stroke_kaki, null) + playerImageView.playerImage.setImageDrawable(null) + playerImageView.playerInitial.text = playerInitial + playerImageView.playerInitial.setTextSize(TypedValue.COMPLEX_UNIT_SP, size.getFontSize()) + } + + // Player color + val color = if (player.color != null) { + player.color as Int + } else if (player.hasPicture()) { + Color.TRANSPARENT + } else { + getColor(context, R.color.kaki) + } + + // Stroke & initial + val drawable = playerImageView.playerStroke.background as GradientDrawable? + drawable?.setStroke(size.getStrokeSize(), color) + playerImageView.playerInitial.setTextColor(color) + + + // Click listener + if (isClickable) { + playerImageView.playerImageSelection.setOnClickListener { + onImageClickListener?.onClick(it) + } + } else { + playerImageView.playerImageSelection.background = null + } + } + + /** + * Set image click listener + */ + fun setOnImageClickListener(onImageClickListener: OnClickListener) { + this.onImageClickListener = onImageClickListener + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt index 50c9551e..2239258c 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt @@ -19,6 +19,9 @@ interface EditDataSource { fun editingDescriptors(map: Map): ArrayList? { return null } + +// val valueCanBeClearedWhenEditing: Boolean +// get() { return true } } interface DefaultEditDataSource : EditDataSource, Localizable { 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 4525306d..08093edb 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 @@ -26,6 +26,7 @@ import net.pokeranalytics.android.calculus.bankroll.BankrollReportManager import net.pokeranalytics.android.model.TableSize import net.pokeranalytics.android.model.extensions.getFormattedGameType import net.pokeranalytics.android.model.realm.CustomField +import net.pokeranalytics.android.model.realm.Player import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter @@ -59,6 +60,7 @@ enum class RowViewType(private var layoutRes: Int) { HEADER_TITLE_VALUE(R.layout.row_header_title_value), HEADER_TITLE_AMOUNT(R.layout.row_header_title_amount), HEADER_TITLE_AMOUNT_BIG(R.layout.row_header_title_amount_big), + HEADER_SUBTITLE(R.layout.row_header_subtitle), LOCATION_TITLE(R.layout.row_title), INFO(R.layout.row_info), @@ -76,6 +78,8 @@ enum class RowViewType(private var layoutRes: Int) { TITLE_CHECK(R.layout.row_title_check), TITLE_VALUE_CHECK(R.layout.row_title_value_check), LOADER(R.layout.row_loader), + CONTENT(R.layout.row_content), + TITLE_SUBTITLE(R.layout.row_title_subtitle), // Custom row ROW_SESSION(R.layout.row_feed_session), @@ -88,6 +92,8 @@ enum class RowViewType(private var layoutRes: Int) { GRAPH(R.layout.row_graph), LEGEND_DEFAULT(R.layout.row_legend_default), LIST(R.layout.row_list), + ROW_PLAYER(R.layout.row_player), + ROW_PLAYER_IMAGE(R.layout.row_player_image), // Separator SEPARATOR(R.layout.row_separator); @@ -104,7 +110,7 @@ enum class RowViewType(private var layoutRes: Int) { // Row View Holder HEADER_TITLE, HEADER_TITLE_VALUE, HEADER_TITLE_AMOUNT, HEADER_TITLE_AMOUNT_BIG, LOCATION_TITLE, INFO, TITLE, TITLE_ARROW, TITLE_ICON_ARROW, TITLE_VALUE, TITLE_VALUE_ARROW, TITLE_VALUE_ACTION, TITLE_GRID, - TITLE_SWITCH, TITLE_CHECK, TITLE_VALUE_CHECK, + TITLE_SWITCH, TITLE_CHECK, TITLE_VALUE_CHECK, CONTENT, TITLE_SUBTITLE, HEADER_SUBTITLE, DATA, BOTTOM_SHEET_DATA, LOADER -> RowViewHolder(layout) // Row Session @@ -116,6 +122,11 @@ enum class RowViewType(private var layoutRes: Int) { // Row Transaction ROW_TOP_10 -> RowTop10ViewHolder(layout) + + ROW_PLAYER -> RowPlayerViewHolder(layout) + + ROW_PLAYER_IMAGE -> RowPlayerImageViewHolder(layout) + // Row Button ROW_BUTTON -> RowButtonViewHolder(layout) @@ -623,6 +634,47 @@ enum class RowViewType(private var layoutRes: Int) { } + /** + * Display a player image view + */ + inner class RowPlayerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { + override fun bind(position: Int, row: RowRepresentable, adapter: RowRepresentableAdapter) { + + if (row is Player) { + itemView.findViewById(R.id.playerImage)?.let { playerImageView -> + playerImageView.setPlayer(row, PlayerImageView.Size.SMALL, false) + } + itemView.findViewById(R.id.playerName)?.let { textView -> + textView.text = row.name + } + itemView.findViewById(R.id.playerSummary)?.let { textView -> + textView.text = row.summary + textView.isVisible = row.summary.isNotEmpty() + } + + val listener = View.OnClickListener { + adapter.delegate?.onRowSelected(position, row) + } + itemView.setOnClickListener(listener) + } + } + } + + /** + * Display a player image view + */ + inner class RowPlayerImageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { + override fun bind(position: Int, row: RowRepresentable, adapter: RowRepresentableAdapter) { + itemView.findViewById(R.id.playerImageView)?.let { playerImageView -> + val listener = View.OnClickListener { + adapter.delegate?.onRowSelected(position, PlayerRow.IMAGE) + } + playerImageView.setPlayer(row as Player) + playerImageView.setOnImageClickListener(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..44d37f87 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/PlayerRow.kt @@ -0,0 +1,69 @@ +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 { + IMAGE, + NAME, + SUMMARY; + + override val resId: Int? + get() { + return when (this) { + IMAGE -> null + NAME -> R.string.name + SUMMARY -> R.string.summary + } + } + + override val viewType: Int + get() { + return when (this) { + IMAGE -> RowViewType.ROW_PLAYER_IMAGE.ordinal + NAME -> RowViewType.TITLE_SUBTITLE.ordinal + SUMMARY -> RowViewType.TITLE_SUBTITLE.ordinal + } + } + + override val bottomSheetType: BottomSheetType + get() { + return when (this) { + IMAGE -> BottomSheetType.NONE + NAME -> BottomSheetType.EDIT_TEXT + SUMMARY -> BottomSheetType.EDIT_TEXT_MULTI_LINES + } + } + + override fun editingDescriptors(map: Map): ArrayList? { + return null + } + + + // override fun editingDescriptors(map: Map): ArrayList? { +// +// +// } + +// override fun startEditing(dataSource: Any?, parent: Fragment?) { +// if (dataSource == null) return +// if (dataSource !is Player) return +// if (parent == null) return +// if (parent !is RowRepresentableDelegate) return +// val data = RowEditableDataSource() +// when (this) { +// NAME -> data.append(dataSource.name) +// SUMMARY -> data.append(dataSource.summary) +// else -> PokerAnalyticsException.InputFragmentException +// } +// +// InputFragment.buildAndShow(this, parent, data) +// } + +} \ No newline at end of file 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 5ed0d293..ae72f8f2 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 @@ -11,6 +11,7 @@ enum class SettingRow : RowRepresentable { // More BANKROLL_REPORT, TOP_10, + PLAYERS, // About SUBSCRIPTION, @@ -50,7 +51,7 @@ enum class SettingRow : RowRepresentable { val rows = ArrayList() rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.reports)) - rows.addAll(arrayListOf(BANKROLL_REPORT, TOP_10)) + rows.addAll(arrayListOf(BANKROLL_REPORT, TOP_10, PLAYERS)) rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.information)) // rows.addAll(arrayListOf(VERSION, RATE_APP, CONTACT_US, BUG_REPORT)) @@ -86,6 +87,7 @@ enum class SettingRow : RowRepresentable { return when (this) { BANKROLL_REPORT -> R.string.bankroll TOP_10 -> R.string.top_10 + PLAYERS -> R.string.players SUBSCRIPTION -> R.string.subscription VERSION -> R.string.version RATE_APP -> R.string.releasenote_rating @@ -106,7 +108,7 @@ enum class SettingRow : RowRepresentable { override val viewType: Int get() { return when (this) { - BANKROLL_REPORT, TOP_10 -> RowViewType.TITLE_ICON_ARROW.ordinal + BANKROLL_REPORT, TOP_10, PLAYERS -> RowViewType.TITLE_ICON_ARROW.ordinal VERSION, SUBSCRIPTION -> RowViewType.TITLE_VALUE.ordinal LANGUAGE, CURRENCY -> RowViewType.TITLE_VALUE_ARROW.ordinal FOLLOW_US -> RowViewType.ROW_FOLLOW_US.ordinal @@ -119,6 +121,7 @@ enum class SettingRow : RowRepresentable { return when(this) { BANKROLL_REPORT -> R.drawable.ic_outline_lock TOP_10 -> R.drawable.ic_outline_star + PLAYERS -> R.drawable.ic_outline_people else -> null } } 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/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt b/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt index 7a2b5687..de1d9451 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt @@ -1,16 +1,10 @@ package net.pokeranalytics.android.util.extensions -import io.realm.Realm -import io.realm.RealmModel -import io.realm.RealmResults -import io.realm.Sort +import io.realm.* import net.pokeranalytics.android.model.interfaces.CountableUsage import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.model.interfaces.NameManageable -import net.pokeranalytics.android.model.realm.Filter -import net.pokeranalytics.android.model.realm.TournamentFeature -import net.pokeranalytics.android.model.realm.Transaction -import net.pokeranalytics.android.model.realm.TransactionType +import net.pokeranalytics.android.model.realm.* fun Realm.count(clazz: Class) : Long { return this.where(clazz).count() @@ -107,4 +101,22 @@ fun Realm.updateUsageCount(clazz: Class) { } } +} + +/** + * Returns all entities of the [clazz] which contain the given search content + */ +fun < T : RealmModel> Realm.find(clazz: Class, searchContent: String?) : RealmResults { + val query = this.where(clazz) + when (clazz.kotlin) { + Player::class -> { + query.contains("name", searchContent ?: "", Case.INSENSITIVE).or() + query.contains("summary", searchContent ?: "", Case.INSENSITIVE) + } + } + + val items = query.findAll() + val sortField = arrayOf("name") + val resultSort = arrayOf(Sort.ASCENDING) + return items.sort(sortField, resultSort) } \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/circle_player_color_1.xml b/app/src/main/res/drawable-xxhdpi/circle_player_color_1.xml new file mode 100644 index 00000000..8cf572ee --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/circle_player_color_1.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/circle_player_color_2.xml b/app/src/main/res/drawable-xxhdpi/circle_player_color_2.xml new file mode 100644 index 00000000..c803cf76 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/circle_player_color_2.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/circle_player_color_3.xml b/app/src/main/res/drawable-xxhdpi/circle_player_color_3.xml new file mode 100644 index 00000000..5b5ab8e3 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/circle_player_color_3.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/circle_player_color_4.xml b/app/src/main/res/drawable-xxhdpi/circle_player_color_4.xml new file mode 100644 index 00000000..39f8c91b --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/circle_player_color_4.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/circle_player_color_5.xml b/app/src/main/res/drawable-xxhdpi/circle_player_color_5.xml new file mode 100644 index 00000000..bd3aa3ec --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/circle_player_color_5.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/circle_player_color_6.xml b/app/src/main/res/drawable-xxhdpi/circle_player_color_6.xml new file mode 100644 index 00000000..9f41cab8 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/circle_player_color_6.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/circle_player_color_7.xml b/app/src/main/res/drawable-xxhdpi/circle_player_color_7.xml new file mode 100644 index 00000000..68ebe9e7 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/circle_player_color_7.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/circle_player_color_8.xml b/app/src/main/res/drawable-xxhdpi/circle_player_color_8.xml new file mode 100644 index 00000000..5d725ca5 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/circle_player_color_8.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/circle_player_color_9.xml b/app/src/main/res/drawable-xxhdpi/circle_player_color_9.xml new file mode 100644 index 00000000..053ad739 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/circle_player_color_9.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/circle_stroke_kaki.xml b/app/src/main/res/drawable-xxhdpi/circle_stroke_kaki.xml new file mode 100644 index 00000000..547635d3 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/circle_stroke_kaki.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/ic_outline_people.xml b/app/src/main/res/drawable-xxhdpi/ic_outline_people.xml new file mode 100644 index 00000000..1a2f6f89 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_outline_people.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-xxhdpi/ic_outline_search.xml b/app/src/main/res/drawable-xxhdpi/ic_outline_search.xml new file mode 100644 index 00000000..6a5ca808 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_outline_search.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_color_picker.xml b/app/src/main/res/layout/activity_color_picker.xml new file mode 100644 index 00000000..e2942fff --- /dev/null +++ b/app/src/main/res/layout/activity_color_picker.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..e24000ab --- /dev/null +++ b/app/src/main/res/layout/fragment_player.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/row_content.xml b/app/src/main/res/layout/row_content.xml new file mode 100644 index 00000000..49c1bf77 --- /dev/null +++ b/app/src/main/res/layout/row_content.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/row_header_subtitle.xml b/app/src/main/res/layout/row_header_subtitle.xml new file mode 100644 index 00000000..1e65603f --- /dev/null +++ b/app/src/main/res/layout/row_header_subtitle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/row_player.xml b/app/src/main/res/layout/row_player.xml new file mode 100644 index 00000000..7f2d73c5 --- /dev/null +++ b/app/src/main/res/layout/row_player.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..2a8a6c20 --- /dev/null +++ b/app/src/main/res/layout/row_player_image.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/layout/row_title_subtitle.xml b/app/src/main/res/layout/row_title_subtitle.xml new file mode 100644 index 00000000..7e00d358 --- /dev/null +++ b/app/src/main/res/layout/row_title_subtitle.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_player_image.xml b/app/src/main/res/layout/view_player_image.xml new file mode 100644 index 00000000..5ab69c6d --- /dev/null +++ b/app/src/main/res/layout/view_player_image.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/menu/toolbar_data_list.xml b/app/src/main/res/menu/toolbar_data_list.xml new file mode 100644 index 00000000..6c0816b9 --- /dev/null +++ b/app/src/main/res/menu/toolbar_data_list.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ec3eb74a..bea117ef 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -11,6 +11,7 @@ #8AFFFFFF #141414 #1B1F1B + #808080 #e0e0e0 #40000000 @@ -43,4 +44,14 @@ #3c8c50 #71fb94 + #00000000 + #ffdf3e + #65FF82 + #8efde7 + #3aaeff + #6150ff + #ff56ff + #ff573d + #ff971e + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 802da918..2fc85f30 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -5,4 +5,8 @@ 12sp 11sp + + 48dp + 8dp + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 69bc6a13..63c46bef 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -108,6 +108,11 @@ 18sp + + + + + + @@ -266,6 +285,16 @@ 16sp + + diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml index 45ed9d3f..43873cba 100644 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -1,6 +1,11 @@ - + + + + \ No newline at end of file