Merge pull request #3 from HabitRPG/develop

fork update
This commit is contained in:
Alejandro Rios 2021-04-06 06:49:51 -05:00 committed by GitHub
commit 63bfbdff53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 409 additions and 274 deletions

View file

@ -150,8 +150,8 @@ 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
versionName "3.2.1"
versionCode 2895
versionName "3.2.2"
}
buildFeatures {

View file

@ -58,11 +58,13 @@
<ImageView
android:id="@+id/button_icon_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"/>
<TextView
android:id="@+id/price_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
</LinearLayout>
</LinearLayout>
<TextView

View file

@ -5,14 +5,14 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="?attr/colorContentBackground">
<RelativeLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_centerVertical="true"
android:paddingStart="@dimen/spacing_large"
android:paddingEnd="@dimen/spacing_large"
android:paddingBottom="20dp"
@ -31,12 +31,11 @@
</LinearLayout>
<View
android:id="@+id/rightBorderView"
android:layout_width="5dp"
android:layout_width="8dp"
android:layout_height="match_parent"
android:gravity="center"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true" />
</RelativeLayout>
android:background="@color/red_10"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"

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

@ -935,8 +935,8 @@
<string name="mount_category">%s Monturas</string>
<string name="pet_category">%s Mascotas</string>
<string name="challenge_task_name">%s Tareas del Desafío</string>
<string name="delete_x_tasks">Eliminar %s Tareas</string>
<string name="keep_x_tasks">Mantener %s Tareas</string>
<string name="delete_x_tasks">Eliminar %d Tareas</string>
<string name="keep_x_tasks">Mantener %d Tareas</string>
<string name="broken_challenge">Desafío Terminado</string>
<string name="pms_disabled_description">Tu puedes enviar mensajes todavía, pero nadie puede enviartelas a ti. Puedes habilitarlo de nuevo desde Configuraciones.</string>
<string name="pms_disabled">Los Mensajes Privados están desactivados</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="brand">@color/brand_400</color>
<color name="color_accent">@color/brand_400</color>
<color name="text_primary">@color/gray_600</color>
<color name="text_secondary">@color/gray_500</color>
@ -61,4 +62,4 @@
<color name="text_brand_white">@color/white</color>
<color name="lightly_tinted_background">@color/brand_50</color>
<color name="dialog_background">@color/gray_10</color>
</resources>
</resources>

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

@ -994,7 +994,7 @@
<string name="update_available">Güncelleme uygun: %1$s (%2$d)</string>
<string name="streak">Seri</string>
<string name="assigned_stat">Atanan İstatistik</string>
<string name="join_party_description_guild">Aşağıdaki kullanıcı adını bir arkadaşınıza verin veya potansiyel yoldaşlarla tanışmak için % s gidin!</string>
<string name="join_party_description_guild">Aşağıdaki kullanıcı adını bir arkadaşınıza verin veya potansiyel yoldaşlarla tanışmak için %s gidin!</string>
<string name="spooky_promo_info_prompt">Değerli Taş Satışı, bu yılki Sonbahar Galasının en sonunda tekrar gündeme geldi! Bu, her zamankinden daha fazla Mücevher almak için son bir şans, bu yüzden bu süre boyunca stok yapın!</string>
<string name="spooky_promo_info_limitations">Bu promosyon yalnızca sınırlı süreli etkinlik sırasında geçerlidir. Bu etkinlik 29 Ekim\'de 08:00 EDT\'de (12:00 UTC) başlayacak ve 2 Kasım 20:00 EDT\'de (00:00 UTC) sona erecek. Promosyon teklifi yalnızca kendiniz için Mücevher satın alırken geçerlidir.</string>
<string name="spooky_promo_info_instructions">29 Ekim ile 2 Kasım arasında, her zamanki gibi herhangi bir Mücevher paketi satın alın ve hesabınıza promosyon miktarda Mücevher yatırılacaktır. Gelecek sürümler için harcayabileceğiniz, paylaşabileceğiniz veya saklayabileceğiniz daha fazla Mücevher!</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

@ -5,6 +5,7 @@ import androidx.preference.PreferenceManager
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.ContentRepository
import com.habitrpg.android.habitica.data.local.ContentLocalRepository
import com.habitrpg.android.habitica.helpers.AprilFoolsHandler
import com.habitrpg.android.habitica.models.ContentResult
import com.habitrpg.android.habitica.models.WorldState
import com.habitrpg.android.habitica.models.inventory.SpecialItem
@ -43,16 +44,18 @@ 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()
for (event in it.events) {
if (event.aprilFools != null && event.isCurrentlyActive) {
AprilFoolsHandler.handle(event.aprilFools, event.end)
}
}
}
} 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
@ -38,16 +39,24 @@ class UserRepositoryImpl(localRepository: UserLocalRepository, apiClient: ApiCli
override fun getUser(userID: String): Flowable<User> = localRepository.getUser(userID)
override fun updateUser(updateData: Map<String, Any>): Flowable<User> {
private fun updateUser(userID: String, updateData: Map<String, Any>): Flowable<User> {
return Flowable.zip(apiClient.updateUser(updateData),
localRepository.getUser(userID).firstElement().toFlowable(),
{ newUser, user -> mergeUser(user, newUser) })
}
override fun updateUser(key: String, value: Any): Flowable<User> {
private fun updateUser(userID: String, key: String, value: Any): Flowable<User> {
val updateData = HashMap<String, Any>()
updateData[key] = value
return updateUser(updateData)
return updateUser(userID, updateData)
}
override fun updateUser(updateData: Map<String, Any>): Flowable<User> {
return updateUser(userID, updateData)
}
override fun updateUser(key: String, value: Any): Flowable<User> {
return updateUser(userID, key, value)
}
override fun retrieveUser(withTasks: Boolean): Flowable<User> =
@ -75,7 +84,7 @@ class UserRepositoryImpl(localRepository: UserLocalRepository, apiClient: ApiCli
val timeZone = calendar.timeZone
val offset = -TimeUnit.MINUTES.convert(timeZone.getOffset(calendar.timeInMillis).toLong(), TimeUnit.MILLISECONDS)
if (offset.toInt() != user.preferences?.timezoneOffset ?: 0) {
return@flatMap updateUser("preferences.timezoneOffset", offset.toString())
return@flatMap updateUser(user.id ?: "", "preferences.timezoneOffset", offset.toString())
} else {
return@flatMap Flowable.just(user)
}
@ -164,7 +173,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 +389,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 +433,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

@ -0,0 +1,32 @@
package com.habitrpg.android.habitica.helpers
import com.facebook.imagepipeline.request.BasePostprocessor
import com.habitrpg.android.habitica.helpers.postProcessors.InvertPostProcessor
import com.habitrpg.android.habitica.ui.AvatarView
import java.util.*
class AprilFoolsHandler {
companion object {
var eventEnd = Date(2020, 3, 1)
fun handle(name: String?, endDate: Date?) {
if (endDate != null) {
this.eventEnd = endDate
}
when(name) {
"invert" -> invertFools()
}
}
private fun invertFools() {
AvatarView.postProcessors[AvatarView.LayerType.PET] = {
if (Date().after(eventEnd)) {
null
} else {
InvertPostProcessor()
}
}
}
}
}

View file

@ -17,16 +17,13 @@ class HabiticaFirebaseMessagingService : FirebaseMessagingService() {
@Inject
internal lateinit var pushNotificationManager: PushNotificationManager
@Inject
internal lateinit var userRepository: UserRepository
override fun onMessageReceived(remoteMessage: RemoteMessage) {
userComponent?.inject(this)
if (this::pushNotificationManager.isInitialized) {
pushNotificationManager.displayNotification(remoteMessage)
if (remoteMessage.data["identifier"]?.contains(PushNotificationManager.WON_CHALLENGE_PUSH_NOTIFICATION_KEY) == true) {
userRepository.retrieveUser(true).subscribe({}, RxErrorHandler.handleEmptyError())
//userRepository.retrieveUser(true).subscribe({}, RxErrorHandler.handleEmptyError())
}
}
}

View file

@ -32,6 +32,9 @@ class PushNotificationManager(var apiClient: ApiClient, private val sharedPrefer
fun addPushDeviceUsingStoredToken() {
FirebaseMessaging.getInstance().token.addOnCompleteListener {
if (!it.isSuccessful) {
return@addOnCompleteListener
}
this.refreshedToken = it.result
if (this.refreshedToken.isEmpty() || this.user == null || this.userHasPushDevice() || !this.userIsSubscribedToNotifications()) {
return@addOnCompleteListener

View file

@ -0,0 +1,37 @@
package com.habitrpg.android.habitica.helpers.postProcessors
import android.graphics.*
import com.facebook.cache.common.CacheKey
import com.facebook.cache.common.SimpleCacheKey
import com.facebook.imagepipeline.request.BasePostprocessor
class InvertPostProcessor: BasePostprocessor() {
override fun getName(): String {
return "invertPostProcessor"
}
override fun process(bitmap: Bitmap) {
val canvas = Canvas(bitmap)
val paint = Paint()
val matrixInvert = ColorMatrix()
matrixInvert.set(floatArrayOf(
-1.0f, 0.0f, 0.0f, 0.0f, 255.0f,
0.0f, -1.0f, 0.0f, 0.0f, 255.0f,
0.0f, 0.0f, -1.0f, 0.0f, 255.0f,
0.0f, 0.0f, 0.0f, 1.0f, 0.0f
))
val filter = ColorMatrixColorFilter(matrixInvert)
paint.colorFilter = filter
canvas.drawBitmap(bitmap, 0f, 0f, paint)
}
override fun getPostprocessorCacheKey(): CacheKey {
return SimpleCacheKey("0")
}
}

View file

@ -1,18 +1,22 @@
package com.habitrpg.android.habitica.models
import com.google.gson.annotations.SerializedName
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
@SerializedName("currentEventList")
var events: RealmList<WorldStateEvent> = RealmList()
}

View file

@ -0,0 +1,31 @@
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 {
val isCurrentlyActive: Boolean
get() {
val now = Date()
return (start?.before(now) == true) && (end?.after(now) == true)
}
@PrimaryKey
@SerializedName("event")
var eventKey: String? = null
var start: Date? = null
var end: Date? = null
var promo: String? = null
var npcImageSuffix: String? = null
var aprilFools: 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

@ -1,48 +0,0 @@
package com.habitrpg.android.habitica.models.auth;
/**
* Created by magicmicky on 04/02/15.
*/
public class UserAuthResponse {
//we need apiToken and token, as both are possible returns
private String apiToken;
private String token;
private Boolean newUser = false;
private String id;
public String getToken() {
if (this.token == null) {
return this.apiToken;
} else {
return this.token;
}
}
public void setToken(String token) {
this.token = token;
}
public String getApiToken() {
return apiToken;
}
public void setApiToken(String apiToken) {
this.apiToken = apiToken;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Boolean getNewUser() {
return newUser;
}
public void setNewUser(Boolean newUser) {
this.newUser = newUser;
}
}

View file

@ -0,0 +1,15 @@
package com.habitrpg.android.habitica.models.auth
class UserAuthResponse {
//we need apiToken and token, as both are possible returns
var apiToken: String = ""
var token: String
get() {
return apiToken
}
set(value) {
apiToken = value
}
var newUser = false
var id: String = ""
}

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

@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.*
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.text.TextUtils
import android.util.AttributeSet
import android.util.Log
@ -15,6 +16,8 @@ import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
import com.facebook.drawee.view.DraweeHolder
import com.facebook.drawee.view.MultiDraweeHolder
import com.facebook.imagepipeline.image.ImageInfo
import com.facebook.imagepipeline.request.BasePostprocessor
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.helpers.AppConfigManager
@ -50,6 +53,15 @@ 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
@ -122,8 +134,14 @@ class AvatarView : View {
draweeHolder.topLevelDrawable?.callback = this
multiDraweeHolder.add(draweeHolder)
val uri = Uri.parse(IMAGE_URI_ROOT + DataBindingUtils.getFullFilename(layerName, null))
var request = ImageRequestBuilder.newBuilderWithSource(uri)
postProcessors[layerKey]?.let {
request = request.setPostprocessor(it())
}
val controller = Fresco.newDraweeControllerBuilder()
.setUri(IMAGE_URI_ROOT + DataBindingUtils.getFullFilename(layerName, null))
.setImageRequest(request.build())
.setControllerListener(object : BaseControllerListener<ImageInfo>() {
override fun onFinalImageSet(
id: String?,
@ -151,8 +169,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 +179,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 +187,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
}
@ -243,6 +260,7 @@ class AvatarView : View {
hasVisualBuffs = true
}
val hair = prefs.hair
if (!hasVisualBuffs) {
if (!TextUtils.isEmpty(prefs.chair)) {
layerMap[LayerType.CHAIR] = prefs.chair
@ -279,7 +297,6 @@ class AvatarView : View {
layerMap[LayerType.SHIRT] = prefs.size + "_shirt_" + prefs.shirt
layerMap[LayerType.HEAD_0] = "head_0"
val hair = prefs.hair
if (hair != null) {
val hairColor = hair.color
@ -295,17 +312,11 @@ class AvatarView : View {
if (hair.isAvailable(hair.beard)) {
layerMap[LayerType.HAIR_BEARD] = "hair_beard_" + hair.beard + "_" + hairColor
}
if (hair.isAvailable(hair.flower)) {
layerMap[LayerType.HAIR_FLOWER] = "hair_flower_" + hair.flower
}
}
} else {
val hair = prefs.hair
}
// Show flower all the time!
if (hair != null && hair.isAvailable(hair.flower)) {
layerMap[LayerType.HAIR_FLOWER] = "hair_flower_" + hair.flower
}
if (hair != null && hair.isAvailable(hair.flower)) {
layerMap[LayerType.HAIR_FLOWER] = "hair_flower_" + hair.flower
}
return layerMap
@ -409,7 +420,7 @@ class AvatarView : View {
val equals = currentLayers != null && currentLayers == newLayerMap
if (!equals) {
if (!equals || postProcessors.isNotEmpty()) {
multiDraweeHolder.clear()
numberLayersInProcess.set(0)
}
@ -521,5 +532,6 @@ class AvatarView : View {
private val COMPACT_HERO_RECT = Rect(0, 0, 114, 114)
private val HERO_ONLY_RECT = Rect(0, 0, 90, 90)
val postProcessors: MutableMap<LayerType, (() -> BasePostprocessor?)> = mutableMapOf()
}
}

View file

@ -371,7 +371,9 @@ class FullProfileActivity : BaseActivity() {
binding.equipmentTableLayout.removeAllViews()
for (index in 1 until binding.attributesTableLayout.childCount) {
binding.attributesTableLayout.removeViewAt(index)
if (binding.attributesTableLayout != null) {
binding.attributesTableLayout.removeViewAt(index)
}
}
addLevelAttributes(user)

View file

@ -86,6 +86,12 @@ class GiftSubscriptionActivity : BaseActivity() {
giftedUserID = it.id
giftedUsername = it.username
}, RxErrorHandler.handleEmptyError()))
if (appConfigManager.activePromo()?.identifier == "g1g1") {
binding.giftSubscriptionContainer.visibility = View.VISIBLE
} else {
binding.giftSubscriptionContainer.visibility = View.GONE
}
}
override fun onStart() {
@ -198,7 +204,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

@ -364,7 +364,7 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
hideProgress()
dismissKeyboard()
try {
saveTokens(userAuthResponse.token, userAuthResponse.id)
saveTokens(userAuthResponse.apiToken, userAuthResponse.id)
} catch (e: Exception) {
crashlyticsProxy.logException(e)
}
@ -375,7 +375,7 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
FirebaseAnalytics.getInstance(this).logEvent("user_registered", null)
}
compositeSubscription.add(userRepository.retrieveUser(true)
compositeSubscription.add(userRepository.retrieveUser(true, true)
.subscribe({
if (userAuthResponse.newUser) {
this.startSetupActivity()

View file

@ -364,7 +364,7 @@ open class MainActivity : BaseActivity(), TutorialView.OnTutorialReaction {
when (launchScreen) {
"/party" -> {
if (user?.party?.id != null) {
if (user == null || user?.party?.id != null) {
MainNavigationController.navigate(R.id.partyFragment)
}
}
@ -634,17 +634,18 @@ open class MainActivity : BaseActivity(), TutorialView.OnTutorialReaction {
protected fun retrieveUser(forced: Boolean = false) {
if (hostConfig.hasAuthentication()) {
compositeSubscription.add(this.userRepository.retrieveUser(true, forced)
.doOnNext { user1 ->
FirebaseAnalytics.getInstance(this).setUserProperty("has_party", if (user1.party?.id?.isNotEmpty() == true) "true" else "false")
FirebaseAnalytics.getInstance(this).setUserProperty("is_subscribed", if (user1.isSubscribed) "true" else "false")
pushNotificationManager.setUser(user1)
pushNotificationManager.addPushDeviceUsingStoredToken()
}
.flatMap { userRepository.retrieveTeamPlans() }
.flatMap { contentRepository.retrieveContent(this,false) }
.flatMap { contentRepository.retrieveWorldState(this) }
.subscribe({ }, RxErrorHandler.handleEmptyError()))
compositeSubscription.add(
contentRepository.retrieveWorldState(this)
.flatMap { userRepository.retrieveUser(true, forced) }
.doOnNext { user1 ->
FirebaseAnalytics.getInstance(this).setUserProperty("has_party", if (user1.party?.id?.isNotEmpty() == true) "true" else "false")
FirebaseAnalytics.getInstance(this).setUserProperty("is_subscribed", if (user1.isSubscribed) "true" else "false")
pushNotificationManager.setUser(user1)
pushNotificationManager.addPushDeviceUsingStoredToken()
}
.flatMap { userRepository.retrieveTeamPlans() }
.flatMap { contentRepository.retrieveContent(this) }
.subscribe({ }, RxErrorHandler.handleEmptyError()))
}
}

View file

@ -3,6 +3,7 @@ package com.habitrpg.android.habitica.ui.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.SkillTaskItemCardBinding
@ -57,7 +58,7 @@ class SkillTasksRecyclerViewAdapter : BaseRecyclerViewAdapter<Task, SkillTasksRe
binding.notesTextView.visibility = View.VISIBLE
binding.notesTextView.text = task.markdownNotes { binding.notesTextView.text = it }
}
binding.rightBorderView.setBackgroundResource(task.lightTaskColor)
binding.rightBorderView.setBackgroundColor(ContextCompat.getColor(itemView.context, task.lightTaskColor))
}
override fun onClick(v: View) {

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

@ -164,6 +164,9 @@ open class ShopFragment : BaseMainFragment<FragmentRecyclerviewBinding>() {
}
if (shop1.identifier == Shop.TIME_TRAVELERS_SHOP) {
formatTimeTravelersShop(shop1)
} else if (shop1.identifier == Shop.SEASONAL_SHOP) {
shop1.categories.reverse()
shop1
} else {
shop1
}

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

@ -14,7 +14,6 @@ import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.activities.MainActivity
import com.habitrpg.android.habitica.ui.activities.SkillMemberActivity
import com.habitrpg.android.habitica.ui.adapter.tasks.RewardsRecyclerViewAdapter
@ -123,7 +122,7 @@ class RewardsRecyclerviewFragment : TaskRecyclerViewFragment() {
fun newInstance(context: Context?, classType: String, showCustomRewards: Boolean): RewardsRecyclerviewFragment {
val fragment = RewardsRecyclerviewFragment()
fragment.retainInstance = true
fragment.classType = classType
fragment.taskType = classType
fragment.showCustomRewards = showCustomRewards
if (context != null) {

View file

@ -28,8 +28,6 @@ import com.habitrpg.android.habitica.helpers.TaskFilterHelper
import com.habitrpg.android.habitica.models.responses.TaskDirection
import com.habitrpg.android.habitica.models.responses.TaskScoringResult
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.activities.MainActivity
import com.habitrpg.android.habitica.ui.activities.TaskFormActivity
import com.habitrpg.android.habitica.ui.adapter.BaseRecyclerViewAdapter
@ -41,10 +39,10 @@ import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Named
open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBinding>(), androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener {
internal var canEditTasks: Boolean = true
@ -55,6 +53,7 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
return FragmentRefreshRecyclerviewBinding.inflate(inflater, container, false)
}
protected var recyclerSubscription: CompositeDisposable = CompositeDisposable()
var recyclerAdapter: TaskRecyclerViewAdapter? = null
var itemAnimator = SafeDefaultItemAnimator()
var ownerID: String = ""
@ -75,29 +74,28 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
internal var layoutManager: RecyclerView.LayoutManager? = null
internal var classType: String? = null
internal var taskType: String = Task.TYPE_HABIT
private var itemTouchCallback: ItemTouchHelper.Callback? = null
var refreshAction: ((() -> Unit) -> Unit)? = null
internal val className: String
get() = this.classType ?: ""
get() = this.taskType
// TODO needs a bit of cleanup
private fun setInnerAdapter() {
val adapter: BaseRecyclerViewAdapter<*, *>? = when (this.classType) {
Task.TYPE_HABIT -> {
HabitsRecyclerViewAdapter(null, true, R.layout.habit_item_card, taskFilterHelper)
}
Task.TYPE_DAILY -> {
DailiesRecyclerViewHolder(null, true, R.layout.daily_item_card, taskFilterHelper)
}
Task.TYPE_TODO -> {
TodosRecyclerViewAdapter(null, true, R.layout.todo_item_card, taskFilterHelper)
}
Task.TYPE_REWARD -> {
RewardsRecyclerViewAdapter(null, R.layout.reward_item_card)
}
if (binding?.recyclerView?.adapter != null && binding?.recyclerView?.adapter == recyclerAdapter && !recyclerSubscription.isDisposed) {
return
}
if (!recyclerSubscription.isDisposed) {
recyclerSubscription.dispose()
}
recyclerSubscription = CompositeDisposable()
val adapter: BaseRecyclerViewAdapter<*, *>? = when (this.taskType) {
Task.TYPE_HABIT -> HabitsRecyclerViewAdapter(null, true, R.layout.habit_item_card, taskFilterHelper)
Task.TYPE_DAILY -> DailiesRecyclerViewHolder(null, true, R.layout.daily_item_card, taskFilterHelper)
Task.TYPE_TODO -> TodosRecyclerViewAdapter(null, true, R.layout.todo_item_card, taskFilterHelper)
Task.TYPE_REWARD -> RewardsRecyclerViewAdapter(null, R.layout.reward_item_card)
else -> null
}
@ -106,10 +104,29 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
binding?.recyclerView?.adapter = adapter
context?.let { recyclerAdapter?.taskDisplayMode = configManager.taskDisplayMode(it) }
recyclerAdapter?.errorButtonEvents?.subscribe({
taskRepository.syncErroredTasks().subscribe({}, RxErrorHandler.handleEmptyError())
}, RxErrorHandler.handleEmptyError())?.let { recyclerSubscription.add(it) }
recyclerAdapter?.taskOpenEvents?.subscribeWithErrorHandler {
openTaskForm(it)
}?.let { recyclerSubscription.add(it) }
recyclerAdapter?.taskScoreEvents
?.doOnNext { playSound(it.second) }
?.subscribeWithErrorHandler { scoreTask(it.first, it.second) }?.let { recyclerSubscription.add(it) }
recyclerAdapter?.checklistItemScoreEvents
?.flatMap { taskRepository.scoreChecklistItem(it.first.id ?: "", it.second.id ?: "")
}?.subscribeWithErrorHandler {}?.let { recyclerSubscription.add(it) }
recyclerAdapter?.brokenTaskEvents?.subscribeWithErrorHandler { showBrokenChallengeDialog(it) }?.let { recyclerSubscription.add(it) }
recyclerSubscription.add(taskRepository.getTasks(this.taskType, ownerID).subscribe({
this.recyclerAdapter?.updateUnfilteredData(it)
this.recyclerAdapter?.filter()
}, RxErrorHandler.handleEmptyError()))
}
private fun handleTaskResult(result: TaskScoringResult, value: Int) {
if (classType == Task.TYPE_REWARD) {
if (taskType == Task.TYPE_REWARD) {
(activity as? MainActivity)?.let { activity ->
HabiticaSnackbar.showSnackbar(activity.snackbarContainer, null, getString(R.string.notification_purchase_reward),
BitmapDrawable(resources, HabiticaIconsHelper.imageOfGold()),
@ -123,7 +140,7 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
}
private fun playSound(direction: TaskDirection) {
val soundName = when (classType) {
val soundName = when (taskType) {
Task.TYPE_HABIT -> if (direction == TaskDirection.UP) SoundManager.SoundPlusHabit else SoundManager.SoundMinusHabit
Task.TYPE_DAILY -> SoundManager.SoundDaily
Task.TYPE_TODO -> SoundManager.SoundTodo
@ -160,18 +177,20 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.let { binding?.recyclerView?.setBackgroundColor(ContextCompat.getColor(it, R.color.content_background)) }
savedInstanceState?.let { this.classType = savedInstanceState.getString(CLASS_TYPE_KEY, "") }
savedInstanceState?.let { this.taskType = savedInstanceState.getString(CLASS_TYPE_KEY, "") }
when (classType) {
when (taskType) {
Task.TYPE_TODO -> taskFilterHelper.setActiveFilter(Task.TYPE_TODO, Task.FILTER_ACTIVE)
Task.TYPE_DAILY -> {
val user = (activity as? MainActivity)?.user
if (user?.isValid == true && user?.preferences?.dailyDueDefaultView == true) {
if (user?.isValid == true && user.preferences?.dailyDueDefaultView == true) {
taskFilterHelper.setActiveFilter(Task.TYPE_DAILY, Task.FILTER_ACTIVE)
}
}
}
recyclerAdapter?.filter()
itemTouchCallback = object : ItemTouchHelper.Callback() {
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
@ -221,7 +240,7 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
if (validTaskId != null) {
recyclerAdapter?.ignoreUpdates = true
compositeSubscription.add(taskRepository.updateTaskPosition(
classType ?: "", validTaskId, viewHolder.adapterPosition
taskType, validTaskId, viewHolder.adapterPosition
)
.delay(1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
@ -233,40 +252,15 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
}
binding?.recyclerView?.setScaledPadding(context, 0, 0, 0, 48)
binding?.recyclerView?.adapter = recyclerAdapter as? RecyclerView.Adapter<*>
recyclerAdapter?.filter()
layoutManager = getLayoutManager(context)
layoutManager?.isItemPrefetchEnabled = false
binding?.recyclerView?.layoutManager = layoutManager
if (binding?.recyclerView?.adapter == null) {
this.setInnerAdapter()
}
this.setInnerAdapter()
allowReordering()
if (this.classType != null) {
recyclerAdapter?.errorButtonEvents?.subscribe({
taskRepository.syncErroredTasks().subscribe({}, RxErrorHandler.handleEmptyError())
}, RxErrorHandler.handleEmptyError())?.let { compositeSubscription.add(it) }
recyclerAdapter?.taskOpenEvents?.subscribeWithErrorHandler {
openTaskForm(it)
}?.let { compositeSubscription.add(it) }
recyclerAdapter?.taskScoreEvents
?.doOnNext { playSound(it.second) }
?.subscribeWithErrorHandler { scoreTask(it.first, it.second) }?.let { compositeSubscription.add(it) }
recyclerAdapter?.checklistItemScoreEvents
?.flatMap { taskRepository.scoreChecklistItem(it.first.id ?: "", it.second.id ?: "")
}?.subscribeWithErrorHandler {}?.let { compositeSubscription.add(it) }
recyclerAdapter?.brokenTaskEvents?.subscribeWithErrorHandler { showBrokenChallengeDialog(it) }?.let { compositeSubscription.add(it) }
compositeSubscription.add(taskRepository.getTasks(this.classType ?: "", ownerID).subscribe({
this.recyclerAdapter?.updateUnfilteredData(it)
this.recyclerAdapter?.filter()
}, RxErrorHandler.handleEmptyError()))
}
val bottomPadding = ((binding?.recyclerView?.paddingBottom ?: 0) + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 60f, resources.displayMetrics)).toInt()
binding?.recyclerView?.setPadding(0, 0, 0, bottomPadding)
binding?.recyclerView?.itemAnimator = itemAnimator
@ -319,52 +313,50 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
}
private fun setEmptyLabels() {
if (this.classType != null) {
binding?.recyclerView?.setEmptyView(binding?.emptyView)
context?.let { binding?.emptyIconView?.setColorFilter(ContextCompat.getColor(it, R.color.text_dimmed), android.graphics.PorterDuff.Mode.MULTIPLY) }
if (taskFilterHelper.howMany(classType) > 0) {
when (this.classType) {
Task.TYPE_HABIT -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_habits)
binding?.emptyViewTitle?.setText(R.string.empty_title_habits_filtered)
binding?.emptyViewDescription?.setText(R.string.empty_description_habits_filtered)
}
Task.TYPE_DAILY -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_dailies)
binding?.emptyViewTitle?.setText(R.string.empty_title_dailies_filtered)
binding?.emptyViewDescription?.setText(R.string.empty_description_dailies_filtered)
}
Task.TYPE_TODO -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_todos)
binding?.emptyViewTitle?.setText(R.string.empty_title_todos_filtered)
binding?.emptyViewDescription?.setText(R.string.empty_description_todos_filtered)
}
Task.TYPE_REWARD -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_rewards)
binding?.emptyViewTitle?.setText(R.string.empty_title_rewards)
}
binding?.recyclerView?.setEmptyView(binding?.emptyView)
context?.let { binding?.emptyIconView?.setColorFilter(ContextCompat.getColor(it, R.color.text_dimmed), android.graphics.PorterDuff.Mode.MULTIPLY) }
if (taskFilterHelper.howMany(taskType) > 0) {
when (this.taskType) {
Task.TYPE_HABIT -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_habits)
binding?.emptyViewTitle?.setText(R.string.empty_title_habits_filtered)
binding?.emptyViewDescription?.setText(R.string.empty_description_habits_filtered)
}
} else {
when (this.classType) {
Task.TYPE_HABIT -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_habits)
binding?.emptyViewTitle?.setText(R.string.empty_title_habits)
binding?.emptyViewDescription?.setText(R.string.empty_description_habits)
}
Task.TYPE_DAILY -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_dailies)
binding?.emptyViewTitle?.setText(R.string.empty_title_dailies)
binding?.emptyViewDescription?.setText(R.string.empty_description_dailies)
}
Task.TYPE_TODO -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_todos)
binding?.emptyViewTitle?.setText(R.string.empty_title_todos)
binding?.emptyViewDescription?.setText(R.string.empty_description_todos)
}
Task.TYPE_REWARD -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_rewards)
binding?.emptyViewTitle?.setText(R.string.empty_title_rewards)
}
Task.TYPE_DAILY -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_dailies)
binding?.emptyViewTitle?.setText(R.string.empty_title_dailies_filtered)
binding?.emptyViewDescription?.setText(R.string.empty_description_dailies_filtered)
}
Task.TYPE_TODO -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_todos)
binding?.emptyViewTitle?.setText(R.string.empty_title_todos_filtered)
binding?.emptyViewDescription?.setText(R.string.empty_description_todos_filtered)
}
Task.TYPE_REWARD -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_rewards)
binding?.emptyViewTitle?.setText(R.string.empty_title_rewards)
}
}
} else {
when (this.taskType) {
Task.TYPE_HABIT -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_habits)
binding?.emptyViewTitle?.setText(R.string.empty_title_habits)
binding?.emptyViewDescription?.setText(R.string.empty_description_habits)
}
Task.TYPE_DAILY -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_dailies)
binding?.emptyViewTitle?.setText(R.string.empty_title_dailies)
binding?.emptyViewDescription?.setText(R.string.empty_description_dailies)
}
Task.TYPE_TODO -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_todos)
binding?.emptyViewTitle?.setText(R.string.empty_title_todos)
binding?.emptyViewDescription?.setText(R.string.empty_description_todos)
}
Task.TYPE_REWARD -> {
binding?.emptyIconView?.setImageResource(R.drawable.icon_rewards)
binding?.emptyViewTitle?.setText(R.string.empty_title_rewards)
}
}
}
@ -378,11 +370,11 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(CLASS_TYPE_KEY, this.classType)
outState.putString(CLASS_TYPE_KEY, this.taskType)
}
override val displayedClassName: String?
get() = this.classType + super.displayedClassName
get() = this.taskType + super.displayedClassName
override fun onRefresh() {
binding?.refreshLayout?.isRefreshing = true
@ -391,13 +383,22 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
}
}
override fun onStart() {
super.onStart()
if (binding?.recyclerView?.adapter == null || recyclerAdapter == null) {
setInnerAdapter()
}
}
override fun onResume() {
super.onResume()
context?.let { recyclerAdapter?.taskDisplayMode = configManager.taskDisplayMode(it) }
setInnerAdapter()
recyclerAdapter?.filter()
}
fun setActiveFilter(activeFilter: String) {
taskFilterHelper.setActiveFilter(classType ?: "", activeFilter)
taskFilterHelper.setActiveFilter(taskType, activeFilter)
recyclerAdapter?.filter()
setEmptyLabels()
@ -431,10 +432,10 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
fun newInstance(context: Context?, classType: String): TaskRecyclerViewFragment {
val fragment = TaskRecyclerViewFragment()
fragment.retainInstance = true
fragment.classType = classType
fragment.taskType = classType
var tutorialTexts: List<String>? = null
if (context != null) {
when (fragment.classType) {
when (fragment.taskType) {
Task.TYPE_HABIT -> {
fragment.tutorialStepIdentifier = "habits"
tutorialTexts = listOf(context.getString(R.string.tutorial_overview), context.getString(R.string.tutorial_habits_1), context.getString(R.string.tutorial_habits_2), context.getString(R.string.tutorial_habits_3), context.getString(R.string.tutorial_habits_4))

View file

@ -18,7 +18,6 @@ import com.habitrpg.android.habitica.extensions.getThemeColor
import com.habitrpg.android.habitica.extensions.setTintWith
import com.habitrpg.android.habitica.helpers.*
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.activities.TaskFormActivity
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
@ -178,7 +177,7 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
disposable = tagRepository.getTags().subscribe({ tagsList -> dialog.setTags(tagsList)}, RxErrorHandler.handleEmptyError())
dialog.setActiveTags(taskFilterHelper.tags)
if (activeFragment != null) {
val taskType = activeFragment?.classType
val taskType = activeFragment?.taskType
if (taskType != null) {
dialog.setTaskType(taskType, taskFilterHelper.getActiveFilter(taskType))
}
@ -257,9 +256,9 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
}
private fun updateFilterIcon() {
val filterCount = taskFilterHelper.howMany(activeFragment?.classType)
val filterCount = taskFilterHelper.howMany(activeFragment?.taskType)
filterMenuItem?.isVisible = activeFragment?.classType != Task.TYPE_REWARD
filterMenuItem?.isVisible = activeFragment?.taskType != Task.TYPE_REWARD
if (filterCount == 0) {
filterMenuItem?.setIcon(R.drawable.ic_action_filter_list)
context?.let {

View file

@ -178,7 +178,7 @@ class TeamBoardFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchVi
disposable = tagRepository.getTags().subscribe({ tagsList -> dialog.setTags(tagsList)}, RxErrorHandler.handleEmptyError())
dialog.setActiveTags(taskFilterHelper.tags)
if (activeFragment != null) {
val taskType = activeFragment?.classType
val taskType = activeFragment?.taskType
if (taskType != null) {
dialog.setTaskType(taskType, taskFilterHelper.getActiveFilter(taskType))
}
@ -265,7 +265,7 @@ class TeamBoardFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchVi
}
var filterCount = 0
if (activeFragment != null) {
filterCount = taskFilterHelper.howMany(activeFragment?.classType)
filterCount = taskFilterHelper.howMany(activeFragment?.taskType)
}
if (filterCount == 0) {
filterMenuItem?.setIcon(R.drawable.ic_action_filter_list)

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

@ -67,7 +67,9 @@ class ChallengeDeserializer : JsonDeserializer<Challenge>, JsonSerializer<Challe
challenge.updatedAt = context.deserialize(jsonObject.get("updatedAt"), Date::class.java)
}
challenge.summary = jsonObject.get("summary").asString
if (jsonObject.has("summary")) {
challenge.summary = jsonObject.get("summary").asString
}
val groupElement = jsonObject.get("group")

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("currentEventList")) {
val events = RealmList<WorldStateEvent>()
for (element in json.asJsonObject.getAsJsonArray("currentEventList")) {
context?.deserialize<WorldStateEvent>(element, WorldStateEvent::class.java)?.let { events.add(it) }
}
state.events = events
}
}
} catch (e: Exception) {

View file

@ -14,12 +14,12 @@ buildscript {
classpath 'com.android.tools.build:gradle:4.1.2'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
classpath 'com.google.gms:google-services:4.3.5'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.0'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1'
classpath "io.realm:realm-gradle-plugin:10.3.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.1.0"
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0"
classpath 'com.google.firebase:perf-plugin:1.3.4'
classpath 'com.google.firebase:perf-plugin:1.3.5'
}
}

View file

@ -1 +1 @@
In this update we've cleaned up Guilds and Challenges to look better than ever. Check them out and maybe find something new! Starting or leaving a Quest will update the Party view right away. Snowballs will actually transform your party members now. Certain notifications work better, bringing you to the right place and no longer crashing the app in some cases. Subscriber Mystery Boxes have a fancy new view when opening as well. Check these out and lots of other fixes by downloading the update!
Weve added the ability to select different default launch screens! Do you mainly use Dailies or find yourself constantly checking off To Dos? Now you can set the app to automatically launch to either of those screens, and more! Go to Settings to explore the various launch screen options. Weve also fixed a few bugs with views not updating automatically. Hope you enjoy this newest Habitica update!