Fix various minor issues

This commit is contained in:
Phillip Thelen 2021-03-04 11:29:11 +01:00
parent 8cd288f902
commit f20f7b13c3
24 changed files with 121 additions and 64 deletions

View file

@ -150,7 +150,7 @@ android {
buildConfigField "String", "TESTING_LEVEL", "\"production\""
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 2847
versionCode 2858
versionName "3.2.1"
}

View file

@ -344,7 +344,6 @@
<string name="subscribe">Абониране</string>
<string name="subscribe_listitem1_description">Александър Търговеца вече ще Ви продава диаманти на цената от 20 злато за диамант.\n\nМесечните му доставки отначало ще бъдат ограничени до 25 диаманта на месец, но може да се увеличат в зависимост от продължителността на абонамента Ви.\n\nОграничението се вдига с по 5 диаманта след всеки три месеца непрекъснат абонамент, докато не достигне 50 диаманта на месец!</string>
<string name="subscribe_listitem2_description">Всеки месец ще получавате уникален козметичен предмет за героя си!\n\nОсвен това, за всеки три месеца непрекъснат абонамент, тайнствените пътешественици във времето ще Ви дават достъп до исторически козметични предмети (както и такива от бъдещето).</string>
<string name="subscribe_listitem3_description">Прави завършените задачи и историята на задачите налични за по-дълго време.</string>
<string name="subscribe_listitem4_description">С двойно увеличеното ограничение на паданията ще получавате повече предмети от завършените си задачи всеки ден, което ще Ви помогне да завършите конюшнята си по-бързо!</string>
<string name="subscribe1month_gemcap">Ограничение от 25 диаманта</string>
<string name="subscribe3month_gemcap">Ограничение от 30 диаманта</string>

View file

@ -393,8 +393,8 @@
<string name="empty_description_dailies">Dailies are tasks that repeat on a regular basis. Choose the schedule that works for you!</string>
<string name="empty_title_todos">You don\'t have any To Do\'s</string>
<string name="empty_description_todos">To Do\'s are tasks that only need to be completed once. Add checklists to your To Do\'s to increase their value.</string>
<string name="empty_title_rewards">You don&amp;rsquo;t have any Rewards</string>
<string name="reset_walkthrough">Reset Justin&amp;rsquo;s Walkthrough</string>
<string name="empty_title_rewards">You don\'t have any Rewards</string>
<string name="reset_walkthrough">Reset Justins Walkthrough</string>
<string name="read_community_guidelines">Review our <u>Community Guidelines</u> before posting</string>
<string name="maintenance">Maintenance</string>
<string name="reload_content">Reload Content</string>

View file

@ -942,7 +942,6 @@
<string name="hatch_pet">Вырастить питомца</string>
<string name="adventure_guide_description">Выполните эти задания и в итоге получите &lt;b&gt;5 достижений&lt;/b&gt; и &lt;font color=#EE9109&gt;&lt;b&gt;100 золота&lt;/b&gt;&lt;/font&gt;!</string>
<string name="pet_ownership_fraction">%1$d / %2$d</string>
<string name="subscribe_listitem3_description_new">Подпишитесь сейчас, чтобы сразу же получить , а также получать новые предметы каждый месяц!</string>
<string name="subscribe_listitem5">Удвойте количество выпадающих предметов</string>
<string name="pms_disabled">Личные сообщения заблокированы</string>
<string name="disablePrivateMessages">Заблокировать получение личных сообщений</string>

View file

