Implement bulk buying gems

This commit is contained in:
Phillip Thelen 2020-04-02 13:55:16 +02:00
parent 95c9590be4
commit 2ea68a1b58
20 changed files with 171 additions and 56 deletions

View file

@ -155,7 +155,7 @@ android {
multiDexEnabled true
resConfigs "en", "bg", "de", "en-rGB", "es", "fr", "hr-rHR", "in", "it", "iw", "ja", "ko", "lt", "nl", "pl", "pt-rBR", "pt-rPT", "ru", "tr", "zh", "zh-rTW"
versionCode 2390
versionCode 2392
versionName "2.5"
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="48dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="48dp">
<path android:fillColor="#33878190" android:pathData="M7,14l5,-5 5,5z"/>
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:drawable="@drawable/ic_arrow_drop_up_gray_48dp_disabled" />
<item android:drawable="@drawable/ic_arrow_drop_up_gray_48dp" />
</selector>

View file

@ -83,7 +83,7 @@
android:layout_height="wrap_content"
android:text="@string/cost"
style="@style/TaskFormSectionheader"/>
<com.habitrpg.android.habitica.ui.views.tasks.form.RewardValueFormView
<com.habitrpg.android.habitica.ui.views.tasks.form.StepperValueFormView
android:id="@+id/reward_value"
android:layout_width="match_parent"
android:layout_height="56dp" />

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/shopitem_dialog_content_inset"
android:paddingRight="@dimen/shopitem_dialog_content_inset"
android:gravity="center_horizontal"
tools:parentTag="LinearLayout"
tools:background="@color/white"
tools:orientation="vertical">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/imageView"
android:layout_width="@dimen/shopitem_image_size"
android:layout_height="@dimen/shopitem_image_size" />
<TextView
android:id="@+id/titleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Headline"
tools:text="This is the Title"
android:gravity="center"
android:layout_marginTop="14dp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/notesTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Body2"
android:textColor="@color/black_50_alpha"
tools:text="These are the notes"
android:gravity="center"/>
<com.habitrpg.android.habitica.ui.views.tasks.form.StepperValueFormView
android:id="@+id/stepper_view"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="@dimen/spacing_large"
app:defaultValue="1"
app:minValue="1"
/>
</merge>

View file

@ -20,7 +20,7 @@
android:layout_width="56dp"
android:layout_height="match_parent"
android:background="@color/gray_600"
android:src="@drawable/ic_arrow_drop_up_gray_48dp"
android:src="@drawable/ic_arrow_drop_up_gray_48dp_states"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
/>

View file

@ -137,4 +137,10 @@
<attr name="description" />
<attr name="iconDrawable" />
</declare-styleable>
<declare-styleable name="StepperValueFormView">
<attr name="defaultValue" />
<attr name="iconDrawable" />
<attr name="maxValue" format="integer" />
<attr name="minValue" format="integer" />
</declare-styleable>
</resources>

View file

@ -62,10 +62,10 @@ interface ApiService {
fun equipItem(@Path("type") type: String, @Path("key") itemKey: String): Flowable<HabitResponse<Items>>
@POST("user/buy/{key}")
fun buyItem(@Path("key") itemKey: String): Flowable<HabitResponse<BuyResponse>>
fun buyItem(@Path("key") itemKey: String, @Body quantity: Map<String, Int>): Flowable<HabitResponse<BuyResponse>>
@POST("user/purchase/{type}/{key}")
fun purchaseItem(@Path("type") type: String, @Path("key") itemKey: String): Flowable<HabitResponse<Void>>
fun purchaseItem(@Path("type") type: String, @Path("key") itemKey: String, @Body quantity: Map<String, Int>): Flowable<HabitResponse<Void>>
@POST("user/purchase-hourglass/{type}/{key}")
fun purchaseHourglassItem(@Path("type") type: String, @Path("key") itemKey: String): Flowable<HabitResponse<Void>>

View file

@ -52,9 +52,9 @@ interface ApiClient {
fun equipItem(type: String, itemKey: String): Flowable<Items>
fun buyItem(itemKey: String): Flowable<BuyResponse>
fun buyItem(itemKey: String, purchaseQuantity: Int): Flowable<BuyResponse>
fun purchaseItem(type: String, itemKey: String): Flowable<Any>
fun purchaseItem(type: String, itemKey: String, purchaseQuantity: Int): Flowable<Any>
fun purchaseHourglassItem(type: String, itemKey: String): Flowable<Any>

View file

@ -60,7 +60,7 @@ interface InventoryRepository : BaseRepository {
fun inviteToQuest(quest: QuestContent): Flowable<Quest>
fun buyItem(user: User?, id: String, value: Double): Flowable<BuyResponse>
fun buyItem(user: User?, id: String, value: Double, purchaseQuantity: Int): Flowable<BuyResponse>
fun retrieveShopInventory(identifier: String): Flowable<Shop>
fun retrieveMarketGear(): Flowable<Shop>
@ -71,7 +71,7 @@ interface InventoryRepository : BaseRepository {
fun purchaseQuest(key: String): Flowable<Any>
fun purchaseItem(purchaseType: String, key: String): Flowable<Any>
fun purchaseItem(purchaseType: String, key: String, purchaseQuantity: Int): Flowable<Any>
fun togglePinnedItem(item: ShopItem): Flowable<List<ShopItem>>
fun getItems(itemClass: Class<out Item>, keys: Array<String>, user: User?): Flowable<out RealmResults<out Item>>

View file

@ -321,12 +321,12 @@ class ApiClientImpl//private OnHabitsAPIResult mResultListener;
return apiService.equipItem(type, itemKey).compose(configureApiCallObserver())
}
override fun buyItem(itemKey: String): Flowable<BuyResponse> {
return apiService.buyItem(itemKey).compose(configureApiCallObserver())
override fun buyItem(itemKey: String, purchaseQuantity: Int): Flowable<BuyResponse> {
return apiService.buyItem(itemKey, mapOf(Pair("quantity", purchaseQuantity))).compose(configureApiCallObserver())
}
override fun purchaseItem(type: String, itemKey: String): Flowable<Any> {
return apiService.purchaseItem(type, itemKey).compose(configureApiCallObserver())
override fun purchaseItem(type: String, itemKey: String, purchaseQuantity: Int): Flowable<Any> {
return apiService.purchaseItem(type, itemKey, mapOf(Pair("quantity", purchaseQuantity))).compose(configureApiCallObserver())
}
override fun validateSubscription(request: SubscriptionValidationRequest): Flowable<Any> {

View file

@ -228,8 +228,8 @@ class InventoryRepositoryImpl(localRepository: InventoryLocalRepository, apiClie
.doOnNext { localRepository.changeOwnedCount("quests", quest.key, userID, -1) }
}
override fun buyItem(user: User?, id: String, value: Double): Flowable<BuyResponse> {
return apiClient.buyItem(id)
override fun buyItem(user: User?, id: String, value: Double, purchaseQuantity: Int): Flowable<BuyResponse> {
return apiClient.buyItem(id, purchaseQuantity)
.doOnNext { buyResponse ->
if (user == null) {
return@doOnNext
@ -251,7 +251,7 @@ class InventoryRepositoryImpl(localRepository: InventoryLocalRepository, apiClie
if (buyResponse.gp != null) {
copiedUser.stats?.gp = buyResponse.gp
} else {
copiedUser.stats?.gp = copiedUser.stats?.gp ?: 0 - value
copiedUser.stats?.gp = copiedUser.stats?.gp ?: 0 - (value * purchaseQuantity)
}
if (buyResponse.lvl != null) {
copiedUser.stats?.lvl = buyResponse.lvl
@ -280,8 +280,8 @@ class InventoryRepositoryImpl(localRepository: InventoryLocalRepository, apiClie
return apiClient.purchaseQuest(key)
}
override fun purchaseItem(purchaseType: String, key: String): Flowable<Any> {
return apiClient.purchaseItem(purchaseType, key)
override fun purchaseItem(purchaseType: String, key: String, purchaseQuantity: Int): Flowable<Any> {
return apiClient.purchaseItem(purchaseType, key, purchaseQuantity)
}
override fun togglePinnedItem(item: ShopItem): Flowable<List<ShopItem>> {

View file

@ -68,10 +68,10 @@ open class ShopItem : RealmObject() {
val isTypeAnimal: Boolean
get() = "pets" == purchaseType || "mounts" == purchaseType
fun canAfford(user: User?, canAlwaysAffordSpecial: Boolean): Boolean = when(currency) {
"gold" -> value <= user?.stats?.gp ?: 0.0
"gems" -> if (canAlwaysAffordSpecial) true else value <= user?.gemCount ?: 0
"hourglasses" -> if (canAlwaysAffordSpecial) true else value <= user?.purchased?.plan?.consecutive?.trinkets ?: 0
fun canAfford(user: User?, quantity: Int): Boolean = when(currency) {
"gold" -> (value * quantity) <= user?.stats?.gp ?: 0.0
"gems" -> true
"hourglasses" -> true
else -> false
}

View file

@ -85,7 +85,7 @@ class TaskFormActivity : BaseActivity() {
private val statPerceptionButton: TextView by bindView(R.id.stat_perception_button)
private val rewardValueTitleView: TextView by bindView(R.id.reward_value_title)
private val rewardValueFormView: RewardValueFormView by bindView(R.id.reward_value)
private val stepperValueFormView: StepperValueFormView by bindView(R.id.reward_value)
private val tagsTitleView: TextView by bindView(R.id.tags_title)
private val tagsWrapper: LinearLayout by bindView(R.id.tags_wrapper)
@ -271,7 +271,7 @@ class TaskFormActivity : BaseActivity() {
val rewardViewsVisibility = if (taskType == Task.TYPE_REWARD) View.VISIBLE else View.GONE
rewardValueTitleView.visibility = rewardViewsVisibility
rewardValueFormView.visibility = rewardViewsVisibility
stepperValueFormView.visibility = rewardViewsVisibility
tagsTitleView.visibility = if (isChallengeTask) View.GONE else View.VISIBLE
tagsWrapper.visibility = if (isChallengeTask) View.GONE else View.VISIBLE
@ -346,7 +346,7 @@ class TaskFormActivity : BaseActivity() {
taskSchedulingControls.frequency = task.frequency ?: Task.FREQUENCY_DAILY
}
Task.TYPE_TODO -> taskSchedulingControls.dueDate = task.dueDate
Task.TYPE_REWARD -> rewardValueFormView.value = task.value
Task.TYPE_REWARD -> stepperValueFormView.value = task.value
}
if (taskType == Task.TYPE_DAILY || taskType == Task.TYPE_TODO) {
task.checklist?.let { checklistContainer.checklistItems = it }
@ -420,7 +420,7 @@ class TaskFormActivity : BaseActivity() {
} else if (taskType == Task.TYPE_TODO) {
thisTask.dueDate = taskSchedulingControls.dueDate
} else if (taskType == Task.TYPE_REWARD) {
thisTask.value = rewardValueFormView.value
thisTask.value = stepperValueFormView.value
}
val resultIntent = Intent()

View file

@ -139,7 +139,7 @@ class ShopRecyclerAdapter(private val configManager: AppConfigManager) : android
ShopItem::class.java -> {
val item = obj as? ShopItem ?: return
val itemHolder = holder as? ShopItemViewHolder ?: return
itemHolder.bind(item, item.canAfford(user, configManager.insufficientGemPurchase()))
itemHolder.bind(item, item.canAfford(user, 1))
if (ownedItems.containsKey(item.key+"-"+item.pinType)) {
itemHolder.itemCount = ownedItems[item.key+"-"+item.pinType]?.numberOwned ?: 0
}

View file

@ -75,7 +75,7 @@ class RewardsRecyclerViewAdapter(private var customRewards: OrderedRealmCollecti
} else if (inAppRewards != null) {
val item = inAppRewards?.get(position - customRewardCount) ?: return
if (holder is ShopItemViewHolder) {
holder.bind(item, item.canAfford(user, configManager.insufficientGemPurchase()))
holder.bind(item, item.canAfford(user, 1))
holder.isPinned = true
holder.hidePinIndicator()
}

View file

@ -55,7 +55,7 @@ class ProfilePreferencesFragment: BasePreferencesFragment(), SharedPreferences.O
val profileCategory = findPreference("profile") as? PreferenceCategory
configurePreference(profileCategory?.findPreference(key), sharedPreferences?.getString(key, ""))
if (sharedPreferences != null) {
val newValue = sharedPreferences.getString(key, "") ?: ""
val newValue = sharedPreferences.getString(key, "") ?: return
val observable: Flowable<User>? = when (key) {
"display_name" -> {
if (newValue != user?.profile?.name) {
@ -65,16 +65,14 @@ class ProfilePreferencesFragment: BasePreferencesFragment(), SharedPreferences.O
}
}
"photo_url" -> {
val newName = sharedPreferences.getString(key, "") ?: ""
if (newName != user?.profile?.imageUrl) {
if (newValue != user?.profile?.imageUrl) {
userRepository.updateUser(user, "profile.imageUrl", newValue)
} else {
null
}
}
"about" -> {
val newName = sharedPreferences.getString(key, "") ?: ""
if (newName != user?.profile?.blurb) {
if (newValue != user?.profile?.blurb) {
userRepository.updateUser(user, "profile.blurb", newValue)
} else {
null
@ -85,5 +83,4 @@ class ProfilePreferencesFragment: BasePreferencesFragment(), SharedPreferences.O
observable?.subscribe(Consumer {}, RxErrorHandler.handleEmptyError())?.let { compositeSubscription.add(it) }
}
}
}

View file

@ -60,6 +60,8 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
private val buyLabel: TextView
private val pinButton: Button by bindView(customHeader, R.id.pin_button)
private var purchaseQuantity = 1
var purchaseCardAction: ((ShopItem) -> Unit)? = null
private var shopItem: ShopItem = item
@ -74,7 +76,7 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
}
if (shopItem.lockedReason(context) == null) {
priceLabel.value = shopItem.value.toDouble()
updatePurchaseTotal()
priceLabel.currency = shopItem.currency
} else {
limitedTextView.text = shopItem.lockedReason(context)
@ -100,13 +102,34 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
inventoryRepository.getEquipment(shopItem.key).firstElement().subscribe(Consumer<Equipment> { contentView.setEquipment(it) }, RxErrorHandler.handleEmptyError())
checkGearClass()
}
"gems" == shopItem.purchaseType -> contentView = PurchaseDialogGemsContent(context)
"gems" == shopItem.purchaseType -> {
val gemContent = PurchaseDialogGemsContent(context)
gemContent.stepperView.onValueChanged = {
purchaseQuantity = it.toInt()
updatePurchaseTotal()
}
contentView = gemContent
}
else -> contentView = PurchaseDialogBaseContent(context)
}
contentView.setItem(shopItem)
setAdditionalContentView(contentView)
}
fun updatePurchaseTotal() {
priceLabel.value = shopItem.value.toDouble() * purchaseQuantity
if (shopItem.canAfford(user, purchaseQuantity) && !shopItem.locked) {
buyButton.background = context.getDrawable(R.drawable.button_background_primary)
priceLabel.setTextColor(ContextCompat.getColor(context, R.color.white))
buyLabel.setTextColor(ContextCompat.getColor(context, R.color.white))
} else {
buyButton.background = context.getDrawable(R.drawable.button_background_gray_600)
priceLabel.setTextColor(ContextCompat.getColor(context, R.color.gray_100))
buyLabel.setTextColor(ContextCompat.getColor(context, R.color.gray_100))
}
}
private fun checkGearClass() {
val user = user ?: return
@ -183,15 +206,7 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
}
buyButton.elevation = 0f
if (shopItem.canAfford(user, configManager.insufficientGemPurchase()) && !shopItem.locked) {
buyButton.background = context.getDrawable(R.drawable.button_background_primary)
priceLabel.setTextColor(ContextCompat.getColor(context, R.color.white))
buyLabel.setTextColor(ContextCompat.getColor(context, R.color.white))
} else {
buyButton.background = context.getDrawable(R.drawable.button_background_gray_600)
priceLabel.setTextColor(ContextCompat.getColor(context, R.color.gray_100))
buyLabel.setTextColor(ContextCompat.getColor(context, R.color.gray_100))
}
updatePurchaseTotal()
if (shopItem.isTypeGear) {
checkGearClass()
@ -211,7 +226,7 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
val snackbarText = arrayOf("")
if (shopItem.isValid && !shopItem.locked) {
val gemsLeft = if (shopItem.limitedNumberLeft != null) shopItem.limitedNumberLeft else 0
if ((gemsLeft == 0 && shopItem.purchaseType == "gems") || shopItem.canAfford(user, false)) {
if ((gemsLeft == 0 && shopItem.purchaseType == "gems") || shopItem.canAfford(user, purchaseQuantity)) {
val observable: Flowable<Any>
if (shopIdentifier != null && shopIdentifier == Shop.TIME_TRAVELERS_SHOP || "mystery_set" == shopItem.purchaseType || shopItem.currency == "hourglasses") {
observable = if (shopItem.purchaseType == "gear") {
@ -226,7 +241,7 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
dismiss()
return
} else if ("gold" == shopItem.currency && "gem" != shopItem.key) {
observable = inventoryRepository.buyItem(user, shopItem.key, shopItem.value.toDouble()).map { buyResponse ->
observable = inventoryRepository.buyItem(user, shopItem.key, shopItem.value.toDouble(), purchaseQuantity).map { buyResponse ->
if (shopItem.key == "armoire") {
snackbarText[0] = when {
buyResponse.armoire["type"] == "gear" -> context.getString(R.string.armoireEquipment, buyResponse.armoire["dropText"])
@ -237,7 +252,7 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
buyResponse
}
} else {
observable = inventoryRepository.purchaseItem(shopItem.purchaseType, shopItem.key)
observable = inventoryRepository.purchaseItem(shopItem.purchaseType, shopItem.key, purchaseQuantity)
}
observable
.doOnNext {

View file

@ -3,20 +3,29 @@ package com.habitrpg.android.habitica.ui.views.shops
import android.content.Context
import android.widget.TextView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.extensions.asDrawable
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.tasks.form.StepperValueFormView
internal class PurchaseDialogGemsContent(context: Context) : PurchaseDialogContent(context) {
val notesTextView: TextView by bindView(R.id.notesTextView)
val stepperView: StepperValueFormView by bindView(R.id.stepper_view)
override val viewId: Int
get() = R.layout.dialog_purchase_content_item
get() = R.layout.dialog_purchase_gems
init {
stepperView.iconDrawable = HabiticaIconsHelper.imageOfGem().asDrawable(context.resources)
}
override fun setItem(item: ShopItem) {
super.setItem(item)
notesTextView.text = item.notes
stepperView.maxValue = item.limitedNumberLeft?.toDouble()
}
}

View file

@ -1,6 +1,7 @@
package com.habitrpg.android.habitica.ui.views.tasks.form
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.widget.EditText
import android.widget.ImageButton
@ -13,7 +14,7 @@ import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import java.text.DecimalFormat
class RewardValueFormView @JvmOverloads constructor(
class StepperValueFormView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
@ -21,20 +22,36 @@ class RewardValueFormView @JvmOverloads constructor(
private val upButton: ImageButton by bindView(R.id.up_button)
private val downButton: ImageButton by bindView(R.id.down_button)
var onValueChanged: ((Double) -> Unit)? = null
private val decimalFormat = DecimalFormat("0.###")
private var editTextIsFocused = false
var value = 0.0
set(value) {
val newValue = if (value >= 0) value else 0.0
var newValue = if (value >= minValue) value else minValue
maxValue?.let {
if (newValue > it) {
newValue = it
}
}
val oldValue = field
field = newValue
if (oldValue != newValue) {
valueString = decimalFormat.format(newValue)
}
downButton.isEnabled = field > 0
downButton.isEnabled = field > minValue
maxValue?.let {
if (it == 0.0) return@let
upButton.isEnabled = value < it
}
onValueChanged?.invoke(value)
}
var maxValue: Double? = null
var minValue: Double = 0.0
private var valueString = ""
set(value) {
field = value
@ -51,11 +68,28 @@ class RewardValueFormView @JvmOverloads constructor(
}
}
var iconDrawable: Drawable?
get() {
return editText.compoundDrawables.firstOrNull()
}
set(value) {
editText.setCompoundDrawablesWithIntrinsicBounds(value, null, null, null)
}
init {
inflate(R.layout.task_form_reward_value, true)
inflate(R.layout.form_stepper_value, true)
val attributes = context.theme?.obtainStyledAttributes(
attrs,
R.styleable.StepperValueFormView,
0, 0)
//set value here, so that the setter is called and everything is set up correctly
value = 10.0
editText.setCompoundDrawablesWithIntrinsicBounds(HabiticaIconsHelper.imageOfGold().asDrawable(context.resources), null, null, null)
maxValue = attributes?.getFloat(R.styleable.StepperValueFormView_maxValue, 0f)?.toDouble()
minValue = attributes?.getFloat(R.styleable.StepperValueFormView_minValue, 0f)?.toDouble() ?: 0.0
value = attributes?.getFloat(R.styleable.StepperValueFormView_defaultValue, 10.0f)?.toDouble() ?: 10.0
iconDrawable = attributes?.getDrawable(R.styleable.StepperValueFormView_iconDrawable) ?: HabiticaIconsHelper.imageOfGold().asDrawable(context.resources)
upButton.setOnClickListener {
value += 1