format fixes

This commit is contained in:
Phillip Thelen 2024-04-22 16:16:28 +02:00
parent ce28bf83f3
commit bf62f5ac16
30 changed files with 4481 additions and 3777 deletions

View file

@ -52,7 +52,7 @@ interface ApiService {
@GET("status")
suspend fun getStatus(): HabitResponse<Status>
/* user API */
// user API
@GET("user/")
suspend fun getUser(): HabitResponse<User>
@ -61,7 +61,10 @@ interface ApiService {
suspend fun syncUserStats(): HabitResponse<User>
@GET("inbox/messages")
suspend fun getInboxMessages(@Query("conversation") uuid: String, @Query("page") page: Int): HabitResponse<List<ChatMessage>>
suspend fun getInboxMessages(
@Query("conversation") uuid: String,
@Query("page") page: Int,
): HabitResponse<List<ChatMessage>>
@GET("inbox/conversations")
suspend fun getInboxConversations(): HabitResponse<List<InboxConversation>>
@ -73,113 +76,192 @@ interface ApiService {
suspend fun worldState(): HabitResponse<WorldState>
@GET("content")
suspend fun getContent(@Query("language") language: String?): HabitResponse<ContentResult>
suspend fun getContent(
@Query("language") language: String?,
): HabitResponse<ContentResult>
@PUT("user/")
suspend fun updateUser(@Body updateDictionary: Map<String, Any?>): HabitResponse<User>
suspend fun updateUser(
@Body updateDictionary: Map<String, Any?>,
): HabitResponse<User>
@PUT("user/")
suspend fun registrationLanguage(@Header("Accept-Language") registrationLanguage: String): HabitResponse<User>
suspend fun registrationLanguage(
@Header("Accept-Language") registrationLanguage: String,
): HabitResponse<User>
@GET("user/in-app-rewards")
suspend fun retrieveInAppRewards(): HabitResponse<List<ShopItem>>
@POST("user/equip/{type}/{key}")
suspend fun equipItem(@Path("type") type: String, @Path("key") itemKey: String): HabitResponse<Items>
suspend fun equipItem(
@Path("type") type: String,
@Path("key") itemKey: String,
): HabitResponse<Items>
@POST("user/buy/{key}")
suspend fun buyItem(@Path("key") itemKey: String, @Body quantity: Map<String, Int>): HabitResponse<BuyResponse>
suspend fun buyItem(
@Path("key") itemKey: String,
@Body quantity: Map<String, Int>,
): HabitResponse<BuyResponse>
@POST("user/purchase/{type}/{key}")
suspend fun purchaseItem(
@Path("type") type: String,
@Path("key") itemKey: String,
@Body quantity: Map<String, Int>
@Body quantity: Map<String, Int>,
): HabitResponse<Void>
@POST("user/purchase-hourglass/{type}/{key}")
suspend fun purchaseHourglassItem(@Path("type") type: String, @Path("key") itemKey: String): HabitResponse<Void>
suspend fun purchaseHourglassItem(
@Path("type") type: String,
@Path("key") itemKey: String,
): HabitResponse<Void>
@POST("user/buy-mystery-set/{key}")
suspend fun purchaseMysterySet(@Path("key") itemKey: String): HabitResponse<Void>
suspend fun purchaseMysterySet(
@Path("key") itemKey: String,
): HabitResponse<Void>
@POST("user/buy-quest/{key}")
suspend fun purchaseQuest(@Path("key") key: String): HabitResponse<Void>
suspend fun purchaseQuest(
@Path("key") key: String,
): HabitResponse<Void>
@POST("user/buy-special-spell/{key}")
suspend fun purchaseSpecialSpell(@Path("key") key: String): HabitResponse<Void>
suspend fun purchaseSpecialSpell(
@Path("key") key: String,
): HabitResponse<Void>
@POST("user/sell/{type}/{key}")
suspend fun sellItem(@Path("type") itemType: String, @Path("key") itemKey: String): HabitResponse<User>
suspend fun sellItem(
@Path("type") itemType: String,
@Path("key") itemKey: String,
): HabitResponse<User>
@POST("user/feed/{pet}/{food}")
suspend fun feedPet(@Path("pet") petKey: String, @Path("food") foodKey: String): HabitResponse<FeedResponse>
suspend fun feedPet(
@Path("pet") petKey: String,
@Path("food") foodKey: String,
): HabitResponse<FeedResponse>
@POST("user/hatch/{egg}/{hatchingPotion}")
suspend fun hatchPet(@Path("egg") eggKey: String, @Path("hatchingPotion") hatchingPotionKey: String): HabitResponse<Items>
suspend fun hatchPet(
@Path("egg") eggKey: String,
@Path("hatchingPotion") hatchingPotionKey: String,
): HabitResponse<Items>
@GET("tasks/user")
suspend fun getTasks(@Query("type") type: String): HabitResponse<TaskList>
suspend fun getTasks(
@Query("type") type: String,
): HabitResponse<TaskList>
@GET("tasks/user")
suspend fun getTasks(@Query("type") type: String, @Query("dueDate") dueDate: String): HabitResponse<TaskList>
suspend fun getTasks(
@Query("type") type: String,
@Query("dueDate") dueDate: String,
): HabitResponse<TaskList>
@POST("user/unlock")
suspend fun unlockPath(@Query("path") path: String): HabitResponse<UnlockResponse>
suspend fun unlockPath(
@Query("path") path: String,
): HabitResponse<UnlockResponse>
@GET("tasks/{id}")
suspend fun getTask(@Path("id") id: String): HabitResponse<Task>
suspend fun getTask(
@Path("id") id: String,
): HabitResponse<Task>
@POST("tasks/{id}/score/{direction}")
suspend fun postTaskDirection(@Path("id") id: String, @Path("direction") direction: String): HabitResponse<TaskDirectionData>
suspend fun postTaskDirection(
@Path("id") id: String,
@Path("direction") direction: String,
): HabitResponse<TaskDirectionData>
@POST("tasks/bulk-score")
suspend fun bulkScoreTasks(@Body data: List<Map<String, String>>): HabitResponse<BulkTaskScoringData>
suspend fun bulkScoreTasks(
@Body data: List<Map<String, String>>,
): HabitResponse<BulkTaskScoringData>
@POST("tasks/{id}/move/to/{position}")
suspend fun postTaskNewPosition(@Path("id") id: String, @Path("position") position: Int): HabitResponse<List<String>>
suspend fun postTaskNewPosition(
@Path("id") id: String,
@Path("position") position: Int,
): HabitResponse<List<String>>
@POST("tasks/{taskId}/checklist/{itemId}/score")
suspend fun scoreChecklistItem(@Path("taskId") taskId: String, @Path("itemId") itemId: String): HabitResponse<Task>
suspend fun scoreChecklistItem(
@Path("taskId") taskId: String,
@Path("itemId") itemId: String,
): HabitResponse<Task>
@POST("tasks/user")
suspend fun createTask(@Body item: Task): HabitResponse<Task>
suspend fun createTask(
@Body item: Task,
): HabitResponse<Task>
@POST("tasks/group/{groupId}")
suspend fun createGroupTask(@Path("groupId") groupId: String, @Body item: Task): HabitResponse<Task>
suspend fun createGroupTask(
@Path("groupId") groupId: String,
@Body item: Task,
): HabitResponse<Task>
@POST("tasks/user")
suspend fun createTasks(@Body tasks: List<Task>): HabitResponse<List<Task>>
suspend fun createTasks(
@Body tasks: List<Task>,
): HabitResponse<List<Task>>
@PUT("tasks/{id}")
suspend fun updateTask(@Path("id") id: String, @Body item: Task): HabitResponse<Task>
suspend fun updateTask(
@Path("id") id: String,
@Body item: Task,
): HabitResponse<Task>
@DELETE("tasks/{id}")
suspend fun deleteTask(@Path("id") id: String): HabitResponse<Void>
suspend fun deleteTask(
@Path("id") id: String,
): HabitResponse<Void>
@POST("tags")
suspend fun createTag(@Body tag: Tag): HabitResponse<Tag>
suspend fun createTag(
@Body tag: Tag,
): HabitResponse<Tag>
@PUT("tags/{id}")
suspend fun updateTag(@Path("id") id: String, @Body tag: Tag): HabitResponse<Tag>
suspend fun updateTag(
@Path("id") id: String,
@Body tag: Tag,
): HabitResponse<Tag>
@DELETE("tags/{id}")
suspend fun deleteTag(@Path("id") id: String): HabitResponse<Void>
suspend fun deleteTag(
@Path("id") id: String,
): HabitResponse<Void>
@POST("user/auth/local/register")
suspend fun registerUser(@Body auth: UserAuth): HabitResponse<UserAuthResponse>
suspend fun registerUser(
@Body auth: UserAuth,
): HabitResponse<UserAuthResponse>
@POST("user/auth/local/login")
suspend fun connectLocal(@Body auth: UserAuth): HabitResponse<UserAuthResponse>
suspend fun connectLocal(
@Body auth: UserAuth,
): HabitResponse<UserAuthResponse>
@POST("user/auth/social")
suspend fun connectSocial(@Body auth: UserAuthSocial): HabitResponse<UserAuthResponse>
suspend fun connectSocial(
@Body auth: UserAuthSocial,
): HabitResponse<UserAuthResponse>
@DELETE("user/auth/social/{network}")
suspend fun disconnectSocial(@Path("network") network: String): HabitResponse<Void>
suspend fun disconnectSocial(
@Path("network") network: String,
): HabitResponse<Void>
@POST("user/auth/apple")
suspend fun loginApple(@Body auth: Map<String, Any>): HabitResponse<UserAuthResponse>
suspend fun loginApple(
@Body auth: Map<String, Any>,
): HabitResponse<UserAuthResponse>
@POST("user/sleep")
suspend fun sleep(): HabitResponse<Boolean>
@ -191,17 +273,22 @@ interface ApiService {
suspend fun useSkill(
@Path("skill") skillName: String,
@Query("targetType") targetType: String,
@Query("targetId") targetId: String
@Query("targetId") targetId: String,
): HabitResponse<SkillResponse>
@POST("user/class/cast/{skill}")
suspend fun useSkill(@Path("skill") skillName: String, @Query("targetType") targetType: String): HabitResponse<SkillResponse>
suspend fun useSkill(
@Path("skill") skillName: String,
@Query("targetType") targetType: String,
): HabitResponse<SkillResponse>
@POST("user/change-class")
suspend fun changeClass(): HabitResponse<User>
@POST("user/change-class")
suspend fun changeClass(@Query("class") className: String): HabitResponse<User>
suspend fun changeClass(
@Query("class") className: String,
): HabitResponse<User>
@POST("user/disable-classes")
suspend fun disableClasses(): HabitResponse<User>
@ -209,189 +296,301 @@ interface ApiService {
@POST("user/mark-pms-read")
suspend fun markPrivateMessagesRead(): Void?
/* Group API */
// Group API
@GET("groups")
suspend fun listGroups(@Query("type") type: String): HabitResponse<List<Group>>
suspend fun listGroups(
@Query("type") type: String,
): HabitResponse<List<Group>>
@GET("groups/{gid}")
suspend fun getGroup(@Path("gid") groupId: String): HabitResponse<Group>
suspend fun getGroup(
@Path("gid") groupId: String,
): HabitResponse<Group>
@POST("groups")
suspend fun createGroup(@Body item: Group): HabitResponse<Group>
suspend fun createGroup(
@Body item: Group,
): HabitResponse<Group>
@PUT("groups/{id}")
suspend fun updateGroup(@Path("id") id: String, @Body item: Group): HabitResponse<Group>
suspend fun updateGroup(
@Path("id") id: String,
@Body item: Group,
): HabitResponse<Group>
@POST("groups/{groupID}/removeMember/{userID}")
suspend fun removeMemberFromGroup(@Path("groupID") groupID: String, @Path("userID") userID: String): HabitResponse<Void>
suspend fun removeMemberFromGroup(
@Path("groupID") groupID: String,
@Path("userID") userID: String,
): HabitResponse<Void>
@GET("groups/{gid}/chat")
suspend fun listGroupChat(@Path("gid") groupId: String): HabitResponse<List<ChatMessage>>
suspend fun listGroupChat(
@Path("gid") groupId: String,
): HabitResponse<List<ChatMessage>>
@POST("groups/{gid}/join")
suspend fun joinGroup(@Path("gid") groupId: String): HabitResponse<Group>
suspend fun joinGroup(
@Path("gid") groupId: String,
): HabitResponse<Group>
@POST("groups/{gid}/leave")
suspend fun leaveGroup(@Path("gid") groupId: String, @Query("keepChallenges") keepChallenges: String): HabitResponse<Void>
suspend fun leaveGroup(
@Path("gid") groupId: String,
@Query("keepChallenges") keepChallenges: String,
): HabitResponse<Void>
@POST("groups/{gid}/chat")
suspend fun postGroupChat(@Path("gid") groupId: String, @Body message: Map<String, String>): HabitResponse<PostChatMessageResult>
suspend fun postGroupChat(
@Path("gid") groupId: String,
@Body message: Map<String, String>,
): HabitResponse<PostChatMessageResult>
@DELETE("groups/{gid}/chat/{messageId}")
suspend fun deleteMessage(@Path("gid") groupId: String, @Path("messageId") messageId: String): HabitResponse<Void>
suspend fun deleteMessage(
@Path("gid") groupId: String,
@Path("messageId") messageId: String,
): HabitResponse<Void>
@DELETE("inbox/messages/{messageId}")
suspend fun deleteInboxMessage(@Path("messageId") messageId: String): HabitResponse<Void>
suspend fun deleteInboxMessage(
@Path("messageId") messageId: String,
): HabitResponse<Void>
@GET("groups/{gid}/members")
suspend fun getGroupMembers(
@Path("gid") groupId: String,
@Query("includeAllPublicFields") includeAllPublicFields: Boolean?
@Query("includeAllPublicFields") includeAllPublicFields: Boolean?,
): HabitResponse<List<Member>>
@GET("groups/{gid}/members")
suspend fun getGroupMembers(
@Path("gid") groupId: String,
@Query("includeAllPublicFields") includeAllPublicFields: Boolean?,
@Query("lastId") lastId: String
@Query("lastId") lastId: String,
): HabitResponse<List<Member>>
// Like returns the full chat list
@POST("groups/{gid}/chat/{mid}/like")
suspend fun likeMessage(@Path("gid") groupId: String, @Path("mid") mid: String): HabitResponse<ChatMessage>
suspend fun likeMessage(
@Path("gid") groupId: String,
@Path("mid") mid: String,
): HabitResponse<ChatMessage>
@POST("groups/{gid}/chat/{mid}/flag")
suspend fun flagMessage(
@Path("gid") groupId: String,
@Path("mid") mid: String,
@Body data: Map<String, String>
@Body data: Map<String, String>,
): HabitResponse<Void>
@POST("members/{mid}/flag")
suspend fun reportMember(@Path("mid") mid: String, @Body data: Map<String, String>): HabitResponse<Void>
suspend fun reportMember(
@Path("mid") mid: String,
@Body data: Map<String, String>,
): HabitResponse<Void>
@POST("groups/{gid}/chat/seen")
suspend fun seenMessages(@Path("gid") groupId: String): HabitResponse<Void>
suspend fun seenMessages(
@Path("gid") groupId: String,
): HabitResponse<Void>
@POST("groups/{gid}/invite")
suspend fun inviteToGroup(@Path("gid") groupId: String, @Body inviteData: Map<String, Any>): HabitResponse<List<InviteResponse>>
suspend fun inviteToGroup(
@Path("gid") groupId: String,
@Body inviteData: Map<String, Any>,
): HabitResponse<List<InviteResponse>>
@POST("groups/{gid}/reject-invite")
suspend fun rejectGroupInvite(@Path("gid") groupId: String): HabitResponse<Void>
suspend fun rejectGroupInvite(
@Path("gid") groupId: String,
): HabitResponse<Void>
@POST("groups/{gid}/quests/accept")
suspend fun acceptQuest(@Path("gid") groupId: String): HabitResponse<Void>
suspend fun acceptQuest(
@Path("gid") groupId: String,
): HabitResponse<Void>
@POST("groups/{gid}/quests/reject")
suspend fun rejectQuest(@Path("gid") groupId: String): HabitResponse<Void>
suspend fun rejectQuest(
@Path("gid") groupId: String,
): HabitResponse<Void>
@POST("groups/{gid}/quests/cancel")
suspend fun cancelQuest(@Path("gid") groupId: String): HabitResponse<Void>
suspend fun cancelQuest(
@Path("gid") groupId: String,
): HabitResponse<Void>
@POST("groups/{gid}/quests/force-start")
suspend fun forceStartQuest(@Path("gid") groupId: String, @Body group: Group): HabitResponse<Quest>
suspend fun forceStartQuest(
@Path("gid") groupId: String,
@Body group: Group,
): HabitResponse<Quest>
@POST("groups/{gid}/quests/invite/{questKey}")
suspend fun inviteToQuest(@Path("gid") groupId: String, @Path("questKey") questKey: String): HabitResponse<Quest>
suspend fun inviteToQuest(
@Path("gid") groupId: String,
@Path("questKey") questKey: String,
): HabitResponse<Quest>
@GET("groups/{gid}/invites")
suspend fun getGroupInvites(
@Path("gid") groupId: String,
@Query("includeAllPublicFields") includeAllPublicFields: Boolean?
@Query("includeAllPublicFields") includeAllPublicFields: Boolean?,
): HabitResponse<List<Member>>
@POST("groups/{gid}/quests/abort")
suspend fun abortQuest(@Path("gid") groupId: String): HabitResponse<Quest>
suspend fun abortQuest(
@Path("gid") groupId: String,
): HabitResponse<Quest>
@POST("groups/{gid}/quests/leave")
suspend fun leaveQuest(@Path("gid") groupId: String): HabitResponse<Void>
suspend fun leaveQuest(
@Path("gid") groupId: String,
): HabitResponse<Void>
@POST("/iap/android/verify")
suspend fun validatePurchase(@Body request: PurchaseValidationRequest): HabitResponse<PurchaseValidationResult>
suspend fun validatePurchase(
@Body request: PurchaseValidationRequest,
): HabitResponse<PurchaseValidationResult>
@POST("/iap/android/subscribe")
suspend fun validateSubscription(@Body request: PurchaseValidationRequest): HabitResponse<Void>
suspend fun validateSubscription(
@Body request: PurchaseValidationRequest,
): HabitResponse<Void>
@GET("/iap/android/subscribe/cancel")
suspend fun cancelSubscription(): HabitResponse<Void>
@POST("/iap/android/norenew-subscribe")
suspend fun validateNoRenewSubscription(@Body request: PurchaseValidationRequest): HabitResponse<Void>
suspend fun validateNoRenewSubscription(
@Body request: PurchaseValidationRequest,
): HabitResponse<Void>
@POST("user/custom-day-start")
suspend fun changeCustomDayStart(@Body updateObject: Map<String, Any>): HabitResponse<Void>
suspend fun changeCustomDayStart(
@Body updateObject: Map<String, Any>,
): HabitResponse<Void>
// Members URL
@GET("members/{mid}")
suspend fun getMember(@Path("mid") memberId: String): HabitResponse<Member>
suspend fun getMember(
@Path("mid") memberId: String,
): HabitResponse<Member>
@GET("members/username/{username}")
suspend fun getMemberWithUsername(@Path("username") username: String): HabitResponse<Member>
suspend fun getMemberWithUsername(
@Path("username") username: String,
): HabitResponse<Member>
@GET("members/{mid}/achievements")
suspend fun getMemberAchievements(@Path("mid") memberId: String, @Query("lang") language: String?): HabitResponse<List<Achievement>>
suspend fun getMemberAchievements(
@Path("mid") memberId: String,
@Query("lang") language: String?,
): HabitResponse<List<Achievement>>
@POST("members/send-private-message")
suspend fun postPrivateMessage(@Body messageDetails: Map<String, String>): HabitResponse<PostChatMessageResult>
suspend fun postPrivateMessage(
@Body messageDetails: Map<String, String>,
): HabitResponse<PostChatMessageResult>
@GET("members/find/{username}")
suspend fun findUsernames(
@Path("username") username: String,
@Query("context") context: String?,
@Query("id") id: String?
@Query("id") id: String?,
): HabitResponse<List<FindUsernameResult>>
@POST("members/flag-private-message/{mid}")
suspend fun flagInboxMessage(@Path("mid") mid: String, @Body data: Map<String, String>): HabitResponse<Void>
suspend fun flagInboxMessage(
@Path("mid") mid: String,
@Body data: Map<String, String>,
): HabitResponse<Void>
@GET("shops/{identifier}")
suspend fun retrieveShopInventory(@Path("identifier") identifier: String, @Query("lang") language: String?): HabitResponse<Shop>
suspend fun retrieveShopInventory(
@Path("identifier") identifier: String,
@Query("lang") language: String?,
): HabitResponse<Shop>
@GET("shops/market-gear")
suspend fun retrieveMarketGear(@Query("lang") language: String?): HabitResponse<Shop>
suspend fun retrieveMarketGear(
@Query("lang") language: String?,
): HabitResponse<Shop>
// Push notifications
@POST("user/push-devices")
suspend fun addPushDevice(@Body pushDeviceData: Map<String, String>): HabitResponse<List<Void>>
suspend fun addPushDevice(
@Body pushDeviceData: Map<String, String>,
): HabitResponse<List<Void>>
@DELETE("user/push-devices/{regId}")
suspend fun deletePushDevice(@Path("regId") regId: String): HabitResponse<List<Void>>
suspend fun deletePushDevice(
@Path("regId") regId: String,
): HabitResponse<List<Void>>
/* challenges api */
// challenges api
@GET("challenges/user")
suspend fun getUserChallenges(@Query("page") page: Int?, @Query("member") memberOnly: Boolean): HabitResponse<List<Challenge>>
suspend fun getUserChallenges(
@Query("page") page: Int?,
@Query("member") memberOnly: Boolean,
): HabitResponse<List<Challenge>>
@GET("challenges/user")
suspend fun getUserChallenges(@Query("page") page: Int?): HabitResponse<List<Challenge>>
suspend fun getUserChallenges(
@Query("page") page: Int?,
): HabitResponse<List<Challenge>>
@GET("tasks/challenge/{challengeId}")
suspend fun getChallengeTasks(@Path("challengeId") challengeId: String): HabitResponse<TaskList>
suspend fun getChallengeTasks(
@Path("challengeId") challengeId: String,
): HabitResponse<TaskList>
@GET("challenges/{challengeId}")
suspend fun getChallenge(@Path("challengeId") challengeId: String): HabitResponse<Challenge>
suspend fun getChallenge(
@Path("challengeId") challengeId: String,
): HabitResponse<Challenge>
@POST("challenges/{challengeId}/join")
suspend fun joinChallenge(@Path("challengeId") challengeId: String): HabitResponse<Challenge>
suspend fun joinChallenge(
@Path("challengeId") challengeId: String,
): HabitResponse<Challenge>
@POST("challenges/{challengeId}/leave")
suspend fun leaveChallenge(@Path("challengeId") challengeId: String, @Body body: LeaveChallengeBody): HabitResponse<Void>
suspend fun leaveChallenge(
@Path("challengeId") challengeId: String,
@Body body: LeaveChallengeBody,
): HabitResponse<Void>
@POST("challenges")
suspend fun createChallenge(@Body challenge: Challenge): HabitResponse<Challenge>
suspend fun createChallenge(
@Body challenge: Challenge,
): HabitResponse<Challenge>
@POST("tasks/challenge/{challengeId}")
suspend fun createChallengeTasks(@Path("challengeId") challengeId: String, @Body tasks: List<Task>): HabitResponse<List<Task>>
suspend fun createChallengeTasks(
@Path("challengeId") challengeId: String,
@Body tasks: List<Task>,
): HabitResponse<List<Task>>
@POST("tasks/challenge/{challengeId}")
suspend fun createChallengeTask(@Path("challengeId") challengeId: String, @Body task: Task): HabitResponse<Task>
suspend fun createChallengeTask(
@Path("challengeId") challengeId: String,
@Body task: Task,
): HabitResponse<Task>
@PUT("challenges/{challengeId}")
suspend fun updateChallenge(@Path("challengeId") challengeId: String, @Body challenge: Challenge): HabitResponse<Challenge>
suspend fun updateChallenge(
@Path("challengeId") challengeId: String,
@Body challenge: Challenge,
): HabitResponse<Challenge>
@DELETE("challenges/{challengeId}")
suspend fun deleteChallenge(@Path("challengeId") challengeId: String): HabitResponse<Void>
suspend fun deleteChallenge(
@Path("challengeId") challengeId: String,
): HabitResponse<Void>
// DEBUG: These calls only work on a local development server
@ -403,13 +602,19 @@ interface ApiService {
// Notifications
@POST("notifications/{notificationId}/read")
suspend fun readNotification(@Path("notificationId") notificationId: String): HabitResponse<List<Any>>
suspend fun readNotification(
@Path("notificationId") notificationId: String,
): HabitResponse<List<Any>>
@POST("notifications/read")
suspend fun readNotifications(@Body notificationIds: Map<String, List<String>>): HabitResponse<List<Any>>
suspend fun readNotifications(
@Body notificationIds: Map<String, List<String>>,
): HabitResponse<List<Any>>
@POST("notifications/see")
suspend fun seeNotifications(@Body notificationIds: Map<String, List<String>>): HabitResponse<List<Any>>
suspend fun seeNotifications(
@Body notificationIds: Map<String, List<String>>,
): HabitResponse<List<Any>>
@POST("user/open-mystery-item")
suspend fun openMysteryItem(): HabitResponse<Equipment>
@ -418,43 +623,71 @@ interface ApiService {
suspend fun runCron(): HabitResponse<Void>
@POST("user/reset")
suspend fun resetAccount(@Body body: Map<String, String>): HabitResponse<Void>
suspend fun resetAccount(
@Body body: Map<String, String>,
): HabitResponse<Void>
@HTTP(method = "DELETE", path = "user", hasBody = true)
suspend fun deleteAccount(@Body body: Map<String, String>): HabitResponse<Void>
suspend fun deleteAccount(
@Body body: Map<String, String>,
): HabitResponse<Void>
@GET("user/toggle-pinned-item/{pinType}/{path}")
suspend fun togglePinnedItem(@Path("pinType") pinType: String, @Path("path") path: String): HabitResponse<Void>
suspend fun togglePinnedItem(
@Path("pinType") pinType: String,
@Path("path") path: String,
): HabitResponse<Void>
@POST("user/reset-password")
suspend fun sendPasswordResetEmail(@Body data: Map<String, String>): HabitResponse<Void>
suspend fun sendPasswordResetEmail(
@Body data: Map<String, String>,
): HabitResponse<Void>
@PUT("user/auth/update-username")
suspend fun updateLoginName(@Body data: Map<String, String>): HabitResponse<Void>
suspend fun updateLoginName(
@Body data: Map<String, String>,
): HabitResponse<Void>
@POST("user/auth/verify-username")
suspend fun verifyUsername(@Body data: Map<String, String>): HabitResponse<VerifyUsernameResponse>
suspend fun verifyUsername(
@Body data: Map<String, String>,
): HabitResponse<VerifyUsernameResponse>
@PUT("user/auth/update-email")
suspend fun updateEmail(@Body data: Map<String, String>): HabitResponse<Void>
suspend fun updateEmail(
@Body data: Map<String, String>,
): HabitResponse<Void>
@PUT("user/auth/update-password")
suspend fun updatePassword(@Body data: Map<String, String>): HabitResponse<Void>
suspend fun updatePassword(
@Body data: Map<String, String>,
): HabitResponse<Void>
@POST("user/allocate")
suspend fun allocatePoint(@Query("stat") stat: String): HabitResponse<Stats>
suspend fun allocatePoint(
@Query("stat") stat: String,
): HabitResponse<Stats>
@POST("user/allocate-bulk")
suspend fun bulkAllocatePoints(@Body stats: Map<String, Map<String, Int>>): HabitResponse<Stats>
suspend fun bulkAllocatePoints(
@Body stats: Map<String, Map<String, Int>>,
): HabitResponse<Stats>
@POST("members/transfer-gems")
suspend fun transferGems(@Body data: Map<String, Any>): HabitResponse<Void>
suspend fun transferGems(
@Body data: Map<String, Any>,
): HabitResponse<Void>
@POST("tasks/unlink-all/{challengeID}")
suspend fun unlinkAllTasks(@Path("challengeID") challengeID: String?, @Query("keep") keepOption: String): HabitResponse<Void>
suspend fun unlinkAllTasks(
@Path("challengeID") challengeID: String?,
@Query("keep") keepOption: String,
): HabitResponse<Void>
@POST("user/block/{userID}")
suspend fun blockMember(@Path("userID") userID: String): HabitResponse<List<String>>
suspend fun blockMember(
@Path("userID") userID: String,
): HabitResponse<List<String>>
@POST("user/reroll")
suspend fun reroll(): HabitResponse<User>
@ -465,26 +698,47 @@ interface ApiService {
suspend fun getTeamPlans(): HabitResponse<List<TeamPlan>>
@GET("tasks/group/{groupID}")
suspend fun getTeamPlanTasks(@Path("groupID") groupId: String): HabitResponse<TaskList>
suspend fun getTeamPlanTasks(
@Path("groupID") groupId: String,
): HabitResponse<TaskList>
@POST("tasks/{taskID}/assign")
suspend fun assignToTask(@Path("taskID") taskId: String?, @Body ids: List<String>): HabitResponse<Task>
suspend fun assignToTask(
@Path("taskID") taskId: String?,
@Body ids: List<String>,
): HabitResponse<Task>
@POST("tasks/{taskID}/unassign/{userID}")
suspend fun unassignFromTask(@Path("taskID") taskID: String, @Path("userID") userID: String): HabitResponse<Task>
suspend fun unassignFromTask(
@Path("taskID") taskID: String,
@Path("userID") userID: String,
): HabitResponse<Task>
@PUT("hall/heroes/{memberID}")
suspend fun updateUser(@Path("memberID") memberID: String, @Body updateData: Map<String, Map<String, Boolean>>): HabitResponse<Member>
suspend fun updateUser(
@Path("memberID") memberID: String,
@Body updateData: Map<String, Map<String, Boolean>>,
): HabitResponse<Member>
@GET("hall/heroes/{memberID}")
suspend fun getHallMember(@Path("memberID") memberID: String): HabitResponse<Member>
suspend fun getHallMember(
@Path("memberID") memberID: String,
): HabitResponse<Member>
@POST("tasks/{taskID}/needs-work/{userID}")
suspend fun markTaskNeedsWork(@Path("taskID") taskID: String, @Path("userID") userID: String): HabitResponse<Task>
suspend fun markTaskNeedsWork(
@Path("taskID") taskID: String,
@Path("userID") userID: String,
): HabitResponse<Task>
@GET("looking-for-party")
suspend fun retrievePartySeekingUsers(@Query("page") page: Int): HabitResponse<List<Member>>
suspend fun retrievePartySeekingUsers(
@Query("page") page: Int,
): HabitResponse<List<Member>>
@POST("challenges/{challengeId}/flag")
suspend fun reportChallenge(@Path("challengeId") challengeid: String, @Body updateData: Map<String, String>): HabitResponse<Void>
suspend fun reportChallenge(
@Path("challengeId") challengeid: String,
@Body updateData: Map<String, String>,
): HabitResponse<Void>
}

View file

@ -1,288 +1,499 @@
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.ContentResult
import com.habitrpg.android.habitica.models.LeaveChallengeBody
import com.habitrpg.android.habitica.models.Tag
import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.WorldState
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.invitations.InviteResponse
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.BulkTaskScoringData
import com.habitrpg.android.habitica.models.responses.BuyResponse
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
import com.habitrpg.android.habitica.models.responses.SkillResponse
import com.habitrpg.android.habitica.models.responses.UnlockResponse
import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.social.Challenge
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.FindUsernameResult
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskList
import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.common.habitica.api.HostConfig
import com.habitrpg.common.habitica.models.PurchaseValidationRequest
import com.habitrpg.common.habitica.models.PurchaseValidationResult
import com.habitrpg.common.habitica.models.auth.UserAuthResponse
import com.habitrpg.shared.habitica.models.responses.ErrorResponse
import com.habitrpg.shared.habitica.models.responses.FeedResponse
import com.habitrpg.shared.habitica.models.responses.Status
import com.habitrpg.shared.habitica.models.responses.TaskDirectionData
import com.habitrpg.shared.habitica.models.responses.VerifyUsernameResponse
import retrofit2.HttpException
interface ApiClient {
val hostConfig: HostConfig
suspend fun getStatus(): Status?
/* user API */
suspend fun getTasks(): TaskList?
/* challenges api */
suspend fun getUserChallenges(page: Int, memberOnly: Boolean): List<Challenge>?
suspend fun getWorldState(): WorldState?
var languageCode: String?
suspend fun getContent(language: String? = null): ContentResult?
suspend fun updateUser(updateDictionary: Map<String, Any?>): User?
suspend fun registrationLanguage(registrationLanguage: String): User?
suspend fun retrieveInAppRewards(): List<ShopItem>?
suspend fun equipItem(type: String, itemKey: String): Items?
suspend fun buyItem(itemKey: String, purchaseQuantity: Int): BuyResponse?
suspend fun purchaseItem(type: String, itemKey: String, purchaseQuantity: Int): Void?
suspend fun purchaseHourglassItem(type: String, itemKey: String): Void?
suspend fun purchaseMysterySet(itemKey: String): Void?
suspend fun purchaseQuest(key: String): Void?
suspend fun purchaseSpecialSpell(key: String): Void?
suspend fun validateSubscription(request: PurchaseValidationRequest): Any?
suspend fun validateNoRenewSubscription(request: PurchaseValidationRequest): Any?
suspend fun cancelSubscription(): Void?
suspend fun sellItem(itemType: String, itemKey: String): User?
suspend fun feedPet(petKey: String, foodKey: String): FeedResponse?
suspend fun hatchPet(eggKey: String, hatchingPotionKey: String): Items?
suspend fun getTasks(type: String): TaskList?
suspend fun getTasks(type: String, dueDate: String): TaskList?
suspend fun unlockPath(path: String): UnlockResponse?
suspend fun getTask(id: String): Task?
suspend fun postTaskDirection(id: String, direction: String): TaskDirectionData?
suspend fun bulkScoreTasks(data: List<Map<String, String>>): BulkTaskScoringData?
suspend fun postTaskNewPosition(id: String, position: Int): List<String>?
suspend fun scoreChecklistItem(taskId: String, itemId: String): Task?
suspend fun createTask(item: Task): Task?
suspend fun createGroupTask(groupId: String, item: Task): Task?
suspend fun createTasks(tasks: List<Task>): List<Task>?
suspend fun updateTask(id: String, item: Task): Task?
suspend fun deleteTask(id: String): Void?
suspend fun createTag(tag: Tag): Tag?
suspend fun updateTag(id: String, tag: Tag): Tag?
suspend fun deleteTag(id: String): Void?
suspend fun registerUser(username: String, email: String, password: String, confirmPassword: String): UserAuthResponse?
suspend fun connectUser(username: String, password: String): UserAuthResponse?
suspend fun connectSocial(network: String, userId: String, accessToken: String): UserAuthResponse?
suspend fun disconnectSocial(network: String): Void?
suspend fun loginApple(authToken: String): UserAuthResponse?
suspend fun sleep(): Boolean?
suspend fun revive(): Items?
suspend fun useSkill(skillName: String, targetType: String, targetId: String): SkillResponse?
suspend fun useSkill(skillName: String, targetType: String): SkillResponse?
suspend fun changeClass(className: String?): User?
suspend fun disableClasses(): User?
suspend fun markPrivateMessagesRead()
/* Group API */
suspend fun listGroups(type: String): List<Group>?
suspend fun getGroup(groupId: String): Group?
suspend fun createGroup(group: Group): Group?
suspend fun updateGroup(id: String, item: Group): Group?
suspend fun removeMemberFromGroup(groupID: String, userID: String): Void?
suspend fun listGroupChat(groupId: String): List<ChatMessage>?
suspend fun joinGroup(groupId: String): Group?
suspend fun leaveGroup(groupId: String, keepChallenges: String): Void?
suspend fun postGroupChat(groupId: String, message: Map<String, String>): PostChatMessageResult?
suspend fun deleteMessage(groupId: String, messageId: String): Void?
suspend fun deleteInboxMessage(id: String): Void?
suspend fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?): List<Member>?
suspend fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?, lastId: String): List<Member>?
// Like returns the full chat list
suspend fun likeMessage(groupId: String, mid: String): ChatMessage?
suspend fun flagMessage(groupId: String, mid: String, data: MutableMap<String, String>): Void?
suspend fun flagInboxMessage(mid: String, data: MutableMap<String, String>): Void?
suspend fun reportMember(mid: String, data: Map<String, String>): Void?
suspend fun seenMessages(groupId: String): Void?
suspend fun inviteToGroup(groupId: String, inviteData: Map<String, Any>): List<InviteResponse>?
suspend fun rejectGroupInvite(groupId: String): Void?
suspend fun acceptQuest(groupId: String): Void?
suspend fun rejectQuest(groupId: String): Void?
suspend fun cancelQuest(groupId: String): Void?
suspend fun forceStartQuest(groupId: String, group: Group): Quest?
suspend fun inviteToQuest(groupId: String, questKey: String): Quest?
suspend fun abortQuest(groupId: String): Quest?
suspend fun leaveQuest(groupId: String): Void?
suspend fun validatePurchase(request: PurchaseValidationRequest): PurchaseValidationResult?
suspend fun changeCustomDayStart(updateObject: Map<String, Any>): Void?
// Members URL
suspend fun getMember(memberId: String): Member?
suspend fun getMemberWithUsername(username: String): Member?
suspend fun getMemberAchievements(memberId: String): List<Achievement>?
suspend fun postPrivateMessage(messageDetails: Map<String, String>): PostChatMessageResult?
suspend fun retrieveShopIventory(identifier: String): Shop?
// Push notifications
suspend fun addPushDevice(pushDeviceData: Map<String, String>): List<Void>?
suspend fun deletePushDevice(regId: String): List<Void>?
suspend fun getChallengeTasks(challengeId: String): TaskList?
suspend fun getChallenge(challengeId: String): Challenge?
suspend fun joinChallenge(challengeId: String): Challenge?
suspend fun leaveChallenge(challengeId: String, body: LeaveChallengeBody): Void?
suspend fun createChallenge(challenge: Challenge): Challenge?
suspend fun createChallengeTasks(challengeId: String, tasks: List<Task>): List<Task>?
suspend fun createChallengeTask(challengeId: String, task: Task): Task?
suspend fun updateChallenge(challenge: Challenge): Challenge?
suspend fun deleteChallenge(challengeId: String): Void?
// DEBUG: These calls only work on a local development server
suspend fun debugAddTenGems(): Void?
suspend fun getNews(): List<Any>?
// Notifications
suspend fun readNotification(notificationId: String): List<Any>?
suspend fun readNotifications(notificationIds: Map<String, List<String>>): List<Any>?
suspend fun seeNotifications(notificationIds: Map<String, List<String>>): List<Any>?
fun getErrorResponse(throwable: HttpException): ErrorResponse
fun updateAuthenticationCredentials(userID: String?, apiToken: String?)
fun hasAuthenticationKeys(): Boolean
suspend fun retrieveUser(withTasks: Boolean = false): User?
suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>?
suspend fun retrieveInboxConversations(): List<InboxConversation>?
suspend fun openMysteryItem(): Equipment?
suspend fun runCron(): Void?
suspend fun reroll(): User?
suspend fun resetAccount(password: String): Void?
suspend fun deleteAccount(password: String): Void?
suspend fun togglePinnedItem(pinType: String, path: String): Void?
suspend fun sendPasswordResetEmail(email: String): Void?
suspend fun updateLoginName(newLoginName: String, password: String): Void?
suspend fun updateUsername(newLoginName: String): Void?
suspend fun updateEmail(newEmail: String, password: String): Void?
suspend fun updatePassword(oldPassword: String, newPassword: String, newPasswordConfirmation: String): Void?
suspend fun allocatePoint(stat: String): Stats?
suspend fun bulkAllocatePoints(strength: Int, intelligence: Int, constitution: Int, perception: Int): Stats?
suspend fun retrieveMarketGear(): Shop?
suspend fun verifyUsername(username: String): VerifyUsernameResponse?
fun updateServerUrl(newAddress: String?)
suspend fun findUsernames(username: String, context: String?, id: String?): List<FindUsernameResult>?
suspend fun transferGems(giftedID: String, amount: Int): Void?
suspend fun unlinkAllTasks(challengeID: String?, keepOption: String): Void?
suspend fun blockMember(userID: String): List<String>?
suspend fun getTeamPlans(): List<TeamPlan>?
suspend fun getTeamPlanTasks(teamID: String): TaskList?
suspend fun assignToTask(taskId: String, ids: List<String>): Task?
suspend fun unassignFromTask(taskId: String, userID: String): Task?
suspend fun updateMember(memberID: String, updateData: Map<String, Map<String, Boolean>>): Member?
suspend fun getHallMember(userId: String): Member?
suspend fun markTaskNeedsWork(taskID: String, userID: String): Task?
suspend fun retrievePartySeekingUsers(page: Int): List<Member>?
suspend fun getGroupInvites(groupId: String, includeAllPublicFields: Boolean?): List<Member>?
suspend fun syncUserStats(): User?
suspend fun reportChallenge(challengeid: String, updateData: Map<String, String>): Void?
}
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.ContentResult
import com.habitrpg.android.habitica.models.LeaveChallengeBody
import com.habitrpg.android.habitica.models.Tag
import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.WorldState
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.invitations.InviteResponse
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.BulkTaskScoringData
import com.habitrpg.android.habitica.models.responses.BuyResponse
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
import com.habitrpg.android.habitica.models.responses.SkillResponse
import com.habitrpg.android.habitica.models.responses.UnlockResponse
import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.social.Challenge
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.FindUsernameResult
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskList
import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.common.habitica.api.HostConfig
import com.habitrpg.common.habitica.models.PurchaseValidationRequest
import com.habitrpg.common.habitica.models.PurchaseValidationResult
import com.habitrpg.common.habitica.models.auth.UserAuthResponse
import com.habitrpg.shared.habitica.models.responses.ErrorResponse
import com.habitrpg.shared.habitica.models.responses.FeedResponse
import com.habitrpg.shared.habitica.models.responses.Status
import com.habitrpg.shared.habitica.models.responses.TaskDirectionData
import com.habitrpg.shared.habitica.models.responses.VerifyUsernameResponse
import retrofit2.HttpException
interface ApiClient {
val hostConfig: HostConfig
suspend fun getStatus(): Status?
// user API
suspend fun getTasks(): TaskList?
// challenges api
suspend fun getUserChallenges(
page: Int,
memberOnly: Boolean,
): List<Challenge>?
suspend fun getWorldState(): WorldState?
var languageCode: String?
suspend fun getContent(language: String? = null): ContentResult?
suspend fun updateUser(updateDictionary: Map<String, Any?>): User?
suspend fun registrationLanguage(registrationLanguage: String): User?
suspend fun retrieveInAppRewards(): List<ShopItem>?
suspend fun equipItem(
type: String,
itemKey: String,
): Items?
suspend fun buyItem(
itemKey: String,
purchaseQuantity: Int,
): BuyResponse?
suspend fun purchaseItem(
type: String,
itemKey: String,
purchaseQuantity: Int,
): Void?
suspend fun purchaseHourglassItem(
type: String,
itemKey: String,
): Void?
suspend fun purchaseMysterySet(itemKey: String): Void?
suspend fun purchaseQuest(key: String): Void?
suspend fun purchaseSpecialSpell(key: String): Void?
suspend fun validateSubscription(request: PurchaseValidationRequest): Any?
suspend fun validateNoRenewSubscription(request: PurchaseValidationRequest): Any?
suspend fun cancelSubscription(): Void?
suspend fun sellItem(
itemType: String,
itemKey: String,
): User?
suspend fun feedPet(
petKey: String,
foodKey: String,
): FeedResponse?
suspend fun hatchPet(
eggKey: String,
hatchingPotionKey: String,
): Items?
suspend fun getTasks(type: String): TaskList?
suspend fun getTasks(
type: String,
dueDate: String,
): TaskList?
suspend fun unlockPath(path: String): UnlockResponse?
suspend fun getTask(id: String): Task?
suspend fun postTaskDirection(
id: String,
direction: String,
): TaskDirectionData?
suspend fun bulkScoreTasks(data: List<Map<String, String>>): BulkTaskScoringData?
suspend fun postTaskNewPosition(
id: String,
position: Int,
): List<String>?
suspend fun scoreChecklistItem(
taskId: String,
itemId: String,
): Task?
suspend fun createTask(item: Task): Task?
suspend fun createGroupTask(
groupId: String,
item: Task,
): Task?
suspend fun createTasks(tasks: List<Task>): List<Task>?
suspend fun updateTask(
id: String,
item: Task,
): Task?
suspend fun deleteTask(id: String): Void?
suspend fun createTag(tag: Tag): Tag?
suspend fun updateTag(
id: String,
tag: Tag,
): Tag?
suspend fun deleteTag(id: String): Void?
suspend fun registerUser(
username: String,
email: String,
password: String,
confirmPassword: String,
): UserAuthResponse?
suspend fun connectUser(
username: String,
password: String,
): UserAuthResponse?
suspend fun connectSocial(
network: String,
userId: String,
accessToken: String,
): UserAuthResponse?
suspend fun disconnectSocial(network: String): Void?
suspend fun loginApple(authToken: String): UserAuthResponse?
suspend fun sleep(): Boolean?
suspend fun revive(): Items?
suspend fun useSkill(
skillName: String,
targetType: String,
targetId: String,
): SkillResponse?
suspend fun useSkill(
skillName: String,
targetType: String,
): SkillResponse?
suspend fun changeClass(className: String?): User?
suspend fun disableClasses(): User?
suspend fun markPrivateMessagesRead()
// Group API
suspend fun listGroups(type: String): List<Group>?
suspend fun getGroup(groupId: String): Group?
suspend fun createGroup(group: Group): Group?
suspend fun updateGroup(
id: String,
item: Group,
): Group?
suspend fun removeMemberFromGroup(
groupID: String,
userID: String,
): Void?
suspend fun listGroupChat(groupId: String): List<ChatMessage>?
suspend fun joinGroup(groupId: String): Group?
suspend fun leaveGroup(
groupId: String,
keepChallenges: String,
): Void?
suspend fun postGroupChat(
groupId: String,
message: Map<String, String>,
): PostChatMessageResult?
suspend fun deleteMessage(
groupId: String,
messageId: String,
): Void?
suspend fun deleteInboxMessage(id: String): Void?
suspend fun getGroupMembers(
groupId: String,
includeAllPublicFields: Boolean?,
): List<Member>?
suspend fun getGroupMembers(
groupId: String,
includeAllPublicFields: Boolean?,
lastId: String,
): List<Member>?
// Like returns the full chat list
suspend fun likeMessage(
groupId: String,
mid: String,
): ChatMessage?
suspend fun flagMessage(
groupId: String,
mid: String,
data: MutableMap<String, String>,
): Void?
suspend fun flagInboxMessage(
mid: String,
data: MutableMap<String, String>,
): Void?
suspend fun reportMember(
mid: String,
data: Map<String, String>,
): Void?
suspend fun seenMessages(groupId: String): Void?
suspend fun inviteToGroup(
groupId: String,
inviteData: Map<String, Any>,
): List<InviteResponse>?
suspend fun rejectGroupInvite(groupId: String): Void?
suspend fun acceptQuest(groupId: String): Void?
suspend fun rejectQuest(groupId: String): Void?
suspend fun cancelQuest(groupId: String): Void?
suspend fun forceStartQuest(
groupId: String,
group: Group,
): Quest?
suspend fun inviteToQuest(
groupId: String,
questKey: String,
): Quest?
suspend fun abortQuest(groupId: String): Quest?
suspend fun leaveQuest(groupId: String): Void?
suspend fun validatePurchase(request: PurchaseValidationRequest): PurchaseValidationResult?
suspend fun changeCustomDayStart(updateObject: Map<String, Any>): Void?
// Members URL
suspend fun getMember(memberId: String): Member?
suspend fun getMemberWithUsername(username: String): Member?
suspend fun getMemberAchievements(memberId: String): List<Achievement>?
suspend fun postPrivateMessage(messageDetails: Map<String, String>): PostChatMessageResult?
suspend fun retrieveShopIventory(identifier: String): Shop?
// Push notifications
suspend fun addPushDevice(pushDeviceData: Map<String, String>): List<Void>?
suspend fun deletePushDevice(regId: String): List<Void>?
suspend fun getChallengeTasks(challengeId: String): TaskList?
suspend fun getChallenge(challengeId: String): Challenge?
suspend fun joinChallenge(challengeId: String): Challenge?
suspend fun leaveChallenge(
challengeId: String,
body: LeaveChallengeBody,
): Void?
suspend fun createChallenge(challenge: Challenge): Challenge?
suspend fun createChallengeTasks(
challengeId: String,
tasks: List<Task>,
): List<Task>?
suspend fun createChallengeTask(
challengeId: String,
task: Task,
): Task?
suspend fun updateChallenge(challenge: Challenge): Challenge?
suspend fun deleteChallenge(challengeId: String): Void?
// DEBUG: These calls only work on a local development server
suspend fun debugAddTenGems(): Void?
suspend fun getNews(): List<Any>?
// Notifications
suspend fun readNotification(notificationId: String): List<Any>?
suspend fun readNotifications(notificationIds: Map<String, List<String>>): List<Any>?
suspend fun seeNotifications(notificationIds: Map<String, List<String>>): List<Any>?
fun getErrorResponse(throwable: HttpException): ErrorResponse
fun updateAuthenticationCredentials(
userID: String?,
apiToken: String?,
)
fun hasAuthenticationKeys(): Boolean
suspend fun retrieveUser(withTasks: Boolean = false): User?
suspend fun retrieveInboxMessages(
uuid: String,
page: Int,
): List<ChatMessage>?
suspend fun retrieveInboxConversations(): List<InboxConversation>?
suspend fun openMysteryItem(): Equipment?
suspend fun runCron(): Void?
suspend fun reroll(): User?
suspend fun resetAccount(password: String): Void?
suspend fun deleteAccount(password: String): Void?
suspend fun togglePinnedItem(
pinType: String,
path: String,
): Void?
suspend fun sendPasswordResetEmail(email: String): Void?
suspend fun updateLoginName(
newLoginName: String,
password: String,
): Void?
suspend fun updateUsername(newLoginName: String): Void?
suspend fun updateEmail(
newEmail: String,
password: String,
): Void?
suspend fun updatePassword(
oldPassword: String,
newPassword: String,
newPasswordConfirmation: String,
): Void?
suspend fun allocatePoint(stat: String): Stats?
suspend fun bulkAllocatePoints(
strength: Int,
intelligence: Int,
constitution: Int,
perception: Int,
): Stats?
suspend fun retrieveMarketGear(): Shop?
suspend fun verifyUsername(username: String): VerifyUsernameResponse?
fun updateServerUrl(newAddress: String?)
suspend fun findUsernames(
username: String,
context: String?,
id: String?,
): List<FindUsernameResult>?
suspend fun transferGems(
giftedID: String,
amount: Int,
): Void?
suspend fun unlinkAllTasks(
challengeID: String?,
keepOption: String,
): Void?
suspend fun blockMember(userID: String): List<String>?
suspend fun getTeamPlans(): List<TeamPlan>?
suspend fun getTeamPlanTasks(teamID: String): TaskList?
suspend fun assignToTask(
taskId: String,
ids: List<String>,
): Task?
suspend fun unassignFromTask(
taskId: String,
userID: String,
): Task?
suspend fun updateMember(
memberID: String,
updateData: Map<String, Map<String, Boolean>>,
): Member?
suspend fun getHallMember(userId: String): Member?
suspend fun markTaskNeedsWork(
taskID: String,
userID: String,
): Task?
suspend fun retrievePartySeekingUsers(page: Int): List<Member>?
suspend fun getGroupInvites(
groupId: String,
includeAllPublicFields: Boolean?,
): List<Member>?
suspend fun syncUserStats(): User?
suspend fun reportChallenge(
challengeid: String,
updateData: Map<String, String>,
): Void?
}

View file

@ -1,33 +1,34 @@
package com.habitrpg.android.habitica.models.shops
import android.content.Context
import com.habitrpg.android.habitica.R
class Shop {
var identifier: String = ""
var text: String = ""
var notes: String = ""
var imageName: String = ""
var categories: MutableList<ShopCategory> = ArrayList()
val npcNameResource: Int
get() = when (identifier) {
MARKET -> R.string.market_owner
QUEST_SHOP -> R.string.questShop_owner
SEASONAL_SHOP -> R.string.seasonalShop_owner
TIME_TRAVELERS_SHOP -> R.string.timetravelers_owner
CUSTOMIZATIONS -> R.string.customizations_owner
else -> R.string.market_owner
}
fun getNpcName(context: Context): String = context.getString(npcNameResource)
companion object {
const val MARKET = "market"
const val QUEST_SHOP = "questShop"
const val TIME_TRAVELERS_SHOP = "timeTravelersShop"
const val SEASONAL_SHOP = "seasonalShop"
const val CUSTOMIZATIONS = "customizations"
}
}
package com.habitrpg.android.habitica.models.shops
import android.content.Context
import com.habitrpg.android.habitica.R
class Shop {
var identifier: String = ""
var text: String = ""
var notes: String = ""
var imageName: String = ""
var categories: MutableList<ShopCategory> = ArrayList()
val npcNameResource: Int
get() =
when (identifier) {
MARKET -> R.string.market_owner
QUEST_SHOP -> R.string.questShop_owner
SEASONAL_SHOP -> R.string.seasonalShop_owner
TIME_TRAVELERS_SHOP -> R.string.timetravelers_owner
CUSTOMIZATIONS -> R.string.customizations_owner
else -> R.string.market_owner
}
fun getNpcName(context: Context): String = context.getString(npcNameResource)
companion object {
const val MARKET = "market"
const val QUEST_SHOP = "questShop"
const val TIME_TRAVELERS_SHOP = "timeTravelersShop"
const val SEASONAL_SHOP = "seasonalShop"
const val CUSTOMIZATIONS = "customizations"
}
}

View file

@ -1,225 +1,232 @@
package com.habitrpg.android.habitica.models.shops
import android.content.Context
import android.content.res.Resources
import com.google.gson.annotations.SerializedName
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.models.BaseObject
import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.models.inventory.CustomizationSet
import com.habitrpg.android.habitica.models.inventory.ItemEvent
import com.habitrpg.android.habitica.models.user.User
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import java.util.Date
open class ShopItem : RealmObject(), BaseObject {
@PrimaryKey
var key: String = ""
var text: String? = ""
var notes: String? = ""
@SerializedName("class")
var imageName: String? = null
get() {
return if (field != null) {
if (field!!.contains(" ")) {
field!!.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]
} else {
field
}
} else {
"shop_$key"
}
}
var value: Int = 0
var locked: Boolean = false
var isLimited: Boolean = false
var currency: String? = null
var purchaseType: String = ""
var categoryIdentifier: String = ""
var limitedNumberLeft: Int? = null
var unlockCondition: ShopItemUnlockCondition? = null
var path: String? = null
var unlockPath: String? = null
var isSuggested: String? = null
var pinType: String? = null
@SerializedName("klass")
var habitClass: String? = null
var previous: String? = null
@SerializedName("lvl")
var level: Int? = null
var event: ItemEvent? = null
var endDate: Date? = null
val availableUntil: Date?
get() {
return endDate ?: event?.end
}
var setImageNames = RealmList<String>()
val isTypeItem: Boolean
get() = "eggs" == purchaseType || "hatchingPotions" == purchaseType || "food" == purchaseType || "armoire" == purchaseType || "potion" == purchaseType || "debuffPotion" == purchaseType || "fortify" == purchaseType
val isTypeQuest: Boolean
get() = "quests" == purchaseType
val isTypeGear: Boolean
get() = "gear" == purchaseType
val isTypeAnimal: Boolean
get() = "pets" == purchaseType || "mounts" == purchaseType
val canPurchaseBulk: Boolean
get() = "eggs" == purchaseType || "hatchingPotions" == purchaseType || "food" == purchaseType || "gems" == purchaseType
fun canAfford(user: User?, quantity: Int): Boolean = when (currency) {
"gold" -> (value * quantity) <= (user?.stats?.gp ?: 0.0)
"gems" -> (value * quantity) <= (user?.gemCount ?: 0)
"hourglasses" -> (value * quantity) <= (user?.hourglassCount ?: 0)
else -> true
}
override fun equals(other: Any?): Boolean {
if (other != null && ShopItem::class.java.isAssignableFrom(other.javaClass)) {
val otherItem = other as? ShopItem
return this.key == otherItem?.key
}
return super.equals(other)
}
override fun hashCode(): Int {
return this.key.hashCode()
}
fun shortLockedReason(context: Context): String? {
return when {
unlockCondition != null -> {
unlockCondition?.shortReadableUnlockCondition(context)
}
previous != null -> {
try {
val thisNumber = Character.getNumericValue(key.last())
context.getString(R.string.unlock_previous_short, thisNumber - 1)
} catch (e: NumberFormatException) {
null
}
}
level != null -> {
context.getString(R.string.level_unabbreviated, level ?: 0)
}
else -> null
}
}
fun lockedReason(context: Context): String? {
return when {
unlockCondition != null -> {
unlockCondition?.readableUnlockCondition(context)
}
previous != null -> {
try {
val thisNumber = Character.getNumericValue(key.last())
context.getString(R.string.unlock_previous, thisNumber - 1)
} catch (e: NumberFormatException) {
null
}
}
level != null -> {
context.getString(R.string.unlock_level, level ?: 0)
}
else -> null
}
}
companion object {
private const val GEM_FOR_GOLD = "gem"
fun makeGemItem(res: Resources?): ShopItem {
val item = ShopItem()
item.key = GEM_FOR_GOLD
item.text = res?.getString(R.string.gem_shop) ?: ""
item.notes = res?.getString(R.string.gem_for_gold_description) ?: ""
item.imageName = "gem_shop"
item.value = 20
item.currency = "gold"
item.purchaseType = "gems"
item.pinType = "gem"
item.path = "special.gems"
return item
}
fun makeFortifyItem(res: Resources?): ShopItem {
val item = ShopItem()
item.key = "fortify"
item.text = res?.getString(R.string.fortify_shop) ?: ""
item.notes = res?.getString(R.string.fortify_shop_description) ?: ""
item.imageName = "inventory_special_fortify"
item.value = 4
item.currency = "gems"
item.pinType = "fortify"
item.path = "special.fortify"
item.purchaseType = "fortify"
return item
}
fun fromCustomization(customization: Customization, userSize: String?, hairColor: String?): ShopItem {
val item = ShopItem()
item.key = customization.identifier ?: ""
item.text = customization.text
item.currency = "gems"
item.notes = customization.notes
item.value = customization.price ?: 0
item.path = customization.path
item.unlockPath = customization.unlockPath
item.pinType = customization.type
if (customization.type == "background") {
item.purchaseType = "background"
item.imageName = customization.getImageName(userSize, hairColor)
} else {
item.purchaseType = "customization"
item.imageName = customization.getIconName(userSize, hairColor)
}
return item
}
fun fromCustomizationSet(
set: CustomizationSet,
additionalSetItems: List<Customization>?,
userSize: String?,
hairColor: String?
): ShopItem {
val item = ShopItem()
var path = ""
for (customization in set.customizations) {
path = path + "," + customization.unlockPath
item.setImageNames.add(customization.getIconName(userSize, hairColor))
}
for (customization in additionalSetItems ?: emptyList()) {
path = path + "," + customization.unlockPath
item.setImageNames.add(customization.getIconName(userSize, hairColor))
}
if (path.isEmpty()) {
item.unlockPath = path
} else {
item.unlockPath = path.substring(1)
}
item.text = set.text
item.key = set.identifier ?: ""
item.currency = "gems"
item.value = set.price
item.purchaseType = "customizationSet"
if (set.customizations.firstOrNull()?.type == "background") {
// TODO: Needs a way to be translated.
item.notes = "Get all three Backgrounds in this bundle."
}
return item
}
}
}
package com.habitrpg.android.habitica.models.shops
import android.content.Context
import android.content.res.Resources
import com.google.gson.annotations.SerializedName
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.models.BaseObject
import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.models.inventory.CustomizationSet
import com.habitrpg.android.habitica.models.inventory.ItemEvent
import com.habitrpg.android.habitica.models.user.User
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import java.util.Date
open class ShopItem : RealmObject(), BaseObject {
@PrimaryKey
var key: String = ""
var text: String? = ""
var notes: String? = ""
@SerializedName("class")
var imageName: String? = null
get() {
return if (field != null) {
if (field!!.contains(" ")) {
field!!.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]
} else {
field
}
} else {
"shop_$key"
}
}
var value: Int = 0
var locked: Boolean = false
var isLimited: Boolean = false
var currency: String? = null
var purchaseType: String = ""
var categoryIdentifier: String = ""
var limitedNumberLeft: Int? = null
var unlockCondition: ShopItemUnlockCondition? = null
var path: String? = null
var unlockPath: String? = null
var isSuggested: String? = null
var pinType: String? = null
@SerializedName("klass")
var habitClass: String? = null
var previous: String? = null
@SerializedName("lvl")
var level: Int? = null
var event: ItemEvent? = null
var endDate: Date? = null
val availableUntil: Date?
get() {
return endDate ?: event?.end
}
var setImageNames = RealmList<String>()
val isTypeItem: Boolean
get() = "eggs" == purchaseType || "hatchingPotions" == purchaseType || "food" == purchaseType || "armoire" == purchaseType || "potion" == purchaseType || "debuffPotion" == purchaseType || "fortify" == purchaseType
val isTypeQuest: Boolean
get() = "quests" == purchaseType
val isTypeGear: Boolean
get() = "gear" == purchaseType
val isTypeAnimal: Boolean
get() = "pets" == purchaseType || "mounts" == purchaseType
val canPurchaseBulk: Boolean
get() = "eggs" == purchaseType || "hatchingPotions" == purchaseType || "food" == purchaseType || "gems" == purchaseType
fun canAfford(
user: User?,
quantity: Int,
): Boolean =
when (currency) {
"gold" -> (value * quantity) <= (user?.stats?.gp ?: 0.0)
"gems" -> (value * quantity) <= (user?.gemCount ?: 0)
"hourglasses" -> (value * quantity) <= (user?.hourglassCount ?: 0)
else -> true
}
override fun equals(other: Any?): Boolean {
if (other != null && ShopItem::class.java.isAssignableFrom(other.javaClass)) {
val otherItem = other as? ShopItem
return this.key == otherItem?.key
}
return super.equals(other)
}
override fun hashCode(): Int {
return this.key.hashCode()
}
fun shortLockedReason(context: Context): String? {
return when {
unlockCondition != null -> {
unlockCondition?.shortReadableUnlockCondition(context)
}
previous != null -> {
try {
val thisNumber = Character.getNumericValue(key.last())
context.getString(R.string.unlock_previous_short, thisNumber - 1)
} catch (e: NumberFormatException) {
null
}
}
level != null -> {
context.getString(R.string.level_unabbreviated, level ?: 0)
}
else -> null
}
}
fun lockedReason(context: Context): String? {
return when {
unlockCondition != null -> {
unlockCondition?.readableUnlockCondition(context)
}
previous != null -> {
try {
val thisNumber = Character.getNumericValue(key.last())
context.getString(R.string.unlock_previous, thisNumber - 1)
} catch (e: NumberFormatException) {
null
}
}
level != null -> {
context.getString(R.string.unlock_level, level ?: 0)
}
else -> null
}
}
companion object {
private const val GEM_FOR_GOLD = "gem"
fun makeGemItem(res: Resources?): ShopItem {
val item = ShopItem()
item.key = GEM_FOR_GOLD
item.text = res?.getString(R.string.gem_shop) ?: ""
item.notes = res?.getString(R.string.gem_for_gold_description) ?: ""
item.imageName = "gem_shop"
item.value = 20
item.currency = "gold"
item.purchaseType = "gems"
item.pinType = "gem"
item.path = "special.gems"
return item
}
fun makeFortifyItem(res: Resources?): ShopItem {
val item = ShopItem()
item.key = "fortify"
item.text = res?.getString(R.string.fortify_shop) ?: ""
item.notes = res?.getString(R.string.fortify_shop_description) ?: ""
item.imageName = "inventory_special_fortify"
item.value = 4
item.currency = "gems"
item.pinType = "fortify"
item.path = "special.fortify"
item.purchaseType = "fortify"
return item
}
fun fromCustomization(
customization: Customization,
userSize: String?,
hairColor: String?,
): ShopItem {
val item = ShopItem()
item.key = customization.identifier ?: ""
item.text = customization.text
item.currency = "gems"
item.notes = customization.notes
item.value = customization.price ?: 0
item.path = customization.path
item.unlockPath = customization.unlockPath
item.pinType = customization.type
if (customization.type == "background") {
item.purchaseType = "background"
item.imageName = customization.getImageName(userSize, hairColor)
} else {
item.purchaseType = "customization"
item.imageName = customization.getIconName(userSize, hairColor)
}
return item
}
fun fromCustomizationSet(
set: CustomizationSet,
additionalSetItems: List<Customization>?,
userSize: String?,
hairColor: String?,
): ShopItem {
val item = ShopItem()
var path = ""
for (customization in set.customizations) {
path = path + "," + customization.unlockPath
item.setImageNames.add(customization.getIconName(userSize, hairColor))
}
for (customization in additionalSetItems ?: emptyList()) {
path = path + "," + customization.unlockPath
item.setImageNames.add(customization.getIconName(userSize, hairColor))
}
if (path.isEmpty()) {
item.unlockPath = path
} else {
item.unlockPath = path.substring(1)
}
item.text = set.text
item.key = set.identifier ?: ""
item.currency = "gems"
item.value = set.price
item.purchaseType = "customizationSet"
if (set.customizations.firstOrNull()?.type == "background") {
// TODO: Needs a way to be translated.
item.notes = "Get all three Backgrounds in this bundle."
}
return item
}
}
}

View file

@ -23,9 +23,8 @@ class ChallengeTasksRecyclerViewAdapter(
newContext: Context,
userID: String,
private val openTaskDisabled: Boolean,
private val taskActionsDisabled: Boolean
private val taskActionsDisabled: Boolean,
) : BaseTasksRecyclerViewAdapter<BindableViewHolder<Task>>(TaskType.HABIT, viewModel, layoutResource, newContext, userID) {
val taskList: MutableList<Task>
get() = content?.map { t -> t }?.toMutableList() ?: mutableListOf()
@ -44,7 +43,10 @@ class ChallengeTasksRecyclerViewAdapter(
}
}
fun addTaskUnder(taskToAdd: Task, taskAbove: Task?): Int {
fun addTaskUnder(
taskToAdd: Task,
taskAbove: Task?,
): Int {
val position = content?.indexOfFirst { t -> t.id == taskAbove?.id } ?: 0
content?.add(position + 1, taskToAdd)
@ -53,23 +55,31 @@ class ChallengeTasksRecyclerViewAdapter(
return position
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindableViewHolder<Task> {
val viewHolder: BindableViewHolder<Task> = when (viewType) {
TYPE_HABIT -> HabitViewHolder(getContentView(parent, R.layout.habit_item_card), { _, _ -> }, { task, _ ->
onTaskOpen?.invoke(task)
}, {}, null)
TYPE_DAILY -> DailyViewHolder(getContentView(parent, R.layout.daily_item_card), { _, _ -> }, { _, _ -> }, { task, _ ->
onTaskOpen?.invoke(task)
}, { }, null)
TYPE_TODO -> TodoViewHolder(getContentView(parent, R.layout.todo_item_card), { _, _ -> }, { _, _ -> }, { task, _ ->
onTaskOpen?.invoke(task)
}, {}, null)
TYPE_REWARD -> RewardViewHolder(getContentView(parent, R.layout.reward_item_card), { _, _ -> }, { task, _ ->
onTaskOpen?.invoke(task)
}, { }, null)
TYPE_ADD_ITEM -> AddItemViewHolder(getContentView(parent, R.layout.challenge_add_task_item), onAddItem)
else -> DividerViewHolder(getContentView(parent, R.layout.challenge_task_divider))
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): BindableViewHolder<Task> {
val viewHolder: BindableViewHolder<Task> =
when (viewType) {
TYPE_HABIT ->
HabitViewHolder(getContentView(parent, R.layout.habit_item_card), { _, _ -> }, { task, _ ->
onTaskOpen?.invoke(task)
}, {}, null)
TYPE_DAILY ->
DailyViewHolder(getContentView(parent, R.layout.daily_item_card), { _, _ -> }, { _, _ -> }, { task, _ ->
onTaskOpen?.invoke(task)
}, { }, null)
TYPE_TODO ->
TodoViewHolder(getContentView(parent, R.layout.todo_item_card), { _, _ -> }, { _, _ -> }, { task, _ ->
onTaskOpen?.invoke(task)
}, {}, null)
TYPE_REWARD ->
RewardViewHolder(getContentView(parent, R.layout.reward_item_card), { _, _ -> }, { task, _ ->
onTaskOpen?.invoke(task)
}, { }, null)
TYPE_ADD_ITEM -> AddItemViewHolder(getContentView(parent, R.layout.challenge_add_task_item), onAddItem)
else -> DividerViewHolder(getContentView(parent, R.layout.challenge_task_divider))
}
(viewHolder as? BaseTaskViewHolder)?.setDisabled(openTaskDisabled, taskActionsDisabled)
return viewHolder
@ -99,9 +109,8 @@ class ChallengeTasksRecyclerViewAdapter(
inner class AddItemViewHolder internal constructor(
itemView: View,
private val callback: ((Task) -> Unit)?
private val callback: ((Task) -> Unit)?,
) : BindableViewHolder<Task>(itemView) {
private val addBtn: Button = itemView.findViewById(R.id.btn_add_task)
private var newTask: Task? = null
@ -113,7 +122,7 @@ class ChallengeTasksRecyclerViewAdapter(
override fun bind(
data: Task,
position: Int,
displayMode: String
displayMode: String,
) {
this.newTask = data
addBtn.text = data.text
@ -121,13 +130,12 @@ class ChallengeTasksRecyclerViewAdapter(
}
private class DividerViewHolder(itemView: View) : BindableViewHolder<Task>(itemView) {
private val dividerName: TextView = itemView.findViewById(R.id.divider_name)
override fun bind(
data: Task,
position: Int,
displayMode: String
displayMode: String,
) {
dividerName.text = data.text
}

View file

@ -6,8 +6,10 @@ import com.habitrpg.android.habitica.ui.viewHolders.tasks.DailyViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
class DailiesRecyclerViewHolder(layoutResource: Int, viewModel: TasksViewModel) : RealmBaseTasksRecyclerViewAdapter(layoutResource, viewModel) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): RecyclerView.ViewHolder {
return if (viewType == 0) {
DailyViewHolder(
getContentView(parent),
@ -21,7 +23,7 @@ class DailiesRecyclerViewHolder(layoutResource: Int, viewModel: TasksViewModel)
task ->
brokenTaskEvents?.invoke(task)
},
viewModel
viewModel,
)
} else {
super.onCreateViewHolder(parent, viewType)

View file

@ -6,8 +6,10 @@ import com.habitrpg.android.habitica.ui.viewHolders.tasks.HabitViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
class HabitsRecyclerViewAdapter(layoutResource: Int, viewModel: TasksViewModel) : RealmBaseTasksRecyclerViewAdapter(layoutResource, viewModel) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): RecyclerView.ViewHolder {
return if (viewType == 0) {
HabitViewHolder(
getContentView(parent),
@ -18,7 +20,7 @@ class HabitsRecyclerViewAdapter(layoutResource: Int, viewModel: TasksViewModel)
{ task ->
brokenTaskEvents?.invoke(task)
},
viewModel
viewModel,
)
} else {
super.onCreateViewHolder(parent, viewType)

View file

@ -1,155 +1,161 @@
package com.habitrpg.android.habitica.ui.adapter.tasks
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.adapter.BaseRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.viewHolders.ShopItemViewHolder
import com.habitrpg.android.habitica.ui.viewHolders.tasks.RewardViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
import com.habitrpg.shared.habitica.models.responses.TaskDirection
class RewardsRecyclerViewAdapter(
private var customRewards: List<Task>?,
private val layoutResource: Int,
val viewModel: TasksViewModel
) : BaseRecyclerViewAdapter<Task, RecyclerView.ViewHolder>(), TaskRecyclerViewAdapter {
override var user: User? = null
set(value) {
if (field?.versionNumber == value?.versionNumber) {
return
}
field = value
notifyDataSetChanged()
}
override var showAdventureGuide: Boolean = false
private var inAppRewards: List<ShopItem>? = null
override var errorButtonEvents: ((String) -> Unit)? = null
override var taskScoreEvents: ((Task, TaskDirection) -> Unit)? = null
override var checklistItemScoreEvents: ((Task, ChecklistItem) -> Unit)? = null
override var taskOpenEvents: ((Task, View) -> Unit)? = null
override var brokenTaskEvents: ((Task) -> Unit)? = null
override var adventureGuideOpenEvents: ((Boolean) -> Unit)? = null
var purchaseCardEvents: ((ShopItem) -> Unit)? = null
var onShowPurchaseDialog: ((ShopItem, Boolean) -> Unit)? = null
var goldGemsLeft: Int? = null
override var taskDisplayMode: String = "standard"
set(value) {
if (field != value) {
field = value
notifyDataSetChanged()
}
}
private val inAppRewardCount: Int
get() {
// if (inAppRewards?.isValid != true) return 0
return inAppRewards?.size ?: 0
}
private val customRewardCount: Int
get() {
// if (customRewards?.isValid != true) return 0
return customRewards?.size ?: 0
}
private fun getContentView(parent: ViewGroup): View {
return LayoutInflater.from(parent.context).inflate(layoutResource, parent, false)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == VIEWTYPE_CUSTOM_REWARD) {
RewardViewHolder(
getContentView(parent),
{ task, direction ->
if (task.value <= (user?.stats?.gp ?: 0.0)) {
taskScoreEvents?.invoke(task, direction)
}
},
{ task, view ->
taskOpenEvents?.invoke(task, view)
},
{
task ->
brokenTaskEvents?.invoke(task)
},
viewModel
)
} else {
val viewHolder = ShopItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.row_shopitem, parent, false))
viewHolder.purchaseCardAction = { purchaseCardEvents?.invoke(it) }
viewHolder.onShowPurchaseDialog = onShowPurchaseDialog
viewHolder
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (customRewards != null && position < customRewardCount) {
val reward = customRewards?.get(position) ?: return
val gold = user?.stats?.gp ?: 0.0
(holder as? RewardViewHolder)?.isLocked = !viewModel.canScoreTask(reward)
(holder as? RewardViewHolder)?.bind(reward, position, reward.value <= gold, taskDisplayMode, viewModel.ownerID.value)
} else if (inAppRewards != null) {
val item = inAppRewards?.get(position - customRewardCount) ?: return
if (holder is ShopItemViewHolder) {
if (item.key == "gem") {
holder.limitedNumberLeft = goldGemsLeft
}
holder.bind(item, item.canAfford(user, 1), 0)
holder.isPinned = true
holder.hidePinIndicator()
}
}
}
override fun getItemViewType(position: Int): Int {
return if ((customRewards != null && position < customRewardCount) || (customRewardCount == 0 && inAppRewardCount == 0)) {
VIEWTYPE_CUSTOM_REWARD
} else {
VIEWTYPE_IN_APP_REWARD
}
}
override fun updateUnfilteredData(data: List<Task>?) {
updateData(data)
}
override fun getItemCount(): Int {
var rewardCount = customRewardCount
if (viewModel.isPersonalBoard) {
rewardCount += inAppRewardCount
}
return rewardCount
}
fun updateData(tasks: List<Task>?) {
this.customRewards = tasks
notifyDataSetChanged()
}
fun updateItemRewards(items: List<ShopItem>) {
if (items.isNotEmpty()) {
if (Task::class.java.isAssignableFrom(items.first().javaClass)) {
// this catches a weird bug where the observable gets a list of tasks for no apparent reason.
return
}
}
this.inAppRewards = items
notifyDataSetChanged()
}
override fun filter() { /* no-on */ }
companion object {
private const val VIEWTYPE_CUSTOM_REWARD = 0
private const val VIEWTYPE_IN_APP_REWARD = 3
}
}
package com.habitrpg.android.habitica.ui.adapter.tasks
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.adapter.BaseRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.viewHolders.ShopItemViewHolder
import com.habitrpg.android.habitica.ui.viewHolders.tasks.RewardViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
import com.habitrpg.shared.habitica.models.responses.TaskDirection
class RewardsRecyclerViewAdapter(
private var customRewards: List<Task>?,
private val layoutResource: Int,
val viewModel: TasksViewModel,
) : BaseRecyclerViewAdapter<Task, RecyclerView.ViewHolder>(), TaskRecyclerViewAdapter {
override var user: User? = null
set(value) {
if (field?.versionNumber == value?.versionNumber) {
return
}
field = value
notifyDataSetChanged()
}
override var showAdventureGuide: Boolean = false
private var inAppRewards: List<ShopItem>? = null
override var errorButtonEvents: ((String) -> Unit)? = null
override var taskScoreEvents: ((Task, TaskDirection) -> Unit)? = null
override var checklistItemScoreEvents: ((Task, ChecklistItem) -> Unit)? = null
override var taskOpenEvents: ((Task, View) -> Unit)? = null
override var brokenTaskEvents: ((Task) -> Unit)? = null
override var adventureGuideOpenEvents: ((Boolean) -> Unit)? = null
var purchaseCardEvents: ((ShopItem) -> Unit)? = null
var onShowPurchaseDialog: ((ShopItem, Boolean) -> Unit)? = null
var goldGemsLeft: Int? = null
override var taskDisplayMode: String = "standard"
set(value) {
if (field != value) {
field = value
notifyDataSetChanged()
}
}
private val inAppRewardCount: Int
get() {
// if (inAppRewards?.isValid != true) return 0
return inAppRewards?.size ?: 0
}
private val customRewardCount: Int
get() {
// if (customRewards?.isValid != true) return 0
return customRewards?.size ?: 0
}
private fun getContentView(parent: ViewGroup): View {
return LayoutInflater.from(parent.context).inflate(layoutResource, parent, false)
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): RecyclerView.ViewHolder {
return if (viewType == VIEWTYPE_CUSTOM_REWARD) {
RewardViewHolder(
getContentView(parent),
{ task, direction ->
if (task.value <= (user?.stats?.gp ?: 0.0)) {
taskScoreEvents?.invoke(task, direction)
}
},
{ task, view ->
taskOpenEvents?.invoke(task, view)
},
{
task ->
brokenTaskEvents?.invoke(task)
},
viewModel,
)
} else {
val viewHolder = ShopItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.row_shopitem, parent, false))
viewHolder.purchaseCardAction = { purchaseCardEvents?.invoke(it) }
viewHolder.onShowPurchaseDialog = onShowPurchaseDialog
viewHolder
}
}
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
) {
if (customRewards != null && position < customRewardCount) {
val reward = customRewards?.get(position) ?: return
val gold = user?.stats?.gp ?: 0.0
(holder as? RewardViewHolder)?.isLocked = !viewModel.canScoreTask(reward)
(holder as? RewardViewHolder)?.bind(reward, position, reward.value <= gold, taskDisplayMode, viewModel.ownerID.value)
} else if (inAppRewards != null) {
val item = inAppRewards?.get(position - customRewardCount) ?: return
if (holder is ShopItemViewHolder) {
if (item.key == "gem") {
holder.limitedNumberLeft = goldGemsLeft
}
holder.bind(item, item.canAfford(user, 1), 0)
holder.isPinned = true
holder.hidePinIndicator()
}
}
}
override fun getItemViewType(position: Int): Int {
return if ((customRewards != null && position < customRewardCount) || (customRewardCount == 0 && inAppRewardCount == 0)) {
VIEWTYPE_CUSTOM_REWARD
} else {
VIEWTYPE_IN_APP_REWARD
}
}
override fun updateUnfilteredData(data: List<Task>?) {
updateData(data)
}
override fun getItemCount(): Int {
var rewardCount = customRewardCount
if (viewModel.isPersonalBoard) {
rewardCount += inAppRewardCount
}
return rewardCount
}
fun updateData(tasks: List<Task>?) {
this.customRewards = tasks
notifyDataSetChanged()
}
fun updateItemRewards(items: List<ShopItem>) {
if (items.isNotEmpty()) {
if (Task::class.java.isAssignableFrom(items.first().javaClass)) {
// this catches a weird bug where the observable gets a list of tasks for no apparent reason.
return
}
}
this.inAppRewards = items
notifyDataSetChanged()
}
override fun filter() { /* no-on */ }
companion object {
private const val VIEWTYPE_CUSTOM_REWARD = 0
private const val VIEWTYPE_IN_APP_REWARD = 3
}
}

View file

@ -6,8 +6,10 @@ import com.habitrpg.android.habitica.ui.viewHolders.tasks.TodoViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
class TodosRecyclerViewAdapter(layoutResource: Int, viewModel: TasksViewModel) : RealmBaseTasksRecyclerViewAdapter(layoutResource, viewModel) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): RecyclerView.ViewHolder {
return if (viewType == 0) {
TodoViewHolder(
getContentView(parent),
@ -19,7 +21,7 @@ class TodosRecyclerViewAdapter(layoutResource: Int, viewModel: TasksViewModel) :
{ task ->
brokenTaskEvents?.invoke(task)
},
viewModel
viewModel,
)
} else {
super.onCreateViewHolder(parent, viewType)

View file

@ -63,7 +63,6 @@ import kotlin.time.toDuration
@AndroidEntryPoint
class NavigationDrawerFragment : DialogFragment() {
private var binding: DrawerMainBinding? = null
@Inject
@ -104,14 +103,15 @@ class NavigationDrawerFragment : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
val context = context
adapter = if (context != null) {
NavigationDrawerAdapter(
context.getThemeColor(R.attr.colorPrimaryText),
context.getThemeColor(R.attr.colorPrimaryOffset)
)
} else {
NavigationDrawerAdapter(0, 0)
}
adapter =
if (context != null) {
NavigationDrawerAdapter(
context.getThemeColor(R.attr.colorPrimaryText),
context.getThemeColor(R.attr.colorPrimaryOffset),
)
} else {
NavigationDrawerAdapter(0, 0)
}
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
@ -125,12 +125,15 @@ class NavigationDrawerFragment : DialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View? = inflater.inflate(R.layout.drawer_main, container, false) as? ViewGroup
private var updatingJobs = mutableMapOf<String, Job>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
binding = DrawerMainBinding.bind(view)
binding?.avatarView?.configManager = configManager
@ -155,7 +158,7 @@ class NavigationDrawerFragment : DialogFragment() {
lifecycleScope.launchCatching {
contentRepository.getWorldState()
.combine(
inventoryRepository.getAvailableLimitedItems()
inventoryRepository.getAvailableLimitedItems(),
) { state, items -> Pair(state, items) }
.collect { pair ->
val gearEvent = pair.first.events.firstOrNull { it.gear }
@ -166,7 +169,7 @@ class NavigationDrawerFragment : DialogFragment() {
val diff = (gearEvent?.end?.time ?: 0) - Date().time
if (diff < (1.toDuration(DurationUnit.HOURS).inWholeMilliseconds)) {
1.toDuration(
DurationUnit.SECONDS
DurationUnit.SECONDS,
)
} else {
1.toDuration(DurationUnit.MINUTES)
@ -205,7 +208,7 @@ class NavigationDrawerFragment : DialogFragment() {
R.id.inboxFragment,
null,
true,
preventReselection = false
preventReselection = false,
)
}
binding?.settingsButtonWrapper?.setOnClickListener {
@ -213,7 +216,7 @@ class NavigationDrawerFragment : DialogFragment() {
R.id.prefsActivity,
null,
true,
preventReselection = false
preventReselection = false,
)
}
binding?.notificationsButtonWrapper?.setOnClickListener { startNotificationsActivity() }
@ -223,21 +226,25 @@ class NavigationDrawerFragment : DialogFragment() {
key: String,
endingCondition: () -> Boolean,
delayFunc: () -> Duration,
function: () -> Unit
function: () -> Unit,
) {
function()
if (updatingJobs[key]?.isActive == true) {
updatingJobs[key]?.cancel()
}
updatingJobs[key] = lifecycleScope.launch(Dispatchers.Main) {
while (endingCondition()) {
function()
delay(delayFunc())
updatingJobs[key] =
lifecycleScope.launch(Dispatchers.Main) {
while (endingCondition()) {
function()
delay(delayFunc())
}
}
}
}
private fun updateSeasonalMenuEntries(gearEvent: WorldStateEvent?, items: List<Item>) {
private fun updateSeasonalMenuEntries(
gearEvent: WorldStateEvent?,
items: List<Item>,
) {
val market = getItemWithIdentifier(SIDEBAR_SHOPS_MARKET) ?: return
val item = items.firstOrNull()
if (item?.isValid() == true && item.event?.end?.after(Date()) == true) {
@ -283,8 +290,8 @@ class NavigationDrawerFragment : DialogFragment() {
item.isVisible = false
} else {
if ((
user.stats?.lvl
?: 0
user.stats?.lvl
?: 0
) < HabiticaSnackbar.MIN_LEVEL_FOR_SKILLS && (!hasSpecialItems)
) {
item.pillText = getString(R.string.unlock_lvl_11)
@ -320,11 +327,12 @@ class NavigationDrawerFragment : DialogFragment() {
context?.let {
subscriptionItem?.subtitle =
user.purchased?.plan?.dateTerminated?.getRemainingString(it.resources)
subscriptionItem?.subtitleTextColor = when {
daysDiff <= 2 -> ContextCompat.getColor(it, R.color.red_100)
daysDiff <= 7 -> ContextCompat.getColor(it, R.color.brand_400)
else -> it.getThemeColor(R.attr.textColorSecondary)
}
subscriptionItem?.subtitleTextColor =
when {
daysDiff <= 2 -> ContextCompat.getColor(it, R.color.red_100)
daysDiff <= 7 -> ContextCompat.getColor(it, R.color.brand_400)
else -> it.getThemeColor(R.attr.textColorSecondary)
}
}
}
} else if (user.isSubscribed) {
@ -370,29 +378,29 @@ class NavigationDrawerFragment : DialogFragment() {
HabiticaDrawerItem(
R.id.tasksFragment,
SIDEBAR_TASKS,
context.getString(R.string.sidebar_tasks)
)
context.getString(R.string.sidebar_tasks),
),
)
items.add(
HabiticaDrawerItem(
R.id.skillsFragment,
SIDEBAR_SKILLS,
context.getString(R.string.sidebar_skills)
)
context.getString(R.string.sidebar_skills),
),
)
items.add(
HabiticaDrawerItem(
R.id.statsFragment,
SIDEBAR_STATS,
context.getString(R.string.sidebar_stats)
)
context.getString(R.string.sidebar_stats),
),
)
items.add(
HabiticaDrawerItem(
R.id.achievementsFragment,
SIDEBAR_ACHIEVEMENTS,
context.getString(R.string.sidebar_achievements)
)
context.getString(R.string.sidebar_achievements),
),
)
items.add(
@ -400,45 +408,46 @@ class NavigationDrawerFragment : DialogFragment() {
0,
SIDEBAR_INVENTORY,
context.getString(R.string.sidebar_shops),
isHeader = true
)
isHeader = true,
),
)
items.add(
HabiticaDrawerItem(
R.id.marketFragment,
SIDEBAR_SHOPS_MARKET,
context.getString(R.string.market)
)
context.getString(R.string.market),
),
)
items.add(
HabiticaDrawerItem(
R.id.questShopFragment,
SIDEBAR_SHOPS_QUEST,
context.getString(R.string.questShop)
)
context.getString(R.string.questShop),
),
)
if (configManager.enableCustomizationShop()) {
items.add(
HabiticaDrawerItem(
R.id.customizationsShopFragment,
SIDEBAR_SHOPS_CUSTOMIZATIONS,
context.getString(R.string.customizations)
)
context.getString(R.string.customizations),
),
)
}
val seasonalShopEntry = HabiticaDrawerItem(
R.id.seasonalShopFragment,
SIDEBAR_SHOPS_SEASONAL,
context.getString(R.string.seasonalShop)
)
val seasonalShopEntry =
HabiticaDrawerItem(
R.id.seasonalShopFragment,
SIDEBAR_SHOPS_SEASONAL,
context.getString(R.string.seasonalShop),
)
seasonalShopEntry.isVisible = false
items.add(seasonalShopEntry)
items.add(
HabiticaDrawerItem(
R.id.timeTravelersShopFragment,
SIDEBAR_SHOPS_TIMETRAVEL,
context.getString(R.string.timeTravelers)
)
context.getString(R.string.timeTravelers),
),
)
items.add(
@ -446,73 +455,73 @@ class NavigationDrawerFragment : DialogFragment() {
0,
SIDEBAR_INVENTORY,
context.getString(R.string.sidebar_section_inventory),
isHeader = true
)
isHeader = true,
),
)
items.add(
HabiticaDrawerItem(
R.id.avatarOverviewFragment,
SIDEBAR_AVATAR,
context.getString(R.string.sidebar_avatar)
)
context.getString(R.string.sidebar_avatar),
),
)
items.add(
HabiticaDrawerItem(
R.id.equipmentOverviewFragment,
SIDEBAR_EQUIPMENT,
context.getString(R.string.sidebar_equipment)
)
context.getString(R.string.sidebar_equipment),
),
)
items.add(
HabiticaDrawerItem(
R.id.itemsFragment,
SIDEBAR_ITEMS,
context.getString(R.string.sidebar_items)
)
context.getString(R.string.sidebar_items),
),
)
items.add(
HabiticaDrawerItem(
R.id.stableFragment,
SIDEBAR_STABLE,
context.getString(R.string.sidebar_stable)
)
context.getString(R.string.sidebar_stable),
),
)
items.add(
HabiticaDrawerItem(
R.id.gemPurchaseActivity,
SIDEBAR_GEMS,
context.getString(R.string.sidebar_gems)
)
context.getString(R.string.sidebar_gems),
),
)
items.add(
HabiticaDrawerItem(
R.id.subscriptionPurchaseActivity,
SIDEBAR_SUBSCRIPTION,
context.getString(R.string.sidebar_subscription)
)
context.getString(R.string.sidebar_subscription),
),
)
items.add(
HabiticaDrawerItem(
0,
SIDEBAR_SOCIAL,
context.getString(R.string.sidebar_section_social),
isHeader = true
)
isHeader = true,
),
)
items.add(
HabiticaDrawerItem(
R.id.partyFragment,
SIDEBAR_PARTY,
context.getString(R.string.sidebar_party)
)
context.getString(R.string.sidebar_party),
),
)
if (!configManager.hideChallenges()) {
items.add(
HabiticaDrawerItem(
R.id.challengesOverviewFragment,
SIDEBAR_CHALLENGES,
context.getString(R.string.sidebar_challenges)
)
context.getString(R.string.sidebar_challenges),
),
)
}
@ -521,29 +530,29 @@ class NavigationDrawerFragment : DialogFragment() {
0,
SIDEBAR_ABOUT_HEADER,
context.getString(R.string.sidebar_about),
isHeader = true
)
isHeader = true,
),
)
items.add(
HabiticaDrawerItem(
R.id.newsFragment,
SIDEBAR_NEWS,
context.getString(R.string.sidebar_news)
)
context.getString(R.string.sidebar_news),
),
)
items.add(
HabiticaDrawerItem(
R.id.supportMainFragment,
SIDEBAR_HELP,
context.getString(R.string.sidebar_help)
)
context.getString(R.string.sidebar_help),
),
)
items.add(
HabiticaDrawerItem(
R.id.aboutFragment,
SIDEBAR_ABOUT,
context.getString(R.string.sidebar_about)
)
context.getString(R.string.sidebar_about),
),
)
}
@ -565,7 +574,7 @@ class NavigationDrawerFragment : DialogFragment() {
transitionId: Int?,
bundle: Bundle? = null,
openSelection: Boolean = true,
preventReselection: Boolean = true
preventReselection: Boolean = true,
) {
if (!isTabletUI) {
closeDrawer()
@ -605,7 +614,7 @@ class NavigationDrawerFragment : DialogFragment() {
if (it.resultCode == Activity.RESULT_OK) {
(activity as? MainActivity)?.notificationsViewModel?.click(
it.data?.getStringExtra("notificationId") ?: "",
MainNavigationController
MainNavigationController,
)
}
}
@ -619,7 +628,7 @@ class NavigationDrawerFragment : DialogFragment() {
fun setUp(
fragmentId: Int,
drawerLayout: DrawerLayout,
viewModel: NotificationsViewModel
viewModel: NotificationsViewModel,
) {
fragmentContainerView = activity?.findViewById(fragmentId)
this.drawerLayout = drawerLayout
@ -705,11 +714,12 @@ class NavigationDrawerFragment : DialogFragment() {
private fun setNotificationsSeen(allSeen: Boolean) {
context?.let {
val color = if (allSeen) {
ContextCompat.getColor(it, R.color.gray_200)
} else {
it.getThemeColor(R.attr.colorAccent)
}
val color =
if (allSeen) {
ContextCompat.getColor(it, R.color.gray_200)
} else {
it.getThemeColor(R.attr.colorAccent)
}
val bg = binding?.notificationsBadge?.background as? GradientDrawable
bg?.color = ColorStateList.valueOf(color)
@ -723,11 +733,12 @@ class NavigationDrawerFragment : DialogFragment() {
binding?.messagesBadge?.visibility = View.VISIBLE
binding?.messagesBadge?.text = numOfUnreadMessages.toString()
context?.let {
val color = if (inbox?.hasUserSeenInbox != true) {
it.getThemeColor(R.attr.colorAccent)
} else {
ContextCompat.getColor(it, R.color.gray_200)
}
val color =
if (inbox?.hasUserSeenInbox != true) {
it.getThemeColor(R.attr.colorAccent)
} else {
ContextCompat.getColor(it, R.color.gray_200)
}
val background = binding?.messagesBadge?.background as? GradientDrawable
background?.color = ColorStateList.valueOf(color)
binding?.messagesBadge?.setTextColor(ContextCompat.getColor(it, R.color.white))
@ -772,10 +783,11 @@ class NavigationDrawerFragment : DialogFragment() {
1.toDuration(diff.getMinuteOrSeconds())
}) {
if (activePromo.isActive) {
promotedItem.subtitle = context?.getString(
R.string.sale_ends_in,
activePromo.endDate.getShortRemainingString()
)
promotedItem.subtitle =
context?.getString(
R.string.sale_ends_in,
activePromo.endDate.getShortRemainingString(),
)
adapter.updateItem(promotedItem)
} else {
promotedItem.subtitle = null
@ -790,7 +802,6 @@ class NavigationDrawerFragment : DialogFragment() {
}
companion object {
const val SIDEBAR_TASKS = "tasks"
const val SIDEBAR_SKILLS = "skills"
const val SIDEBAR_STATS = "stats"

View file

@ -20,48 +20,55 @@ import javax.inject.Inject
@AndroidEntryPoint
class NewsFragment : BaseMainFragment<FragmentNewsBinding>() {
@Inject
lateinit var hostConfig: HostConfig
override var binding: FragmentNewsBinding? = null
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentNewsBinding {
override fun createBinding(
inflater: LayoutInflater,
container: ViewGroup?,
): FragmentNewsBinding {
return FragmentNewsBinding.inflate(inflater, container, false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View? {
this.hidesToolbar = true
return super.onCreateView(inflater, container, savedInstanceState)
}
private val webviewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
if (request?.url?.path == "/static/new-stuff") {
view?.loadUrl(request.url.toString())
} else {
request?.url?.let { MainNavigationController.navigate(it) }
private val webviewClient =
object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?,
): Boolean {
if (request?.url?.path == "/static/new-stuff") {
view?.loadUrl(request.url.toString())
} else {
request?.url?.let { MainNavigationController.navigate(it) }
}
return true
}
return true
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
val webSettings = binding?.newsWebview?.settings
webSettings?.javaScriptEnabled = true
webSettings?.domStorageEnabled = true
binding?.newsWebview?.webViewClient = webviewClient
binding?.newsWebview?.webChromeClient = object : WebChromeClient() {
}
binding?.newsWebview?.webChromeClient =
object : WebChromeClient() {
}
binding?.newsWebview?.loadUrl("${hostConfig.address}/static/new-stuff")
}

View file

@ -52,11 +52,13 @@ import javax.inject.Inject
class AvatarCustomizationFragment :
BaseMainFragment<FragmentRefreshRecyclerviewBinding>(),
SwipeRefreshLayout.OnRefreshListener {
private var filterMenuItem: MenuItem? = null
override var binding: FragmentRefreshRecyclerviewBinding? = null
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRefreshRecyclerviewBinding {
override fun createBinding(
inflater: LayoutInflater,
container: ViewGroup?,
): FragmentRefreshRecyclerviewBinding {
return FragmentRefreshRecyclerviewBinding.inflate(inflater, container, false)
}
@ -82,7 +84,7 @@ class AvatarCustomizationFragment :
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View? {
showsBackButton = true
adapter.onCustomizationSelected = { customization ->
@ -96,7 +98,7 @@ class AvatarCustomizationFragment :
userRepository.useCustomization(
customization.type ?: "",
customization.category,
customization.identifier ?: ""
customization.identifier ?: "",
)
}
}
@ -115,7 +117,10 @@ class AvatarCustomizationFragment :
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
arguments?.let {
val args = AvatarCustomizationFragmentArgs.fromBundle(it)
@ -158,7 +163,10 @@ class AvatarCustomizationFragment :
super.onDestroy()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
override fun onCreateOptionsMenu(
menu: Menu,
inflater: MenuInflater,
) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_list_customizations, menu)
@ -215,7 +223,7 @@ class AvatarCustomizationFragment :
displayedCustomizations.reversed()
} else {
displayedCustomizations
}
},
)
} else {
adapter.setCustomizations(
@ -223,7 +231,7 @@ class AvatarCustomizationFragment :
customizations.reversed()
} else {
customizations
}
},
)
}
}
@ -241,7 +249,7 @@ class AvatarCustomizationFragment :
private fun shouldSkip(
filter: CustomizationFilter,
ownedCustomizations: List<OwnedCustomization>,
customization: Customization
customization: Customization,
): Boolean {
return if (filter.onlyPurchased && ownedCustomizations.find { it.key == customization.identifier } == null) {
true
@ -266,22 +274,24 @@ class AvatarCustomizationFragment :
return
}
val prefs = user.preferences
val activeCustomization = when (this.type) {
"skin" -> prefs?.skin
"shirt" -> prefs?.shirt
"background" -> prefs?.background
"chair" -> prefs?.chair
"hair" -> when (this.category) {
"bangs" -> prefs?.hair?.bangs.toString()
"base" -> prefs?.hair?.base.toString()
"color" -> prefs?.hair?.color
"flower" -> prefs?.hair?.flower.toString()
"beard" -> prefs?.hair?.beard.toString()
"mustache" -> prefs?.hair?.mustache.toString()
val activeCustomization =
when (this.type) {
"skin" -> prefs?.skin
"shirt" -> prefs?.shirt
"background" -> prefs?.background
"chair" -> prefs?.chair
"hair" ->
when (this.category) {
"bangs" -> prefs?.hair?.bangs.toString()
"base" -> prefs?.hair?.base.toString()
"color" -> prefs?.hair?.color
"flower" -> prefs?.hair?.flower.toString()
"beard" -> prefs?.hair?.beard.toString()
"mustache" -> prefs?.hair?.mustache.toString()
else -> ""
}
else -> ""
}
else -> ""
}
if (activeCustomization != null) {
this.activeCustomization = activeCustomization
this.adapter.activeCustomization = activeCustomization
@ -340,7 +350,11 @@ class AvatarCustomizationFragment :
dialog.show()
}
private fun configureMonthFilterButton(button: CheckBox, value: Int, filter: CustomizationFilter) {
private fun configureMonthFilterButton(
button: CheckBox,
value: Int,
filter: CustomizationFilter,
) {
val identifier = value.toString().padStart(2, '0')
button.isChecked = filter.months.contains(identifier)
button.text
@ -358,4 +372,4 @@ class AvatarCustomizationFragment :
currentFilter.value = newFilter
}
}
}
}

View file

@ -1,284 +1,304 @@
package com.habitrpg.android.habitica.ui.fragments.inventory.customization
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.databinding.FragmentComposeScrollingBinding
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.interactors.ShareAvatarUseCase
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.ui.activities.BaseActivity
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.theme.colors
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.SegmentedControl
import com.habitrpg.android.habitica.ui.views.equipment.AvatarCustomizationOverviewView
import com.habitrpg.android.habitica.ui.views.equipment.EquipmentOverviewView
import com.habitrpg.common.habitica.helpers.MainNavigationController
import com.habitrpg.common.habitica.helpers.launchCatching
import com.habitrpg.common.habitica.theme.HabiticaTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.firstOrNull
import javax.inject.Inject
@AndroidEntryPoint
open class AvatarOverviewFragment :
BaseMainFragment<FragmentComposeScrollingBinding>(),
AdapterView.OnItemSelectedListener {
@Inject
lateinit var userViewModel: MainUserViewModel
@Inject
lateinit var inventoryRepository: InventoryRepository
@Inject
lateinit var appConfigManager: AppConfigManager
override var binding: FragmentComposeScrollingBinding? = null
protected var showCustomization = true
private val battleGearWeapon = mutableStateOf<Equipment?>(null)
private val costumeWeapon = mutableStateOf<Equipment?>(null)
override fun createBinding(
inflater: LayoutInflater,
container: ViewGroup?
): FragmentComposeScrollingBinding {
return FragmentComposeScrollingBinding.inflate(inflater, container, false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
binding?.composeView?.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
HabiticaTheme {
AvatarOverviewView(
userViewModel,
showCustomization,
!showCustomization,
battleGearWeapon.value?.twoHanded == true,
costumeWeapon.value?.twoHanded == true,
{ type, category ->
displayCustomizationFragment(type, category)
},
{ type, category ->
displayAvatarEquipmentFragment(type, category)
},
{ type, equipped, isCostume ->
displayEquipmentFragment(type, equipped, isCostume)
}
)
}
}
}
userViewModel.user.map { Pair(it?.items?.gear?.equipped?.weapon, it?.items?.gear?.costume?.weapon) }
.observe(viewLifecycleOwner) {
lifecycleScope.launchCatching {
battleGearWeapon.value = it.first?.let { key ->
inventoryRepository.getEquipment(
key
).firstOrNull()
}
costumeWeapon.value = it.second?.let { key ->
inventoryRepository.getEquipment(
key
).firstOrNull()
}
}
}
return view
}
private fun displayCustomizationFragment(type: String, category: String?) {
if (appConfigManager.enableCustomizationShop()) {
MainNavigationController.navigate(
AvatarOverviewFragmentDirections.openComposeAvatarDetail(
type,
category ?: ""
)
)
} else {
MainNavigationController.navigate(
AvatarOverviewFragmentDirections.openAvatarDetail(
type,
category ?: ""
)
)
}
}
private fun displayAvatarEquipmentFragment(type: String, category: String?) {
MainNavigationController.navigate(AvatarOverviewFragmentDirections.openAvatarEquipment(type, category ?: ""))
}
private fun displayEquipmentFragment(type: String, equipped: String?, isCostume: Boolean = false) {
MainNavigationController.navigate(AvatarOverviewFragmentDirections.openEquipmentDetail(type, isCostume, equipped ?: ""))
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_share_avatar, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.share_avatar) {
userViewModel.user.value?.let {
val usecase = ShareAvatarUseCase()
lifecycleScope.launchCatching {
usecase.callInteractor(
ShareAvatarUseCase.RequestValues(
requireActivity() as BaseActivity,
it,
"Check out my avatar on Habitica!",
"avatar_customization"
)
)
}
}
}
return super.onOptionsItemSelected(item)
}
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
val newSize: String = if (position == 0) "slim" else "broad"
lifecycleScope.launchCatching {
userRepository.updateUser("preferences.size", newSize)
}
}
override fun onNothingSelected(parent: AdapterView<*>) { /* no-on */
}
}
@Composable
fun AvatarOverviewView(
userViewModel: MainUserViewModel,
showCustomization: Boolean = true,
showEquipment: Boolean = true,
battleGearTwoHanded: Boolean = false,
costumeTwoHanded: Boolean = false,
onCustomizationTap: (String, String?) -> Unit,
onAvatarEquipmentTap: (String, String?) -> Unit,
onEquipmentTap: (String, String?, Boolean) -> Unit,
) {
val user by userViewModel.user.observeAsState()
Column(
Modifier
.padding(horizontal = 8.dp)
.padding(bottom = 16.dp)
) {
if (showCustomization) {
Row(
Modifier.padding(horizontal = 12.dp, vertical = 15.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
stringResource(R.string.avatar_size),
style = HabiticaTheme.typography.subtitle2,
color = HabiticaTheme.colors.textSecondary
)
Spacer(modifier = Modifier.weight(1f))
SegmentedControl(
items = listOf(
stringResource(R.string.avatar_size_slim),
stringResource(
R.string.avatar_size_broad
)
),
defaultSelectedItemIndex = if (user?.preferences?.size == "slim") 0 else 1,
onItemSelection = {
userViewModel.updateUser(
"preferences.size",
if (it == 0) "slim" else "broad"
)
}
)
}
AvatarCustomizationOverviewView(user?.preferences, user?.items?.gear?.equipped, onCustomizationTap, onAvatarEquipmentTap)
}
if (showEquipment) {
Row(
Modifier
.padding(horizontal = 12.dp)
.padding(top = 15.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
stringResource(R.string.equipped),
style = HabiticaTheme.typography.subtitle2,
color = HabiticaTheme.colors.textSecondary
)
Spacer(modifier = Modifier.weight(1f))
Text(
stringResource(R.string.equip_automatically),
style = HabiticaTheme.typography.body2,
color = HabiticaTheme.colors.textPrimary
)
Switch(checked = user?.preferences?.autoEquip == true, onCheckedChange = {
userViewModel.updateUser("preferences.autoEquip", it)
})
}
EquipmentOverviewView(user?.items?.gear?.equipped, battleGearTwoHanded, { type, equipped ->
onEquipmentTap(type, equipped, false)
})
Row(
Modifier
.padding(horizontal = 12.dp)
.padding(top = 15.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
stringResource(R.string.costume),
style = HabiticaTheme.typography.subtitle2,
color = HabiticaTheme.colors.textSecondary
)
Spacer(modifier = Modifier.weight(1f))
Text(
stringResource(R.string.wear_costume),
style = HabiticaTheme.typography.body2,
color = HabiticaTheme.colors.textPrimary
)
Switch(checked = user?.preferences?.costume == true, onCheckedChange = {
userViewModel.updateUser("preferences.costume", it)
})
}
EquipmentOverviewView(user?.items?.gear?.costume, costumeTwoHanded, { type, equipped ->
onEquipmentTap(type, equipped, true)
}, modifier = Modifier.alpha(if (user?.preferences?.costume == true) 1.0f else 0.5f))
}
}
}
package com.habitrpg.android.habitica.ui.fragments.inventory.customization
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.databinding.FragmentComposeScrollingBinding
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.interactors.ShareAvatarUseCase
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.ui.activities.BaseActivity
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.theme.colors
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.SegmentedControl
import com.habitrpg.android.habitica.ui.views.equipment.AvatarCustomizationOverviewView
import com.habitrpg.android.habitica.ui.views.equipment.EquipmentOverviewView
import com.habitrpg.common.habitica.helpers.MainNavigationController
import com.habitrpg.common.habitica.helpers.launchCatching
import com.habitrpg.common.habitica.theme.HabiticaTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.firstOrNull
import javax.inject.Inject
@AndroidEntryPoint
open class AvatarOverviewFragment :
BaseMainFragment<FragmentComposeScrollingBinding>(),
AdapterView.OnItemSelectedListener {
@Inject
lateinit var userViewModel: MainUserViewModel
@Inject
lateinit var inventoryRepository: InventoryRepository
@Inject
lateinit var appConfigManager: AppConfigManager
override var binding: FragmentComposeScrollingBinding? = null
protected var showCustomization = true
private val battleGearWeapon = mutableStateOf<Equipment?>(null)
private val costumeWeapon = mutableStateOf<Equipment?>(null)
override fun createBinding(
inflater: LayoutInflater,
container: ViewGroup?,
): FragmentComposeScrollingBinding {
return FragmentComposeScrollingBinding.inflate(inflater, container, false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
binding?.composeView?.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
HabiticaTheme {
AvatarOverviewView(
userViewModel,
showCustomization,
!showCustomization,
battleGearWeapon.value?.twoHanded == true,
costumeWeapon.value?.twoHanded == true,
{ type, category ->
displayCustomizationFragment(type, category)
},
{ type, category ->
displayAvatarEquipmentFragment(type, category)
},
{ type, equipped, isCostume ->
displayEquipmentFragment(type, equipped, isCostume)
},
)
}
}
}
userViewModel.user.map { Pair(it?.items?.gear?.equipped?.weapon, it?.items?.gear?.costume?.weapon) }
.observe(viewLifecycleOwner) {
lifecycleScope.launchCatching {
battleGearWeapon.value =
it.first?.let { key ->
inventoryRepository.getEquipment(
key,
).firstOrNull()
}
costumeWeapon.value =
it.second?.let { key ->
inventoryRepository.getEquipment(
key,
).firstOrNull()
}
}
}
return view
}
private fun displayCustomizationFragment(
type: String,
category: String?,
) {
if (appConfigManager.enableCustomizationShop()) {
MainNavigationController.navigate(
AvatarOverviewFragmentDirections.openComposeAvatarDetail(
type,
category ?: "",
),
)
} else {
MainNavigationController.navigate(
AvatarOverviewFragmentDirections.openAvatarDetail(
type,
category ?: "",
),
)
}
}
private fun displayAvatarEquipmentFragment(
type: String,
category: String?,
) {
MainNavigationController.navigate(AvatarOverviewFragmentDirections.openAvatarEquipment(type, category ?: ""))
}
private fun displayEquipmentFragment(
type: String,
equipped: String?,
isCostume: Boolean = false,
) {
MainNavigationController.navigate(AvatarOverviewFragmentDirections.openEquipmentDetail(type, isCostume, equipped ?: ""))
}
override fun onCreateOptionsMenu(
menu: Menu,
inflater: MenuInflater,
) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_share_avatar, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.share_avatar) {
userViewModel.user.value?.let {
val usecase = ShareAvatarUseCase()
lifecycleScope.launchCatching {
usecase.callInteractor(
ShareAvatarUseCase.RequestValues(
requireActivity() as BaseActivity,
it,
"Check out my avatar on Habitica!",
"avatar_customization",
),
)
}
}
}
return super.onOptionsItemSelected(item)
}
override fun onItemSelected(
parent: AdapterView<*>,
view: View?,
position: Int,
id: Long,
) {
val newSize: String = if (position == 0) "slim" else "broad"
lifecycleScope.launchCatching {
userRepository.updateUser("preferences.size", newSize)
}
}
override fun onNothingSelected(parent: AdapterView<*>) { // no-on
}
}
@Composable
fun AvatarOverviewView(
userViewModel: MainUserViewModel,
showCustomization: Boolean = true,
showEquipment: Boolean = true,
battleGearTwoHanded: Boolean = false,
costumeTwoHanded: Boolean = false,
onCustomizationTap: (String, String?) -> Unit,
onAvatarEquipmentTap: (String, String?) -> Unit,
onEquipmentTap: (String, String?, Boolean) -> Unit,
) {
val user by userViewModel.user.observeAsState()
Column(
Modifier
.padding(horizontal = 8.dp)
.padding(bottom = 16.dp),
) {
if (showCustomization) {
Row(
Modifier.padding(horizontal = 12.dp, vertical = 15.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
stringResource(R.string.avatar_size),
style = HabiticaTheme.typography.subtitle2,
color = HabiticaTheme.colors.textSecondary,
)
Spacer(modifier = Modifier.weight(1f))
SegmentedControl(
items =
listOf(
stringResource(R.string.avatar_size_slim),
stringResource(
R.string.avatar_size_broad,
),
),
defaultSelectedItemIndex = if (user?.preferences?.size == "slim") 0 else 1,
onItemSelection = {
userViewModel.updateUser(
"preferences.size",
if (it == 0) "slim" else "broad",
)
},
)
}
AvatarCustomizationOverviewView(user?.preferences, user?.items?.gear?.equipped, onCustomizationTap, onAvatarEquipmentTap)
}
if (showEquipment) {
Row(
Modifier
.padding(horizontal = 12.dp)
.padding(top = 15.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
stringResource(R.string.equipped),
style = HabiticaTheme.typography.subtitle2,
color = HabiticaTheme.colors.textSecondary,
)
Spacer(modifier = Modifier.weight(1f))
Text(
stringResource(R.string.equip_automatically),
style = HabiticaTheme.typography.body2,
color = HabiticaTheme.colors.textPrimary,
)
Switch(checked = user?.preferences?.autoEquip == true, onCheckedChange = {
userViewModel.updateUser("preferences.autoEquip", it)
})
}
EquipmentOverviewView(user?.items?.gear?.equipped, battleGearTwoHanded, { type, equipped ->
onEquipmentTap(type, equipped, false)
})
Row(
Modifier
.padding(horizontal = 12.dp)
.padding(top = 15.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
stringResource(R.string.costume),
style = HabiticaTheme.typography.subtitle2,
color = HabiticaTheme.colors.textSecondary,
)
Spacer(modifier = Modifier.weight(1f))
Text(
stringResource(R.string.wear_costume),
style = HabiticaTheme.typography.body2,
color = HabiticaTheme.colors.textPrimary,
)
Switch(checked = user?.preferences?.costume == true, onCheckedChange = {
userViewModel.updateUser("preferences.costume", it)
})
}
EquipmentOverviewView(user?.items?.gear?.costume, costumeTwoHanded, { type, equipped ->
onEquipmentTap(type, equipped, true)
}, modifier = Modifier.alpha(if (user?.preferences?.costume == true) 1.0f else 0.5f))
}
}
}

View file

@ -12,7 +12,7 @@ class CustomizationsShopFragment : ShopFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View? {
shopIdentifier = Shop.CUSTOMIZATIONS
return super.onCreateView(inflater, container, savedInstanceState)

View file

@ -1,419 +1,432 @@
package com.habitrpg.android.habitica.ui.fragments.inventory.shops
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.databinding.FragmentRefreshRecyclerviewBinding
import com.habitrpg.android.habitica.helpers.Analytics
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.EventCategory
import com.habitrpg.android.habitica.helpers.HitType
import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopCategory
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.ui.adapter.inventory.ShopRecyclerAdapter
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.fragments.purchases.EventOutcomeSubscriptionBottomSheetFragment
import com.habitrpg.android.habitica.ui.fragments.purchases.SubscriptionBottomSheetFragment
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.CurrencyText
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaProgressDialog
import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientGemsDialog
import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog
import com.habitrpg.common.habitica.helpers.ExceptionHandler
import com.habitrpg.common.habitica.helpers.RecyclerViewState
import com.habitrpg.common.habitica.helpers.launchCatching
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
open class ShopFragment : BaseMainFragment<FragmentRefreshRecyclerviewBinding>() {
internal val currencyView: ComposeView by lazy {
return@lazy ComposeView(requireContext())
}
var adapter: ShopRecyclerAdapter? = null
var shopIdentifier: String? = null
var shop: Shop? = null
internal val hourglasses = mutableStateOf<Double?>(null)
private val gems = mutableStateOf<Double?>(null)
private val gold = mutableStateOf<Double?>(null)
@Inject
lateinit var inventoryRepository: InventoryRepository
@Inject
lateinit var socialRepository: SocialRepository
@Inject
lateinit var configManager: AppConfigManager
@Inject
lateinit var userViewModel: MainUserViewModel
private var layoutManager: GridLayoutManager? = null
private var gearCategories: MutableList<ShopCategory>? = null
override var binding: FragmentRefreshRecyclerviewBinding? = null
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRefreshRecyclerviewBinding {
return FragmentRefreshRecyclerviewBinding.inflate(inflater, container, false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
this.hidesToolbar = true
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onDestroyView() {
inventoryRepository.close()
socialRepository.close()
toolbarAccessoryContainer?.removeView(currencyView)
super.onDestroyView()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initializeCurrencyViews()
toolbarAccessoryContainer?.addView(currencyView)
binding?.recyclerView?.setBackgroundResource(R.color.content_background)
binding?.recyclerView?.onRefresh = {
loadShopInventory()
}
binding?.refreshLayout?.setOnRefreshListener {
loadShopInventory()
}
adapter = binding?.recyclerView?.adapter as? ShopRecyclerAdapter
if (adapter == null) {
adapter = ShopRecyclerAdapter()
adapter?.onNeedsRefresh = {
loadShopInventory()
if (Shop.MARKET == shopIdentifier) {
loadMarketGear()
}
}
adapter?.onShowPurchaseDialog = { item, isPinned ->
if (item.key == "gem" && userViewModel.user.value?.isSubscribed != true) {
Analytics.sendEvent("View gems for gold CTA", EventCategory.BEHAVIOUR, HitType.EVENT)
val subscriptionBottomSheet = EventOutcomeSubscriptionBottomSheetFragment().apply {
eventType = EventOutcomeSubscriptionBottomSheetFragment.EVENT_GEMS_FOR_GOLD
}
activity?.let { activity ->
subscriptionBottomSheet.show(activity.supportFragmentManager, SubscriptionBottomSheetFragment.TAG)
}
} else {
val dialog = PurchaseDialog(
requireContext(),
userRepository,
inventoryRepository,
item,
mainActivity
)
dialog.shopIdentifier = shopIdentifier
dialog.isPinned = isPinned
dialog.onShopNeedsRefresh = {
loadShopInventory()
if (Shop.MARKET == shopIdentifier) {
loadMarketGear()
}
}
dialog.show()
}
}
adapter?.context = context
adapter?.mainActivity = mainActivity
binding?.recyclerView?.adapter = adapter
binding?.recyclerView?.itemAnimator = SafeDefaultItemAnimator()
adapter?.changeClassEvents = {
showClassChangeDialog(it)
}
lifecycleScope.launchCatching {
inventoryRepository.getInAppReward("armoire").collect {
adapter?.armoireItem = it
}
}
lifecycleScope.launchCatching {
inventoryRepository.getArmoireRemainingCount().collect {
adapter?.armoireCount = it
}
}
}
if (binding?.recyclerView?.layoutManager == null) {
layoutManager = GridLayoutManager(context, 2)
layoutManager?.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if ((adapter?.getItemViewType(position) ?: 0) < 4) {
layoutManager?.spanCount ?: 1
} else {
1
}
}
}
binding?.recyclerView?.layoutManager = layoutManager
}
if (savedInstanceState != null) {
this.shopIdentifier = savedInstanceState.getString(SHOP_IDENTIFIER_KEY, "")
}
adapter?.selectedGearCategory = userViewModel.user.value?.stats?.habitClass ?: ""
if (shop != null) {
adapter?.setShop(shop)
}
adapter?.shopSpriteSuffix = configManager.shopSpriteSuffix()
val categories = gearCategories
if (categories != null) {
adapter?.gearCategories = categories
} else {
if (Shop.MARKET == shopIdentifier) {
loadMarketGear()
}
}
userViewModel.user.observe(viewLifecycleOwner) {
adapter?.user = it
hourglasses.value = it?.hourglassCount?.toDouble() ?: 0.0
gems.value = it?.gemCount?.toDouble() ?: 0.0
gold.value = it?.stats?.gp ?: 0.0
}
lifecycleScope.launch(ExceptionHandler.coroutine()) {
socialRepository.getGroup(Group.TAVERN_ID)
.filter { it?.hasActiveQuest == true }
.filter { group -> group?.quest?.rageStrikes?.any { it.key == shopIdentifier } ?: false }
.filter { group -> group?.quest?.rageStrikes?.filter { it.key == shopIdentifier }?.get(0)?.wasHit == true }
.collect {
adapter?.shopSpriteSuffix = "_" + it?.quest?.key
}
}
view.post { setGridSpanCount(view.width) }
lifecycleScope.launchCatching {
inventoryRepository.getOwnedItems()
.collect { adapter?.setOwnedItems(it) }
}
lifecycleScope.launchCatching {
inventoryRepository.getInAppRewards()
.map { rewards -> rewards.map { it.key } }
.collect { adapter?.setPinnedItemKeys(it) }
}
Analytics.sendNavigationEvent("$shopIdentifier screen")
}
open fun initializeCurrencyViews() {
currencyView.setContent {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
gems.value?.let { CurrencyText(currency = "gems", value = it) }
gold.value?.let { CurrencyText(currency = "gold", value = it) }
}
}
}
private fun showClassChangeDialog(classIdentifier: String) {
lifecycleScope.launch(ExceptionHandler.coroutine()) {
val user = userViewModel.user.value ?: return@launch
context?.let { context ->
if (user.gemCount <= 2) {
val dialog = mainActivity?.let { InsufficientGemsDialog(it, 3) }
Analytics.sendEvent("show insufficient gems modal", EventCategory.BEHAVIOUR, HitType.EVENT, mapOf("reason" to "class change"))
dialog?.show()
return@launch
}
if (user.flags?.classSelected == true && user.preferences?.disableClasses == false) {
val alert = HabiticaAlertDialog(context)
alert.setTitle(getString(R.string.change_class_selected_confirmation, classIdentifier))
alert.setMessage(getString(R.string.change_class_equipment_warning))
alert.addButton(R.string.choose_class, true) { _, _ ->
val dialog = HabiticaProgressDialog.show(
requireActivity(),
getString(R.string.changing_class_progress),
300
)
lifecycleScope.launch(Dispatchers.Main) {
userRepository.changeClass(classIdentifier)
dialog.dismiss()
displayClassChanged(classIdentifier)
loadMarketGear()
}
}
alert.addButton(R.string.close, false)
alert.show()
} else {
val alert = HabiticaAlertDialog(context)
alert.setTitle(getString(R.string.class_confirmation, classIdentifier))
alert.addButton(R.string.choose_class, true) { _, _ ->
val dialog = HabiticaProgressDialog.show(
requireActivity(),
getString(R.string.changing_class_progress),
300
)
lifecycleScope.launch(Dispatchers.Main) {
userRepository.changeClass(classIdentifier)
dialog.dismiss()
displayClassChanged(classIdentifier)
loadMarketGear()
}
}
alert.addButton(R.string.close, false)
alert.show()
}
}
}
}
override fun onResume() {
super.onResume()
if (shop == null) {
loadShopInventory()
}
}
private fun loadShopInventory() {
val shopUrl = when (this.shopIdentifier) {
Shop.MARKET -> "market"
Shop.QUEST_SHOP -> "quests"
Shop.TIME_TRAVELERS_SHOP -> "time-travelers"
Shop.SEASONAL_SHOP -> "seasonal"
Shop.CUSTOMIZATIONS -> "customizations"
else -> ""
}
lifecycleScope.launchCatching({
binding?.recyclerView?.state = RecyclerViewState.FAILED
}) {
val shop1 = inventoryRepository.retrieveShopInventory(shopUrl) ?: return@launchCatching
when (shop1.identifier) {
Shop.MARKET -> {
val user = userViewModel.user.value
val specialCategory = ShopCategory()
specialCategory.text = getString(R.string.special)
val item = ShopItem.makeGemItem(context?.resources)
if (user?.isSubscribed == true) {
item.limitedNumberLeft = user.purchased?.plan?.numberOfGemsLeft
} else {
item.limitedNumberLeft = -1
}
specialCategory.items.add(item)
specialCategory.items.add(ShopItem.makeFortifyItem(context?.resources))
shop1.categories.add(specialCategory)
}
Shop.TIME_TRAVELERS_SHOP -> {
formatTimeTravelersShop(shop1)
}
Shop.SEASONAL_SHOP -> {
shop1.categories.sortWith(
compareBy<ShopCategory> { it.items.firstOrNull()?.currency != "gold" }
.thenByDescending { it.items.firstOrNull()?.event?.end }
.thenBy { it.items.firstOrNull()?.locked }
)
}
}
shop = shop1
adapter?.setShop(shop1)
binding?.refreshLayout?.isRefreshing = false
}
}
private fun formatTimeTravelersShop(shop: Shop): Shop {
val newCategories = mutableListOf<ShopCategory>()
for (category in shop.categories) {
if (category.pinType != "mystery_set") {
newCategories.add(category)
} else {
val newCategory = newCategories.find { it.identifier == "mystery_sets" } ?: ShopCategory()
if (newCategory.identifier.isEmpty()) {
newCategory.identifier = "mystery_sets"
newCategory.text = getString(R.string.mystery_sets)
newCategories.add(newCategory)
}
val item = category.items.firstOrNull() ?: continue
item.key = category.identifier
item.text = category.text
item.imageName = "shop_set_mystery_${item.key}"
item.pinType = "mystery_set"
item.path = "mystery.${item.key}"
newCategory.items.add(item)
}
}
shop.categories = newCategories
return shop
}
private fun loadMarketGear() {
lifecycleScope.launchCatching {
val shop = inventoryRepository.retrieveMarketGear()
val equipment = inventoryRepository.getOwnedEquipment()
.map { equipment -> equipment.map { it.key } }.firstOrNull()
for (category in shop?.categories ?: emptyList()) {
val items = category.items.asSequence().filter {
equipment?.contains(it.key) == false
}.sortedBy { it.locked }.toList()
category.items.clear()
category.items.addAll(items)
}
gearCategories = shop?.categories
adapter?.gearCategories = shop?.categories ?: mutableListOf()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(SHOP_IDENTIFIER_KEY, this.shopIdentifier)
}
private fun setGridSpanCount(width: Int) {
var spanCount = 0
context?.let { context ->
val itemWidth: Float = context.resources.getDimension(R.dimen.reward_width)
spanCount = (width / itemWidth).toInt()
}
if (spanCount == 0) {
spanCount = 1
}
layoutManager?.spanCount = spanCount
layoutManager?.requestLayout()
}
private fun displayClassChanged(selectedClass: String) {
context?.let { context ->
val alert = HabiticaAlertDialog(context)
alert.setMessage(getString(R.string.class_changed_description, selectedClass))
alert.addButton(getString(R.string.complete_tutorial), true) { _, _ -> alert.dismiss() }
alert.show()
}
}
companion object {
private const val SHOP_IDENTIFIER_KEY = "SHOP_IDENTIFIER_KEY"
}
}
package com.habitrpg.android.habitica.ui.fragments.inventory.shops
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.databinding.FragmentRefreshRecyclerviewBinding
import com.habitrpg.android.habitica.helpers.Analytics
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.EventCategory
import com.habitrpg.android.habitica.helpers.HitType
import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopCategory
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.ui.adapter.inventory.ShopRecyclerAdapter
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.fragments.purchases.EventOutcomeSubscriptionBottomSheetFragment
import com.habitrpg.android.habitica.ui.fragments.purchases.SubscriptionBottomSheetFragment
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.CurrencyText
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaProgressDialog
import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientGemsDialog
import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog
import com.habitrpg.common.habitica.helpers.ExceptionHandler
import com.habitrpg.common.habitica.helpers.RecyclerViewState
import com.habitrpg.common.habitica.helpers.launchCatching
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
open class ShopFragment : BaseMainFragment<FragmentRefreshRecyclerviewBinding>() {
internal val currencyView: ComposeView by lazy {
return@lazy ComposeView(requireContext())
}
var adapter: ShopRecyclerAdapter? = null
var shopIdentifier: String? = null
var shop: Shop? = null
internal val hourglasses = mutableStateOf<Double?>(null)
private val gems = mutableStateOf<Double?>(null)
private val gold = mutableStateOf<Double?>(null)
@Inject
lateinit var inventoryRepository: InventoryRepository
@Inject
lateinit var socialRepository: SocialRepository
@Inject
lateinit var configManager: AppConfigManager
@Inject
lateinit var userViewModel: MainUserViewModel
private var layoutManager: GridLayoutManager? = null
private var gearCategories: MutableList<ShopCategory>? = null
override var binding: FragmentRefreshRecyclerviewBinding? = null
override fun createBinding(
inflater: LayoutInflater,
container: ViewGroup?,
): FragmentRefreshRecyclerviewBinding {
return FragmentRefreshRecyclerviewBinding.inflate(inflater, container, false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
this.hidesToolbar = true
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onDestroyView() {
inventoryRepository.close()
socialRepository.close()
toolbarAccessoryContainer?.removeView(currencyView)
super.onDestroyView()
}
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
initializeCurrencyViews()
toolbarAccessoryContainer?.addView(currencyView)
binding?.recyclerView?.setBackgroundResource(R.color.content_background)
binding?.recyclerView?.onRefresh = {
loadShopInventory()
}
binding?.refreshLayout?.setOnRefreshListener {
loadShopInventory()
}
adapter = binding?.recyclerView?.adapter as? ShopRecyclerAdapter
if (adapter == null) {
adapter = ShopRecyclerAdapter()
adapter?.onNeedsRefresh = {
loadShopInventory()
if (Shop.MARKET == shopIdentifier) {
loadMarketGear()
}
}
adapter?.onShowPurchaseDialog = { item, isPinned ->
if (item.key == "gem" && userViewModel.user.value?.isSubscribed != true) {
Analytics.sendEvent("View gems for gold CTA", EventCategory.BEHAVIOUR, HitType.EVENT)
val subscriptionBottomSheet =
EventOutcomeSubscriptionBottomSheetFragment().apply {
eventType = EventOutcomeSubscriptionBottomSheetFragment.EVENT_GEMS_FOR_GOLD
}
activity?.let { activity ->
subscriptionBottomSheet.show(activity.supportFragmentManager, SubscriptionBottomSheetFragment.TAG)
}
} else {
val dialog =
PurchaseDialog(
requireContext(),
userRepository,
inventoryRepository,
item,
mainActivity,
)
dialog.shopIdentifier = shopIdentifier
dialog.isPinned = isPinned
dialog.onShopNeedsRefresh = {
loadShopInventory()
if (Shop.MARKET == shopIdentifier) {
loadMarketGear()
}
}
dialog.show()
}
}
adapter?.context = context
adapter?.mainActivity = mainActivity
binding?.recyclerView?.adapter = adapter
binding?.recyclerView?.itemAnimator = SafeDefaultItemAnimator()
adapter?.changeClassEvents = {
showClassChangeDialog(it)
}
lifecycleScope.launchCatching {
inventoryRepository.getInAppReward("armoire").collect {
adapter?.armoireItem = it
}
}
lifecycleScope.launchCatching {
inventoryRepository.getArmoireRemainingCount().collect {
adapter?.armoireCount = it
}
}
}
if (binding?.recyclerView?.layoutManager == null) {
layoutManager = GridLayoutManager(context, 2)
layoutManager?.spanSizeLookup =
object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if ((adapter?.getItemViewType(position) ?: 0) < 4) {
layoutManager?.spanCount ?: 1
} else {
1
}
}
}
binding?.recyclerView?.layoutManager = layoutManager
}
if (savedInstanceState != null) {
this.shopIdentifier = savedInstanceState.getString(SHOP_IDENTIFIER_KEY, "")
}
adapter?.selectedGearCategory = userViewModel.user.value?.stats?.habitClass ?: ""
if (shop != null) {
adapter?.setShop(shop)
}
adapter?.shopSpriteSuffix = configManager.shopSpriteSuffix()
val categories = gearCategories
if (categories != null) {
adapter?.gearCategories = categories
} else {
if (Shop.MARKET == shopIdentifier) {
loadMarketGear()
}
}
userViewModel.user.observe(viewLifecycleOwner) {
adapter?.user = it
hourglasses.value = it?.hourglassCount?.toDouble() ?: 0.0
gems.value = it?.gemCount?.toDouble() ?: 0.0
gold.value = it?.stats?.gp ?: 0.0
}
lifecycleScope.launch(ExceptionHandler.coroutine()) {
socialRepository.getGroup(Group.TAVERN_ID)
.filter { it?.hasActiveQuest == true }
.filter { group -> group?.quest?.rageStrikes?.any { it.key == shopIdentifier } ?: false }
.filter { group -> group?.quest?.rageStrikes?.filter { it.key == shopIdentifier }?.get(0)?.wasHit == true }
.collect {
adapter?.shopSpriteSuffix = "_" + it?.quest?.key
}
}
view.post { setGridSpanCount(view.width) }
lifecycleScope.launchCatching {
inventoryRepository.getOwnedItems()
.collect { adapter?.setOwnedItems(it) }
}
lifecycleScope.launchCatching {
inventoryRepository.getInAppRewards()
.map { rewards -> rewards.map { it.key } }
.collect { adapter?.setPinnedItemKeys(it) }
}
Analytics.sendNavigationEvent("$shopIdentifier screen")
}
open fun initializeCurrencyViews() {
currencyView.setContent {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
gems.value?.let { CurrencyText(currency = "gems", value = it) }
gold.value?.let { CurrencyText(currency = "gold", value = it) }
}
}
}
private fun showClassChangeDialog(classIdentifier: String) {
lifecycleScope.launch(ExceptionHandler.coroutine()) {
val user = userViewModel.user.value ?: return@launch
context?.let { context ->
if (user.gemCount <= 2) {
val dialog = mainActivity?.let { InsufficientGemsDialog(it, 3) }
Analytics.sendEvent("show insufficient gems modal", EventCategory.BEHAVIOUR, HitType.EVENT, mapOf("reason" to "class change"))
dialog?.show()
return@launch
}
if (user.flags?.classSelected == true && user.preferences?.disableClasses == false) {
val alert = HabiticaAlertDialog(context)
alert.setTitle(getString(R.string.change_class_selected_confirmation, classIdentifier))
alert.setMessage(getString(R.string.change_class_equipment_warning))
alert.addButton(R.string.choose_class, true) { _, _ ->
val dialog =
HabiticaProgressDialog.show(
requireActivity(),
getString(R.string.changing_class_progress),
300,
)
lifecycleScope.launch(Dispatchers.Main) {
userRepository.changeClass(classIdentifier)
dialog.dismiss()
displayClassChanged(classIdentifier)
loadMarketGear()
}
}
alert.addButton(R.string.close, false)
alert.show()
} else {
val alert = HabiticaAlertDialog(context)
alert.setTitle(getString(R.string.class_confirmation, classIdentifier))
alert.addButton(R.string.choose_class, true) { _, _ ->
val dialog =
HabiticaProgressDialog.show(
requireActivity(),
getString(R.string.changing_class_progress),
300,
)
lifecycleScope.launch(Dispatchers.Main) {
userRepository.changeClass(classIdentifier)
dialog.dismiss()
displayClassChanged(classIdentifier)
loadMarketGear()
}
}
alert.addButton(R.string.close, false)
alert.show()
}
}
}
}
override fun onResume() {
super.onResume()
if (shop == null) {
loadShopInventory()
}
}
private fun loadShopInventory() {
val shopUrl =
when (this.shopIdentifier) {
Shop.MARKET -> "market"
Shop.QUEST_SHOP -> "quests"
Shop.TIME_TRAVELERS_SHOP -> "time-travelers"
Shop.SEASONAL_SHOP -> "seasonal"
Shop.CUSTOMIZATIONS -> "customizations"
else -> ""
}
lifecycleScope.launchCatching({
binding?.recyclerView?.state = RecyclerViewState.FAILED
}) {
val shop1 = inventoryRepository.retrieveShopInventory(shopUrl) ?: return@launchCatching
when (shop1.identifier) {
Shop.MARKET -> {
val user = userViewModel.user.value
val specialCategory = ShopCategory()
specialCategory.text = getString(R.string.special)
val item = ShopItem.makeGemItem(context?.resources)
if (user?.isSubscribed == true) {
item.limitedNumberLeft = user.purchased?.plan?.numberOfGemsLeft
} else {
item.limitedNumberLeft = -1
}
specialCategory.items.add(item)
specialCategory.items.add(ShopItem.makeFortifyItem(context?.resources))
shop1.categories.add(specialCategory)
}
Shop.TIME_TRAVELERS_SHOP -> {
formatTimeTravelersShop(shop1)
}
Shop.SEASONAL_SHOP -> {
shop1.categories.sortWith(
compareBy<ShopCategory> { it.items.firstOrNull()?.currency != "gold" }
.thenByDescending { it.items.firstOrNull()?.event?.end }
.thenBy { it.items.firstOrNull()?.locked },
)
}
}
shop = shop1
adapter?.setShop(shop1)
binding?.refreshLayout?.isRefreshing = false
}
}
private fun formatTimeTravelersShop(shop: Shop): Shop {
val newCategories = mutableListOf<ShopCategory>()
for (category in shop.categories) {
if (category.pinType != "mystery_set") {
newCategories.add(category)
} else {
val newCategory = newCategories.find { it.identifier == "mystery_sets" } ?: ShopCategory()
if (newCategory.identifier.isEmpty()) {
newCategory.identifier = "mystery_sets"
newCategory.text = getString(R.string.mystery_sets)
newCategories.add(newCategory)
}
val item = category.items.firstOrNull() ?: continue
item.key = category.identifier
item.text = category.text
item.imageName = "shop_set_mystery_${item.key}"
item.pinType = "mystery_set"
item.path = "mystery.${item.key}"
newCategory.items.add(item)
}
}
shop.categories = newCategories
return shop
}
private fun loadMarketGear() {
lifecycleScope.launchCatching {
val shop = inventoryRepository.retrieveMarketGear()
val equipment =
inventoryRepository.getOwnedEquipment()
.map { equipment -> equipment.map { it.key } }.firstOrNull()
for (category in shop?.categories ?: emptyList()) {
val items =
category.items.asSequence().filter {
equipment?.contains(it.key) == false
}.sortedBy { it.locked }.toList()
category.items.clear()
category.items.addAll(items)
}
gearCategories = shop?.categories
adapter?.gearCategories = shop?.categories ?: mutableListOf()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(SHOP_IDENTIFIER_KEY, this.shopIdentifier)
}
private fun setGridSpanCount(width: Int) {
var spanCount = 0
context?.let { context ->
val itemWidth: Float = context.resources.getDimension(R.dimen.reward_width)
spanCount = (width / itemWidth).toInt()
}
if (spanCount == 0) {
spanCount = 1
}
layoutManager?.spanCount = spanCount
layoutManager?.requestLayout()
}
private fun displayClassChanged(selectedClass: String) {
context?.let { context ->
val alert = HabiticaAlertDialog(context)
alert.setMessage(getString(R.string.class_changed_description, selectedClass))
alert.addButton(getString(R.string.complete_tutorial), true) { _, _ -> alert.dismiss() }
alert.show()
}
}
companion object {
private const val SHOP_IDENTIFIER_KEY = "SHOP_IDENTIFIER_KEY"
}
}

View file

@ -1,305 +1,319 @@
package com.habitrpg.android.habitica.ui.viewHolders.tasks
import android.content.Context
import android.text.method.LinkMovementMethod
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.ui.viewHolders.BindableViewHolder
import com.habitrpg.android.habitica.ui.views.EllipsisTextView
import com.habitrpg.common.habitica.extensions.dpToPx
import com.habitrpg.common.habitica.extensions.getThemeColor
import com.habitrpg.common.habitica.helpers.MarkdownParser
import com.habitrpg.common.habitica.helpers.setParsedMarkdown
import com.habitrpg.shared.habitica.models.responses.TaskDirection
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class BaseTaskViewHolder(
itemView: View,
var scoreTaskFunc: ((Task, TaskDirection) -> Unit),
var openTaskFunc: ((Task, View) -> Unit),
var brokenTaskFunc: ((Task) -> Unit),
var assignedTextProvider: GroupPlanInfoProvider?
) : BindableViewHolder<Task>(itemView), View.OnTouchListener {
private val scope = MainScope()
var task: Task? = null
var movingFromPosition: Int? = null
var errorButtonClicked: (() -> Unit)? = null
var userID: String? = null
var isLocked = false
protected var context: Context
private val mainTaskWrapper: ViewGroup = itemView.findViewById(R.id.main_task_wrapper)
protected val assignedTextView: TextView = itemView.findViewById(R.id.assigned_textview)
protected val completedCountTextView: TextView = itemView.findViewById(R.id.completed_textview)
protected val titleTextView: EllipsisTextView = itemView.findViewById(R.id.checkedTextView)
protected val notesTextView: EllipsisTextView? = itemView.findViewById(R.id.notesTextView)
protected val calendarIconView: ImageView? = itemView.findViewById(R.id.iconViewCalendar)
protected val iconViewTeam: ImageView? = itemView.findViewById(R.id.iconViewTeamTask)
protected val specialTaskTextView: TextView? = itemView.findViewById(R.id.specialTaskText)
private val iconViewChallenge: ImageView? = itemView.findViewById(R.id.iconviewChallenge)
private val iconViewReminder: ImageView? = itemView.findViewById(R.id.iconviewReminder)
private val taskIconWrapper: LinearLayout? = itemView.findViewById(R.id.taskIconWrapper)
private val approvalRequiredTextView: TextView =
itemView.findViewById(R.id.approvalRequiredTextField)
private val expandNotesButton: Button? = itemView.findViewById(R.id.expand_notes_button)
private val syncingView: ProgressBar? = itemView.findViewById(R.id.syncing_view)
private val errorIconView: ImageButton? = itemView.findViewById(R.id.error_icon)
protected val taskGray: Int =
ContextCompat.getColor(itemView.context, R.color.offset_background)
protected val streakIconView: ImageView = itemView.findViewById(R.id.iconViewStreak)
protected val streakTextView: TextView = itemView.findViewById(R.id.streakTextView)
protected val reminderTextView: TextView = itemView.findViewById(R.id.reminder_textview)
private var openTaskDisabled: Boolean = false
private var taskActionsDisabled: Boolean = false
private var notesExpanded = false
protected open val taskIconWrapperIsVisible: Boolean
get() {
var isVisible = false
if (iconViewTeam?.visibility == View.VISIBLE) {
isVisible = true
}
if (iconViewReminder?.visibility == View.VISIBLE) {
isVisible = true
}
if (iconViewChallenge?.visibility == View.VISIBLE) {
isVisible = true
}
if (iconViewReminder?.visibility == View.VISIBLE) {
isVisible = true
}
if (specialTaskTextView?.visibility == View.VISIBLE) {
isVisible = true
}
if (this.streakTextView.visibility == View.VISIBLE) {
isVisible = true
}
return isVisible
}
init {
itemView.setOnTouchListener(this)
itemView.isClickable = true
mainTaskWrapper.clipToOutline = true
titleTextView.setOnClickListener { onTouch(it, null) }
notesTextView?.setOnClickListener { onTouch(it, null) }
errorIconView?.setOnClickListener { errorButtonClicked?.invoke() }
notesTextView?.movementMethod = LinkMovementMethod.getInstance()
titleTextView.movementMethod = LinkMovementMethod.getInstance()
expandNotesButton?.setOnClickListener { expandTask() }
iconViewChallenge?.setOnClickListener {
task?.let { t ->
if (task?.challengeBroken?.isNotBlank() == true) brokenTaskFunc(t)
}
}
notesTextView?.addEllipsesListener(object : EllipsisTextView.EllipsisListener {
override fun ellipsisStateChanged(ellipses: Boolean) {
scope.launch(Dispatchers.Main.immediate) {
if (ellipses && notesTextView.maxLines != 3) {
notesTextView.maxLines = 3
}
expandNotesButton?.visibility =
if (ellipses || notesExpanded) View.VISIBLE else View.GONE
}
}
})
context = itemView.context
}
private fun expandTask() {
notesExpanded = !notesExpanded
if (notesExpanded) {
notesTextView?.maxLines = 100
expandNotesButton?.text = context.getString(R.string.collapse_notes)
} else {
notesTextView?.maxLines = 8
expandNotesButton?.text = context.getString(R.string.expand_notes)
}
}
override fun bind(data: Task, position: Int, displayMode: String) {
bind(data, position, displayMode, null)
}
open fun bind(
data: Task,
position: Int,
displayMode: String,
ownerID: String?
) {
notesExpanded = false
task = data
itemView.setBackgroundColor(context.getThemeColor(R.attr.colorContentBackground))
expandNotesButton?.visibility = View.GONE
notesExpanded = false
notesTextView?.maxLines = 8
if (data.notes?.isNotEmpty() == true) {
notesTextView?.visibility = View.VISIBLE
notesTextView?.setTextColor(ContextCompat.getColor(context, R.color.text_ternary))
} else {
notesTextView?.visibility = View.GONE
}
titleTextView.text = data.text
scope.launch(Dispatchers.IO) {
if (data.text.isNotEmpty() && MarkdownParser.containsMarkdown(data.text)) {
val parsedText = MarkdownParser.parseMarkdown(data.text)
withContext(Dispatchers.Main) {
data.parsedText = parsedText
titleTextView.setParsedMarkdown(parsedText)
}
}
}
if (displayMode != "minimal") {
notesTextView?.text = data.notes
data.notes?.let { notes ->
scope.launch(Dispatchers.IO) {
if (notes.isEmpty() || !MarkdownParser.containsMarkdown(notes)) {
return@launch
}
val parsedNotes = MarkdownParser.parseMarkdown(notes)
withContext(Dispatchers.Main) {
data.parsedNotes = parsedNotes
notesTextView?.setParsedMarkdown(parsedNotes)
}
}
}
} else {
notesTextView?.visibility = View.GONE
}
titleTextView.setTextColor(ContextCompat.getColor(context, R.color.text_primary))
if (displayMode == "standard") {
iconViewReminder?.visibility =
if ((data.reminders?.size ?: 0) > 0) View.VISIBLE else View.GONE
iconViewChallenge?.visibility =
if (task?.challengeID != null) View.VISIBLE else View.GONE
if (task?.challengeID != null) {
if (task?.challengeBroken?.isNotBlank() == true) {
iconViewChallenge?.alpha = 1.0f
iconViewChallenge?.imageTintList =
ContextCompat.getColorStateList(context, R.color.white)
iconViewChallenge?.setImageResource(R.drawable.task_broken_megaphone)
} else {
iconViewChallenge?.alpha = 0.3f
iconViewChallenge?.imageTintList =
ContextCompat.getColorStateList(context, R.color.text_ternary)
iconViewChallenge?.setImageResource(R.drawable.task_megaphone)
}
}
configureSpecialTaskTextView(data)
iconViewTeam?.visibility =
if (data.isGroupTask && ownerID != data.group?.groupID) View.VISIBLE else View.GONE
taskIconWrapper?.visibility = if (taskIconWrapperIsVisible) View.VISIBLE else View.GONE
} else {
taskIconWrapper?.visibility = View.GONE
mainTaskWrapper.minimumHeight = 48.dpToPx(context)
}
if (data.isPendingApproval) {
approvalRequiredTextView.visibility = View.VISIBLE
} else {
approvalRequiredTextView.visibility = View.GONE
}
if (data.group?.assignedUsers?.isNotEmpty() == true) {
assignedTextView.text = assignedTextProvider?.assignedTextForTask(
context.resources,
data.group?.assignedUsers ?: emptyList()
)
assignedTextView.visibility = View.VISIBLE
} else {
assignedTextView.visibility = View.GONE
}
val completedCount = data.group?.assignedUsersDetail?.filter { it.completed }?.size ?: 0
if (completedCount > 0) {
completedCountTextView.text =
"$completedCount/${data.group?.assignedUsersDetail?.size}"
completedCountTextView.visibility = View.VISIBLE
} else {
completedCountTextView.visibility = View.GONE
}
syncingView?.visibility = if (task?.isSaving == true) View.VISIBLE else View.GONE
errorIconView?.visibility = if (task?.hasErrored == true) View.VISIBLE else View.GONE
}
protected open fun configureSpecialTaskTextView(task: Task) {
specialTaskTextView?.visibility = View.INVISIBLE
calendarIconView?.visibility = View.GONE
}
open fun onLeftActionTouched() {}
open fun onRightActionTouched() {}
override fun onTouch(view: View?, motionEvent: MotionEvent?): Boolean {
if (motionEvent != null) {
if (motionEvent.action != MotionEvent.ACTION_UP) return true
if (motionEvent.y <= mainTaskWrapper.height + 5.dpToPx(context)) {
if ((this.task?.checklist?.isNotEmpty() != true)) {
if (motionEvent.x <= 72.dpToPx(context)) {
onLeftActionTouched()
return true
} else if ((itemView.width - motionEvent.x <= 72.dpToPx(context))) {
onRightActionTouched()
return true
}
} else {
val checkboxHolder: ViewGroup = itemView.findViewById(R.id.checkBoxHolder)
if (mainTaskWrapper.height == checkboxHolder.height) {
if (motionEvent.x <= 72.dpToPx(context)) {
onLeftActionTouched()
return true
} else if ((itemView.width - motionEvent.x <= 72.dpToPx(context))) {
onRightActionTouched()
return true
}
} else {
if (motionEvent.y <= (checkboxHolder.height + 5.dpToPx(context))) {
if (motionEvent.x <= 72.dpToPx(context)) {
onLeftActionTouched()
return true
} else if ((itemView.width - motionEvent.x <= 72.dpToPx(context))) {
onRightActionTouched()
return true
}
}
}
}
}
}
task?.let {
openTaskFunc(it, mainTaskWrapper)
}
return true
}
open fun setDisabled(openTaskDisabled: Boolean, taskActionsDisabled: Boolean) {
this.openTaskDisabled = openTaskDisabled
this.taskActionsDisabled = taskActionsDisabled
}
}
package com.habitrpg.android.habitica.ui.viewHolders.tasks
import android.content.Context
import android.text.method.LinkMovementMethod
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.ui.viewHolders.BindableViewHolder
import com.habitrpg.android.habitica.ui.views.EllipsisTextView
import com.habitrpg.common.habitica.extensions.dpToPx
import com.habitrpg.common.habitica.extensions.getThemeColor
import com.habitrpg.common.habitica.helpers.MarkdownParser
import com.habitrpg.common.habitica.helpers.setParsedMarkdown
import com.habitrpg.shared.habitica.models.responses.TaskDirection
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class BaseTaskViewHolder(
itemView: View,
var scoreTaskFunc: ((Task, TaskDirection) -> Unit),
var openTaskFunc: ((Task, View) -> Unit),
var brokenTaskFunc: ((Task) -> Unit),
var assignedTextProvider: GroupPlanInfoProvider?,
) : BindableViewHolder<Task>(itemView), View.OnTouchListener {
private val scope = MainScope()
var task: Task? = null
var movingFromPosition: Int? = null
var errorButtonClicked: (() -> Unit)? = null
var userID: String? = null
var isLocked = false
protected var context: Context
private val mainTaskWrapper: ViewGroup = itemView.findViewById(R.id.main_task_wrapper)
protected val assignedTextView: TextView = itemView.findViewById(R.id.assigned_textview)
protected val completedCountTextView: TextView = itemView.findViewById(R.id.completed_textview)
protected val titleTextView: EllipsisTextView = itemView.findViewById(R.id.checkedTextView)
protected val notesTextView: EllipsisTextView? = itemView.findViewById(R.id.notesTextView)
protected val calendarIconView: ImageView? = itemView.findViewById(R.id.iconViewCalendar)
protected val iconViewTeam: ImageView? = itemView.findViewById(R.id.iconViewTeamTask)
protected val specialTaskTextView: TextView? = itemView.findViewById(R.id.specialTaskText)
private val iconViewChallenge: ImageView? = itemView.findViewById(R.id.iconviewChallenge)
private val iconViewReminder: ImageView? = itemView.findViewById(R.id.iconviewReminder)
private val taskIconWrapper: LinearLayout? = itemView.findViewById(R.id.taskIconWrapper)
private val approvalRequiredTextView: TextView =
itemView.findViewById(R.id.approvalRequiredTextField)
private val expandNotesButton: Button? = itemView.findViewById(R.id.expand_notes_button)
private val syncingView: ProgressBar? = itemView.findViewById(R.id.syncing_view)
private val errorIconView: ImageButton? = itemView.findViewById(R.id.error_icon)
protected val taskGray: Int =
ContextCompat.getColor(itemView.context, R.color.offset_background)
protected val streakIconView: ImageView = itemView.findViewById(R.id.iconViewStreak)
protected val streakTextView: TextView = itemView.findViewById(R.id.streakTextView)
protected val reminderTextView: TextView = itemView.findViewById(R.id.reminder_textview)
private var openTaskDisabled: Boolean = false
private var taskActionsDisabled: Boolean = false
private var notesExpanded = false
protected open val taskIconWrapperIsVisible: Boolean
get() {
var isVisible = false
if (iconViewTeam?.visibility == View.VISIBLE) {
isVisible = true
}
if (iconViewReminder?.visibility == View.VISIBLE) {
isVisible = true
}
if (iconViewChallenge?.visibility == View.VISIBLE) {
isVisible = true
}
if (iconViewReminder?.visibility == View.VISIBLE) {
isVisible = true
}
if (specialTaskTextView?.visibility == View.VISIBLE) {
isVisible = true
}
if (this.streakTextView.visibility == View.VISIBLE) {
isVisible = true
}
return isVisible
}
init {
itemView.setOnTouchListener(this)
itemView.isClickable = true
mainTaskWrapper.clipToOutline = true
titleTextView.setOnClickListener { onTouch(it, null) }
notesTextView?.setOnClickListener { onTouch(it, null) }
errorIconView?.setOnClickListener { errorButtonClicked?.invoke() }
notesTextView?.movementMethod = LinkMovementMethod.getInstance()
titleTextView.movementMethod = LinkMovementMethod.getInstance()
expandNotesButton?.setOnClickListener { expandTask() }
iconViewChallenge?.setOnClickListener {
task?.let { t ->
if (task?.challengeBroken?.isNotBlank() == true) brokenTaskFunc(t)
}
}
notesTextView?.addEllipsesListener(
object : EllipsisTextView.EllipsisListener {
override fun ellipsisStateChanged(ellipses: Boolean) {
scope.launch(Dispatchers.Main.immediate) {
if (ellipses && notesTextView.maxLines != 3) {
notesTextView.maxLines = 3
}
expandNotesButton?.visibility =
if (ellipses || notesExpanded) View.VISIBLE else View.GONE
}
}
},
)
context = itemView.context
}
private fun expandTask() {
notesExpanded = !notesExpanded
if (notesExpanded) {
notesTextView?.maxLines = 100
expandNotesButton?.text = context.getString(R.string.collapse_notes)
} else {
notesTextView?.maxLines = 8
expandNotesButton?.text = context.getString(R.string.expand_notes)
}
}
override fun bind(
data: Task,
position: Int,
displayMode: String,
) {
bind(data, position, displayMode, null)
}
open fun bind(
data: Task,
position: Int,
displayMode: String,
ownerID: String?,
) {
notesExpanded = false
task = data
itemView.setBackgroundColor(context.getThemeColor(R.attr.colorContentBackground))
expandNotesButton?.visibility = View.GONE
notesExpanded = false
notesTextView?.maxLines = 8
if (data.notes?.isNotEmpty() == true) {
notesTextView?.visibility = View.VISIBLE
notesTextView?.setTextColor(ContextCompat.getColor(context, R.color.text_ternary))
} else {
notesTextView?.visibility = View.GONE
}
titleTextView.text = data.text
scope.launch(Dispatchers.IO) {
if (data.text.isNotEmpty() && MarkdownParser.containsMarkdown(data.text)) {
val parsedText = MarkdownParser.parseMarkdown(data.text)
withContext(Dispatchers.Main) {
data.parsedText = parsedText
titleTextView.setParsedMarkdown(parsedText)
}
}
}
if (displayMode != "minimal") {
notesTextView?.text = data.notes
data.notes?.let { notes ->
scope.launch(Dispatchers.IO) {
if (notes.isEmpty() || !MarkdownParser.containsMarkdown(notes)) {
return@launch
}
val parsedNotes = MarkdownParser.parseMarkdown(notes)
withContext(Dispatchers.Main) {
data.parsedNotes = parsedNotes
notesTextView?.setParsedMarkdown(parsedNotes)
}
}
}
} else {
notesTextView?.visibility = View.GONE
}
titleTextView.setTextColor(ContextCompat.getColor(context, R.color.text_primary))
if (displayMode == "standard") {
iconViewReminder?.visibility =
if ((data.reminders?.size ?: 0) > 0) View.VISIBLE else View.GONE
iconViewChallenge?.visibility =
if (task?.challengeID != null) View.VISIBLE else View.GONE
if (task?.challengeID != null) {
if (task?.challengeBroken?.isNotBlank() == true) {
iconViewChallenge?.alpha = 1.0f
iconViewChallenge?.imageTintList =
ContextCompat.getColorStateList(context, R.color.white)
iconViewChallenge?.setImageResource(R.drawable.task_broken_megaphone)
} else {
iconViewChallenge?.alpha = 0.3f
iconViewChallenge?.imageTintList =
ContextCompat.getColorStateList(context, R.color.text_ternary)
iconViewChallenge?.setImageResource(R.drawable.task_megaphone)
}
}
configureSpecialTaskTextView(data)
iconViewTeam?.visibility =
if (data.isGroupTask && ownerID != data.group?.groupID) View.VISIBLE else View.GONE
taskIconWrapper?.visibility = if (taskIconWrapperIsVisible) View.VISIBLE else View.GONE
} else {
taskIconWrapper?.visibility = View.GONE
mainTaskWrapper.minimumHeight = 48.dpToPx(context)
}
if (data.isPendingApproval) {
approvalRequiredTextView.visibility = View.VISIBLE
} else {
approvalRequiredTextView.visibility = View.GONE
}
if (data.group?.assignedUsers?.isNotEmpty() == true) {
assignedTextView.text =
assignedTextProvider?.assignedTextForTask(
context.resources,
data.group?.assignedUsers ?: emptyList(),
)
assignedTextView.visibility = View.VISIBLE
} else {
assignedTextView.visibility = View.GONE
}
val completedCount = data.group?.assignedUsersDetail?.filter { it.completed }?.size ?: 0
if (completedCount > 0) {
completedCountTextView.text =
"$completedCount/${data.group?.assignedUsersDetail?.size}"
completedCountTextView.visibility = View.VISIBLE
} else {
completedCountTextView.visibility = View.GONE
}
syncingView?.visibility = if (task?.isSaving == true) View.VISIBLE else View.GONE
errorIconView?.visibility = if (task?.hasErrored == true) View.VISIBLE else View.GONE
}
protected open fun configureSpecialTaskTextView(task: Task) {
specialTaskTextView?.visibility = View.INVISIBLE
calendarIconView?.visibility = View.GONE
}
open fun onLeftActionTouched() {}
open fun onRightActionTouched() {}
override fun onTouch(
view: View?,
motionEvent: MotionEvent?,
): Boolean {
if (motionEvent != null) {
if (motionEvent.action != MotionEvent.ACTION_UP) return true
if (motionEvent.y <= mainTaskWrapper.height + 5.dpToPx(context)) {
if ((this.task?.checklist?.isNotEmpty() != true)) {
if (motionEvent.x <= 72.dpToPx(context)) {
onLeftActionTouched()
return true
} else if ((itemView.width - motionEvent.x <= 72.dpToPx(context))) {
onRightActionTouched()
return true
}
} else {
val checkboxHolder: ViewGroup = itemView.findViewById(R.id.checkBoxHolder)
if (mainTaskWrapper.height == checkboxHolder.height) {
if (motionEvent.x <= 72.dpToPx(context)) {
onLeftActionTouched()
return true
} else if ((itemView.width - motionEvent.x <= 72.dpToPx(context))) {
onRightActionTouched()
return true
}
} else {
if (motionEvent.y <= (checkboxHolder.height + 5.dpToPx(context))) {
if (motionEvent.x <= 72.dpToPx(context)) {
onLeftActionTouched()
return true
} else if ((itemView.width - motionEvent.x <= 72.dpToPx(context))) {
onRightActionTouched()
return true
}
}
}
}
}
}
task?.let {
openTaskFunc(it, mainTaskWrapper)
}
return true
}
open fun setDisabled(
openTaskDisabled: Boolean,
taskActionsDisabled: Boolean,
) {
this.openTaskDisabled = openTaskDisabled
this.taskActionsDisabled = taskActionsDisabled
}
}

View file

@ -1,240 +1,247 @@
package com.habitrpg.android.habitica.ui.viewHolders.tasks
import android.content.Context
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.common.habitica.extensions.getThemeColor
import com.habitrpg.common.habitica.extensions.isUsingNightModeResources
import com.habitrpg.common.habitica.helpers.MarkdownParser
import com.habitrpg.common.habitica.helpers.setParsedMarkdown
import com.habitrpg.shared.habitica.models.responses.TaskDirection
import com.habitrpg.shared.habitica.models.tasks.TaskType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class ChecklistedViewHolder(
itemView: View,
scoreTaskFunc: ((Task, TaskDirection) -> Unit),
var scoreChecklistItemFunc: ((Task, ChecklistItem) -> Unit),
openTaskFunc: ((Task, View) -> Unit),
brokenTaskFunc: ((Task) -> Unit),
assignedTextProvider: GroupPlanInfoProvider?
) : BaseTaskViewHolder(itemView, scoreTaskFunc, openTaskFunc, brokenTaskFunc, assignedTextProvider) {
private val checkboxHolder: ViewGroup = itemView.findViewById(R.id.checkBoxHolder)
private val checkmarkView: ImageView = itemView.findViewById(R.id.checkmark)
private val lockView: ImageView = itemView.findViewById(R.id.lock_view)
private val checkboxBackground: View = itemView.findViewById(R.id.checkBoxBackground)
private val checklistView: LinearLayout = itemView.findViewById(R.id.checklistView)
private val checklistIndicatorWrapper: ViewGroup = itemView.findViewById(R.id.checklistIndicatorWrapper)
private val checklistCompletedTextView: TextView = itemView.findViewById(R.id.checkListCompletedTextView)
private val checklistAllTextView: TextView = itemView.findViewById(R.id.checkListAllTextView)
private val checklistDivider: View = itemView.findViewById(R.id.checklistDivider)
init {
checklistIndicatorWrapper.isClickable = true
checklistIndicatorWrapper.setOnClickListener { onChecklistIndicatorClicked() }
}
override fun bind(
data: Task,
position: Int,
displayMode: String,
ownerID: String?
) {
var completed = data.completed(userID)
if (data.isPendingApproval) {
completed = false
}
if (isLocked) {
this.checkmarkView.visibility = View.GONE
this.lockView.visibility = View.VISIBLE
val icon = AppCompatResources.getDrawable(context, R.drawable.task_lock)
icon?.setTint(ContextCompat.getColor(context, if (data.isDue == true || data.type == TaskType.TODO) data.extraExtraDarkTaskColor else R.color.text_dimmed))
lockView.setImageDrawable(icon)
} else {
this.checkmarkView.visibility = if (completed) View.VISIBLE else View.GONE
checkmarkView.drawable.setTint(ContextCompat.getColor(context, R.color.gray_400))
this.lockView.visibility = View.GONE
}
this.checklistCompletedTextView.text = data.completedChecklistCount.toString()
this.checklistAllTextView.text = data.checklist?.size.toString()
this.checklistView.removeAllViews()
this.updateChecklistDisplay()
this.checklistIndicatorWrapper.visibility = if (data.checklist?.size == 0) View.GONE else View.VISIBLE
super.bind(data, position, displayMode, ownerID)
val regularBoxBackground = if (task?.type == TaskType.DAILY) R.drawable.daily_unchecked else R.drawable.todo_unchecked
val completedBoxBackground = if (task?.type == TaskType.DAILY) R.drawable.daily_checked else R.drawable.todo_checked
val inactiveBoxBackground = R.drawable.daily_inactive
if (this.shouldDisplayAsActive(data, userID) && !data.isPendingApproval) {
this.checkboxHolder.setBackgroundResource(data.lightTaskColor)
checkboxBackground.setBackgroundResource(regularBoxBackground)
} else {
if (completed) {
titleTextView.setTextColor(ContextCompat.getColor(context, R.color.text_quad))
notesTextView?.setTextColor(ContextCompat.getColor(context, R.color.text_quad))
this.checkboxHolder.setBackgroundColor(context.getThemeColor(R.attr.colorWindowBackground))
checkboxBackground.setBackgroundResource(completedBoxBackground)
} else {
this.checkboxHolder.setBackgroundColor(this.taskGray)
notesTextView?.setTextColor(ContextCompat.getColor(context, R.color.text_ternary))
checkboxBackground.setBackgroundResource(regularBoxBackground)
checkboxBackground.setBackgroundResource(inactiveBoxBackground)
}
}
}
abstract fun shouldDisplayAsActive(task: Task?, userID: String?): Boolean
private fun updateChecklistDisplay() {
// This needs to be a LinearLayout, as ListViews can not be inside other ListViews.
if (this.shouldDisplayExpandedChecklist()) {
val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater
if (this.task?.checklist?.isValid == true) {
checklistView.removeAllViews()
for (item in this.task?.checklist ?: emptyList<ChecklistItem>()) {
val itemView = layoutInflater?.inflate(R.layout.checklist_item_row, this.checklistView, false)
val checkboxBackground = itemView?.findViewById<View>(R.id.checkBoxBackground)
if (task?.type == TaskType.TODO) {
checkboxBackground?.setBackgroundResource(R.drawable.round_checklist_unchecked)
}
checkboxBackground?.backgroundTintList = ContextCompat.getColorStateList(
context,
(
if (context.isUsingNightModeResources()) {
if (task?.completed(userID) == true || (task?.type == TaskType.DAILY && task?.isDue == false)) {
R.color.checkbox_fill
} else {
task?.lightTaskColor
}
} else {
R.color.checkbox_fill
}
) ?: R.color.checkbox_fill
)
val textView = itemView?.findViewById<TextView>(R.id.checkedTextView)
// Populate the data into the template view using the data object
textView?.text = item.text
textView?.setTextColor(ContextCompat.getColor(context, if (item.completed) R.color.text_dimmed else R.color.text_secondary))
if (item.text != null) {
MainScope().launch(Dispatchers.IO) {
val parsedText = MarkdownParser.parseMarkdown(item.text ?: "")
withContext(Dispatchers.Main) {
textView?.setParsedMarkdown(parsedText)
}
}
}
val checkmark = itemView?.findViewById<ImageView>(R.id.checkmark)
checkmark?.drawable?.setTintMode(PorterDuff.Mode.SRC_ATOP)
checkmark?.visibility = if (item.completed) View.VISIBLE else View.GONE
val checkboxHolder = itemView?.findViewById<View>(R.id.checkBoxHolder) as? ViewGroup
checkboxHolder?.setOnClickListener { _ ->
task?.let { scoreChecklistItemFunc(it, item) }
}
val color = ContextCompat.getColor(
context,
if (task?.completed(userID) == true || (task?.type == TaskType.DAILY && task?.isDue == false)) {
checkmark?.drawable?.setTint(ContextCompat.getColor(context, R.color.text_dimmed))
R.color.offset_background
} else {
val color = if (context.isUsingNightModeResources()) task?.extraExtraDarkTaskColor else task?.darkTaskColor
checkmark?.drawable?.setTint(ContextCompat.getColor(context, color ?: R.color.text_dimmed))
task?.extraLightTaskColor ?: R.color.offset_background
}
)
color.let { checkboxHolder?.setBackgroundColor(it) }
this.checklistView.addView(itemView)
}
}
this.checklistView.visibility = View.VISIBLE
} else {
this.checklistView.removeAllViewsInLayout()
this.checklistView.visibility = View.GONE
}
}
protected fun setChecklistIndicatorBackgroundActive(isActive: Boolean) {
val drawable = ContextCompat.getDrawable(context, R.drawable.checklist_indicator_background)
if (isActive) {
drawable?.setTint(ContextCompat.getColor(context, R.color.gray_200))
val textColor = if (context.isUsingNightModeResources()) {
ContextCompat.getColor(context, R.color.gray_600)
} else {
ContextCompat.getColor(context, R.color.gray_500)
}
checklistCompletedTextView.setTextColor(textColor)
checklistAllTextView.setTextColor(textColor)
checklistDivider.setBackgroundColor(textColor)
} else {
drawable?.setTint(ContextCompat.getColor(context, R.color.offset_background))
val textColor = ContextCompat.getColor(context, R.color.text_quad)
checklistCompletedTextView.setTextColor(textColor)
checklistAllTextView.setTextColor(textColor)
checklistDivider.setBackgroundColor(textColor)
}
drawable?.setTintMode(PorterDuff.Mode.MULTIPLY)
checklistIndicatorWrapper.background = drawable
}
private fun onChecklistIndicatorClicked() {
expandedChecklistRow = if (this.shouldDisplayExpandedChecklist()) null else bindingAdapterPosition
if (this.shouldDisplayExpandedChecklist()) {
val recyclerView = this.checklistView.parent.parent as? RecyclerView
val layoutManager = recyclerView?.layoutManager as? LinearLayoutManager
layoutManager?.scrollToPositionWithOffset(this.bindingAdapterPosition, 15)
}
updateChecklistDisplay()
}
override fun onLeftActionTouched() {
super.onLeftActionTouched()
if (task?.isValid == true && !isLocked) {
onCheckedChanged(!(task?.completed(userID) ?: false))
}
}
override fun onRightActionTouched() {
super.onRightActionTouched()
onChecklistIndicatorClicked()
}
private fun shouldDisplayExpandedChecklist(): Boolean {
return expandedChecklistRow != null && bindingAdapterPosition == expandedChecklistRow
}
private fun onCheckedChanged(isChecked: Boolean) {
if (task?.isValid != true) {
return
}
if (isChecked != task?.completed(userID)) {
task?.let { scoreTaskFunc(it, if (task?.completed(userID) == false) TaskDirection.UP else TaskDirection.DOWN) }
}
}
override fun setDisabled(openTaskDisabled: Boolean, taskActionsDisabled: Boolean) {
super.setDisabled(openTaskDisabled, taskActionsDisabled)
this.checkboxHolder.isEnabled = !taskActionsDisabled
}
companion object {
private var expandedChecklistRow: Int? = null
}
}
package com.habitrpg.android.habitica.ui.viewHolders.tasks
import android.content.Context
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.common.habitica.extensions.getThemeColor
import com.habitrpg.common.habitica.extensions.isUsingNightModeResources
import com.habitrpg.common.habitica.helpers.MarkdownParser
import com.habitrpg.common.habitica.helpers.setParsedMarkdown
import com.habitrpg.shared.habitica.models.responses.TaskDirection
import com.habitrpg.shared.habitica.models.tasks.TaskType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class ChecklistedViewHolder(
itemView: View,
scoreTaskFunc: ((Task, TaskDirection) -> Unit),
var scoreChecklistItemFunc: ((Task, ChecklistItem) -> Unit),
openTaskFunc: ((Task, View) -> Unit),
brokenTaskFunc: ((Task) -> Unit),
assignedTextProvider: GroupPlanInfoProvider?,
) : BaseTaskViewHolder(itemView, scoreTaskFunc, openTaskFunc, brokenTaskFunc, assignedTextProvider) {
private val checkboxHolder: ViewGroup = itemView.findViewById(R.id.checkBoxHolder)
private val checkmarkView: ImageView = itemView.findViewById(R.id.checkmark)
private val lockView: ImageView = itemView.findViewById(R.id.lock_view)
private val checkboxBackground: View = itemView.findViewById(R.id.checkBoxBackground)
private val checklistView: LinearLayout = itemView.findViewById(R.id.checklistView)
private val checklistIndicatorWrapper: ViewGroup = itemView.findViewById(R.id.checklistIndicatorWrapper)
private val checklistCompletedTextView: TextView = itemView.findViewById(R.id.checkListCompletedTextView)
private val checklistAllTextView: TextView = itemView.findViewById(R.id.checkListAllTextView)
private val checklistDivider: View = itemView.findViewById(R.id.checklistDivider)
init {
checklistIndicatorWrapper.isClickable = true
checklistIndicatorWrapper.setOnClickListener { onChecklistIndicatorClicked() }
}
override fun bind(
data: Task,
position: Int,
displayMode: String,
ownerID: String?,
) {
var completed = data.completed(userID)
if (data.isPendingApproval) {
completed = false
}
if (isLocked) {
this.checkmarkView.visibility = View.GONE
this.lockView.visibility = View.VISIBLE
val icon = AppCompatResources.getDrawable(context, R.drawable.task_lock)
icon?.setTint(ContextCompat.getColor(context, if (data.isDue == true || data.type == TaskType.TODO) data.extraExtraDarkTaskColor else R.color.text_dimmed))
lockView.setImageDrawable(icon)
} else {
this.checkmarkView.visibility = if (completed) View.VISIBLE else View.GONE
checkmarkView.drawable.setTint(ContextCompat.getColor(context, R.color.gray_400))
this.lockView.visibility = View.GONE
}
this.checklistCompletedTextView.text = data.completedChecklistCount.toString()
this.checklistAllTextView.text = data.checklist?.size.toString()
this.checklistView.removeAllViews()
this.updateChecklistDisplay()
this.checklistIndicatorWrapper.visibility = if (data.checklist?.size == 0) View.GONE else View.VISIBLE
super.bind(data, position, displayMode, ownerID)
val regularBoxBackground = if (task?.type == TaskType.DAILY) R.drawable.daily_unchecked else R.drawable.todo_unchecked
val completedBoxBackground = if (task?.type == TaskType.DAILY) R.drawable.daily_checked else R.drawable.todo_checked
val inactiveBoxBackground = R.drawable.daily_inactive
if (this.shouldDisplayAsActive(data, userID) && !data.isPendingApproval) {
this.checkboxHolder.setBackgroundResource(data.lightTaskColor)
checkboxBackground.setBackgroundResource(regularBoxBackground)
} else {
if (completed) {
titleTextView.setTextColor(ContextCompat.getColor(context, R.color.text_quad))
notesTextView?.setTextColor(ContextCompat.getColor(context, R.color.text_quad))
this.checkboxHolder.setBackgroundColor(context.getThemeColor(R.attr.colorWindowBackground))
checkboxBackground.setBackgroundResource(completedBoxBackground)
} else {
this.checkboxHolder.setBackgroundColor(this.taskGray)
notesTextView?.setTextColor(ContextCompat.getColor(context, R.color.text_ternary))
checkboxBackground.setBackgroundResource(regularBoxBackground)
checkboxBackground.setBackgroundResource(inactiveBoxBackground)
}
}
}
abstract fun shouldDisplayAsActive(
task: Task?,
userID: String?,
): Boolean
private fun updateChecklistDisplay() {
// This needs to be a LinearLayout, as ListViews can not be inside other ListViews.
if (this.shouldDisplayExpandedChecklist()) {
val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater
if (this.task?.checklist?.isValid == true) {
checklistView.removeAllViews()
for (item in this.task?.checklist ?: emptyList<ChecklistItem>()) {
val itemView = layoutInflater?.inflate(R.layout.checklist_item_row, this.checklistView, false)
val checkboxBackground = itemView?.findViewById<View>(R.id.checkBoxBackground)
if (task?.type == TaskType.TODO) {
checkboxBackground?.setBackgroundResource(R.drawable.round_checklist_unchecked)
}
checkboxBackground?.backgroundTintList =
ContextCompat.getColorStateList(
context,
(
if (context.isUsingNightModeResources()) {
if (task?.completed(userID) == true || (task?.type == TaskType.DAILY && task?.isDue == false)) {
R.color.checkbox_fill
} else {
task?.lightTaskColor
}
} else {
R.color.checkbox_fill
}
) ?: R.color.checkbox_fill,
)
val textView = itemView?.findViewById<TextView>(R.id.checkedTextView)
// Populate the data into the template view using the data object
textView?.text = item.text
textView?.setTextColor(ContextCompat.getColor(context, if (item.completed) R.color.text_dimmed else R.color.text_secondary))
if (item.text != null) {
MainScope().launch(Dispatchers.IO) {
val parsedText = MarkdownParser.parseMarkdown(item.text ?: "")
withContext(Dispatchers.Main) {
textView?.setParsedMarkdown(parsedText)
}
}
}
val checkmark = itemView?.findViewById<ImageView>(R.id.checkmark)
checkmark?.drawable?.setTintMode(PorterDuff.Mode.SRC_ATOP)
checkmark?.visibility = if (item.completed) View.VISIBLE else View.GONE
val checkboxHolder = itemView?.findViewById<View>(R.id.checkBoxHolder) as? ViewGroup
checkboxHolder?.setOnClickListener { _ ->
task?.let { scoreChecklistItemFunc(it, item) }
}
val color =
ContextCompat.getColor(
context,
if (task?.completed(userID) == true || (task?.type == TaskType.DAILY && task?.isDue == false)) {
checkmark?.drawable?.setTint(ContextCompat.getColor(context, R.color.text_dimmed))
R.color.offset_background
} else {
val color = if (context.isUsingNightModeResources()) task?.extraExtraDarkTaskColor else task?.darkTaskColor
checkmark?.drawable?.setTint(ContextCompat.getColor(context, color ?: R.color.text_dimmed))
task?.extraLightTaskColor ?: R.color.offset_background
},
)
color.let { checkboxHolder?.setBackgroundColor(it) }
this.checklistView.addView(itemView)
}
}
this.checklistView.visibility = View.VISIBLE
} else {
this.checklistView.removeAllViewsInLayout()
this.checklistView.visibility = View.GONE
}
}
protected fun setChecklistIndicatorBackgroundActive(isActive: Boolean) {
val drawable = ContextCompat.getDrawable(context, R.drawable.checklist_indicator_background)
if (isActive) {
drawable?.setTint(ContextCompat.getColor(context, R.color.gray_200))
val textColor =
if (context.isUsingNightModeResources()) {
ContextCompat.getColor(context, R.color.gray_600)
} else {
ContextCompat.getColor(context, R.color.gray_500)
}
checklistCompletedTextView.setTextColor(textColor)
checklistAllTextView.setTextColor(textColor)
checklistDivider.setBackgroundColor(textColor)
} else {
drawable?.setTint(ContextCompat.getColor(context, R.color.offset_background))
val textColor = ContextCompat.getColor(context, R.color.text_quad)
checklistCompletedTextView.setTextColor(textColor)
checklistAllTextView.setTextColor(textColor)
checklistDivider.setBackgroundColor(textColor)
}
drawable?.setTintMode(PorterDuff.Mode.MULTIPLY)
checklistIndicatorWrapper.background = drawable
}
private fun onChecklistIndicatorClicked() {
expandedChecklistRow = if (this.shouldDisplayExpandedChecklist()) null else bindingAdapterPosition
if (this.shouldDisplayExpandedChecklist()) {
val recyclerView = this.checklistView.parent.parent as? RecyclerView
val layoutManager = recyclerView?.layoutManager as? LinearLayoutManager
layoutManager?.scrollToPositionWithOffset(this.bindingAdapterPosition, 15)
}
updateChecklistDisplay()
}
override fun onLeftActionTouched() {
super.onLeftActionTouched()
if (task?.isValid == true && !isLocked) {
onCheckedChanged(!(task?.completed(userID) ?: false))
}
}
override fun onRightActionTouched() {
super.onRightActionTouched()
onChecklistIndicatorClicked()
}
private fun shouldDisplayExpandedChecklist(): Boolean {
return expandedChecklistRow != null && bindingAdapterPosition == expandedChecklistRow
}
private fun onCheckedChanged(isChecked: Boolean) {
if (task?.isValid != true) {
return
}
if (isChecked != task?.completed(userID)) {
task?.let { scoreTaskFunc(it, if (task?.completed(userID) == false) TaskDirection.UP else TaskDirection.DOWN) }
}
}
override fun setDisabled(
openTaskDisabled: Boolean,
taskActionsDisabled: Boolean,
) {
super.setDisabled(openTaskDisabled, taskActionsDisabled)
this.checkboxHolder.isEnabled = !taskActionsDisabled
}
companion object {
private var expandedChecklistRow: Int? = null
}
}

View file

@ -1,94 +1,97 @@
package com.habitrpg.android.habitica.ui.viewHolders.tasks
import android.view.View
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.shared.habitica.models.responses.TaskDirection
import java.text.DateFormat
import java.util.Calendar
import java.util.Date
class DailyViewHolder(
itemView: View,
scoreTaskFunc: ((Task, TaskDirection) -> Unit),
scoreChecklistItemFunc: ((Task, ChecklistItem) -> Unit),
openTaskFunc: ((Task, View) -> Unit),
brokenTaskFunc: ((Task) -> Unit),
assignedTextProvider: GroupPlanInfoProvider?
) : ChecklistedViewHolder(itemView, scoreTaskFunc, scoreChecklistItemFunc, openTaskFunc, brokenTaskFunc, assignedTextProvider) {
override val taskIconWrapperIsVisible: Boolean
get() {
var isVisible: Boolean = super.taskIconWrapperIsVisible
if (this.streakTextView.visibility == View.VISIBLE) {
isVisible = true
}
return isVisible
}
override fun bind(
data: Task,
position: Int,
displayMode: String,
ownerID: String?
) {
this.task = data
setChecklistIndicatorBackgroundActive(data.isChecklistDisplayActive)
if (data.reminders?.size == 0) {
reminderTextView.visibility = View.GONE
} else {
reminderTextView.visibility = View.VISIBLE
val now = Date()
val calendar = Calendar.getInstance()
val nextReminder = data.reminders?.firstOrNull {
calendar.time = now
calendar.set(
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DATE),
it.getZonedDateTime()?.hour ?: 0,
it.getZonedDateTime()?.minute ?: 0,
0
)
now < calendar.time
} ?: data.reminders?.first()
var reminderString = ""
if (nextReminder?.time != null) {
val time = Date.from(nextReminder.getLocalZonedDateTimeInstant())
reminderString += formatter.format(time)
}
if ((data.reminders?.size ?: 0) > 1) {
reminderString = "$reminderString (+${(data.reminders?.size ?: 0) - 1})"
}
reminderTextView.text = reminderString
}
super.bind(data, position, displayMode, ownerID)
}
override fun shouldDisplayAsActive(task: Task?, userID: String?): Boolean {
return task?.isDisplayedActiveForUser(userID) ?: false
}
override fun configureSpecialTaskTextView(task: Task) {
super.configureSpecialTaskTextView(task)
if ((task.streak ?: 0) > 0 && !task.isGroupTask) {
this.streakTextView.text = task.streak.toString()
this.streakTextView.visibility = View.VISIBLE
this.streakIconView.visibility = View.VISIBLE
} else {
this.streakTextView.visibility = View.GONE
this.streakIconView.visibility = View.GONE
}
}
companion object {
private val formatter: DateFormat
get() {
return DateFormat.getTimeInstance(DateFormat.SHORT)
}
}
}
package com.habitrpg.android.habitica.ui.viewHolders.tasks
import android.view.View
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.shared.habitica.models.responses.TaskDirection
import java.text.DateFormat
import java.util.Calendar
import java.util.Date
class DailyViewHolder(
itemView: View,
scoreTaskFunc: ((Task, TaskDirection) -> Unit),
scoreChecklistItemFunc: ((Task, ChecklistItem) -> Unit),
openTaskFunc: ((Task, View) -> Unit),
brokenTaskFunc: ((Task) -> Unit),
assignedTextProvider: GroupPlanInfoProvider?,
) : ChecklistedViewHolder(itemView, scoreTaskFunc, scoreChecklistItemFunc, openTaskFunc, brokenTaskFunc, assignedTextProvider) {
override val taskIconWrapperIsVisible: Boolean
get() {
var isVisible: Boolean = super.taskIconWrapperIsVisible
if (this.streakTextView.visibility == View.VISIBLE) {
isVisible = true
}
return isVisible
}
override fun bind(
data: Task,
position: Int,
displayMode: String,
ownerID: String?,
) {
this.task = data
setChecklistIndicatorBackgroundActive(data.isChecklistDisplayActive)
if (data.reminders?.size == 0) {
reminderTextView.visibility = View.GONE
} else {
reminderTextView.visibility = View.VISIBLE
val now = Date()
val calendar = Calendar.getInstance()
val nextReminder =
data.reminders?.firstOrNull {
calendar.time = now
calendar.set(
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DATE),
it.getZonedDateTime()?.hour ?: 0,
it.getZonedDateTime()?.minute ?: 0,
0,
)
now < calendar.time
} ?: data.reminders?.first()
var reminderString = ""
if (nextReminder?.time != null) {
val time = Date.from(nextReminder.getLocalZonedDateTimeInstant())
reminderString += formatter.format(time)
}
if ((data.reminders?.size ?: 0) > 1) {
reminderString = "$reminderString (+${(data.reminders?.size ?: 0) - 1})"
}
reminderTextView.text = reminderString
}
super.bind(data, position, displayMode, ownerID)
}
override fun shouldDisplayAsActive(
task: Task?,
userID: String?,
): Boolean {
return task?.isDisplayedActiveForUser(userID) ?: false
}
override fun configureSpecialTaskTextView(task: Task) {
super.configureSpecialTaskTextView(task)
if ((task.streak ?: 0) > 0 && !task.isGroupTask) {
this.streakTextView.text = task.streak.toString()
this.streakTextView.visibility = View.VISIBLE
this.streakIconView.visibility = View.VISIBLE
} else {
this.streakTextView.visibility = View.GONE
this.streakIconView.visibility = View.GONE
}
}
companion object {
private val formatter: DateFormat
get() {
return DateFormat.getTimeInstance(DateFormat.SHORT)
}
}
}

View file

@ -1,168 +1,174 @@
package com.habitrpg.android.habitica.ui.viewHolders.tasks
import android.graphics.PorterDuff
import android.view.View
import android.widget.Button
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.core.content.ContextCompat
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.shared.habitica.models.responses.TaskDirection
class HabitViewHolder(
itemView: View,
scoreTaskFunc: ((Task, TaskDirection) -> Unit),
openTaskFunc: ((Task, View) -> Unit),
brokenTaskFunc: ((Task) -> Unit),
assignedTextProvider: GroupPlanInfoProvider?
) : BaseTaskViewHolder(itemView, scoreTaskFunc, openTaskFunc, brokenTaskFunc, assignedTextProvider) {
private val btnPlusWrapper: FrameLayout = itemView.findViewById(R.id.btnPlusWrapper)
private val btnPlusIconView: ImageView = itemView.findViewById(R.id.btnPlusIconView)
private val btnPlusCircleView: View = itemView.findViewById(R.id.button_plus_circle_view)
private val btnPlus: Button = itemView.findViewById(R.id.btnPlus)
private val btnMinusWrapper: FrameLayout = itemView.findViewById(R.id.btnMinusWrapper)
private val btnMinusIconView: ImageView = itemView.findViewById(R.id.btnMinusIconView)
private val btnMinusCircleView: View = itemView.findViewById(R.id.button_minus_circle_view)
private val btnMinus: Button = itemView.findViewById(R.id.btnMinus)
init {
btnPlus.setOnClickListener { onPlusButtonClicked() }
btnPlus.isClickable = true
btnMinus.setOnClickListener { onMinusButtonClicked() }
btnMinus.isClickable = true
}
override fun bind(
data: Task,
position: Int,
displayMode: String,
ownerID: String?
) {
this.task = data
if (data.up == true) {
val plusIcon = if (isLocked) {
val icon = ContextCompat.getDrawable(context, R.drawable.task_lock)
icon?.setTint(ContextCompat.getColor(context, data.extraExtraDarkTaskColor))
icon
} else {
val icon = ContextCompat.getDrawable(context, R.drawable.habit_plus)
icon?.setTint(ContextCompat.getColor(context, R.color.white))
icon
}
plusIcon?.setTintMode(PorterDuff.Mode.MULTIPLY)
this.btnPlusIconView.setImageDrawable(plusIcon)
val drawable = ContextCompat.getDrawable(context, R.drawable.habit_circle)
this.btnPlusWrapper.setBackgroundResource(data.lightTaskColor)
drawable?.setTint(ContextCompat.getColor(context, data.mediumTaskColor))
drawable?.setTintMode(PorterDuff.Mode.MULTIPLY)
btnPlusCircleView.background = drawable
this.btnPlus.visibility = View.VISIBLE
this.btnPlus.isClickable = true
} else {
this.btnPlusWrapper.setBackgroundResource(R.color.habit_inactive_gray)
val plusIcon = if (isLocked) {
val icon = ContextCompat.getDrawable(context, R.drawable.task_lock)
icon?.setTint(ContextCompat.getColor(context, R.color.text_dimmed))
icon
} else {
val icon = ContextCompat.getDrawable(context, R.drawable.habit_plus)
icon?.setTint(ContextCompat.getColor(context, R.color.content_background_offset))
icon
}
plusIcon?.setTintMode(PorterDuff.Mode.MULTIPLY)
this.btnPlusIconView.setImageDrawable(plusIcon)
btnPlusCircleView.background = ContextCompat.getDrawable(context, R.drawable.habit_circle_disabled)
this.btnPlus.visibility = View.GONE
this.btnPlus.isClickable = false
}
if (data.down == true) {
this.btnMinusWrapper.setBackgroundResource(data.lightTaskColor)
val minusIcon = if (isLocked) {
val icon = ContextCompat.getDrawable(context, R.drawable.task_lock)
icon?.setTint(ContextCompat.getColor(context, data.extraExtraDarkTaskColor))
icon
} else {
val icon = ContextCompat.getDrawable(context, R.drawable.habit_minus)
icon?.setTint(ContextCompat.getColor(context, R.color.white))
icon
}
minusIcon?.setTintMode(PorterDuff.Mode.MULTIPLY)
this.btnMinusIconView.setImageDrawable(minusIcon)
val drawable = ContextCompat.getDrawable(context, R.drawable.habit_circle)
this.btnMinusWrapper.setBackgroundResource(data.lightTaskColor)
drawable?.setTint(ContextCompat.getColor(context, data.mediumTaskColor))
drawable?.setTintMode(PorterDuff.Mode.MULTIPLY)
btnMinusCircleView.background = drawable
this.btnMinus.visibility = View.VISIBLE
this.btnMinus.isClickable = true
} else {
this.btnMinusWrapper.setBackgroundResource(R.color.habit_inactive_gray)
val minusIcon = if (isLocked) {
val icon = ContextCompat.getDrawable(context, R.drawable.task_lock)
icon?.setTint(ContextCompat.getColor(context, R.color.content_background_offset))
icon
} else {
val icon = ContextCompat.getDrawable(context, R.drawable.habit_minus)
icon?.setTint(ContextCompat.getColor(context, R.color.content_background_offset))
icon
}
minusIcon?.setTintMode(PorterDuff.Mode.MULTIPLY)
this.btnMinusIconView.setImageDrawable(minusIcon)
btnMinusCircleView.background = ContextCompat.getDrawable(context, R.drawable.habit_circle_disabled)
this.btnMinus.visibility = View.GONE
this.btnMinus.isClickable = false
}
val streakString = task?.streakString
if (streakString?.isNotEmpty() == true && task?.isGroupTask != true) {
streakTextView.text = streakString
streakTextView.visibility = View.VISIBLE
streakIconView.visibility = View.VISIBLE
} else {
streakTextView.visibility = View.GONE
streakIconView.visibility = View.GONE
}
reminderTextView.visibility = View.GONE
calendarIconView?.visibility = View.GONE
super.bind(data, position, displayMode, ownerID)
if (data.up == false && data.down == false) {
titleTextView.setTextColor(ContextCompat.getColor(context, R.color.text_quad))
notesTextView?.setTextColor(ContextCompat.getColor(context, R.color.text_quad))
}
}
override fun onLeftActionTouched() {
super.onLeftActionTouched()
if (!isLocked) {
onPlusButtonClicked()
}
}
override fun onRightActionTouched() {
super.onRightActionTouched()
if (!isLocked) {
onMinusButtonClicked()
}
}
private fun onPlusButtonClicked() {
if (task?.up != true) return
task?.let { scoreTaskFunc.invoke(it, TaskDirection.UP) }
}
private fun onMinusButtonClicked() {
if (task?.down != true) return
task?.let { scoreTaskFunc.invoke(it, TaskDirection.DOWN) }
}
override fun setDisabled(openTaskDisabled: Boolean, taskActionsDisabled: Boolean) {
super.setDisabled(openTaskDisabled, taskActionsDisabled)
this.btnPlus.isEnabled = !taskActionsDisabled
this.btnMinus.isEnabled = !taskActionsDisabled
}
}
package com.habitrpg.android.habitica.ui.viewHolders.tasks
import android.graphics.PorterDuff
import android.view.View
import android.widget.Button
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.core.content.ContextCompat
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.shared.habitica.models.responses.TaskDirection
class HabitViewHolder(
itemView: View,
scoreTaskFunc: ((Task, TaskDirection) -> Unit),
openTaskFunc: ((Task, View) -> Unit),
brokenTaskFunc: ((Task) -> Unit),
assignedTextProvider: GroupPlanInfoProvider?,
) : BaseTaskViewHolder(itemView, scoreTaskFunc, openTaskFunc, brokenTaskFunc, assignedTextProvider) {
private val btnPlusWrapper: FrameLayout = itemView.findViewById(R.id.btnPlusWrapper)
private val btnPlusIconView: ImageView = itemView.findViewById(R.id.btnPlusIconView)
private val btnPlusCircleView: View = itemView.findViewById(R.id.button_plus_circle_view)
private val btnPlus: Button = itemView.findViewById(R.id.btnPlus)
private val btnMinusWrapper: FrameLayout = itemView.findViewById(R.id.btnMinusWrapper)
private val btnMinusIconView: ImageView = itemView.findViewById(R.id.btnMinusIconView)
private val btnMinusCircleView: View = itemView.findViewById(R.id.button_minus_circle_view)
private val btnMinus: Button = itemView.findViewById(R.id.btnMinus)
init {
btnPlus.setOnClickListener { onPlusButtonClicked() }
btnPlus.isClickable = true
btnMinus.setOnClickListener { onMinusButtonClicked() }
btnMinus.isClickable = true
}
override fun bind(
data: Task,
position: Int,
displayMode: String,
ownerID: String?,
) {
this.task = data
if (data.up == true) {
val plusIcon =
if (isLocked) {
val icon = ContextCompat.getDrawable(context, R.drawable.task_lock)
icon?.setTint(ContextCompat.getColor(context, data.extraExtraDarkTaskColor))
icon
} else {
val icon = ContextCompat.getDrawable(context, R.drawable.habit_plus)
icon?.setTint(ContextCompat.getColor(context, R.color.white))
icon
}
plusIcon?.setTintMode(PorterDuff.Mode.MULTIPLY)
this.btnPlusIconView.setImageDrawable(plusIcon)
val drawable = ContextCompat.getDrawable(context, R.drawable.habit_circle)
this.btnPlusWrapper.setBackgroundResource(data.lightTaskColor)
drawable?.setTint(ContextCompat.getColor(context, data.mediumTaskColor))
drawable?.setTintMode(PorterDuff.Mode.MULTIPLY)
btnPlusCircleView.background = drawable
this.btnPlus.visibility = View.VISIBLE
this.btnPlus.isClickable = true
} else {
this.btnPlusWrapper.setBackgroundResource(R.color.habit_inactive_gray)
val plusIcon =
if (isLocked) {
val icon = ContextCompat.getDrawable(context, R.drawable.task_lock)
icon?.setTint(ContextCompat.getColor(context, R.color.text_dimmed))
icon
} else {
val icon = ContextCompat.getDrawable(context, R.drawable.habit_plus)
icon?.setTint(ContextCompat.getColor(context, R.color.content_background_offset))
icon
}
plusIcon?.setTintMode(PorterDuff.Mode.MULTIPLY)
this.btnPlusIconView.setImageDrawable(plusIcon)
btnPlusCircleView.background = ContextCompat.getDrawable(context, R.drawable.habit_circle_disabled)
this.btnPlus.visibility = View.GONE
this.btnPlus.isClickable = false
}
if (data.down == true) {
this.btnMinusWrapper.setBackgroundResource(data.lightTaskColor)
val minusIcon =
if (isLocked) {
val icon = ContextCompat.getDrawable(context, R.drawable.task_lock)
icon?.setTint(ContextCompat.getColor(context, data.extraExtraDarkTaskColor))
icon
} else {
val icon = ContextCompat.getDrawable(context, R.drawable.habit_minus)
icon?.setTint(ContextCompat.getColor(context, R.color.white))
icon
}
minusIcon?.setTintMode(PorterDuff.Mode.MULTIPLY)
this.btnMinusIconView.setImageDrawable(minusIcon)
val drawable = ContextCompat.getDrawable(context, R.drawable.habit_circle)
this.btnMinusWrapper.setBackgroundResource(data.lightTaskColor)
drawable?.setTint(ContextCompat.getColor(context, data.mediumTaskColor))
drawable?.setTintMode(PorterDuff.Mode.MULTIPLY)
btnMinusCircleView.background = drawable
this.btnMinus.visibility = View.VISIBLE
this.btnMinus.isClickable = true
} else {
this.btnMinusWrapper.setBackgroundResource(R.color.habit_inactive_gray)
val minusIcon =
if (isLocked) {
val icon = ContextCompat.getDrawable(context, R.drawable.task_lock)
icon?.setTint(ContextCompat.getColor(context, R.color.content_background_offset))
icon
} else {
val icon = ContextCompat.getDrawable(context, R.drawable.habit_minus)
icon?.setTint(ContextCompat.getColor(context, R.color.content_background_offset))
icon
}
minusIcon?.setTintMode(PorterDuff.Mode.MULTIPLY)
this.btnMinusIconView.setImageDrawable(minusIcon)
btnMinusCircleView.background = ContextCompat.getDrawable(context, R.drawable.habit_circle_disabled)
this.btnMinus.visibility = View.GONE
this.btnMinus.isClickable = false
}
val streakString = task?.streakString
if (streakString?.isNotEmpty() == true && task?.isGroupTask != true) {
streakTextView.text = streakString
streakTextView.visibility = View.VISIBLE
streakIconView.visibility = View.VISIBLE
} else {
streakTextView.visibility = View.GONE
streakIconView.visibility = View.GONE
}
reminderTextView.visibility = View.GONE
calendarIconView?.visibility = View.GONE
super.bind(data, position, displayMode, ownerID)
if (data.up == false && data.down == false) {
titleTextView.setTextColor(ContextCompat.getColor(context, R.color.text_quad))
notesTextView?.setTextColor(ContextCompat.getColor(context, R.color.text_quad))
}
}
override fun onLeftActionTouched() {
super.onLeftActionTouched()
if (!isLocked) {
onPlusButtonClicked()
}
}
override fun onRightActionTouched() {
super.onRightActionTouched()
if (!isLocked) {
onMinusButtonClicked()
}
}
private fun onPlusButtonClicked() {
if (task?.up != true) return
task?.let { scoreTaskFunc.invoke(it, TaskDirection.UP) }
}
private fun onMinusButtonClicked() {
if (task?.down != true) return
task?.let { scoreTaskFunc.invoke(it, TaskDirection.DOWN) }
}
override fun setDisabled(
openTaskDisabled: Boolean,
taskActionsDisabled: Boolean,
) {
super.setDisabled(openTaskDisabled, taskActionsDisabled)
this.btnPlus.isEnabled = !taskActionsDisabled
this.btnMinus.isEnabled = !taskActionsDisabled
}
}

View file

@ -1,97 +1,106 @@
package com.habitrpg.android.habitica.ui.viewHolders.tasks
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toDrawable
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.RewardItemCardBinding
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.common.habitica.extensions.dpToPx
import com.habitrpg.common.habitica.helpers.NumberAbbreviator
import com.habitrpg.shared.habitica.models.responses.TaskDirection
class RewardViewHolder(
itemView: View,
scoreTaskFunc: ((Task, TaskDirection) -> Unit),
openTaskFunc: ((Task, View) -> Unit),
brokenTaskFunc: ((Task) -> Unit),
assignedTextProvider: GroupPlanInfoProvider?
) : BaseTaskViewHolder(
itemView,
scoreTaskFunc,
openTaskFunc,
brokenTaskFunc,
assignedTextProvider
) {
private val binding = RewardItemCardBinding.bind(itemView)
init {
binding.buyButton.setOnClickListener {
buyReward()
}
binding.goldIcon.setImageBitmap(HabiticaIconsHelper.imageOfGold())
}
private fun buyReward() {
task?.let { scoreTaskFunc(it, TaskDirection.DOWN) }
}
override fun setDisabled(openTaskDisabled: Boolean, taskActionsDisabled: Boolean) {
super.setDisabled(openTaskDisabled, taskActionsDisabled)
binding.buyButton.isEnabled = !taskActionsDisabled
}
fun bind(reward: Task, position: Int, canBuy: Boolean, displayMode: String, ownerID: String?) {
this.task = reward
streakTextView.visibility = View.GONE
super.bind(reward, position, displayMode, ownerID)
binding.priceLabel.text =
NumberAbbreviator.abbreviate(itemView.context, this.task?.value ?: 0.0)
if (isLocked) {
binding.priceLabel.setCompoundDrawablesWithIntrinsicBounds(
HabiticaIconsHelper.imageOfLocked(
ContextCompat.getColor(context, R.color.gray_1_30),
10,
12
).toDrawable(context.resources),
null,
null,
null
)
binding.priceLabel.compoundDrawablePadding = 2.dpToPx(context)
} else {
binding.priceLabel.setCompoundDrawables(null, null, null, null)
}
if (canBuy && !isLocked) {
binding.goldIcon.alpha = 1.0f
binding.priceLabel.setTextColor(
ContextCompat.getColor(
context,
R.color.reward_buy_button_text
)
)
binding.buyButton.setBackgroundColor(
ContextCompat.getColor(
context,
R.color.reward_buy_button_bg
)
)
} else {
binding.goldIcon.alpha = 0.6f
binding.priceLabel.setTextColor(ContextCompat.getColor(context, R.color.text_quad))
binding.buyButton.setBackgroundColor(
ColorUtils.setAlphaComponent(
ContextCompat.getColor(
context,
R.color.offset_background
),
127
)
)
}
}
}
package com.habitrpg.android.habitica.ui.viewHolders.tasks
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toDrawable
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.RewardItemCardBinding
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.common.habitica.extensions.dpToPx
import com.habitrpg.common.habitica.helpers.NumberAbbreviator
import com.habitrpg.shared.habitica.models.responses.TaskDirection
class RewardViewHolder(
itemView: View,
scoreTaskFunc: ((Task, TaskDirection) -> Unit),
openTaskFunc: ((Task, View) -> Unit),
brokenTaskFunc: ((Task) -> Unit),
assignedTextProvider: GroupPlanInfoProvider?,
) : BaseTaskViewHolder(
itemView,
scoreTaskFunc,
openTaskFunc,
brokenTaskFunc,
assignedTextProvider,
) {
private val binding = RewardItemCardBinding.bind(itemView)
init {
binding.buyButton.setOnClickListener {
buyReward()
}
binding.goldIcon.setImageBitmap(HabiticaIconsHelper.imageOfGold())
}
private fun buyReward() {
task?.let { scoreTaskFunc(it, TaskDirection.DOWN) }
}
override fun setDisabled(
openTaskDisabled: Boolean,
taskActionsDisabled: Boolean,
) {
super.setDisabled(openTaskDisabled, taskActionsDisabled)
binding.buyButton.isEnabled = !taskActionsDisabled
}
fun bind(
reward: Task,
position: Int,
canBuy: Boolean,
displayMode: String,
ownerID: String?,
) {
this.task = reward
streakTextView.visibility = View.GONE
super.bind(reward, position, displayMode, ownerID)
binding.priceLabel.text =
NumberAbbreviator.abbreviate(itemView.context, this.task?.value ?: 0.0)
if (isLocked) {
binding.priceLabel.setCompoundDrawablesWithIntrinsicBounds(
HabiticaIconsHelper.imageOfLocked(
ContextCompat.getColor(context, R.color.gray_1_30),
10,
12,
).toDrawable(context.resources),
null,
null,
null,
)
binding.priceLabel.compoundDrawablePadding = 2.dpToPx(context)
} else {
binding.priceLabel.setCompoundDrawables(null, null, null, null)
}
if (canBuy && !isLocked) {
binding.goldIcon.alpha = 1.0f
binding.priceLabel.setTextColor(
ContextCompat.getColor(
context,
R.color.reward_buy_button_text,
),
)
binding.buyButton.setBackgroundColor(
ContextCompat.getColor(
context,
R.color.reward_buy_button_bg,
),
)
} else {
binding.goldIcon.alpha = 0.6f
binding.priceLabel.setTextColor(ContextCompat.getColor(context, R.color.text_quad))
binding.buyButton.setBackgroundColor(
ColorUtils.setAlphaComponent(
ContextCompat.getColor(
context,
R.color.offset_background,
),
127,
),
)
}
}
}

View file

@ -1,56 +1,58 @@
package com.habitrpg.android.habitica.ui.viewHolders.tasks
import android.view.View
import androidx.core.content.ContextCompat
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.extensions.formatForLocale
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.shared.habitica.models.responses.TaskDirection
class TodoViewHolder(
itemView: View,
scoreTaskFunc: ((Task, TaskDirection) -> Unit),
scoreChecklistItemFunc: ((Task, ChecklistItem) -> Unit),
openTaskFunc: ((Task, View) -> Unit),
brokenTaskFunc: ((Task) -> Unit),
assignedTextProvider: GroupPlanInfoProvider?
) : ChecklistedViewHolder(itemView, scoreTaskFunc, scoreChecklistItemFunc, openTaskFunc, brokenTaskFunc, assignedTextProvider) {
override fun bind(
data: Task,
position: Int,
displayMode: String,
ownerID: String?
) {
this.task = data
setChecklistIndicatorBackgroundActive(data.isChecklistDisplayActive)
reminderTextView.visibility = View.GONE
this.streakTextView.visibility = View.GONE
super.bind(data, position, displayMode, ownerID)
}
override fun configureSpecialTaskTextView(task: Task) {
super.configureSpecialTaskTextView(task)
if (task.dueDate != null) {
if (task.isDueToday() == true) {
specialTaskTextView?.text = context.getString(R.string.today)
} else if (task.isDayOrMorePastDue() == true) {
task.dueDate?.let { specialTaskTextView?.text = it.formatForLocale() }
specialTaskTextView?.setTextColor(ContextCompat.getColor(context, R.color.maroon100_red100))
} else {
task.dueDate?.let { specialTaskTextView?.text = it.formatForLocale() }
specialTaskTextView?.setTextColor(ContextCompat.getColor(context, R.color.gray_300))
}
this.specialTaskTextView?.visibility = View.VISIBLE
calendarIconView?.visibility = View.VISIBLE
} else {
this.specialTaskTextView?.visibility = View.INVISIBLE
}
}
override fun shouldDisplayAsActive(task: Task?, userID: String?): Boolean {
return task?.completed(userID) != true
}
}
package com.habitrpg.android.habitica.ui.viewHolders.tasks
import android.view.View
import androidx.core.content.ContextCompat
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.extensions.formatForLocale
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.shared.habitica.models.responses.TaskDirection
class TodoViewHolder(
itemView: View,
scoreTaskFunc: ((Task, TaskDirection) -> Unit),
scoreChecklistItemFunc: ((Task, ChecklistItem) -> Unit),
openTaskFunc: ((Task, View) -> Unit),
brokenTaskFunc: ((Task) -> Unit),
assignedTextProvider: GroupPlanInfoProvider?,
) : ChecklistedViewHolder(itemView, scoreTaskFunc, scoreChecklistItemFunc, openTaskFunc, brokenTaskFunc, assignedTextProvider) {
override fun bind(
data: Task,
position: Int,
displayMode: String,
ownerID: String?,
) {
this.task = data
setChecklistIndicatorBackgroundActive(data.isChecklistDisplayActive)
reminderTextView.visibility = View.GONE
this.streakTextView.visibility = View.GONE
super.bind(data, position, displayMode, ownerID)
}
override fun configureSpecialTaskTextView(task: Task) {
super.configureSpecialTaskTextView(task)
if (task.dueDate != null) {
if (task.isDueToday() == true) {
specialTaskTextView?.text = context.getString(R.string.today)
} else if (task.isDayOrMorePastDue() == true) {
task.dueDate?.let { specialTaskTextView?.text = it.formatForLocale() }
specialTaskTextView?.setTextColor(ContextCompat.getColor(context, R.color.maroon100_red100))
} else {
task.dueDate?.let { specialTaskTextView?.text = it.formatForLocale() }
specialTaskTextView?.setTextColor(ContextCompat.getColor(context, R.color.gray_300))
}
this.specialTaskTextView?.visibility = View.VISIBLE
calendarIconView?.visibility = View.VISIBLE
} else {
this.specialTaskTextView?.visibility = View.INVISIBLE
}
}
override fun shouldDisplayAsActive(
task: Task?,
userID: String?,
): Boolean {
return task?.completed(userID) != true
}
}

View file

@ -22,13 +22,14 @@ class NPCBannerView(context: Context, attrs: AttributeSet?) : FrameLayout(contex
var shopSpriteSuffix: String? = null
set(value) {
field = if (value.isNullOrEmpty()) {
""
} else if (value.startsWith("_")) {
value
} else {
"_$value"
}
field =
if (value.isNullOrEmpty()) {
""
} else if (value.startsWith("_")) {
value
} else {
"_$value"
}
if (identifier.isNotEmpty()) {
setImage()
}

View file

@ -34,7 +34,6 @@ import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
open class HabiticaAlertDialog(context: Context) : AlertDialog(context, R.style.HabiticaAlertDialogTheme) {
var buttonAxis: Int = LinearLayout.VERTICAL
set(value) {
field = value
@ -186,7 +185,7 @@ open class HabiticaAlertDialog(context: Context) : AlertDialog(context, R.style.
isPrimary: Boolean,
isDestructive: Boolean = false,
autoDismiss: Boolean = true,
function: ((HabiticaAlertDialog, Int) -> Unit)? = null
function: ((HabiticaAlertDialog, Int) -> Unit)? = null,
): Button {
return addButton(context.getString(stringRes), isPrimary, isDestructive, autoDismiss, function)
}
@ -196,21 +195,22 @@ open class HabiticaAlertDialog(context: Context) : AlertDialog(context, R.style.
isPrimary: Boolean,
isDestructive: Boolean = false,
autoDismiss: Boolean = true,
function: ((HabiticaAlertDialog, Int) -> Unit)? = null
function: ((HabiticaAlertDialog, Int) -> Unit)? = null,
): Button {
val button: Button = if (isPrimary) {
if (isDestructive) {
binding.buttonsWrapper.inflate(R.layout.dialog_habitica_primary_destructive_button) as? Button
val button: Button =
if (isPrimary) {
if (isDestructive) {
binding.buttonsWrapper.inflate(R.layout.dialog_habitica_primary_destructive_button) as? Button
} else {
binding.buttonsWrapper.inflate(R.layout.dialog_habitica_primary_button) as? Button
}
} else {
binding.buttonsWrapper.inflate(R.layout.dialog_habitica_primary_button) as? Button
}
} else {
val button = binding.buttonsWrapper.inflate(R.layout.dialog_habitica_secondary_button) as? Button
if (isDestructive) {
button?.setTextColor(ContextCompat.getColor(context, R.color.maroon_100))
}
button
} ?: Button(context)
val button = binding.buttonsWrapper.inflate(R.layout.dialog_habitica_secondary_button) as? Button
if (isDestructive) {
button?.setTextColor(ContextCompat.getColor(context, R.color.maroon_100))
}
button
} ?: Button(context)
button.text = string
button.elevation = 0f
return addButton(button, autoDismiss, function) as Button
@ -219,7 +219,7 @@ open class HabiticaAlertDialog(context: Context) : AlertDialog(context, R.style.
fun addButton(
buttonView: View,
autoDismiss: Boolean = true,
function: ((HabiticaAlertDialog, Int) -> Unit)? = null
function: ((HabiticaAlertDialog, Int) -> Unit)? = null,
): View {
val weakThis = WeakReference(this)
val buttonIndex = binding.buttonsWrapper.childCount
@ -241,13 +241,14 @@ open class HabiticaAlertDialog(context: Context) : AlertDialog(context, R.style.
}
private fun configureButtonLayoutParams(buttonView: View) {
val layoutParams = if (isScrollingLayout) {
val params = LinearLayout.LayoutParams(0, 48.dpToPx(context))
params.weight = 1f
params
} else {
LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 48.dpToPx(context))
}
val layoutParams =
if (isScrollingLayout) {
val params = LinearLayout.LayoutParams(0, 48.dpToPx(context))
params.weight = 1f
params
} else {
LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 48.dpToPx(context))
}
buttonView.layoutParams = layoutParams
buttonView.elevation = 10f
@ -317,8 +318,8 @@ open class HabiticaAlertDialog(context: Context) : AlertDialog(context, R.style.
(dialogQueue[0].context as? BaseActivity)?.lifecycleScope?.launch(context = Dispatchers.Main) {
delay(500L)
if (dialogQueue.size > 0 && (
(dialogQueue[0].context as? Activity)?.isFinishing == false ||
((dialogQueue[0].context as? ContextThemeWrapper)?.baseContext as? Activity)?.isFinishing == false
(dialogQueue[0].context as? Activity)?.isFinishing == false ||
((dialogQueue[0].context as? ContextThemeWrapper)?.baseContext as? Activity)?.isFinishing == false
)
) {
dialogQueue[0].show()

View file

@ -65,9 +65,8 @@ class PurchaseDialog(
private val userRepository: UserRepository,
private val inventoryRepository: InventoryRepository,
val item: ShopItem,
private val parentActivity: AppCompatActivity? = null
private val parentActivity: AppCompatActivity? = null,
) : HabiticaAlertDialog(context) {
private val customHeader: View by lazy {
DialogPurchaseShopitemHeaderBinding.inflate(context.layoutInflater).root
}
@ -205,28 +204,30 @@ class PurchaseDialog(
if (user == null) return
val userLvl = user?.stats?.lvl ?: 0
if (shopItem.habitClass != null && shopItem.habitClass != "special" && shopItem.habitClass != "armoire" && user?.stats?.habitClass != shopItem.habitClass) {
limitedTextView.text = if (userLvl >= 10) {
context.getString(R.string.class_equipment_shop_dialog)
} else {
context.getString(R.string.insufficient_level_equipment_dialog)
}
limitedTextView.text =
if (userLvl >= 10) {
context.getString(R.string.class_equipment_shop_dialog)
} else {
context.getString(R.string.insufficient_level_equipment_dialog)
}
limitedTextView.visibility = View.VISIBLE
limitedTextView.setBackgroundColor(ContextCompat.getColor(context, R.color.inverted_background))
} else if (shopItem.event?.end != null) {
limitedTextViewJob?.cancel()
limitedTextViewJob = MainScope().launch(Dispatchers.Main) {
limitedTextView.visibility = View.VISIBLE
while (shopItem.event?.end?.after(Date()) == true) {
limitedTextView.text = context.getString(R.string.available_for, shopItem.event?.end?.getShortRemainingString())
val diff = (shopItem.event?.end?.time ?: 0) - Date().time
delay(1.toDuration(if (diff < (60 * 60 * 1000)) DurationUnit.SECONDS else DurationUnit.MINUTES))
limitedTextViewJob =
MainScope().launch(Dispatchers.Main) {
limitedTextView.visibility = View.VISIBLE
while (shopItem.event?.end?.after(Date()) == true) {
limitedTextView.text = context.getString(R.string.available_for, shopItem.event?.end?.getShortRemainingString())
val diff = (shopItem.event?.end?.time ?: 0) - Date().time
delay(1.toDuration(if (diff < (60 * 60 * 1000)) DurationUnit.SECONDS else DurationUnit.MINUTES))
}
if (shopItem.event?.end?.before(Date()) == true) {
limitedTextView.text = context.getString(R.string.no_longer_available)
limitedTextView.background = ContextCompat.getColor(context, R.color.offset_background).toDrawable()
limitedTextView.setTextColor(ContextCompat.getColor(context, R.color.text_secondary))
}
}
if (shopItem.event?.end?.before(Date()) == true) {
limitedTextView.text = context.getString(R.string.no_longer_available)
limitedTextView.background = ContextCompat.getColor(context, R.color.offset_background).toDrawable()
limitedTextView.setTextColor(ContextCompat.getColor(context, R.color.text_secondary))
}
}
} else if (shopItem.locked) {
buyLabel.text = context.getString(R.string.locked)
limitedTextView.visibility = View.GONE
@ -273,9 +274,10 @@ class PurchaseDialog(
pinTextView = customHeader.findViewById(R.id.pin_text)
addCloseButton()
buyButton = addButton(DialogPurchaseShopitemButtonBinding.inflate(layoutInflater).root, autoDismiss = false) { _, _ ->
onBuyButtonClicked()
}
buyButton =
addButton(DialogPurchaseShopitemButtonBinding.inflate(layoutInflater).root, autoDismiss = false) { _, _ ->
onBuyButtonClicked()
}
priceLabel = buyButton.findViewById(R.id.priceLabel)
priceLabel.animationDuration = 0L
buyLabel = buyButton.findViewById(R.id.buy_label)
@ -376,9 +378,10 @@ class PurchaseDialog(
if (user?.isSubscribed == true) {
InsufficientHourglassesDialog(context).show()
} else {
val subscriptionBottomSheet = EventOutcomeSubscriptionBottomSheetFragment().apply {
eventType = EventOutcomeSubscriptionBottomSheetFragment.EVENT_HOURGLASS_SHOP_OPENED
}
val subscriptionBottomSheet =
EventOutcomeSubscriptionBottomSheetFragment().apply {
eventType = EventOutcomeSubscriptionBottomSheetFragment.EVENT_HOURGLASS_SHOP_OPENED
}
parentActivity?.let { activity -> subscriptionBottomSheet.show(activity.supportFragmentManager, SubscriptionBottomSheetFragment.TAG) }
}
}
@ -396,18 +399,19 @@ class PurchaseDialog(
bundleOf(
Pair("shop", shopIdentifier),
Pair("type", shopItem.purchaseType),
Pair("key", shopItem.key)
)
Pair("key", shopItem.key),
),
)
HapticFeedbackManager.tap(buyButton)
val snackbarText = arrayOf("")
val observable: (suspend () -> Any?)
if (shopIdentifier != null && shopIdentifier == Shop.TIME_TRAVELERS_SHOP || "mystery_set" == shopItem.purchaseType || shopItem.currency == "hourglasses") {
observable = if (shopItem.purchaseType == "gear") {
{ inventoryRepository.purchaseMysterySet(shopItem.key) }
} else {
{ inventoryRepository.purchaseHourglassItem(shopItem.purchaseType, shopItem.key) }
}
observable =
if (shopItem.purchaseType == "gear") {
{ inventoryRepository.purchaseMysterySet(shopItem.key) }
} else {
{ inventoryRepository.purchaseHourglassItem(shopItem.purchaseType, shopItem.key) }
}
} else if (shopItem.purchaseType == "fortify") {
observable = { userRepository.reroll() }
} else if (shopItem.purchaseType == "quests" && shopItem.currency == "gold") {
@ -432,8 +436,8 @@ class PurchaseDialog(
buyResponse.armoire["type"] ?: "",
buyResponse.armoire["dropText"] ?: "",
buyResponse.armoire["dropKey"] ?: "",
buyResponse.armoire["value"] ?: ""
).arguments
buyResponse.armoire["value"] ?: "",
).arguments,
)
}
}
@ -442,26 +446,28 @@ class PurchaseDialog(
}
lifecycleScope.launchCatching {
observable()
val text = snackbarText[0].ifBlank {
if (shopItem.text?.isNotBlank() == true) {
context.getString(R.string.successful_purchase, shopItem.text)
} else {
context.getString(R.string.purchased)
val text =
snackbarText[0].ifBlank {
if (shopItem.text?.isNotBlank() == true) {
context.getString(R.string.successful_purchase, shopItem.text)
} else {
context.getString(R.string.purchased)
}
}
val rightTextColor =
when (item.currency) {
"gold" -> ContextCompat.getColor(context, R.color.yellow_5)
"gems" -> ContextCompat.getColor(context, R.color.green_10)
"hourglasses" -> ContextCompat.getColor(context, R.color.brand_300)
else -> 0
}
}
val rightTextColor = when (item.currency) {
"gold" -> ContextCompat.getColor(context, R.color.yellow_5)
"gems" -> ContextCompat.getColor(context, R.color.green_10)
"hourglasses" -> ContextCompat.getColor(context, R.color.brand_300)
else -> 0
}
val a = (application?.currentActivity?.get() ?: getActivity() ?: ownerActivity)
(a as? SnackbarActivity)?.showSnackbar(
content = text,
rightIcon = priceLabel.compoundDrawables[0],
rightTextColor = rightTextColor,
rightText = "-" + priceLabel.text,
isCelebratory = true
isCelebratory = true,
)
inventoryRepository.retrieveInAppRewards()
userRepository.retrieveUser(forced = true)
@ -497,9 +503,10 @@ class PurchaseDialog(
val alert = HabiticaAlertDialog(context)
alert.setTitle(R.string.excess_items)
alert.setMessage(context.getString(R.string.excessItemsNoneLeft, item.text, purchaseQuantity, item.text))
alert.addButton(context.getString(R.string.purchaseX, purchaseQuantity),
alert.addButton(
context.getString(R.string.purchaseX, purchaseQuantity),
isPrimary = true,
isDestructive = false
isDestructive = false,
) { _, _ ->
buyItem(purchaseQuantity)
}
@ -516,12 +523,13 @@ class PurchaseDialog(
shouldWarn = inventoryRepository.getPets(item.key, "quest", null).firstOrNull()?.isNotEmpty() ?: false
ownedItems = inventoryRepository.getOwnedItems("eggs").firstOrNull()
} else if (item.purchaseType == "hatchingPotions") {
totalCount = if (item.path?.contains("wacky") == true) {
// Wacky pets can't be raised to mounts, so only need half as many
9
} else {
18
}
totalCount =
if (item.path?.contains("wacky") == true) {
// Wacky pets can't be raised to mounts, so only need half as many
9
} else {
18
}
shouldWarn = inventoryRepository.getPets().firstOrNull()?.any { pet ->
pet.animal == item.key && (pet.type == "premium" || pet.type == "wacky")
} ?: false

View file

@ -39,19 +39,23 @@ class PurchaseDialogCustomizationContent(context: Context) : PurchaseDialogConte
}
}
fun setAvatarWithPreview(user: User, shopItem: ShopItem) {
fun setAvatarWithPreview(
user: User,
shopItem: ShopItem,
) {
val layerMap = EnumMap<AvatarView.LayerType, String>(AvatarView.LayerType::class.java)
val path = shopItem.unlockPath ?: shopItem.path ?: ""
val layerName = when {
path.contains("skin") -> AvatarView.LayerType.SKIN
path.contains("shirt") -> AvatarView.LayerType.SHIRT
path.contains("color") -> AvatarView.LayerType.HAIR_BANGS
path.contains("base") -> AvatarView.LayerType.HAIR_BASE
path.contains("bangs") -> AvatarView.LayerType.HAIR_BANGS
path.contains("beard") -> AvatarView.LayerType.HAIR_BEARD
path.contains("mustache") -> AvatarView.LayerType.HAIR_MUSTACHE
else -> null
}
val layerName =
when {
path.contains("skin") -> AvatarView.LayerType.SKIN
path.contains("shirt") -> AvatarView.LayerType.SHIRT
path.contains("color") -> AvatarView.LayerType.HAIR_BANGS
path.contains("base") -> AvatarView.LayerType.HAIR_BASE
path.contains("bangs") -> AvatarView.LayerType.HAIR_BANGS
path.contains("beard") -> AvatarView.LayerType.HAIR_BEARD
path.contains("mustache") -> AvatarView.LayerType.HAIR_MUSTACHE
else -> null
}
layerName?.let {
layerMap[it] = shopItem.imageName?.replace("shop_", "")?.replace("icon_", "")
}

View file

@ -1,30 +0,0 @@
@file:OptIn(ExperimentalJsExport::class)
package com.habitrpg.shared.habitica
@JsExport
actual class PlatformLogger actual constructor() {
actual val enabled: Boolean
get() = true
actual fun logDebug(tag: String, message: String) {
console.log("[🥦] $tag: $message")
}
actual fun logInfo(tag: String, message: String) {
console.log("[🍋] $tag: $message")
}
actual fun logWarning(tag: String, message: String) {
console.log("[🍊] $tag: $message")
}
actual fun logError(tag: String, message: String) {
console.log("[🍎] $tag: $message")
}
@JsName("logErrorException")
actual fun logError(tag: String, message: String, exception: Throwable) {
console.log("[🍎] $tag: $message\n${exception}")
}
}

View file

@ -5,4 +5,4 @@ actual class Platform actual constructor() {
get() = "JS!"
}
actual interface HParcelable
actual interface HParcelable

View file

@ -0,0 +1,46 @@
@file:OptIn(ExperimentalJsExport::class)
package com.habitrpg.shared.habitica
@JsExport
actual class PlatformLogger actual constructor() {
actual val enabled: Boolean
get() = true
actual fun logDebug(
tag: String,
message: String,
) {
console.log("[🥦] $tag: $message")
}
actual fun logInfo(
tag: String,
message: String,
) {
console.log("[🍋] $tag: $message")
}
actual fun logWarning(
tag: String,
message: String,
) {
console.log("[🍊] $tag: $message")
}
actual fun logError(
tag: String,
message: String,
) {
console.log("[🍎] $tag: $message")
}
@JsName("logErrorException")
actual fun logError(
tag: String,
message: String,
exception: Throwable,
) {
console.log("[🍎] $tag: $message\n$exception")
}
}