@ -339,7 +339,6 @@
<string name="subscribe">加入會員</string>
<string name="subscribe_listitem1_description">您將可以從市場上以每枚20金幣的價格購買寶石</string>
<string name="subscribe_listitem2_description">獲得神秘沙漏在時空穿越者的商店購買物品!</string>
<string name="subscribe_listitem3_description">現在訂閱,可以得到贈品,和每月得到新的物品!</string>
<string name="subscription_hourglasses">%d 個神秘沙漏</string>
<string name="payment_method">付款方式</string>
<string name="subscription">訂閱</string>
@ -702,7 +701,7 @@
<string name="cancel_subscription_google_description">不想再繼續捐助了你可以在Google應用商店裡的“我的應用程序”部分找到退訂選項。</string>
<string name="subscribe_listitem5_description">在Habitica發現更多的物品每天享受雙倍的物品掉落上限。</string>
<string name="subscribe_listitem4_description">如果您變成訂閱者,您可以獲得紫禦鹿角兔寵物。</string>
<string name="subscribe_listitem3_description_new">現在訂閱,可以得到%@和每月得到新的物品!</string>
<string name="subscribe_listitem3_description_new">現在訂閱,可以得到%s和每月得到新的物品!</string>
<string name="subscribe_listitem5">雙倍掉落</string>
<string name="create_account_short">創建一個帳戶</string>
<string name="login_incentive_short_count">簽到%d次</string>

View file

@ -344,7 +344,6 @@
<string name="subscribe">订阅</string>
<string name="subscribe_listitem1_description">您将可以从市场上以每枚20金币的价格购买宝石</string>
<string name="subscribe_listitem2_description">获得神秘沙漏在时空穿越者的商店购买物品!</string>
<string name="subscribe_listitem3_description">现在订阅,可以得到赠品,和每月得到新的物品!</string>
<string name="subscribe_listitem4_description">如果您变成订阅者,您可以获得紫御鹿角兔宠物。</string>
<string name="subscribe1month_gemcap">25 宝石</string>
<string name="subscribe3month_gemcap">30 宝石</string>
@ -898,7 +897,7 @@
<string name="register_btn_fb">Facebook登录</string>
<string name="login_btn_apple">Apple登录</string>
<string name="subscribe_listitem5_description">在Habitica发现更多的物品每天享受双倍的物品掉落上限。</string>
<string name="subscribe_listitem3_description_new">现在订阅,可以得到%@和每月得到新的物品!</string>
<string name="subscribe_listitem3_description_new">现在订阅,可以得到%s和每月得到新的物品!</string>
<string name="subscribe_listitem5">双倍掉落</string>
<string name="create_account_short">创建一个帐户</string>
<string name="login_incentive_short_count">签到%d次</string>

View file

@ -12,4 +12,5 @@ interface ContentRepository {
fun retrieveContent(context: Context?, forced: Boolean): Flowable<ContentResult>
fun retrieveWorldState(context: Context?): Flowable<WorldState>
fun getWorldState(): Flowable<WorldState>
}

View file

@ -43,16 +43,13 @@ abstract class ContentRepositoryImpl<T : ContentLocalRepository>(localRepository
lastWorldStateSync = now
apiClient.worldState.doOnNext {
localRepository.saveWorldState(it)
val editor = PreferenceManager.getDefaultSharedPreferences(context).edit()
editor.putString("currentEvent", it.currentEventKey)
editor.putString("currentEventPromo", it.currentEventPromo)
editor.putLong("currentEventStartDate", it.currentEventStartDate?.time ?: 0)
editor.putLong("currentEventEndDate", it.currentEventEndDate?.time ?: 0)
editor.apply()
}
} else {
Flowable.empty()
}
}
override fun getWorldState(): Flowable<WorldState> {
return localRepository.getWorldState()
}
}

View file

