From bf62f5ac160a0e9e6b5ed08e00648ea83b896739 Mon Sep 17 00:00:00 2001 From: Phillip Thelen Date: Mon, 22 Apr 2024 16:16:28 +0200 Subject: [PATCH] format fixes --- .../android/habitica/api/ApiService.kt | 488 ++++++-- .../android/habitica/data/ApiClient.kt | 787 ++++++++----- .../data/implementation/UserRepositoryImpl.kt | 1028 ++++++++-------- .../android/habitica/models/shops/Shop.kt | 67 +- .../android/habitica/models/shops/ShopItem.kt | 457 ++++---- .../ChallengeTasksRecyclerViewAdapter.kt | 58 +- .../tasks/DailiesRecyclerViewHolder.kt | 8 +- .../tasks/HabitsRecyclerViewAdapter.kt | 8 +- .../tasks/RewardsRecyclerViewAdapter.kt | 316 ++--- .../adapter/tasks/TodosRecyclerViewAdapter.kt | 8 +- .../ui/fragments/NavigationDrawerFragment.kt | 207 ++-- .../habitica/ui/fragments/NewsFragment.kt | 41 +- .../AvatarCustomizationFragment.kt | 64 +- .../customization/AvatarOverviewFragment.kt | 588 +++++----- .../ComposeAvatarCustomizationFragment.kt | 1031 +++++++++-------- .../shops/CustomizationsShopFragment.kt | 2 +- .../fragments/inventory/shops/ShopFragment.kt | 851 +++++++------- .../viewHolders/tasks/BaseTaskViewHolder.kt | 624 +++++----- .../tasks/ChecklistedViewHolder.kt | 487 ++++---- .../ui/viewHolders/tasks/DailyViewHolder.kt | 191 +-- .../ui/viewHolders/tasks/HabitViewHolder.kt | 342 +++--- .../ui/viewHolders/tasks/RewardViewHolder.kt | 203 ++-- .../ui/viewHolders/tasks/TodoViewHolder.kt | 114 +- .../habitica/ui/views/NPCBannerView.kt | 15 +- .../ui/views/dialogs/HabiticaAlertDialog.kt | 51 +- .../habitica/ui/views/shops/PurchaseDialog.kt | 118 +- .../PurchaseDialogCustomizationContent.kt | 26 +- .../com/habitrpg/shared/habitica/Logger.js.kt | 30 - .../habitrpg/shared/habitica/Platform.js.kt | 2 +- .../shared/habitica/PlatformLogger.js.kt | 46 + 30 files changed, 4481 insertions(+), 3777 deletions(-) delete mode 100644 shared/src/jsMain/kotlin/com/habitrpg/shared/habitica/Logger.js.kt create mode 100644 shared/src/jsMain/kotlin/com/habitrpg/shared/habitica/PlatformLogger.js.kt diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt index 551510ae4..a51fbbdf4 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt @@ -52,7 +52,7 @@ interface ApiService { @GET("status") suspend fun getStatus(): HabitResponse - /* user API */ + // user API @GET("user/") suspend fun getUser(): HabitResponse @@ -61,7 +61,10 @@ interface ApiService { suspend fun syncUserStats(): HabitResponse @GET("inbox/messages") - suspend fun getInboxMessages(@Query("conversation") uuid: String, @Query("page") page: Int): HabitResponse> + suspend fun getInboxMessages( + @Query("conversation") uuid: String, + @Query("page") page: Int, + ): HabitResponse> @GET("inbox/conversations") suspend fun getInboxConversations(): HabitResponse> @@ -73,113 +76,192 @@ interface ApiService { suspend fun worldState(): HabitResponse @GET("content") - suspend fun getContent(@Query("language") language: String?): HabitResponse + suspend fun getContent( + @Query("language") language: String?, + ): HabitResponse @PUT("user/") - suspend fun updateUser(@Body updateDictionary: Map): HabitResponse + suspend fun updateUser( + @Body updateDictionary: Map, + ): HabitResponse @PUT("user/") - suspend fun registrationLanguage(@Header("Accept-Language") registrationLanguage: String): HabitResponse + suspend fun registrationLanguage( + @Header("Accept-Language") registrationLanguage: String, + ): HabitResponse @GET("user/in-app-rewards") suspend fun retrieveInAppRewards(): HabitResponse> @POST("user/equip/{type}/{key}") - suspend fun equipItem(@Path("type") type: String, @Path("key") itemKey: String): HabitResponse + suspend fun equipItem( + @Path("type") type: String, + @Path("key") itemKey: String, + ): HabitResponse @POST("user/buy/{key}") - suspend fun buyItem(@Path("key") itemKey: String, @Body quantity: Map): HabitResponse + suspend fun buyItem( + @Path("key") itemKey: String, + @Body quantity: Map, + ): HabitResponse @POST("user/purchase/{type}/{key}") suspend fun purchaseItem( @Path("type") type: String, @Path("key") itemKey: String, - @Body quantity: Map + @Body quantity: Map, ): HabitResponse @POST("user/purchase-hourglass/{type}/{key}") - suspend fun purchaseHourglassItem(@Path("type") type: String, @Path("key") itemKey: String): HabitResponse + suspend fun purchaseHourglassItem( + @Path("type") type: String, + @Path("key") itemKey: String, + ): HabitResponse @POST("user/buy-mystery-set/{key}") - suspend fun purchaseMysterySet(@Path("key") itemKey: String): HabitResponse + suspend fun purchaseMysterySet( + @Path("key") itemKey: String, + ): HabitResponse @POST("user/buy-quest/{key}") - suspend fun purchaseQuest(@Path("key") key: String): HabitResponse + suspend fun purchaseQuest( + @Path("key") key: String, + ): HabitResponse @POST("user/buy-special-spell/{key}") - suspend fun purchaseSpecialSpell(@Path("key") key: String): HabitResponse + suspend fun purchaseSpecialSpell( + @Path("key") key: String, + ): HabitResponse @POST("user/sell/{type}/{key}") - suspend fun sellItem(@Path("type") itemType: String, @Path("key") itemKey: String): HabitResponse + suspend fun sellItem( + @Path("type") itemType: String, + @Path("key") itemKey: String, + ): HabitResponse @POST("user/feed/{pet}/{food}") - suspend fun feedPet(@Path("pet") petKey: String, @Path("food") foodKey: String): HabitResponse + suspend fun feedPet( + @Path("pet") petKey: String, + @Path("food") foodKey: String, + ): HabitResponse @POST("user/hatch/{egg}/{hatchingPotion}") - suspend fun hatchPet(@Path("egg") eggKey: String, @Path("hatchingPotion") hatchingPotionKey: String): HabitResponse + suspend fun hatchPet( + @Path("egg") eggKey: String, + @Path("hatchingPotion") hatchingPotionKey: String, + ): HabitResponse @GET("tasks/user") - suspend fun getTasks(@Query("type") type: String): HabitResponse + suspend fun getTasks( + @Query("type") type: String, + ): HabitResponse @GET("tasks/user") - suspend fun getTasks(@Query("type") type: String, @Query("dueDate") dueDate: String): HabitResponse + suspend fun getTasks( + @Query("type") type: String, + @Query("dueDate") dueDate: String, + ): HabitResponse @POST("user/unlock") - suspend fun unlockPath(@Query("path") path: String): HabitResponse + suspend fun unlockPath( + @Query("path") path: String, + ): HabitResponse @GET("tasks/{id}") - suspend fun getTask(@Path("id") id: String): HabitResponse + suspend fun getTask( + @Path("id") id: String, + ): HabitResponse @POST("tasks/{id}/score/{direction}") - suspend fun postTaskDirection(@Path("id") id: String, @Path("direction") direction: String): HabitResponse + suspend fun postTaskDirection( + @Path("id") id: String, + @Path("direction") direction: String, + ): HabitResponse @POST("tasks/bulk-score") - suspend fun bulkScoreTasks(@Body data: List>): HabitResponse + suspend fun bulkScoreTasks( + @Body data: List>, + ): HabitResponse @POST("tasks/{id}/move/to/{position}") - suspend fun postTaskNewPosition(@Path("id") id: String, @Path("position") position: Int): HabitResponse> + suspend fun postTaskNewPosition( + @Path("id") id: String, + @Path("position") position: Int, + ): HabitResponse> @POST("tasks/{taskId}/checklist/{itemId}/score") - suspend fun scoreChecklistItem(@Path("taskId") taskId: String, @Path("itemId") itemId: String): HabitResponse + suspend fun scoreChecklistItem( + @Path("taskId") taskId: String, + @Path("itemId") itemId: String, + ): HabitResponse @POST("tasks/user") - suspend fun createTask(@Body item: Task): HabitResponse + suspend fun createTask( + @Body item: Task, + ): HabitResponse @POST("tasks/group/{groupId}") - suspend fun createGroupTask(@Path("groupId") groupId: String, @Body item: Task): HabitResponse + suspend fun createGroupTask( + @Path("groupId") groupId: String, + @Body item: Task, + ): HabitResponse @POST("tasks/user") - suspend fun createTasks(@Body tasks: List): HabitResponse> + suspend fun createTasks( + @Body tasks: List, + ): HabitResponse> @PUT("tasks/{id}") - suspend fun updateTask(@Path("id") id: String, @Body item: Task): HabitResponse + suspend fun updateTask( + @Path("id") id: String, + @Body item: Task, + ): HabitResponse @DELETE("tasks/{id}") - suspend fun deleteTask(@Path("id") id: String): HabitResponse + suspend fun deleteTask( + @Path("id") id: String, + ): HabitResponse @POST("tags") - suspend fun createTag(@Body tag: Tag): HabitResponse + suspend fun createTag( + @Body tag: Tag, + ): HabitResponse @PUT("tags/{id}") - suspend fun updateTag(@Path("id") id: String, @Body tag: Tag): HabitResponse + suspend fun updateTag( + @Path("id") id: String, + @Body tag: Tag, + ): HabitResponse @DELETE("tags/{id}") - suspend fun deleteTag(@Path("id") id: String): HabitResponse + suspend fun deleteTag( + @Path("id") id: String, + ): HabitResponse @POST("user/auth/local/register") - suspend fun registerUser(@Body auth: UserAuth): HabitResponse + suspend fun registerUser( + @Body auth: UserAuth, + ): HabitResponse @POST("user/auth/local/login") - suspend fun connectLocal(@Body auth: UserAuth): HabitResponse + suspend fun connectLocal( + @Body auth: UserAuth, + ): HabitResponse @POST("user/auth/social") - suspend fun connectSocial(@Body auth: UserAuthSocial): HabitResponse + suspend fun connectSocial( + @Body auth: UserAuthSocial, + ): HabitResponse @DELETE("user/auth/social/{network}") - suspend fun disconnectSocial(@Path("network") network: String): HabitResponse + suspend fun disconnectSocial( + @Path("network") network: String, + ): HabitResponse @POST("user/auth/apple") - suspend fun loginApple(@Body auth: Map): HabitResponse + suspend fun loginApple( + @Body auth: Map, + ): HabitResponse @POST("user/sleep") suspend fun sleep(): HabitResponse @@ -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 @POST("user/class/cast/{skill}") - suspend fun useSkill(@Path("skill") skillName: String, @Query("targetType") targetType: String): HabitResponse + suspend fun useSkill( + @Path("skill") skillName: String, + @Query("targetType") targetType: String, + ): HabitResponse @POST("user/change-class") suspend fun changeClass(): HabitResponse @POST("user/change-class") - suspend fun changeClass(@Query("class") className: String): HabitResponse + suspend fun changeClass( + @Query("class") className: String, + ): HabitResponse @POST("user/disable-classes") suspend fun disableClasses(): HabitResponse @@ -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> + suspend fun listGroups( + @Query("type") type: String, + ): HabitResponse> @GET("groups/{gid}") - suspend fun getGroup(@Path("gid") groupId: String): HabitResponse + suspend fun getGroup( + @Path("gid") groupId: String, + ): HabitResponse @POST("groups") - suspend fun createGroup(@Body item: Group): HabitResponse + suspend fun createGroup( + @Body item: Group, + ): HabitResponse @PUT("groups/{id}") - suspend fun updateGroup(@Path("id") id: String, @Body item: Group): HabitResponse + suspend fun updateGroup( + @Path("id") id: String, + @Body item: Group, + ): HabitResponse @POST("groups/{groupID}/removeMember/{userID}") - suspend fun removeMemberFromGroup(@Path("groupID") groupID: String, @Path("userID") userID: String): HabitResponse + suspend fun removeMemberFromGroup( + @Path("groupID") groupID: String, + @Path("userID") userID: String, + ): HabitResponse @GET("groups/{gid}/chat") - suspend fun listGroupChat(@Path("gid") groupId: String): HabitResponse> + suspend fun listGroupChat( + @Path("gid") groupId: String, + ): HabitResponse> @POST("groups/{gid}/join") - suspend fun joinGroup(@Path("gid") groupId: String): HabitResponse + suspend fun joinGroup( + @Path("gid") groupId: String, + ): HabitResponse @POST("groups/{gid}/leave") - suspend fun leaveGroup(@Path("gid") groupId: String, @Query("keepChallenges") keepChallenges: String): HabitResponse + suspend fun leaveGroup( + @Path("gid") groupId: String, + @Query("keepChallenges") keepChallenges: String, + ): HabitResponse @POST("groups/{gid}/chat") - suspend fun postGroupChat(@Path("gid") groupId: String, @Body message: Map): HabitResponse + suspend fun postGroupChat( + @Path("gid") groupId: String, + @Body message: Map, + ): HabitResponse @DELETE("groups/{gid}/chat/{messageId}") - suspend fun deleteMessage(@Path("gid") groupId: String, @Path("messageId") messageId: String): HabitResponse + suspend fun deleteMessage( + @Path("gid") groupId: String, + @Path("messageId") messageId: String, + ): HabitResponse @DELETE("inbox/messages/{messageId}") - suspend fun deleteInboxMessage(@Path("messageId") messageId: String): HabitResponse + suspend fun deleteInboxMessage( + @Path("messageId") messageId: String, + ): HabitResponse @GET("groups/{gid}/members") suspend fun getGroupMembers( @Path("gid") groupId: String, - @Query("includeAllPublicFields") includeAllPublicFields: Boolean? + @Query("includeAllPublicFields") includeAllPublicFields: Boolean?, ): HabitResponse> @GET("groups/{gid}/members") suspend fun getGroupMembers( @Path("gid") groupId: String, @Query("includeAllPublicFields") includeAllPublicFields: Boolean?, - @Query("lastId") lastId: String + @Query("lastId") lastId: String, ): HabitResponse> // Like returns the full chat list @POST("groups/{gid}/chat/{mid}/like") - suspend fun likeMessage(@Path("gid") groupId: String, @Path("mid") mid: String): HabitResponse + suspend fun likeMessage( + @Path("gid") groupId: String, + @Path("mid") mid: String, + ): HabitResponse @POST("groups/{gid}/chat/{mid}/flag") suspend fun flagMessage( @Path("gid") groupId: String, @Path("mid") mid: String, - @Body data: Map + @Body data: Map, ): HabitResponse @POST("members/{mid}/flag") - suspend fun reportMember(@Path("mid") mid: String, @Body data: Map): HabitResponse + suspend fun reportMember( + @Path("mid") mid: String, + @Body data: Map, + ): HabitResponse @POST("groups/{gid}/chat/seen") - suspend fun seenMessages(@Path("gid") groupId: String): HabitResponse + suspend fun seenMessages( + @Path("gid") groupId: String, + ): HabitResponse @POST("groups/{gid}/invite") - suspend fun inviteToGroup(@Path("gid") groupId: String, @Body inviteData: Map): HabitResponse> + suspend fun inviteToGroup( + @Path("gid") groupId: String, + @Body inviteData: Map, + ): HabitResponse> @POST("groups/{gid}/reject-invite") - suspend fun rejectGroupInvite(@Path("gid") groupId: String): HabitResponse + suspend fun rejectGroupInvite( + @Path("gid") groupId: String, + ): HabitResponse @POST("groups/{gid}/quests/accept") - suspend fun acceptQuest(@Path("gid") groupId: String): HabitResponse + suspend fun acceptQuest( + @Path("gid") groupId: String, + ): HabitResponse @POST("groups/{gid}/quests/reject") - suspend fun rejectQuest(@Path("gid") groupId: String): HabitResponse + suspend fun rejectQuest( + @Path("gid") groupId: String, + ): HabitResponse @POST("groups/{gid}/quests/cancel") - suspend fun cancelQuest(@Path("gid") groupId: String): HabitResponse + suspend fun cancelQuest( + @Path("gid") groupId: String, + ): HabitResponse @POST("groups/{gid}/quests/force-start") - suspend fun forceStartQuest(@Path("gid") groupId: String, @Body group: Group): HabitResponse + suspend fun forceStartQuest( + @Path("gid") groupId: String, + @Body group: Group, + ): HabitResponse @POST("groups/{gid}/quests/invite/{questKey}") - suspend fun inviteToQuest(@Path("gid") groupId: String, @Path("questKey") questKey: String): HabitResponse + suspend fun inviteToQuest( + @Path("gid") groupId: String, + @Path("questKey") questKey: String, + ): HabitResponse @GET("groups/{gid}/invites") suspend fun getGroupInvites( @Path("gid") groupId: String, - @Query("includeAllPublicFields") includeAllPublicFields: Boolean? + @Query("includeAllPublicFields") includeAllPublicFields: Boolean?, ): HabitResponse> @POST("groups/{gid}/quests/abort") - suspend fun abortQuest(@Path("gid") groupId: String): HabitResponse + suspend fun abortQuest( + @Path("gid") groupId: String, + ): HabitResponse @POST("groups/{gid}/quests/leave") - suspend fun leaveQuest(@Path("gid") groupId: String): HabitResponse + suspend fun leaveQuest( + @Path("gid") groupId: String, + ): HabitResponse @POST("/iap/android/verify") - suspend fun validatePurchase(@Body request: PurchaseValidationRequest): HabitResponse + suspend fun validatePurchase( + @Body request: PurchaseValidationRequest, + ): HabitResponse @POST("/iap/android/subscribe") - suspend fun validateSubscription(@Body request: PurchaseValidationRequest): HabitResponse + suspend fun validateSubscription( + @Body request: PurchaseValidationRequest, + ): HabitResponse @GET("/iap/android/subscribe/cancel") suspend fun cancelSubscription(): HabitResponse @POST("/iap/android/norenew-subscribe") - suspend fun validateNoRenewSubscription(@Body request: PurchaseValidationRequest): HabitResponse + suspend fun validateNoRenewSubscription( + @Body request: PurchaseValidationRequest, + ): HabitResponse @POST("user/custom-day-start") - suspend fun changeCustomDayStart(@Body updateObject: Map): HabitResponse + suspend fun changeCustomDayStart( + @Body updateObject: Map, + ): HabitResponse // Members URL @GET("members/{mid}") - suspend fun getMember(@Path("mid") memberId: String): HabitResponse + suspend fun getMember( + @Path("mid") memberId: String, + ): HabitResponse @GET("members/username/{username}") - suspend fun getMemberWithUsername(@Path("username") username: String): HabitResponse + suspend fun getMemberWithUsername( + @Path("username") username: String, + ): HabitResponse @GET("members/{mid}/achievements") - suspend fun getMemberAchievements(@Path("mid") memberId: String, @Query("lang") language: String?): HabitResponse> + suspend fun getMemberAchievements( + @Path("mid") memberId: String, + @Query("lang") language: String?, + ): HabitResponse> @POST("members/send-private-message") - suspend fun postPrivateMessage(@Body messageDetails: Map): HabitResponse + suspend fun postPrivateMessage( + @Body messageDetails: Map, + ): HabitResponse @GET("members/find/{username}") suspend fun findUsernames( @Path("username") username: String, @Query("context") context: String?, - @Query("id") id: String? + @Query("id") id: String?, ): HabitResponse> @POST("members/flag-private-message/{mid}") - suspend fun flagInboxMessage(@Path("mid") mid: String, @Body data: Map): HabitResponse + suspend fun flagInboxMessage( + @Path("mid") mid: String, + @Body data: Map, + ): HabitResponse @GET("shops/{identifier}") - suspend fun retrieveShopInventory(@Path("identifier") identifier: String, @Query("lang") language: String?): HabitResponse + suspend fun retrieveShopInventory( + @Path("identifier") identifier: String, + @Query("lang") language: String?, + ): HabitResponse @GET("shops/market-gear") - suspend fun retrieveMarketGear(@Query("lang") language: String?): HabitResponse + suspend fun retrieveMarketGear( + @Query("lang") language: String?, + ): HabitResponse // Push notifications @POST("user/push-devices") - suspend fun addPushDevice(@Body pushDeviceData: Map): HabitResponse> + suspend fun addPushDevice( + @Body pushDeviceData: Map, + ): HabitResponse> @DELETE("user/push-devices/{regId}") - suspend fun deletePushDevice(@Path("regId") regId: String): HabitResponse> + suspend fun deletePushDevice( + @Path("regId") regId: String, + ): HabitResponse> - /* challenges api */ + // challenges api @GET("challenges/user") - suspend fun getUserChallenges(@Query("page") page: Int?, @Query("member") memberOnly: Boolean): HabitResponse> + suspend fun getUserChallenges( + @Query("page") page: Int?, + @Query("member") memberOnly: Boolean, + ): HabitResponse> @GET("challenges/user") - suspend fun getUserChallenges(@Query("page") page: Int?): HabitResponse> + suspend fun getUserChallenges( + @Query("page") page: Int?, + ): HabitResponse> @GET("tasks/challenge/{challengeId}") - suspend fun getChallengeTasks(@Path("challengeId") challengeId: String): HabitResponse + suspend fun getChallengeTasks( + @Path("challengeId") challengeId: String, + ): HabitResponse @GET("challenges/{challengeId}") - suspend fun getChallenge(@Path("challengeId") challengeId: String): HabitResponse + suspend fun getChallenge( + @Path("challengeId") challengeId: String, + ): HabitResponse @POST("challenges/{challengeId}/join") - suspend fun joinChallenge(@Path("challengeId") challengeId: String): HabitResponse + suspend fun joinChallenge( + @Path("challengeId") challengeId: String, + ): HabitResponse @POST("challenges/{challengeId}/leave") - suspend fun leaveChallenge(@Path("challengeId") challengeId: String, @Body body: LeaveChallengeBody): HabitResponse + suspend fun leaveChallenge( + @Path("challengeId") challengeId: String, + @Body body: LeaveChallengeBody, + ): HabitResponse @POST("challenges") - suspend fun createChallenge(@Body challenge: Challenge): HabitResponse + suspend fun createChallenge( + @Body challenge: Challenge, + ): HabitResponse @POST("tasks/challenge/{challengeId}") - suspend fun createChallengeTasks(@Path("challengeId") challengeId: String, @Body tasks: List): HabitResponse> + suspend fun createChallengeTasks( + @Path("challengeId") challengeId: String, + @Body tasks: List, + ): HabitResponse> @POST("tasks/challenge/{challengeId}") - suspend fun createChallengeTask(@Path("challengeId") challengeId: String, @Body task: Task): HabitResponse + suspend fun createChallengeTask( + @Path("challengeId") challengeId: String, + @Body task: Task, + ): HabitResponse @PUT("challenges/{challengeId}") - suspend fun updateChallenge(@Path("challengeId") challengeId: String, @Body challenge: Challenge): HabitResponse + suspend fun updateChallenge( + @Path("challengeId") challengeId: String, + @Body challenge: Challenge, + ): HabitResponse @DELETE("challenges/{challengeId}") - suspend fun deleteChallenge(@Path("challengeId") challengeId: String): HabitResponse + suspend fun deleteChallenge( + @Path("challengeId") challengeId: String, + ): HabitResponse // 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> + suspend fun readNotification( + @Path("notificationId") notificationId: String, + ): HabitResponse> @POST("notifications/read") - suspend fun readNotifications(@Body notificationIds: Map>): HabitResponse> + suspend fun readNotifications( + @Body notificationIds: Map>, + ): HabitResponse> @POST("notifications/see") - suspend fun seeNotifications(@Body notificationIds: Map>): HabitResponse> + suspend fun seeNotifications( + @Body notificationIds: Map>, + ): HabitResponse> @POST("user/open-mystery-item") suspend fun openMysteryItem(): HabitResponse @@ -418,43 +623,71 @@ interface ApiService { suspend fun runCron(): HabitResponse @POST("user/reset") - suspend fun resetAccount(@Body body: Map): HabitResponse + suspend fun resetAccount( + @Body body: Map, + ): HabitResponse @HTTP(method = "DELETE", path = "user", hasBody = true) - suspend fun deleteAccount(@Body body: Map): HabitResponse + suspend fun deleteAccount( + @Body body: Map, + ): HabitResponse @GET("user/toggle-pinned-item/{pinType}/{path}") - suspend fun togglePinnedItem(@Path("pinType") pinType: String, @Path("path") path: String): HabitResponse + suspend fun togglePinnedItem( + @Path("pinType") pinType: String, + @Path("path") path: String, + ): HabitResponse @POST("user/reset-password") - suspend fun sendPasswordResetEmail(@Body data: Map): HabitResponse + suspend fun sendPasswordResetEmail( + @Body data: Map, + ): HabitResponse @PUT("user/auth/update-username") - suspend fun updateLoginName(@Body data: Map): HabitResponse + suspend fun updateLoginName( + @Body data: Map, + ): HabitResponse @POST("user/auth/verify-username") - suspend fun verifyUsername(@Body data: Map): HabitResponse + suspend fun verifyUsername( + @Body data: Map, + ): HabitResponse @PUT("user/auth/update-email") - suspend fun updateEmail(@Body data: Map): HabitResponse + suspend fun updateEmail( + @Body data: Map, + ): HabitResponse @PUT("user/auth/update-password") - suspend fun updatePassword(@Body data: Map): HabitResponse + suspend fun updatePassword( + @Body data: Map, + ): HabitResponse @POST("user/allocate") - suspend fun allocatePoint(@Query("stat") stat: String): HabitResponse + suspend fun allocatePoint( + @Query("stat") stat: String, + ): HabitResponse @POST("user/allocate-bulk") - suspend fun bulkAllocatePoints(@Body stats: Map>): HabitResponse + suspend fun bulkAllocatePoints( + @Body stats: Map>, + ): HabitResponse @POST("members/transfer-gems") - suspend fun transferGems(@Body data: Map): HabitResponse + suspend fun transferGems( + @Body data: Map, + ): HabitResponse @POST("tasks/unlink-all/{challengeID}") - suspend fun unlinkAllTasks(@Path("challengeID") challengeID: String?, @Query("keep") keepOption: String): HabitResponse + suspend fun unlinkAllTasks( + @Path("challengeID") challengeID: String?, + @Query("keep") keepOption: String, + ): HabitResponse @POST("user/block/{userID}") - suspend fun blockMember(@Path("userID") userID: String): HabitResponse> + suspend fun blockMember( + @Path("userID") userID: String, + ): HabitResponse> @POST("user/reroll") suspend fun reroll(): HabitResponse @@ -465,26 +698,47 @@ interface ApiService { suspend fun getTeamPlans(): HabitResponse> @GET("tasks/group/{groupID}") - suspend fun getTeamPlanTasks(@Path("groupID") groupId: String): HabitResponse + suspend fun getTeamPlanTasks( + @Path("groupID") groupId: String, + ): HabitResponse @POST("tasks/{taskID}/assign") - suspend fun assignToTask(@Path("taskID") taskId: String?, @Body ids: List): HabitResponse + suspend fun assignToTask( + @Path("taskID") taskId: String?, + @Body ids: List, + ): HabitResponse @POST("tasks/{taskID}/unassign/{userID}") - suspend fun unassignFromTask(@Path("taskID") taskID: String, @Path("userID") userID: String): HabitResponse + suspend fun unassignFromTask( + @Path("taskID") taskID: String, + @Path("userID") userID: String, + ): HabitResponse @PUT("hall/heroes/{memberID}") - suspend fun updateUser(@Path("memberID") memberID: String, @Body updateData: Map>): HabitResponse + suspend fun updateUser( + @Path("memberID") memberID: String, + @Body updateData: Map>, + ): HabitResponse @GET("hall/heroes/{memberID}") - suspend fun getHallMember(@Path("memberID") memberID: String): HabitResponse + suspend fun getHallMember( + @Path("memberID") memberID: String, + ): HabitResponse @POST("tasks/{taskID}/needs-work/{userID}") - suspend fun markTaskNeedsWork(@Path("taskID") taskID: String, @Path("userID") userID: String): HabitResponse + suspend fun markTaskNeedsWork( + @Path("taskID") taskID: String, + @Path("userID") userID: String, + ): HabitResponse @GET("looking-for-party") - suspend fun retrievePartySeekingUsers(@Query("page") page: Int): HabitResponse> + suspend fun retrievePartySeekingUsers( + @Query("page") page: Int, + ): HabitResponse> @POST("challenges/{challengeId}/flag") - suspend fun reportChallenge(@Path("challengeId") challengeid: String, @Body updateData: Map): HabitResponse + suspend fun reportChallenge( + @Path("challengeId") challengeid: String, + @Body updateData: Map, + ): HabitResponse } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt index 567a1a9c1..202113eee 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt @@ -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? - - suspend fun getWorldState(): WorldState? - var languageCode: String? - suspend fun getContent(language: String? = null): ContentResult? - - suspend fun updateUser(updateDictionary: Map): User? - - suspend fun registrationLanguage(registrationLanguage: String): User? - - suspend fun retrieveInAppRewards(): List? - - 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>): BulkTaskScoringData? - - suspend fun postTaskNewPosition(id: String, position: Int): List? - - 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): List? - - 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? - - 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? - - suspend fun joinGroup(groupId: String): Group? - - suspend fun leaveGroup(groupId: String, keepChallenges: String): Void? - - suspend fun postGroupChat(groupId: String, message: Map): PostChatMessageResult? - - suspend fun deleteMessage(groupId: String, messageId: String): Void? - suspend fun deleteInboxMessage(id: String): Void? - - suspend fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?): List? - - suspend fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?, lastId: String): List? - - // Like returns the full chat list - suspend fun likeMessage(groupId: String, mid: String): ChatMessage? - - suspend fun flagMessage(groupId: String, mid: String, data: MutableMap): Void? - suspend fun flagInboxMessage(mid: String, data: MutableMap): Void? - - suspend fun reportMember(mid: String, data: Map): Void? - - suspend fun seenMessages(groupId: String): Void? - - suspend fun inviteToGroup(groupId: String, inviteData: Map): List? - - 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): Void? - - // Members URL - suspend fun getMember(memberId: String): Member? - suspend fun getMemberWithUsername(username: String): Member? - - suspend fun getMemberAchievements(memberId: String): List? - - suspend fun postPrivateMessage(messageDetails: Map): PostChatMessageResult? - - suspend fun retrieveShopIventory(identifier: String): Shop? - - // Push notifications - suspend fun addPushDevice(pushDeviceData: Map): List? - - suspend fun deletePushDevice(regId: String): List? - - 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): List? - 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? - - // Notifications - suspend fun readNotification(notificationId: String): List? - suspend fun readNotifications(notificationIds: Map>): List? - suspend fun seeNotifications(notificationIds: Map>): List? - - 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? - suspend fun retrieveInboxConversations(): List? - - 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? - - suspend fun transferGems(giftedID: String, amount: Int): Void? - suspend fun unlinkAllTasks(challengeID: String?, keepOption: String): Void? - suspend fun blockMember(userID: String): List? - suspend fun getTeamPlans(): List? - suspend fun getTeamPlanTasks(teamID: String): TaskList? - suspend fun assignToTask(taskId: String, ids: List): Task? - suspend fun unassignFromTask(taskId: String, userID: String): Task? - suspend fun updateMember(memberID: String, updateData: Map>): Member? - suspend fun getHallMember(userId: String): Member? - suspend fun markTaskNeedsWork(taskID: String, userID: String): Task? - suspend fun retrievePartySeekingUsers(page: Int): List? - suspend fun getGroupInvites(groupId: String, includeAllPublicFields: Boolean?): List? - suspend fun syncUserStats(): User? - suspend fun reportChallenge(challengeid: String, updateData: Map): 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? + + suspend fun getWorldState(): WorldState? + + var languageCode: String? + + suspend fun getContent(language: String? = null): ContentResult? + + suspend fun updateUser(updateDictionary: Map): User? + + suspend fun registrationLanguage(registrationLanguage: String): User? + + suspend fun retrieveInAppRewards(): List? + + 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>): BulkTaskScoringData? + + suspend fun postTaskNewPosition( + id: String, + position: Int, + ): List? + + 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): List? + + 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? + + 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? + + suspend fun joinGroup(groupId: String): Group? + + suspend fun leaveGroup( + groupId: String, + keepChallenges: String, + ): Void? + + suspend fun postGroupChat( + groupId: String, + message: Map, + ): PostChatMessageResult? + + suspend fun deleteMessage( + groupId: String, + messageId: String, + ): Void? + + suspend fun deleteInboxMessage(id: String): Void? + + suspend fun getGroupMembers( + groupId: String, + includeAllPublicFields: Boolean?, + ): List? + + suspend fun getGroupMembers( + groupId: String, + includeAllPublicFields: Boolean?, + lastId: String, + ): List? + + // Like returns the full chat list + suspend fun likeMessage( + groupId: String, + mid: String, + ): ChatMessage? + + suspend fun flagMessage( + groupId: String, + mid: String, + data: MutableMap, + ): Void? + + suspend fun flagInboxMessage( + mid: String, + data: MutableMap, + ): Void? + + suspend fun reportMember( + mid: String, + data: Map, + ): Void? + + suspend fun seenMessages(groupId: String): Void? + + suspend fun inviteToGroup( + groupId: String, + inviteData: Map, + ): List? + + 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): Void? + + // Members URL + suspend fun getMember(memberId: String): Member? + + suspend fun getMemberWithUsername(username: String): Member? + + suspend fun getMemberAchievements(memberId: String): List? + + suspend fun postPrivateMessage(messageDetails: Map): PostChatMessageResult? + + suspend fun retrieveShopIventory(identifier: String): Shop? + + // Push notifications + suspend fun addPushDevice(pushDeviceData: Map): List? + + suspend fun deletePushDevice(regId: String): List? + + 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, + ): List? + + 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? + + // Notifications + suspend fun readNotification(notificationId: String): List? + + suspend fun readNotifications(notificationIds: Map>): List? + + suspend fun seeNotifications(notificationIds: Map>): List? + + 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? + + suspend fun retrieveInboxConversations(): List? + + 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? + + suspend fun transferGems( + giftedID: String, + amount: Int, + ): Void? + + suspend fun unlinkAllTasks( + challengeID: String?, + keepOption: String, + ): Void? + + suspend fun blockMember(userID: String): List? + + suspend fun getTeamPlans(): List? + + suspend fun getTeamPlanTasks(teamID: String): TaskList? + + suspend fun assignToTask( + taskId: String, + ids: List, + ): Task? + + suspend fun unassignFromTask( + taskId: String, + userID: String, + ): Task? + + suspend fun updateMember( + memberID: String, + updateData: Map>, + ): Member? + + suspend fun getHallMember(userId: String): Member? + + suspend fun markTaskNeedsWork( + taskID: String, + userID: String, + ): Task? + + suspend fun retrievePartySeekingUsers(page: Int): List? + + suspend fun getGroupInvites( + groupId: String, + includeAllPublicFields: Boolean?, + ): List? + + suspend fun syncUserStats(): User? + + suspend fun reportChallenge( + challengeid: String, + updateData: Map, + ): Void? +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/UserRepositoryImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/UserRepositoryImpl.kt index dab1acfd0..1c47a1e88 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/UserRepositoryImpl.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/UserRepositoryImpl.kt @@ -1,493 +1,535 @@ -package com.habitrpg.android.habitica.data.implementation - -import com.habitrpg.android.habitica.data.ApiClient -import com.habitrpg.android.habitica.data.TaskRepository -import com.habitrpg.android.habitica.data.UserRepository -import com.habitrpg.android.habitica.data.local.UserLocalRepository -import com.habitrpg.android.habitica.helpers.AppConfigManager -import com.habitrpg.android.habitica.models.Achievement -import com.habitrpg.android.habitica.models.QuestAchievement -import com.habitrpg.android.habitica.models.TeamPlan -import com.habitrpg.android.habitica.models.inventory.Customization -import com.habitrpg.android.habitica.models.inventory.Equipment -import com.habitrpg.android.habitica.models.responses.SkillResponse -import com.habitrpg.android.habitica.models.responses.UnlockResponse -import com.habitrpg.android.habitica.models.social.Group -import com.habitrpg.android.habitica.models.social.GroupMembership -import com.habitrpg.android.habitica.models.tasks.Task -import com.habitrpg.android.habitica.models.user.Stats -import com.habitrpg.android.habitica.models.user.User -import com.habitrpg.android.habitica.models.user.UserQuestStatus -import com.habitrpg.android.habitica.modules.AuthenticationHandler -import com.habitrpg.common.habitica.models.Notification -import com.habitrpg.common.habitica.models.notifications.NewStuffData -import com.habitrpg.shared.habitica.models.responses.TaskDirection -import com.habitrpg.shared.habitica.models.tasks.Attribute -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import java.util.Date -import java.util.GregorianCalendar -import java.util.concurrent.TimeUnit - -@OptIn(ExperimentalCoroutinesApi::class) -class UserRepositoryImpl( - localRepository: UserLocalRepository, - apiClient: ApiClient, - authenticationHandler: AuthenticationHandler, - private val taskRepository: TaskRepository, - private val appConfigManager: AppConfigManager, -) : BaseRepositoryImpl(localRepository, apiClient, authenticationHandler), UserRepository { - - companion object { - private var lastReadNotification: String? = null - private var lastSync: Date? = null - } - - override fun getUser(): Flow = authenticationHandler.userIDFlow.flatMapLatest { getUser(it) } - override fun getUser(userID: String): Flow = localRepository.getUser(userID) - - override suspend fun syncUserStats(): User? { - val user = apiClient.syncUserStats() - if (user != null && (user.stats?.toNextLevel ?: 0) > 1 - && (user.stats?.maxMP ?: 0) > 1) { - localRepository.saveUser(user) - } else { - retrieveUser(false, true) - } - return user - } - - private suspend fun updateUser(userID: String, updateData: Map): User? { - val networkUser = apiClient.updateUser(updateData) ?: return null - val oldUser = localRepository.getUser(userID).firstOrNull() - return mergeUser(oldUser, networkUser) - } - - private suspend fun updateUser(userID: String, key: String, value: Any?): User? { - return updateUser(userID, mapOf(key to value)) - } - - override suspend fun updateUser(updateData: Map): User? { - return updateUser(currentUserID, updateData) - } - - override suspend fun updateUser(key: String, value: Any?): User? { - return updateUser(currentUserID, key, value) - } - - @Suppress("ReturnCount") - override suspend fun retrieveUser(withTasks: Boolean, forced: Boolean, overrideExisting: Boolean): User? { - // Only retrieve again after 3 minutes or it's forced. - if (forced || lastSync == null || Date().time - (lastSync?.time ?: 0) > 180000) { - val user = apiClient.retrieveUser(withTasks) ?: return null - lastSync = Date() - withContext(Dispatchers.Main) { - localRepository.saveUser(user) - } - if (withTasks) { - val id = user.id - val tasksOrder = user.tasksOrder - val tasks = user.tasks - if (id != null && tasksOrder != null && tasks != null) { - taskRepository.saveTasks(id, tasksOrder, tasks) - } - } - val calendar = GregorianCalendar() - val timeZone = calendar.timeZone - val offset = -TimeUnit.MINUTES.convert(timeZone.getOffset(calendar.timeInMillis).toLong(), TimeUnit.MILLISECONDS) - return if (offset.toInt() != (user.preferences?.timezoneOffset ?: 0)) { - updateUser(user.id ?: "", "preferences.timezoneOffset", offset.toString()) - } else { - user - } - } else { - return null - } - } - - override suspend fun revive(): Equipment? { - val items = apiClient.revive() - val currentUser = localRepository.getLiveUser(currentUserID) - var brokenItem: Equipment? = null - if (items != null && currentUser != null) { - brokenItem = items.gear?.owned?.filter { it.owned == false }?.firstOrNull { equipment -> - currentUser.items?.gear?.owned?.firstOrNull { it.key == equipment.key && it.owned == true } != null - } - } - retrieveUser(false, true) - return brokenItem - } - - override suspend fun resetTutorial(): User? { - val tutorialSteps = localRepository.getTutorialSteps().firstOrNull() ?: return null - val updateData = HashMap() - for (step in tutorialSteps) { - updateData[step.flagPath] = false - } - return updateUser(updateData) - } - - override suspend fun sleep(user: User): User { - val newValue = !(user.preferences?.sleep ?: false) - localRepository.modify(user) { it.preferences?.sleep = newValue } - if (apiClient.sleep() == null) { - localRepository.modify(user) { it.preferences?.sleep = !newValue } - } - return user - } - - override fun getSkills(user: User) = localRepository.getSkills(user) - - override fun getSpecialItems(user: User) = localRepository.getSpecialItems(user) - - override suspend fun useSkill(key: String, target: String?, taskId: String): SkillResponse? { - val response = apiClient.useSkill(key, target ?: "", taskId) ?: return null - val user = getLiveUser() ?: return response - response.hpDiff = (response.user?.stats?.hp ?: 0.0) - (user.stats?.hp ?: 0.0) - response.expDiff = (response.user?.stats?.exp ?: 0.0) - (user.stats?.exp ?: 0.0) - response.goldDiff = (response.user?.stats?.gp ?: 0.0) - (user.stats?.gp ?: 0.0) - response.damage = (response.user?.party?.quest?.progress?.up ?: 0.0f) - (user.party?.quest?.progress?.up ?: 0.0f) - response.user?.let { mergeUser(user, it) } - return response - } - - override suspend fun useSkill(key: String, target: String?): SkillResponse? { - val response = apiClient.useSkill(key, target ?: "") ?: return null - val user = getLiveUser() ?: return response - response.hpDiff = (response.user?.stats?.hp ?: 0.0) - (user.stats?.hp ?: 0.0) - response.expDiff = (response.user?.stats?.exp ?: 0.0) - (user.stats?.exp ?: 0.0) - response.goldDiff = (response.user?.stats?.gp ?: 0.0) - (user.stats?.gp ?: 0.0) - response.damage = (response.user?.party?.quest?.progress?.up ?: 0.0f) - (user.party?.quest?.progress?.up ?: 0.0f) - response.user?.let { mergeUser(user, it) } - return response - } - - override suspend fun disableClasses(): User? = apiClient.disableClasses() - - override suspend fun changeClass(selectedClass: String?): User? { - apiClient.changeClass(selectedClass) - return retrieveUser(false, forced = true) - } - - override suspend fun unlockPath(customization: Customization): UnlockResponse? { - return unlockPath(customization.path, customization.price ?: 0) - } - - override suspend fun unlockPath(path: String, price: Int): UnlockResponse? { - val unlockResponse = apiClient.unlockPath(path) ?: return null - val user = localRepository.getUser(currentUserID).firstOrNull() ?: return unlockResponse - localRepository.modify(user) { liveUser -> - unlockResponse.preferences?.let { liveUser.preferences = it } - liveUser.purchased = unlockResponse.purchased - liveUser.items = unlockResponse.items - liveUser.balance = liveUser.balance - (price / 4.0) - } - return unlockResponse - } - - override suspend fun runCron() { - runCron(ArrayList()) - } - - override suspend fun getNews(): List? { - return apiClient.getNews() - } - - override suspend fun getNewsNotification(): Notification { - val baileyNews = apiClient.getNews() - val baileyAnnouncement = (baileyNews?.first() as? Map<*, *>)?.get("title") as? String - val notification = Notification() - notification.id = "custom-new-stuff-notification" - notification.type = Notification.Type.NEW_STUFF.type - val data = NewStuffData() - data.title = baileyAnnouncement - notification.data = data - return notification - } - - override suspend fun readNotification(id: String): List? { - if (lastReadNotification == id) return null - lastReadNotification = id - return apiClient.readNotification(id) - } - override fun getUserQuestStatus(): Flow { - return localRepository.getUserQuestStatus(currentUserID) - } - - override suspend fun reroll(): User? { - return apiClient.reroll() - } - - override suspend fun readNotifications(notificationIds: Map>) = apiClient.readNotifications(notificationIds) - - override suspend fun seeNotifications(notificationIds: Map>) = apiClient.seeNotifications(notificationIds) - - override suspend fun changeCustomDayStart(dayStartTime: Int): User? { - val updateObject = HashMap() - updateObject["dayStart"] = dayStartTime - apiClient.changeCustomDayStart(updateObject) - val liveUser = getLiveUser() - if (liveUser != null) { - localRepository.executeTransaction { - liveUser.preferences?.dayStart = dayStartTime - } - } - return liveUser - } - - override suspend fun updateLanguage(languageCode: String): User? { - val user = updateUser("preferences.language", languageCode) - apiClient.languageCode = languageCode - return mergeWithExistingUser(user) - } - - override suspend fun resetAccount(password: String): User? { - apiClient.resetAccount(password) - return retrieveUser(withTasks = true, forced = true) - } - - override suspend fun deleteAccount(password: String) = apiClient.deleteAccount(password) - - override suspend fun sendPasswordResetEmail(email: String) = apiClient.sendPasswordResetEmail(email) - - override suspend fun updateLoginName(newLoginName: String, password: String?): User? { - if (!password.isNullOrEmpty()) { - apiClient.updateLoginName(newLoginName.trim(), password.trim()) - } else { - apiClient.updateUsername(newLoginName.trim()) - } - val user = localRepository.getUser(currentUserID).firstOrNull() ?: return null - localRepository.modify(user) { liveUser -> - liveUser.authentication?.localAuthentication?.username = newLoginName - liveUser.flags?.verifiedUsername = true - } - return user - } - - override suspend fun verifyUsername(username: String) = apiClient.verifyUsername(username.trim()) - - override suspend fun updateEmail(newEmail: String, password: String) = apiClient.updateEmail(newEmail.trim(), password) - - override suspend fun updatePassword( - oldPassword: String, - newPassword: String, - newPasswordConfirmation: String - ) = apiClient.updatePassword(oldPassword.trim(), newPassword.trim(), newPasswordConfirmation.trim()) - - override suspend fun allocatePoint(stat: Attribute): Stats? { - val liveUser = getLiveUser() - if (liveUser != null) { - localRepository.executeTransaction { - when (stat) { - Attribute.STRENGTH -> liveUser.stats?.strength = liveUser.stats?.strength?.inc() - Attribute.INTELLIGENCE -> liveUser.stats?.intelligence = liveUser.stats?.intelligence?.inc() - Attribute.CONSTITUTION -> liveUser.stats?.constitution = liveUser.stats?.constitution?.inc() - Attribute.PERCEPTION -> liveUser.stats?.per = liveUser.stats?.per?.inc() - } - liveUser.stats?.points = liveUser.stats?.points?.dec() - } - } - val stats = apiClient.allocatePoint(stat.value) ?: return null - if (liveUser != null) { - localRepository.executeTransaction { - liveUser.stats?.strength = stats.strength - liveUser.stats?.constitution = stats.constitution - liveUser.stats?.per = stats.per - liveUser.stats?.intelligence = stats.intelligence - liveUser.stats?.points = stats.points - liveUser.stats?.mp = stats.mp - } - } - return stats - } - - override suspend fun bulkAllocatePoints( - strength: Int, - intelligence: Int, - constitution: Int, - perception: Int - ): Stats? { - val stats = apiClient.bulkAllocatePoints( - strength, - intelligence, - constitution, - perception - ) ?: return null - val user = getLiveUser() - if (user != null) { - localRepository.modify(user) { liveUser -> - liveUser.stats?.strength = stats.strength - liveUser.stats?.constitution = stats.constitution - liveUser.stats?.per = stats.per - liveUser.stats?.intelligence = stats.intelligence - liveUser.stats?.points = stats.points - liveUser.stats?.mp = stats.mp - } - } - return stats - } - - override suspend fun runCron(tasks: MutableList) { - val user = getLiveUser() - if (user != null) { - localRepository.modify(user) { liveUser -> - liveUser.needsCron = false - liveUser.lastCron = Date() - } - } - if (tasks.isNotEmpty()) { - val scoringList = mutableListOf>() - for (task in tasks) { - val map = mutableMapOf() - map["id"] = task.id ?: "" - map["direction"] = TaskDirection.UP.text - scoringList.add(map) - } - taskRepository.bulkScoreTasks(scoringList) - } - apiClient.runCron() - retrieveUser(true, true) - } - - override suspend fun useCustomization(type: String, category: String?, identifier: String): User? { - if (appConfigManager.enableLocalChanges()) { - val liveUser = getLiveUser() - if (liveUser != null) { - localRepository.modify(liveUser) { user -> - when (type) { - "skin" -> user.preferences?.skin = identifier - "shirt" -> user.preferences?.shirt = identifier - "hair" -> { - when (category) { - "color" -> user.preferences?.hair?.color = identifier - "flower" -> user.preferences?.hair?.flower = identifier.toInt() - "mustache" -> user.preferences?.hair?.mustache = identifier.toInt() - "beard" -> user.preferences?.hair?.beard = identifier.toInt() - "bangs" -> user.preferences?.hair?.bangs = identifier.toInt() - "base" -> user.preferences?.hair?.base = identifier.toInt() - } - } - "background" -> user.preferences?.background = identifier - "chair" -> user.preferences?.chair = identifier - } - } - } - } - return if (type == "background") { - apiClient.unlockPath("background.$identifier") - retrieveUser(false, true) - } else { - var updatePath = "preferences.$type" - if (category != null) { - updatePath = "$updatePath.$category" - } - updateUser(updatePath, identifier) - } - } - - override suspend fun retrieveAchievements(): List? { - val achievements = apiClient.getMemberAchievements(currentUserID) ?: return null - localRepository.save(achievements) - return achievements - } - - override fun getAchievements(): Flow> { - return localRepository.getAchievements() - } - - override fun getQuestAchievements(): Flow> { - return localRepository.getQuestAchievements(currentUserID) - } - - override suspend fun retrieveTeamPlans(): List? { - val teams = apiClient.getTeamPlans() ?: return null - teams.forEach { it.userID = currentUserID } - localRepository.save(teams) - return teams - } - - override fun getTeamPlans(): Flow> { - return localRepository.getTeamPlans(currentUserID) - } - - override suspend fun retrieveTeamPlan(teamID: String): Group? { - val team = apiClient.getGroup(teamID) ?: return null - val tasks = apiClient.getTeamPlanTasks(teamID) - localRepository.save(team) - val id = team.id - val tasksOrder = team.tasksOrder - if (id.isNotBlank() && tasksOrder != null && tasks != null) { - taskRepository.saveTasks(id, tasksOrder, tasks) - } - val members = apiClient.getGroupMembers(teamID, true) ?: return team - localRepository.save( - members.map { - GroupMembership(it.id, id) - } - ) - members.let { localRepository.save(members) } - return team - } - - override fun getTeamPlan(teamID: String): Flow { - return localRepository.getTeamPlan(teamID) - .map { - it ?: retrieveTeamPlan(teamID) - } - } - - private suspend fun getLiveUser(): User? { - val user = localRepository.getUser(currentUserID).firstOrNull() ?: return null - return localRepository.getLiveObject(user) - } - - private suspend fun mergeWithExistingUser(newUser: User?): User? { - val oldUser = localRepository.getUser(currentUserID).firstOrNull() - if (newUser == null) { - return oldUser - } - return mergeUser(oldUser, newUser) - } - - private fun mergeUser(oldUser: User?, newUser: User): User { - if (oldUser == null || !oldUser.isValid) { - return oldUser ?: newUser - } - val copiedUser: User = if (oldUser.isManaged) { - localRepository.getUnmanagedCopy(oldUser) - } else { - oldUser - } - if (newUser.inbox != null) { - copiedUser.inbox = newUser.inbox - } - if (newUser.items != null) { - copiedUser.items = newUser.items - } - if (newUser.preferences != null) { - copiedUser.preferences = newUser.preferences - } - if (newUser.flags != null) { - copiedUser.flags = newUser.flags - } - if (newUser.stats != null) { - copiedUser.stats?.merge(newUser.stats) - } - if (newUser.profile != null) { - copiedUser.profile = newUser.profile - } - if (newUser.party != null) { - copiedUser.party = newUser.party - } - copiedUser.needsCron = newUser.needsCron - copiedUser.versionNumber = newUser.versionNumber - - localRepository.saveUser(copiedUser, false) - return copiedUser - } -} +package com.habitrpg.android.habitica.data.implementation + +import com.habitrpg.android.habitica.data.ApiClient +import com.habitrpg.android.habitica.data.TaskRepository +import com.habitrpg.android.habitica.data.UserRepository +import com.habitrpg.android.habitica.data.local.UserLocalRepository +import com.habitrpg.android.habitica.helpers.AppConfigManager +import com.habitrpg.android.habitica.models.Achievement +import com.habitrpg.android.habitica.models.QuestAchievement +import com.habitrpg.android.habitica.models.TeamPlan +import com.habitrpg.android.habitica.models.inventory.Customization +import com.habitrpg.android.habitica.models.inventory.Equipment +import com.habitrpg.android.habitica.models.responses.SkillResponse +import com.habitrpg.android.habitica.models.responses.UnlockResponse +import com.habitrpg.android.habitica.models.social.Group +import com.habitrpg.android.habitica.models.social.GroupMembership +import com.habitrpg.android.habitica.models.tasks.Task +import com.habitrpg.android.habitica.models.user.Stats +import com.habitrpg.android.habitica.models.user.User +import com.habitrpg.android.habitica.models.user.UserQuestStatus +import com.habitrpg.android.habitica.modules.AuthenticationHandler +import com.habitrpg.common.habitica.models.Notification +import com.habitrpg.common.habitica.models.notifications.NewStuffData +import com.habitrpg.shared.habitica.models.responses.TaskDirection +import com.habitrpg.shared.habitica.models.tasks.Attribute +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import java.util.Date +import java.util.GregorianCalendar +import java.util.concurrent.TimeUnit + +@OptIn(ExperimentalCoroutinesApi::class) +class UserRepositoryImpl( + localRepository: UserLocalRepository, + apiClient: ApiClient, + authenticationHandler: AuthenticationHandler, + private val taskRepository: TaskRepository, + private val appConfigManager: AppConfigManager, +) : BaseRepositoryImpl(localRepository, apiClient, authenticationHandler), UserRepository { + companion object { + private var lastReadNotification: String? = null + private var lastSync: Date? = null + } + + override fun getUser(): Flow = authenticationHandler.userIDFlow.flatMapLatest { getUser(it) } + + override fun getUser(userID: String): Flow = localRepository.getUser(userID) + + override suspend fun syncUserStats(): User? { + val user = apiClient.syncUserStats() + if (user != null && (user.stats?.toNextLevel ?: 0) > 1 && + (user.stats?.maxMP ?: 0) > 1 + ) { + localRepository.saveUser(user) + } else { + retrieveUser(false, true) + } + return user + } + + private suspend fun updateUser( + userID: String, + updateData: Map, + ): User? { + val networkUser = apiClient.updateUser(updateData) ?: return null + val oldUser = localRepository.getUser(userID).firstOrNull() + return mergeUser(oldUser, networkUser) + } + + private suspend fun updateUser( + userID: String, + key: String, + value: Any?, + ): User? { + return updateUser(userID, mapOf(key to value)) + } + + override suspend fun updateUser(updateData: Map): User? { + return updateUser(currentUserID, updateData) + } + + override suspend fun updateUser( + key: String, + value: Any?, + ): User? { + return updateUser(currentUserID, key, value) + } + + @Suppress("ReturnCount") + override suspend fun retrieveUser( + withTasks: Boolean, + forced: Boolean, + overrideExisting: Boolean, + ): User? { + // Only retrieve again after 3 minutes or it's forced. + if (forced || lastSync == null || Date().time - (lastSync?.time ?: 0) > 180000) { + val user = apiClient.retrieveUser(withTasks) ?: return null + lastSync = Date() + withContext(Dispatchers.Main) { + localRepository.saveUser(user) + } + if (withTasks) { + val id = user.id + val tasksOrder = user.tasksOrder + val tasks = user.tasks + if (id != null && tasksOrder != null && tasks != null) { + taskRepository.saveTasks(id, tasksOrder, tasks) + } + } + val calendar = GregorianCalendar() + val timeZone = calendar.timeZone + val offset = -TimeUnit.MINUTES.convert(timeZone.getOffset(calendar.timeInMillis).toLong(), TimeUnit.MILLISECONDS) + return if (offset.toInt() != (user.preferences?.timezoneOffset ?: 0)) { + updateUser(user.id ?: "", "preferences.timezoneOffset", offset.toString()) + } else { + user + } + } else { + return null + } + } + + override suspend fun revive(): Equipment? { + val items = apiClient.revive() + val currentUser = localRepository.getLiveUser(currentUserID) + var brokenItem: Equipment? = null + if (items != null && currentUser != null) { + brokenItem = + items.gear?.owned?.filter { it.owned == false }?.firstOrNull { equipment -> + currentUser.items?.gear?.owned?.firstOrNull { it.key == equipment.key && it.owned == true } != null + } + } + retrieveUser(false, true) + return brokenItem + } + + override suspend fun resetTutorial(): User? { + val tutorialSteps = localRepository.getTutorialSteps().firstOrNull() ?: return null + val updateData = HashMap() + for (step in tutorialSteps) { + updateData[step.flagPath] = false + } + return updateUser(updateData) + } + + override suspend fun sleep(user: User): User { + val newValue = !(user.preferences?.sleep ?: false) + localRepository.modify(user) { it.preferences?.sleep = newValue } + if (apiClient.sleep() == null) { + localRepository.modify(user) { it.preferences?.sleep = !newValue } + } + return user + } + + override fun getSkills(user: User) = localRepository.getSkills(user) + + override fun getSpecialItems(user: User) = localRepository.getSpecialItems(user) + + override suspend fun useSkill( + key: String, + target: String?, + taskId: String, + ): SkillResponse? { + val response = apiClient.useSkill(key, target ?: "", taskId) ?: return null + val user = getLiveUser() ?: return response + response.hpDiff = (response.user?.stats?.hp ?: 0.0) - (user.stats?.hp ?: 0.0) + response.expDiff = (response.user?.stats?.exp ?: 0.0) - (user.stats?.exp ?: 0.0) + response.goldDiff = (response.user?.stats?.gp ?: 0.0) - (user.stats?.gp ?: 0.0) + response.damage = (response.user?.party?.quest?.progress?.up ?: 0.0f) - (user.party?.quest?.progress?.up ?: 0.0f) + response.user?.let { mergeUser(user, it) } + return response + } + + override suspend fun useSkill( + key: String, + target: String?, + ): SkillResponse? { + val response = apiClient.useSkill(key, target ?: "") ?: return null + val user = getLiveUser() ?: return response + response.hpDiff = (response.user?.stats?.hp ?: 0.0) - (user.stats?.hp ?: 0.0) + response.expDiff = (response.user?.stats?.exp ?: 0.0) - (user.stats?.exp ?: 0.0) + response.goldDiff = (response.user?.stats?.gp ?: 0.0) - (user.stats?.gp ?: 0.0) + response.damage = (response.user?.party?.quest?.progress?.up ?: 0.0f) - (user.party?.quest?.progress?.up ?: 0.0f) + response.user?.let { mergeUser(user, it) } + return response + } + + override suspend fun disableClasses(): User? = apiClient.disableClasses() + + override suspend fun changeClass(selectedClass: String?): User? { + apiClient.changeClass(selectedClass) + return retrieveUser(false, forced = true) + } + + override suspend fun unlockPath(customization: Customization): UnlockResponse? { + return unlockPath(customization.path, customization.price ?: 0) + } + + override suspend fun unlockPath( + path: String, + price: Int, + ): UnlockResponse? { + val unlockResponse = apiClient.unlockPath(path) ?: return null + val user = localRepository.getUser(currentUserID).firstOrNull() ?: return unlockResponse + localRepository.modify(user) { liveUser -> + unlockResponse.preferences?.let { liveUser.preferences = it } + liveUser.purchased = unlockResponse.purchased + liveUser.items = unlockResponse.items + liveUser.balance = liveUser.balance - (price / 4.0) + } + return unlockResponse + } + + override suspend fun runCron() { + runCron(ArrayList()) + } + + override suspend fun getNews(): List? { + return apiClient.getNews() + } + + override suspend fun getNewsNotification(): Notification { + val baileyNews = apiClient.getNews() + val baileyAnnouncement = (baileyNews?.first() as? Map<*, *>)?.get("title") as? String + val notification = Notification() + notification.id = "custom-new-stuff-notification" + notification.type = Notification.Type.NEW_STUFF.type + val data = NewStuffData() + data.title = baileyAnnouncement + notification.data = data + return notification + } + + override suspend fun readNotification(id: String): List? { + if (lastReadNotification == id) return null + lastReadNotification = id + return apiClient.readNotification(id) + } + + override fun getUserQuestStatus(): Flow { + return localRepository.getUserQuestStatus(currentUserID) + } + + override suspend fun reroll(): User? { + return apiClient.reroll() + } + + override suspend fun readNotifications(notificationIds: Map>) = apiClient.readNotifications(notificationIds) + + override suspend fun seeNotifications(notificationIds: Map>) = apiClient.seeNotifications(notificationIds) + + override suspend fun changeCustomDayStart(dayStartTime: Int): User? { + val updateObject = HashMap() + updateObject["dayStart"] = dayStartTime + apiClient.changeCustomDayStart(updateObject) + val liveUser = getLiveUser() + if (liveUser != null) { + localRepository.executeTransaction { + liveUser.preferences?.dayStart = dayStartTime + } + } + return liveUser + } + + override suspend fun updateLanguage(languageCode: String): User? { + val user = updateUser("preferences.language", languageCode) + apiClient.languageCode = languageCode + return mergeWithExistingUser(user) + } + + override suspend fun resetAccount(password: String): User? { + apiClient.resetAccount(password) + return retrieveUser(withTasks = true, forced = true) + } + + override suspend fun deleteAccount(password: String) = apiClient.deleteAccount(password) + + override suspend fun sendPasswordResetEmail(email: String) = apiClient.sendPasswordResetEmail(email) + + override suspend fun updateLoginName( + newLoginName: String, + password: String?, + ): User? { + if (!password.isNullOrEmpty()) { + apiClient.updateLoginName(newLoginName.trim(), password.trim()) + } else { + apiClient.updateUsername(newLoginName.trim()) + } + val user = localRepository.getUser(currentUserID).firstOrNull() ?: return null + localRepository.modify(user) { liveUser -> + liveUser.authentication?.localAuthentication?.username = newLoginName + liveUser.flags?.verifiedUsername = true + } + return user + } + + override suspend fun verifyUsername(username: String) = apiClient.verifyUsername(username.trim()) + + override suspend fun updateEmail( + newEmail: String, + password: String, + ) = apiClient.updateEmail(newEmail.trim(), password) + + override suspend fun updatePassword( + oldPassword: String, + newPassword: String, + newPasswordConfirmation: String, + ) = apiClient.updatePassword(oldPassword.trim(), newPassword.trim(), newPasswordConfirmation.trim()) + + override suspend fun allocatePoint(stat: Attribute): Stats? { + val liveUser = getLiveUser() + if (liveUser != null) { + localRepository.executeTransaction { + when (stat) { + Attribute.STRENGTH -> liveUser.stats?.strength = liveUser.stats?.strength?.inc() + Attribute.INTELLIGENCE -> liveUser.stats?.intelligence = liveUser.stats?.intelligence?.inc() + Attribute.CONSTITUTION -> liveUser.stats?.constitution = liveUser.stats?.constitution?.inc() + Attribute.PERCEPTION -> liveUser.stats?.per = liveUser.stats?.per?.inc() + } + liveUser.stats?.points = liveUser.stats?.points?.dec() + } + } + val stats = apiClient.allocatePoint(stat.value) ?: return null + if (liveUser != null) { + localRepository.executeTransaction { + liveUser.stats?.strength = stats.strength + liveUser.stats?.constitution = stats.constitution + liveUser.stats?.per = stats.per + liveUser.stats?.intelligence = stats.intelligence + liveUser.stats?.points = stats.points + liveUser.stats?.mp = stats.mp + } + } + return stats + } + + override suspend fun bulkAllocatePoints( + strength: Int, + intelligence: Int, + constitution: Int, + perception: Int, + ): Stats? { + val stats = + apiClient.bulkAllocatePoints( + strength, + intelligence, + constitution, + perception, + ) ?: return null + val user = getLiveUser() + if (user != null) { + localRepository.modify(user) { liveUser -> + liveUser.stats?.strength = stats.strength + liveUser.stats?.constitution = stats.constitution + liveUser.stats?.per = stats.per + liveUser.stats?.intelligence = stats.intelligence + liveUser.stats?.points = stats.points + liveUser.stats?.mp = stats.mp + } + } + return stats + } + + override suspend fun runCron(tasks: MutableList) { + val user = getLiveUser() + if (user != null) { + localRepository.modify(user) { liveUser -> + liveUser.needsCron = false + liveUser.lastCron = Date() + } + } + if (tasks.isNotEmpty()) { + val scoringList = mutableListOf>() + for (task in tasks) { + val map = mutableMapOf() + map["id"] = task.id ?: "" + map["direction"] = TaskDirection.UP.text + scoringList.add(map) + } + taskRepository.bulkScoreTasks(scoringList) + } + apiClient.runCron() + retrieveUser(true, true) + } + + override suspend fun useCustomization( + type: String, + category: String?, + identifier: String, + ): User? { + if (appConfigManager.enableLocalChanges()) { + val liveUser = getLiveUser() + if (liveUser != null) { + localRepository.modify(liveUser) { user -> + when (type) { + "skin" -> user.preferences?.skin = identifier + "shirt" -> user.preferences?.shirt = identifier + "hair" -> { + when (category) { + "color" -> user.preferences?.hair?.color = identifier + "flower" -> user.preferences?.hair?.flower = identifier.toInt() + "mustache" -> user.preferences?.hair?.mustache = identifier.toInt() + "beard" -> user.preferences?.hair?.beard = identifier.toInt() + "bangs" -> user.preferences?.hair?.bangs = identifier.toInt() + "base" -> user.preferences?.hair?.base = identifier.toInt() + } + } + "background" -> user.preferences?.background = identifier + "chair" -> user.preferences?.chair = identifier + } + } + } + } + return if (type == "background") { + apiClient.unlockPath("background.$identifier") + retrieveUser(false, true) + } else { + var updatePath = "preferences.$type" + if (category != null) { + updatePath = "$updatePath.$category" + } + updateUser(updatePath, identifier) + } + } + + override suspend fun retrieveAchievements(): List? { + val achievements = apiClient.getMemberAchievements(currentUserID) ?: return null + localRepository.save(achievements) + return achievements + } + + override fun getAchievements(): Flow> { + return localRepository.getAchievements() + } + + override fun getQuestAchievements(): Flow> { + return localRepository.getQuestAchievements(currentUserID) + } + + override suspend fun retrieveTeamPlans(): List? { + val teams = apiClient.getTeamPlans() ?: return null + teams.forEach { it.userID = currentUserID } + localRepository.save(teams) + return teams + } + + override fun getTeamPlans(): Flow> { + return localRepository.getTeamPlans(currentUserID) + } + + override suspend fun retrieveTeamPlan(teamID: String): Group? { + val team = apiClient.getGroup(teamID) ?: return null + val tasks = apiClient.getTeamPlanTasks(teamID) + localRepository.save(team) + val id = team.id + val tasksOrder = team.tasksOrder + if (id.isNotBlank() && tasksOrder != null && tasks != null) { + taskRepository.saveTasks(id, tasksOrder, tasks) + } + val members = apiClient.getGroupMembers(teamID, true) ?: return team + localRepository.save( + members.map { + GroupMembership(it.id, id) + }, + ) + members.let { localRepository.save(members) } + return team + } + + override fun getTeamPlan(teamID: String): Flow { + return localRepository.getTeamPlan(teamID) + .map { + it ?: retrieveTeamPlan(teamID) + } + } + + private suspend fun getLiveUser(): User? { + val user = localRepository.getUser(currentUserID).firstOrNull() ?: return null + return localRepository.getLiveObject(user) + } + + private suspend fun mergeWithExistingUser(newUser: User?): User? { + val oldUser = localRepository.getUser(currentUserID).firstOrNull() + if (newUser == null) { + return oldUser + } + return mergeUser(oldUser, newUser) + } + + private fun mergeUser( + oldUser: User?, + newUser: User, + ): User { + if (oldUser == null || !oldUser.isValid) { + return oldUser ?: newUser + } + val copiedUser: User = + if (oldUser.isManaged) { + localRepository.getUnmanagedCopy(oldUser) + } else { + oldUser + } + if (newUser.inbox != null) { + copiedUser.inbox = newUser.inbox + } + if (newUser.items != null) { + copiedUser.items = newUser.items + } + if (newUser.preferences != null) { + copiedUser.preferences = newUser.preferences + } + if (newUser.flags != null) { + copiedUser.flags = newUser.flags + } + if (newUser.stats != null) { + copiedUser.stats?.merge(newUser.stats) + } + if (newUser.profile != null) { + copiedUser.profile = newUser.profile + } + if (newUser.party != null) { + copiedUser.party = newUser.party + } + copiedUser.needsCron = newUser.needsCron + copiedUser.versionNumber = newUser.versionNumber + + localRepository.saveUser(copiedUser, false) + return copiedUser + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/shops/Shop.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/shops/Shop.kt index be4f8ec1c..57108013e 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/shops/Shop.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/shops/Shop.kt @@ -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 = 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 = 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" + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/shops/ShopItem.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/shops/ShopItem.kt index 4d753340e..e3d6f699d 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/shops/ShopItem.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/shops/ShopItem.kt @@ -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() - - 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?, - 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() + + 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?, + 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 + } + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/challenges/ChallengeTasksRecyclerViewAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/challenges/ChallengeTasksRecyclerViewAdapter.kt index 53284cae1..cac68ac7c 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/challenges/ChallengeTasksRecyclerViewAdapter.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/challenges/ChallengeTasksRecyclerViewAdapter.kt @@ -23,9 +23,8 @@ class ChallengeTasksRecyclerViewAdapter( newContext: Context, userID: String, private val openTaskDisabled: Boolean, - private val taskActionsDisabled: Boolean + private val taskActionsDisabled: Boolean, ) : BaseTasksRecyclerViewAdapter>(TaskType.HABIT, viewModel, layoutResource, newContext, userID) { - val taskList: MutableList 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 { - val viewHolder: BindableViewHolder = 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 { + val viewHolder: BindableViewHolder = + 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(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(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 } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/DailiesRecyclerViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/DailiesRecyclerViewHolder.kt index 17cacdeb1..a3b3bb08b 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/DailiesRecyclerViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/DailiesRecyclerViewHolder.kt @@ -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) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/HabitsRecyclerViewAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/HabitsRecyclerViewAdapter.kt index c39644f73..bb75b522d 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/HabitsRecyclerViewAdapter.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/HabitsRecyclerViewAdapter.kt @@ -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) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RewardsRecyclerViewAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RewardsRecyclerViewAdapter.kt index b04e54dd0..cbbe96399 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RewardsRecyclerViewAdapter.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RewardsRecyclerViewAdapter.kt @@ -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?, - private val layoutResource: Int, - val viewModel: TasksViewModel -) : BaseRecyclerViewAdapter(), 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? = 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?) { - updateData(data) - } - - override fun getItemCount(): Int { - var rewardCount = customRewardCount - if (viewModel.isPersonalBoard) { - rewardCount += inAppRewardCount - } - return rewardCount - } - - fun updateData(tasks: List?) { - this.customRewards = tasks - notifyDataSetChanged() - } - - fun updateItemRewards(items: List) { - 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?, + private val layoutResource: Int, + val viewModel: TasksViewModel, +) : BaseRecyclerViewAdapter(), 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? = 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?) { + updateData(data) + } + + override fun getItemCount(): Int { + var rewardCount = customRewardCount + if (viewModel.isPersonalBoard) { + rewardCount += inAppRewardCount + } + return rewardCount + } + + fun updateData(tasks: List?) { + this.customRewards = tasks + notifyDataSetChanged() + } + + fun updateItemRewards(items: List) { + 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 + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/TodosRecyclerViewAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/TodosRecyclerViewAdapter.kt index 84465a21d..1501a6b4a 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/TodosRecyclerViewAdapter.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/TodosRecyclerViewAdapter.kt @@ -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) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt index 87af495c2..0ccf882e5 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt @@ -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() - 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) { + private fun updateSeasonalMenuEntries( + gearEvent: WorldStateEvent?, + items: List, + ) { 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" diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NewsFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NewsFragment.kt index a773c58e9..1c4e32755 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NewsFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NewsFragment.kt @@ -20,48 +20,55 @@ import javax.inject.Inject @AndroidEntryPoint class NewsFragment : BaseMainFragment() { - @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") } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarCustomizationFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarCustomizationFragment.kt index 2d2fa787f..d13582d44 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarCustomizationFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarCustomizationFragment.kt @@ -52,11 +52,13 @@ import javax.inject.Inject class AvatarCustomizationFragment : BaseMainFragment(), 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, - 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 } } -} \ No newline at end of file +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarOverviewFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarOverviewFragment.kt index b528bb62b..35c531ab5 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarOverviewFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarOverviewFragment.kt @@ -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(), - 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(null) - private val costumeWeapon = mutableStateOf(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(), + 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(null) + private val costumeWeapon = mutableStateOf(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)) + } + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarCustomizationFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarCustomizationFragment.kt index 4e4b786aa..bb148f690 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarCustomizationFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarCustomizationFragment.kt @@ -1,499 +1,532 @@ -package com.habitrpg.android.habitica.ui.fragments.inventory.customization - -import android.graphics.PorterDuff -import android.graphics.Typeface -import android.os.Bundle -import android.util.Log -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.CheckBox -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.platform.rememberNestedScrollInteropConnection -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat -import androidx.fragment.app.viewModels -import androidx.lifecycle.ViewModel -import androidx.lifecycle.lifecycleScope -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import com.habitrpg.android.habitica.R -import com.habitrpg.android.habitica.data.CustomizationRepository -import com.habitrpg.android.habitica.data.InventoryRepository -import com.habitrpg.android.habitica.databinding.BottomSheetBackgroundsFilterBinding -import com.habitrpg.android.habitica.databinding.FragmentComposeBinding -import com.habitrpg.android.habitica.helpers.Analytics -import com.habitrpg.android.habitica.helpers.AppConfigManager -import com.habitrpg.android.habitica.models.CustomizationFilter -import com.habitrpg.android.habitica.models.inventory.Customization -import com.habitrpg.android.habitica.models.user.OwnedCustomization -import com.habitrpg.android.habitica.models.user.User -import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment -import com.habitrpg.android.habitica.ui.helpers.ToolbarColorHelper -import com.habitrpg.android.habitica.ui.theme.colors -import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel -import com.habitrpg.android.habitica.ui.views.PixelArtView -import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaBottomSheetDialog -import com.habitrpg.common.habitica.extensions.getThemeColor -import com.habitrpg.common.habitica.extensions.setTintWith -import com.habitrpg.common.habitica.helpers.ExceptionHandler -import com.habitrpg.common.habitica.helpers.MainNavigationController -import com.habitrpg.common.habitica.helpers.launchCatching -import com.habitrpg.common.habitica.theme.HabiticaTheme -import com.habitrpg.common.habitica.views.ComposableAvatarView -import com.habitrpg.shared.habitica.models.Avatar -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import javax.inject.Inject - -class CustomizationViewModel : ViewModel() { - var type: String? = null - var category: String? = null - - val customizations = mutableStateListOf() - val activeCustomization = mutableStateOf(null) - - val userSize = mutableStateOf("slim") - val hairColor = mutableStateOf(null) - - val typeNameId: Int - get() = when (type) { - "shirt" -> R.string.avatar_shirts - "skin" -> R.string.avatar_skins - "hair" -> { - when (category) { - "color" -> R.string.avatar_hair_colors - "base" -> R.string.avatar_hair_styles - "bangs" -> R.string.avatar_hair_bangs - "mustache" -> R.string.avatar_mustaches - "beard" -> R.string.avatar_beards - "flower" -> R.string.avatar_accents - else -> R.string.avatar_hair - } - } - - "background" -> R.string.standard_backgrounds - else -> R.string.customizations - } -} - -@AndroidEntryPoint -class ComposeAvatarCustomizationFragment : - BaseMainFragment(), - SwipeRefreshLayout.OnRefreshListener { - - private var filterMenuItem: MenuItem? = null - override var binding: FragmentComposeBinding? = null - - private val viewModel: CustomizationViewModel by viewModels() - - override fun createBinding( - inflater: LayoutInflater, - container: ViewGroup? - ): FragmentComposeBinding { - return FragmentComposeBinding.inflate(inflater, container, false) - } - - @Inject - lateinit var configManager: AppConfigManager - - @Inject - lateinit var customizationRepository: CustomizationRepository - - @Inject - lateinit var inventoryRepository: InventoryRepository - - @Inject - lateinit var userViewModel: MainUserViewModel - - var type: String? = null - var category: String? = null - private var activeCustomization: String? = null - - private val currentFilter = MutableStateFlow(CustomizationFilter(false, true)) - private val ownedCustomizations = MutableStateFlow>(emptyList()) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - showsBackButton = true - hidesToolbar = true - - val view = super.onCreateView(inflater, container, savedInstanceState) - binding?.composeView?.apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - HabiticaTheme { - val userSize by viewModel.userSize - val hairColor by viewModel.hairColor - val activeCustomization by viewModel.activeCustomization - val avatar by userViewModel.user.observeAsState() - AvatarCustomizationView(avatar = avatar, configManager = configManager, viewModel.customizations, userSize, hairColor, type, stringResource(viewModel.typeNameId), activeCustomization) { customization -> - lifecycleScope.launchCatching { - if (customization.identifier?.isNotBlank() != true) { - userRepository.useCustomization(type ?: "", category, activeCustomization ?: "") - } else if (customization.type == "background" && ownedCustomizations.value.firstOrNull { it.key == customization.identifier } == null) { - userRepository.unlockPath(customization) - userRepository.retrieveUser(false, true, true) - } else { - userRepository.useCustomization( - customization.type ?: "", - customization.category, - customization.identifier ?: "" - ) - } - } - } - } - } - } - return view - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - arguments?.let { - val args = ComposeAvatarCustomizationFragmentArgs.fromBundle(it) - type = args.type - viewModel.type = type - if (args.category.isNotEmpty()) { - category = args.category - viewModel.category = category - } - currentFilter.value.ascending = type != "background" - } - this.loadCustomizations() - - userViewModel.user.observe(viewLifecycleOwner) { updateUser(it) } - - lifecycleScope.launchCatching { - currentFilter.collect { - Log.e("NewFilter", it.toString()) - } - } - - Analytics.sendNavigationEvent("$type screen") - } - - override fun onDestroy() { - customizationRepository.close() - super.onDestroy() - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.menu_list_customizations, menu) - - filterMenuItem = menu.findItem(R.id.action_filter) - if (type == "background") { - updateFilterIcon() - } else { - filterMenuItem?.isVisible = false - } - - mainActivity?.toolbar?.let { - val color = ContextCompat.getColor(requireContext(), R.color.window_background) - ToolbarColorHelper.colorizeToolbar(it, mainActivity, backgroundColor = color) - requireActivity().window.statusBarColor = color - } - } - - private fun updateFilterIcon() { - if (!currentFilter.value.isFiltering) { - filterMenuItem?.setIcon(R.drawable.ic_action_filter_list) - context?.let { - val filterIcon = ContextCompat.getDrawable(it, R.drawable.ic_action_filter_list) - filterIcon?.setTintWith(it.getThemeColor(R.attr.headerTextColor), PorterDuff.Mode.MULTIPLY) - filterMenuItem?.setIcon(filterIcon) - } - } else { - context?.let { - val filterIcon = ContextCompat.getDrawable(it, R.drawable.ic_filters_active) - filterIcon?.setTintWith(it.getThemeColor(R.attr.textColorPrimaryDark), PorterDuff.Mode.MULTIPLY) - filterMenuItem?.setIcon(filterIcon) - } - } - } - - @Suppress("ReturnCount") - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_filter -> { - showFilterDialog() - return true - } - } - return super.onOptionsItemSelected(item) - } - - private fun loadCustomizations() { - val type = this.type ?: return - lifecycleScope.launchCatching { - customizationRepository.getCustomizations(type, category, false) - .combine(currentFilter) { customizations, filter -> Pair(customizations, filter) } - .combine(ownedCustomizations) { pair, ownedCustomizations -> - val ownedKeys = ownedCustomizations.map { it.key } - return@combine Pair(pair.first.filter { ownedKeys.contains(it.identifier) || (it.price ?: 0) == 0 }, pair.second) - } - .map { (customizations, filter) -> - var displayedCustomizations = customizations - if (filter.isFiltering) { - displayedCustomizations = mutableListOf() - for (customization in customizations) { - if (shouldSkip(filter, customization)) continue - displayedCustomizations.add(customization) - } - } - if (!filter.ascending) { - displayedCustomizations.reversed() - } else { - displayedCustomizations - } - } - .collect { customizations -> - viewModel.customizations.clear() - viewModel.customizations.addAll(customizations) - } - } - } - - private fun shouldSkip( - filter: CustomizationFilter, - customization: Customization - ): Boolean { - return if (filter.onlyPurchased) { - true - } else { - filter.months.isNotEmpty() && !filter.months.contains(customization.customizationSet?.substringAfter('.')) - } - } - - fun updateUser(user: User?) { - if (user == null) return - this.updateActiveCustomization(user) - ownedCustomizations.value = user.purchased?.customizations?.filter { it.type == this.type && it.purchased } ?: emptyList() - viewModel.userSize.value = user.preferences?.size ?: "slim" - viewModel.hairColor.value = user.preferences?.hair?.color - } - - private fun updateActiveCustomization(user: User) { - if (this.type == null || user.preferences == null) { - 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() - else -> "" - } - - else -> "" - } - if (activeCustomization != null) { - this.activeCustomization = activeCustomization - viewModel.activeCustomization.value = activeCustomization - } - } - - override fun onRefresh() { - lifecycleScope.launch(ExceptionHandler.coroutine()) { - userRepository.retrieveUser(true, true) - } - } - - private fun showFilterDialog() { - val filter = currentFilter.value - val context = context ?: return - val dialog = HabiticaBottomSheetDialog(context) - val binding = BottomSheetBackgroundsFilterBinding.inflate(layoutInflater) - binding.showMeWrapper.check(if (filter.onlyPurchased) R.id.show_purchased_button else R.id.show_all_button) - binding.showMeWrapper.setOnCheckedChangeListener { _, checkedId -> - val newFilter = filter.copy() - newFilter.onlyPurchased = checkedId == R.id.show_purchased_button - currentFilter.value = newFilter - } - binding.clearButton.setOnClickListener { - currentFilter.value = CustomizationFilter(false, type != "background") - dialog.dismiss() - } - if (type == "background") { - binding.sortByWrapper.check(if (filter.ascending) R.id.oldest_button else R.id.newest_button) - binding.sortByWrapper.setOnCheckedChangeListener { _, checkedId -> - val newFilter = filter.copy() - newFilter.ascending = checkedId == R.id.oldest_button - currentFilter.value = newFilter - } - configureMonthFilterButton(binding.januaryButton, 1, filter) - configureMonthFilterButton(binding.febuaryButton, 2, filter) - configureMonthFilterButton(binding.marchButton, 3, filter) - configureMonthFilterButton(binding.aprilButton, 4, filter) - configureMonthFilterButton(binding.mayButton, 5, filter) - configureMonthFilterButton(binding.juneButton, 6, filter) - configureMonthFilterButton(binding.julyButton, 7, filter) - configureMonthFilterButton(binding.augustButton, 8, filter) - configureMonthFilterButton(binding.septemberButton, 9, filter) - configureMonthFilterButton(binding.octoberButton, 10, filter) - configureMonthFilterButton(binding.novemberButton, 11, filter) - configureMonthFilterButton(binding.decemberButton, 12, filter) - } else { - binding.sortByTitle.visibility = View.GONE - binding.sortByWrapper.visibility = View.GONE - binding.monthReleasedTitle.visibility = View.GONE - binding.monthReleasedWrapper.visibility = View.GONE - } - dialog.setContentView(binding.root) - dialog.setOnDismissListener { updateFilterIcon() } - dialog.show() - } - - private fun configureMonthFilterButton(button: CheckBox, value: Int, filter: CustomizationFilter) { - val identifier = value.toString().padStart(2, '0') - button.isChecked = filter.months.contains(identifier) - button.text - button.setOnCheckedChangeListener { _, isChecked -> - val newFilter = filter.copy() - newFilter.months = mutableListOf() - newFilter.months.addAll(currentFilter.value.months) - if (!isChecked && newFilter.months.contains(identifier)) { - button.typeface = Typeface.create("sans-serif", Typeface.NORMAL) - newFilter.months.remove(identifier) - } else if (isChecked && !newFilter.months.contains(identifier)) { - button.typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL) - newFilter.months.add(identifier) - } - currentFilter.value = newFilter - } - } -} - -@Composable -private fun AvatarCustomizationView(avatar: Avatar?, configManager: AppConfigManager, customizations: List, userSize: String, hairColor: String?, type: String?, typeName: String, activeCustomization: String?, onSelect: (Customization) -> Unit) { - val nestedScrollInterop = rememberNestedScrollInteropConnection() - val totalWidth = LocalConfiguration.current.screenWidthDp.dp - val horizontalPadding = (totalWidth - (84.dp * 3)) / 2 - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.background(colorResource(R.color.window_background))) { - ComposableAvatarView( - avatar = avatar, configManager = configManager, modifier = Modifier - .padding(vertical = 24.dp) - .size(140.dp, 147.dp) - ) - Box( - Modifier - .background(colorResource(R.color.content_background), RoundedCornerShape(topStart = 22.dp, topEnd = 22.dp)) - .fillMaxWidth() - .height(22.dp) - ) - } - LazyVerticalGrid( - columns = GridCells.Adaptive(76.dp), - horizontalArrangement = Arrangement.Center, - contentPadding = PaddingValues(horizontal = horizontalPadding), - modifier = Modifier - .nestedScroll(nestedScrollInterop) - .background(colorResource(R.color.content_background)) - ) { - item(span = { GridItemSpan(3) }) { - Text( - typeName.uppercase(), - fontSize = 12.sp, - fontWeight = FontWeight.Medium, - color = colorResource(id = R.color.text_ternary), - textAlign = TextAlign.Center, - modifier = Modifier.padding(10.dp) - ) - } - if (customizations.size > 1) { - items(customizations) { customization -> - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .padding(4.dp) - .border(if (activeCustomization == customization.identifier) 2.dp else 0.dp, if (activeCustomization == customization.identifier) HabiticaTheme.colors.tintedUiMain else colorResource(R.color.transparent), RoundedCornerShape(8.dp)) - .clip(RoundedCornerShape(8.dp)) - .clickable { - onSelect(customization) - } - .background(colorResource(id = R.color.window_background))) { - if (customization.identifier.isNullOrBlank() || customization.identifier == "0") { - Image(painterResource(R.drawable.empty_slot), contentDescription = null, contentScale = ContentScale.None, modifier = Modifier.size(68.dp)) - } else { - PixelArtView( - imageName = customization.getImageName(userSize, hairColor), - Modifier.size(68.dp) - ) - } - } - } - } - item(span = { GridItemSpan(3) }) { - Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(top = 40.dp).clickable { - MainNavigationController.navigate(R.id.customizationsShopFragment) - }) { - Image( - painterResource(if (type == "backgrounds") R.drawable.customization_background else R.drawable.customization_mix), - null, modifier = Modifier.padding(bottom = 12.dp) - ) - if (customizations.size <= 1) { - Text(stringResource(R.string.customizations_no_owned), fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = colorResource(R.color.text_secondary)) - Text(stringResource(R.string.customization_shop_check_out), fontSize = 13.sp, color = colorResource(R.color.text_ternary), textAlign = TextAlign.Center) - } else { - Text(stringResource(R.string.looking_for_more), fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = colorResource(R.color.text_secondary)) - Text(stringResource(R.string.customization_shop_more), fontSize = 13.sp, color = colorResource(R.color.text_ternary), textAlign = TextAlign.Center) - } - } - } - } - } -} \ No newline at end of file +package com.habitrpg.android.habitica.ui.fragments.inventory.customization + +import android.graphics.PorterDuff +import android.graphics.Typeface +import android.os.Bundle +import android.util.Log +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.CheckBox +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModel +import androidx.lifecycle.lifecycleScope +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.data.CustomizationRepository +import com.habitrpg.android.habitica.data.InventoryRepository +import com.habitrpg.android.habitica.databinding.BottomSheetBackgroundsFilterBinding +import com.habitrpg.android.habitica.databinding.FragmentComposeBinding +import com.habitrpg.android.habitica.helpers.Analytics +import com.habitrpg.android.habitica.helpers.AppConfigManager +import com.habitrpg.android.habitica.models.CustomizationFilter +import com.habitrpg.android.habitica.models.inventory.Customization +import com.habitrpg.android.habitica.models.user.OwnedCustomization +import com.habitrpg.android.habitica.models.user.User +import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment +import com.habitrpg.android.habitica.ui.helpers.ToolbarColorHelper +import com.habitrpg.android.habitica.ui.theme.colors +import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel +import com.habitrpg.android.habitica.ui.views.PixelArtView +import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaBottomSheetDialog +import com.habitrpg.common.habitica.extensions.getThemeColor +import com.habitrpg.common.habitica.extensions.setTintWith +import com.habitrpg.common.habitica.helpers.ExceptionHandler +import com.habitrpg.common.habitica.helpers.MainNavigationController +import com.habitrpg.common.habitica.helpers.launchCatching +import com.habitrpg.common.habitica.theme.HabiticaTheme +import com.habitrpg.common.habitica.views.ComposableAvatarView +import com.habitrpg.shared.habitica.models.Avatar +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import javax.inject.Inject + +class CustomizationViewModel : ViewModel() { + var type: String? = null + var category: String? = null + + val customizations = mutableStateListOf() + val activeCustomization = mutableStateOf(null) + + val userSize = mutableStateOf("slim") + val hairColor = mutableStateOf(null) + + val typeNameId: Int + get() = + when (type) { + "shirt" -> R.string.avatar_shirts + "skin" -> R.string.avatar_skins + "hair" -> { + when (category) { + "color" -> R.string.avatar_hair_colors + "base" -> R.string.avatar_hair_styles + "bangs" -> R.string.avatar_hair_bangs + "mustache" -> R.string.avatar_mustaches + "beard" -> R.string.avatar_beards + "flower" -> R.string.avatar_accents + else -> R.string.avatar_hair + } + } + + "background" -> R.string.standard_backgrounds + else -> R.string.customizations + } +} + +@AndroidEntryPoint +class ComposeAvatarCustomizationFragment : + BaseMainFragment(), + SwipeRefreshLayout.OnRefreshListener { + private var filterMenuItem: MenuItem? = null + override var binding: FragmentComposeBinding? = null + + private val viewModel: CustomizationViewModel by viewModels() + + override fun createBinding( + inflater: LayoutInflater, + container: ViewGroup?, + ): FragmentComposeBinding { + return FragmentComposeBinding.inflate(inflater, container, false) + } + + @Inject + lateinit var configManager: AppConfigManager + + @Inject + lateinit var customizationRepository: CustomizationRepository + + @Inject + lateinit var inventoryRepository: InventoryRepository + + @Inject + lateinit var userViewModel: MainUserViewModel + + var type: String? = null + var category: String? = null + private var activeCustomization: String? = null + + private val currentFilter = MutableStateFlow(CustomizationFilter(false, true)) + private val ownedCustomizations = MutableStateFlow>(emptyList()) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + showsBackButton = true + hidesToolbar = true + + val view = super.onCreateView(inflater, container, savedInstanceState) + binding?.composeView?.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + HabiticaTheme { + val userSize by viewModel.userSize + val hairColor by viewModel.hairColor + val activeCustomization by viewModel.activeCustomization + val avatar by userViewModel.user.observeAsState() + AvatarCustomizationView(avatar = avatar, configManager = configManager, viewModel.customizations, userSize, hairColor, type, stringResource(viewModel.typeNameId), activeCustomization) { customization -> + lifecycleScope.launchCatching { + if (customization.identifier?.isNotBlank() != true) { + userRepository.useCustomization(type ?: "", category, activeCustomization ?: "") + } else if (customization.type == "background" && ownedCustomizations.value.firstOrNull { it.key == customization.identifier } == null) { + userRepository.unlockPath(customization) + userRepository.retrieveUser(false, true, true) + } else { + userRepository.useCustomization( + customization.type ?: "", + customization.category, + customization.identifier ?: "", + ) + } + } + } + } + } + } + return view + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + arguments?.let { + val args = ComposeAvatarCustomizationFragmentArgs.fromBundle(it) + type = args.type + viewModel.type = type + if (args.category.isNotEmpty()) { + category = args.category + viewModel.category = category + } + currentFilter.value.ascending = type != "background" + } + this.loadCustomizations() + + userViewModel.user.observe(viewLifecycleOwner) { updateUser(it) } + + lifecycleScope.launchCatching { + currentFilter.collect { + Log.e("NewFilter", it.toString()) + } + } + + Analytics.sendNavigationEvent("$type screen") + } + + override fun onDestroy() { + customizationRepository.close() + super.onDestroy() + } + + override fun onCreateOptionsMenu( + menu: Menu, + inflater: MenuInflater, + ) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.menu_list_customizations, menu) + + filterMenuItem = menu.findItem(R.id.action_filter) + if (type == "background") { + updateFilterIcon() + } else { + filterMenuItem?.isVisible = false + } + + mainActivity?.toolbar?.let { + val color = ContextCompat.getColor(requireContext(), R.color.window_background) + ToolbarColorHelper.colorizeToolbar(it, mainActivity, backgroundColor = color) + requireActivity().window.statusBarColor = color + } + } + + private fun updateFilterIcon() { + if (!currentFilter.value.isFiltering) { + filterMenuItem?.setIcon(R.drawable.ic_action_filter_list) + context?.let { + val filterIcon = ContextCompat.getDrawable(it, R.drawable.ic_action_filter_list) + filterIcon?.setTintWith(it.getThemeColor(R.attr.headerTextColor), PorterDuff.Mode.MULTIPLY) + filterMenuItem?.setIcon(filterIcon) + } + } else { + context?.let { + val filterIcon = ContextCompat.getDrawable(it, R.drawable.ic_filters_active) + filterIcon?.setTintWith(it.getThemeColor(R.attr.textColorPrimaryDark), PorterDuff.Mode.MULTIPLY) + filterMenuItem?.setIcon(filterIcon) + } + } + } + + @Suppress("ReturnCount") + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_filter -> { + showFilterDialog() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun loadCustomizations() { + val type = this.type ?: return + lifecycleScope.launchCatching { + customizationRepository.getCustomizations(type, category, false) + .combine(currentFilter) { customizations, filter -> Pair(customizations, filter) } + .combine(ownedCustomizations) { pair, ownedCustomizations -> + val ownedKeys = ownedCustomizations.map { it.key } + return@combine Pair(pair.first.filter { ownedKeys.contains(it.identifier) || (it.price ?: 0) == 0 }, pair.second) + } + .map { (customizations, filter) -> + var displayedCustomizations = customizations + if (filter.isFiltering) { + displayedCustomizations = mutableListOf() + for (customization in customizations) { + if (shouldSkip(filter, customization)) continue + displayedCustomizations.add(customization) + } + } + if (!filter.ascending) { + displayedCustomizations.reversed() + } else { + displayedCustomizations + } + } + .collect { customizations -> + viewModel.customizations.clear() + viewModel.customizations.addAll(customizations) + } + } + } + + private fun shouldSkip( + filter: CustomizationFilter, + customization: Customization, + ): Boolean { + return if (filter.onlyPurchased) { + true + } else { + filter.months.isNotEmpty() && !filter.months.contains(customization.customizationSet?.substringAfter('.')) + } + } + + fun updateUser(user: User?) { + if (user == null) return + this.updateActiveCustomization(user) + ownedCustomizations.value = user.purchased?.customizations?.filter { it.type == this.type && it.purchased } ?: emptyList() + viewModel.userSize.value = user.preferences?.size ?: "slim" + viewModel.hairColor.value = user.preferences?.hair?.color + } + + private fun updateActiveCustomization(user: User) { + if (this.type == null || user.preferences == null) { + 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() + else -> "" + } + + else -> "" + } + if (activeCustomization != null) { + this.activeCustomization = activeCustomization + viewModel.activeCustomization.value = activeCustomization + } + } + + override fun onRefresh() { + lifecycleScope.launch(ExceptionHandler.coroutine()) { + userRepository.retrieveUser(true, true) + } + } + + private fun showFilterDialog() { + val filter = currentFilter.value + val context = context ?: return + val dialog = HabiticaBottomSheetDialog(context) + val binding = BottomSheetBackgroundsFilterBinding.inflate(layoutInflater) + binding.showMeWrapper.check(if (filter.onlyPurchased) R.id.show_purchased_button else R.id.show_all_button) + binding.showMeWrapper.setOnCheckedChangeListener { _, checkedId -> + val newFilter = filter.copy() + newFilter.onlyPurchased = checkedId == R.id.show_purchased_button + currentFilter.value = newFilter + } + binding.clearButton.setOnClickListener { + currentFilter.value = CustomizationFilter(false, type != "background") + dialog.dismiss() + } + if (type == "background") { + binding.sortByWrapper.check(if (filter.ascending) R.id.oldest_button else R.id.newest_button) + binding.sortByWrapper.setOnCheckedChangeListener { _, checkedId -> + val newFilter = filter.copy() + newFilter.ascending = checkedId == R.id.oldest_button + currentFilter.value = newFilter + } + configureMonthFilterButton(binding.januaryButton, 1, filter) + configureMonthFilterButton(binding.febuaryButton, 2, filter) + configureMonthFilterButton(binding.marchButton, 3, filter) + configureMonthFilterButton(binding.aprilButton, 4, filter) + configureMonthFilterButton(binding.mayButton, 5, filter) + configureMonthFilterButton(binding.juneButton, 6, filter) + configureMonthFilterButton(binding.julyButton, 7, filter) + configureMonthFilterButton(binding.augustButton, 8, filter) + configureMonthFilterButton(binding.septemberButton, 9, filter) + configureMonthFilterButton(binding.octoberButton, 10, filter) + configureMonthFilterButton(binding.novemberButton, 11, filter) + configureMonthFilterButton(binding.decemberButton, 12, filter) + } else { + binding.sortByTitle.visibility = View.GONE + binding.sortByWrapper.visibility = View.GONE + binding.monthReleasedTitle.visibility = View.GONE + binding.monthReleasedWrapper.visibility = View.GONE + } + dialog.setContentView(binding.root) + dialog.setOnDismissListener { updateFilterIcon() } + dialog.show() + } + + private fun configureMonthFilterButton( + button: CheckBox, + value: Int, + filter: CustomizationFilter, + ) { + val identifier = value.toString().padStart(2, '0') + button.isChecked = filter.months.contains(identifier) + button.text + button.setOnCheckedChangeListener { _, isChecked -> + val newFilter = filter.copy() + newFilter.months = mutableListOf() + newFilter.months.addAll(currentFilter.value.months) + if (!isChecked && newFilter.months.contains(identifier)) { + button.typeface = Typeface.create("sans-serif", Typeface.NORMAL) + newFilter.months.remove(identifier) + } else if (isChecked && !newFilter.months.contains(identifier)) { + button.typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL) + newFilter.months.add(identifier) + } + currentFilter.value = newFilter + } + } +} + +@Composable +private fun AvatarCustomizationView( + avatar: Avatar?, + configManager: AppConfigManager, + customizations: List, + userSize: String, + hairColor: String?, + type: String?, + typeName: String, + activeCustomization: String?, + onSelect: (Customization) -> Unit, +) { + val nestedScrollInterop = rememberNestedScrollInteropConnection() + val totalWidth = LocalConfiguration.current.screenWidthDp.dp + val horizontalPadding = (totalWidth - (84.dp * 3)) / 2 + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.background(colorResource(R.color.window_background))) { + ComposableAvatarView( + avatar = avatar, + configManager = configManager, + modifier = + Modifier + .padding(vertical = 24.dp) + .size(140.dp, 147.dp), + ) + Box( + Modifier + .background(colorResource(R.color.content_background), RoundedCornerShape(topStart = 22.dp, topEnd = 22.dp)) + .fillMaxWidth() + .height(22.dp), + ) + } + LazyVerticalGrid( + columns = GridCells.Adaptive(76.dp), + horizontalArrangement = Arrangement.Center, + contentPadding = PaddingValues(horizontal = horizontalPadding), + modifier = + Modifier + .nestedScroll(nestedScrollInterop) + .background(colorResource(R.color.content_background)), + ) { + item(span = { GridItemSpan(3) }) { + Text( + typeName.uppercase(), + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = colorResource(id = R.color.text_ternary), + textAlign = TextAlign.Center, + modifier = Modifier.padding(10.dp), + ) + } + if (customizations.size > 1) { + items(customizations) { customization -> + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier + .padding(4.dp) + .border(if (activeCustomization == customization.identifier) 2.dp else 0.dp, if (activeCustomization == customization.identifier) HabiticaTheme.colors.tintedUiMain else colorResource(R.color.transparent), RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .clickable { + onSelect(customization) + } + .background(colorResource(id = R.color.window_background)), + ) { + if (customization.identifier.isNullOrBlank() || customization.identifier == "0") { + Image(painterResource(R.drawable.empty_slot), contentDescription = null, contentScale = ContentScale.None, modifier = Modifier.size(68.dp)) + } else { + PixelArtView( + imageName = customization.getImageName(userSize, hairColor), + Modifier.size(68.dp), + ) + } + } + } + } + item(span = { GridItemSpan(3) }) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = + Modifier.padding(top = 40.dp).clickable { + MainNavigationController.navigate(R.id.customizationsShopFragment) + }, + ) { + Image( + painterResource(if (type == "backgrounds") R.drawable.customization_background else R.drawable.customization_mix), + null, + modifier = Modifier.padding(bottom = 12.dp), + ) + if (customizations.size <= 1) { + Text(stringResource(R.string.customizations_no_owned), fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = colorResource(R.color.text_secondary)) + Text(stringResource(R.string.customization_shop_check_out), fontSize = 13.sp, color = colorResource(R.color.text_ternary), textAlign = TextAlign.Center) + } else { + Text(stringResource(R.string.looking_for_more), fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = colorResource(R.color.text_secondary)) + Text(stringResource(R.string.customization_shop_more), fontSize = 13.sp, color = colorResource(R.color.text_ternary), textAlign = TextAlign.Center) + } + } + } + } + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/shops/CustomizationsShopFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/shops/CustomizationsShopFragment.kt index 50d060bdf..8f4e0169b 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/shops/CustomizationsShopFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/shops/CustomizationsShopFragment.kt @@ -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) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/shops/ShopFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/shops/ShopFragment.kt index bedbe5de1..15067a0f3 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/shops/ShopFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/shops/ShopFragment.kt @@ -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() { - - 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(null) - private val gems = mutableStateOf(null) - private val gold = mutableStateOf(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? = 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 { 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() - 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() { + 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(null) + private val gems = mutableStateOf(null) + private val gold = mutableStateOf(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? = 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 { 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() + 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" + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/BaseTaskViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/BaseTaskViewHolder.kt index ece2654c0..128774df6 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/BaseTaskViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/BaseTaskViewHolder.kt @@ -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(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(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 + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/ChecklistedViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/ChecklistedViewHolder.kt index d5e5d437d..10eb0b1cd 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/ChecklistedViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/ChecklistedViewHolder.kt @@ -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()) { - val itemView = layoutInflater?.inflate(R.layout.checklist_item_row, this.checklistView, false) - val checkboxBackground = itemView?.findViewById(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(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(R.id.checkmark) - checkmark?.drawable?.setTintMode(PorterDuff.Mode.SRC_ATOP) - checkmark?.visibility = if (item.completed) View.VISIBLE else View.GONE - val checkboxHolder = itemView?.findViewById(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()) { + val itemView = layoutInflater?.inflate(R.layout.checklist_item_row, this.checklistView, false) + val checkboxBackground = itemView?.findViewById(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(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(R.id.checkmark) + checkmark?.drawable?.setTintMode(PorterDuff.Mode.SRC_ATOP) + checkmark?.visibility = if (item.completed) View.VISIBLE else View.GONE + val checkboxHolder = itemView?.findViewById(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 + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/DailyViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/DailyViewHolder.kt index 2f217bbe9..67602bd58 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/DailyViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/DailyViewHolder.kt @@ -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) + } + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/HabitViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/HabitViewHolder.kt index 17e1200f5..aa0525a05 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/HabitViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/HabitViewHolder.kt @@ -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 + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/RewardViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/RewardViewHolder.kt index b5919e135..b707b8c3e 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/RewardViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/RewardViewHolder.kt @@ -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, + ), + ) + } + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/TodoViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/TodoViewHolder.kt index 2d7f8d444..19c9470c6 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/TodoViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/TodoViewHolder.kt @@ -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 + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/NPCBannerView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/NPCBannerView.kt index 54d93149a..53777adae 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/NPCBannerView.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/NPCBannerView.kt @@ -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() } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/HabiticaAlertDialog.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/HabiticaAlertDialog.kt index 969531def..3f566373f 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/HabiticaAlertDialog.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/HabiticaAlertDialog.kt @@ -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() diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialog.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialog.kt index 0d6f3f4c0..8736ef6c8 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialog.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialog.kt @@ -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)) - } - 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)) + 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)) + } } - } } 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 diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialogCustomizationContent.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialogCustomizationContent.kt index 68e171e89..04e61c952 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialogCustomizationContent.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialogCustomizationContent.kt @@ -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::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_", "") } diff --git a/shared/src/jsMain/kotlin/com/habitrpg/shared/habitica/Logger.js.kt b/shared/src/jsMain/kotlin/com/habitrpg/shared/habitica/Logger.js.kt deleted file mode 100644 index b19bb7ccb..000000000 --- a/shared/src/jsMain/kotlin/com/habitrpg/shared/habitica/Logger.js.kt +++ /dev/null @@ -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}") - } -} \ No newline at end of file diff --git a/shared/src/jsMain/kotlin/com/habitrpg/shared/habitica/Platform.js.kt b/shared/src/jsMain/kotlin/com/habitrpg/shared/habitica/Platform.js.kt index 10377e357..6c981fbf8 100644 --- a/shared/src/jsMain/kotlin/com/habitrpg/shared/habitica/Platform.js.kt +++ b/shared/src/jsMain/kotlin/com/habitrpg/shared/habitica/Platform.js.kt @@ -5,4 +5,4 @@ actual class Platform actual constructor() { get() = "JS!" } -actual interface HParcelable \ No newline at end of file +actual interface HParcelable diff --git a/shared/src/jsMain/kotlin/com/habitrpg/shared/habitica/PlatformLogger.js.kt b/shared/src/jsMain/kotlin/com/habitrpg/shared/habitica/PlatformLogger.js.kt new file mode 100644 index 000000000..e4aa382f7 --- /dev/null +++ b/shared/src/jsMain/kotlin/com/habitrpg/shared/habitica/PlatformLogger.js.kt @@ -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") + } +}