@ -5,6 +5,7 @@ import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.data.local.UserLocalRepository
import com.habitrpg.android.habitica.data.local.UserQuestStatus
import com.habitrpg.android.habitica.extensions.skipNull
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.Achievement
@ -164,7 +165,7 @@ class UserRepositoryImpl(localRepository: UserLocalRepository, apiClient: ApiCli
path = path.substring(1)
return Flowable.zip(apiClient.unlockPath(path), localRepository.getUser(userID).firstElement().toFlowable()
.map { localRepository.getUnmanagedCopy(it) }
.skipNil(), { unlockResponse, copiedUser ->
.skipNull(), { unlockResponse, copiedUser ->
copiedUser.preferences = unlockResponse.preferences
copiedUser.purchased = unlockResponse.purchased
copiedUser.items = unlockResponse.items
@ -380,7 +381,7 @@ class UserRepositoryImpl(localRepository: UserLocalRepository, apiClient: ApiCli
private fun getLiveUser(): Flowable<User> {
return localRepository.getUser(userID)
.map { localRepository.getLiveObject(it) }
.skipNil()
.skipNull()
}
private fun <T> zipWithLiveUser(flowable: Flowable<T>, mergeFunc: BiFunction<T, User, T>): Flowable<T> {
@ -424,8 +425,3 @@ class UserRepositoryImpl(localRepository: UserLocalRepository, apiClient: ApiCli
return copiedUser
}
}
private fun <T> Flowable<T?>.skipNil(): Flowable<T> {
@Suppress("UNCHECKED_CAST")
return skipWhile { it == null } as? Flowable<T> ?: Flowable.empty()
}

View file

@ -2,8 +2,10 @@ package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.ContentResult
import com.habitrpg.android.habitica.models.WorldState
import io.reactivex.rxjava3.core.Flowable
interface ContentLocalRepository : BaseLocalRepository {
fun saveContent(contentResult: ContentResult)
fun saveWorldState(worldState: WorldState)
fun getWorldState(): Flowable<WorldState>
}

View file

@ -1,10 +1,13 @@
package com.habitrpg.android.habitica.data.local.implementation
import com.habitrpg.android.habitica.data.local.ContentLocalRepository
import com.habitrpg.android.habitica.extensions.skipNull
import com.habitrpg.android.habitica.models.ContentResult
import com.habitrpg.android.habitica.models.WorldState
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.social.Group
import hu.akarnokd.rxjava3.bridge.RxJavaBridge
import io.reactivex.rxjava3.core.Flowable
import io.realm.Realm
@ -33,6 +36,15 @@ open class RealmContentLocalRepository(realm: Realm) : RealmBaseLocalRepository(
}
}
override fun getWorldState(): Flowable<WorldState> {
return RxJavaBridge.toV3Flowable(realm.where(WorldState::class.java)
.findAll()
.asFlowable()
.filter { it.isLoaded && it.size > 0 }
.map { it.first() })
.skipNull()
}
override fun saveWorldState(worldState: WorldState) {
val tavern = getUnmanagedCopy(realm.where(Group::class.java)
.equalTo("id", Group.TAVERN_ID)
@ -47,5 +59,6 @@ open class RealmContentLocalRepository(realm: Realm) : RealmBaseLocalRepository(
tavern.quest?.key = worldState.worldBossKey
tavern.quest?.progress = worldState.progress
save(tavern)
save(worldState)
}
}

View file

@ -7,4 +7,9 @@ import io.reactivex.rxjava3.functions.Consumer
fun <T> Flowable<T>.subscribeWithErrorHandler(function: Consumer<T>): Disposable {
return subscribe(function, RxErrorHandler.handleEmptyError())
}
fun <T> Flowable<T?>.skipNull(): Flowable<T> {
@Suppress("UNCHECKED_CAST")
return skipWhile { it == null } as? Flowable<T> ?: Flowable.empty()
}

View file

@ -6,16 +6,26 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.data.ContentRepository
import com.habitrpg.android.habitica.models.WorldState
import com.habitrpg.android.habitica.models.promotions.HabiticaPromotion
import com.habitrpg.android.habitica.models.promotions.getHabiticaPromotionFromKey
import java.util.*
class AppConfigManager {
class AppConfigManager(private val contentRepository: ContentRepository?) {
private var worldState: WorldState? = null
init {
contentRepository?.getWorldState()?.subscribe( {
worldState = it
}, RxErrorHandler.handleEmptyError())
}
private val remoteConfig = FirebaseRemoteConfig.getInstance()
fun shopSpriteSuffix(): String {
return remoteConfig.getString("shopSpriteSuffix")
return worldState?.npcImageSuffix ?: remoteConfig.getString("shopSpriteSuffix")
}
fun maxChatLength(): Long {
@ -59,10 +69,6 @@ class AppConfigManager {
return remoteConfig.getBoolean("enableLocalTaskScoring")
}
fun flipAddTaskBehaviour(): Boolean {
return remoteConfig.getBoolean("flipAddTaskBehaviour")
}
fun insufficientGemPurchase(): Boolean {
return remoteConfig.getBoolean("insufficientGemPurchase")
}
@ -100,15 +106,12 @@ class AppConfigManager {
return remoteConfig.getBoolean("enableAdventureGuide")
}
fun activePromo(context: Context): HabiticaPromotion? {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val key = preferences.getString("currentEvent", null)
if (key?.isNotBlank() == true) {
val startDateLong = preferences.getLong("currentEventStartDate", 0)
val startDate = if (startDateLong > 0) Date(startDateLong) else null
val endDateLong = preferences.getLong("currentEventEndDate", 0)
val endDate = if (endDateLong > 0) Date(endDateLong) else null
return getHabiticaPromotionFromKey(preferences.getString("currentEventPromo", null) ?: key, startDate, endDate)
fun activePromo(): HabiticaPromotion? {
for (event in worldState?.events ?: listOf(worldState?.currentEvent)) {
if (event == null) return null
if (event.promo != null) {
return getHabiticaPromotionFromKey(event.promo ?: "", event.start, event.end)
}
}
return null
}

View file

@ -2,17 +2,19 @@ package com.habitrpg.android.habitica.models
import com.habitrpg.android.habitica.models.inventory.QuestProgress
import com.habitrpg.android.habitica.models.inventory.QuestRageStrike
import io.realm.RealmList
import io.realm.RealmModel
import io.realm.RealmObject
import java.util.*
class WorldState {
open class WorldState: RealmObject() {
var worldBossKey: String = ""
var worldBossActive: Boolean = false
var progress: QuestProgress? = null
var rageStrikes: MutableList<QuestRageStrike>? = null
var rageStrikes: RealmList<QuestRageStrike>? = null
var currentEventKey: String? = null
var currentEventPromo: String? = null
var currentEventStartDate: Date? = null
var currentEventEndDate: Date? = null
var npcImageSuffix: String? = null
var currentEvent: WorldStateEvent? = null
var events: RealmList<WorldStateEvent> = RealmList()
}

View file

@ -0,0 +1,24 @@
package com.habitrpg.android.habitica.models
import com.google.gson.annotations.SerializedName
import io.realm.RealmModel
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import java.util.*
open class WorldStateEvent: RealmObject(), BaseObject {
@PrimaryKey
@SerializedName("event")
var eventKey: String? = null
var start: Date? = null
var end: Date? = null
var promo: String? = null
var npcImageSuffix: String? = null
override val realmClass: Class<out RealmModel>
get() = WorldStateEvent::class.java
override val primaryIdentifier: String?
get() = eventKey
override val primaryIdentifierName: String
get() = "eventKey"
}

View file

@ -8,6 +8,7 @@ import android.content.res.Resources;
import androidx.preference.PreferenceManager;
import com.habitrpg.android.habitica.data.ApiClient;
import com.habitrpg.android.habitica.data.ContentRepository;
import com.habitrpg.android.habitica.executors.JobExecutor;
import com.habitrpg.android.habitica.executors.PostExecutionThread;
import com.habitrpg.android.habitica.executors.ThreadExecutor;
@ -126,7 +127,7 @@ public class AppModule {
@Provides
@Singleton
AppConfigManager providesRemoteConfigManager() {
return new AppConfigManager();
AppConfigManager providesRemoteConfigManager(ContentRepository contentRepository) {
return new AppConfigManager(contentRepository);
}
}

View file

@ -50,6 +50,16 @@ class AvatarView : View {
return getLayerMap(avatar, true)
}
private var spriteSubstitutions: Map<String, Map<String, String>> = HashMap()
get() {
if (Date().time - (lastSubstitutionCheck?.time ?: 0) > 180000) {
field = AppConfigManager(null).spriteSubstitutions()
lastSubstitutionCheck = Date()
}
return field
}
private var lastSubstitutionCheck: Date? = null
private val originalRect: Rect
get() = if (showMount || showPet) FULL_HERO_RECT else if (showBackground) COMPACT_HERO_RECT else HERO_ONLY_RECT
@ -151,8 +161,7 @@ class AvatarView : View {
}
private fun getLayerMap(avatar: Avatar, resetHasAttributes: Boolean): Map<LayerType, String> {
val substitutions = AppConfigManager().spriteSubstitutions()
val layerMap = getAvatarLayerMap(avatar, substitutions)
val layerMap = getAvatarLayerMap(avatar, spriteSubstitutions)
if (resetHasAttributes) {
hasPet = false
@ -162,7 +171,7 @@ class AvatarView : View {
var mountName = avatar.currentMount
if (showMount && mountName?.isNotEmpty() == true) {
mountName = substituteOrReturn(substitutions["mounts"], mountName)
mountName = substituteOrReturn(spriteSubstitutions["mounts"], mountName)
layerMap[LayerType.MOUNT_BODY] = "Mount_Body_$mountName"
layerMap[LayerType.MOUNT_HEAD] = "Mount_Head_$mountName"
if (resetHasAttributes) hasMount = true
@ -170,14 +179,14 @@ class AvatarView : View {
var petName = avatar.currentPet
if (showPet && petName?.isNotEmpty() == true) {
petName = substituteOrReturn(substitutions["pets"], petName)
petName = substituteOrReturn(spriteSubstitutions["pets"], petName)
layerMap[LayerType.PET] = "Pet-$petName"
if (resetHasAttributes) hasPet = true
}
var backgroundName = avatar.preferences?.background
if (showBackground && backgroundName?.isNotEmpty() == true) {
backgroundName = substituteOrReturn(substitutions["backgrounds"], backgroundName)
backgroundName = substituteOrReturn(spriteSubstitutions["backgrounds"], backgroundName)
layerMap[LayerType.BACKGROUND] = "background_$backgroundName"
if (resetHasAttributes) hasBackground = true
}

View file

@ -198,7 +198,7 @@ class GiftSubscriptionActivity : BaseActivity() {
}
private fun displayConfirmationDialog() {
val message = getString(if (appConfigManager.activePromo(this)?.identifier == "g1g1"){
val message = getString(if (appConfigManager.activePromo()?.identifier == "g1g1"){
R.string.gift_confirmation_text_sub_g1g1
} else {
R.string.gift_confirmation_text_sub

View file

@ -502,7 +502,7 @@ class NavigationDrawerFragment : DialogFragment() {
}
fun updatePromo() {
activePromo = context?.let { configManager.activePromo(it) }
activePromo = configManager.activePromo()
val promoItem = getItemWithIdentifier(SIDEBAR_PROMO) ?: return
if (activePromo != null) {
promoItem.isVisible = true

View file

@ -31,7 +31,7 @@ class PromoInfoFragment : BaseMainFragment<FragmentPromoInfoBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val promo = context?.let { configManager.activePromo(it) }
val promo = configManager.activePromo()
promo?.configureInfoFragment(this)
}

View file

@ -63,7 +63,7 @@ class GemsPurchaseFragment : BaseFragment<FragmentGemPurchaseBinding>(), GemPurc
binding?.headerImageView?.setImageResource(R.drawable.gem_purchase_header_dark)
}
val promo = context?.let { appConfigManager.activePromo(it) }
val promo = appConfigManager.activePromo()
if (promo != null) {
binding?.let {
promo.configurePurchaseBanner(it)

View file

@ -77,7 +77,7 @@ class SubscriptionFragment : BaseFragment<FragmentSubscriptionBinding>(), GemPur
binding?.subscriptionDetails?.visibility = View.GONE
binding?.subscriptionDetails?.onShowSubscriptionOptions = { showSubscriptionOptions() }
binding?.giftSubscriptionButton?.setOnClickListener { context?.let { context -> showGiftSubscriptionDialog(context, appConfigManager.activePromo(context)?.identifier == "g1g1") } }
binding?.giftSubscriptionButton?.setOnClickListener { context?.let { context -> showGiftSubscriptionDialog(context, appConfigManager.activePromo()?.identifier == "g1g1") } }
binding?.subscription1month?.setOnPurchaseClickListener { selectSubscription(PurchaseTypes.Subscription1Month) }
binding?.subscription3month?.setOnPurchaseClickListener { selectSubscription(PurchaseTypes.Subscription3Month) }
@ -86,7 +86,7 @@ class SubscriptionFragment : BaseFragment<FragmentSubscriptionBinding>(), GemPur
binding?.subscribeButton?.setOnClickListener { subscribeUser() }
val promo = context?.let { appConfigManager.activePromo(it) }
val promo = appConfigManager.activePromo()
if (promo != null) {
binding?.let {
promo.configurePurchaseBanner(it)

View file

@ -132,7 +132,7 @@ object DataBindingUtils {
private var spriteSubstitutions: Map<String, String> = HashMap()
get() {
if (Date().time - (lastSubstitutionCheck?.time ?: 0) > 180000) {
field = AppConfigManager().spriteSubstitutions().get("generic") ?: HashMap()
field = AppConfigManager(null).spriteSubstitutions()["generic"] ?: HashMap()
lastSubstitutionCheck = Date()
}
return field

View file

@ -5,8 +5,10 @@ import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.habitrpg.android.habitica.extensions.getAsString
import com.habitrpg.android.habitica.models.WorldState
import com.habitrpg.android.habitica.models.WorldStateEvent
import com.habitrpg.android.habitica.models.inventory.QuestProgress
import com.habitrpg.android.habitica.models.inventory.QuestRageStrike
import io.realm.RealmList
import java.lang.reflect.Type
import java.util.*
@ -37,7 +39,7 @@ class WorldStateSerialization: JsonDeserializer<WorldState> {
val extra = worldBossObject["extra"].asJsonObject
if (extra.has("worldDmg")) {
val worldDmg = extra["worldDmg"].asJsonObject
state.rageStrikes = mutableListOf()
state.rageStrikes = RealmList()
worldDmg.entrySet().forEach { (key, value) ->
val strike = QuestRageStrike(key, value.asBoolean)
state.rageStrikes?.add(strike)
@ -46,14 +48,20 @@ class WorldStateSerialization: JsonDeserializer<WorldState> {
}
}
state.npcImageSuffix = json?.asJsonObject.getAsString("npcImageSuffix")
try {
if (json?.asJsonObject?.has("currentEvent") == true && json.asJsonObject?.get("currentEvent")?.isJsonObject == true) {
val event = json.asJsonObject?.getAsJsonObject("currentEvent")
if (event != null) {
state.currentEventKey = event.getAsString("event")
state.currentEventPromo = if (event.has("promo")) event.getAsString("promo") else null
state.currentEventStartDate = context?.deserialize(event.get("start"), Date::class.java)
state.currentEventEndDate = context?.deserialize(event.get("end"), Date::class.java)
state.currentEvent = context?.deserialize(event, WorldStateEvent::class.java)
}
if (json.asJsonObject.has("events")) {
val events = RealmList<WorldStateEvent>()
for (element in json.asJsonObject.getAsJsonArray("events")) {
context?.deserialize<WorldStateEvent>(element, WorldStateEvent::class.java)?.let { events.add(it) }
}
state.events = events
}
}
} catch (e: Exception) {