Implement first version of achievement screen

This commit is contained in:
Phillip Thelen 2019-05-27 13:21:25 +02:00
parent ec14b054ca
commit 2ffa14907e
43 changed files with 672 additions and 70 deletions

View file

@ -150,7 +150,7 @@ android {
buildConfigField "String", "TESTING_LEVEL", "\"production\""
multiDexEnabled true
versionCode 2140
versionCode 2141
versionName "1.10"
}

View file

@ -63,8 +63,8 @@
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
#keep models
-keep class com.habitrpg.android.habitica.models.** { *; }
#keep Habitica code
-keep class com.habitrpg.android.habitica.** { *; }
#realm
-keep class io.realm.annotations.RealmModule

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/gray_300" />
<corners android:radius="12dp" />
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/gray_700" />
<corners android:radius="12dp" />
</shape>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/gray_600" />
</shape>

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFFFF" android:pathData="M4,14h2c0.55,0 1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1L4,10c-0.55,0 -1,0.45 -1,1v2c0,0.55 0.45,1 1,1zM4,19h2c0.55,0 1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1L4,15c-0.55,0 -1,0.45 -1,1v2c0,0.55 0.45,1 1,1zM4,9h2c0.55,0 1,-0.45 1,-1L7,6c0,-0.55 -0.45,-1 -1,-1L4,5c-0.55,0 -1,0.45 -1,1v2c0,0.55 0.45,1 1,1zM9,14h10c0.55,0 1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1L9,10c-0.55,0 -1,0.45 -1,1v2c0,0.55 0.45,1 1,1zM9,19h10c0.55,0 1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1L9,15c-0.55,0 -1,0.45 -1,1v2c0,0.55 0.45,1 1,1zM8,6v2c0,0.55 0.45,1 1,1h10c0.55,0 1,-0.45 1,-1L20,6c0,-0.55 -0.45,-1 -1,-1L9,5c-0.55,0 -1,0.45 -1,1z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFFFF" android:pathData="M5,11h3c0.55,0 1,-0.45 1,-1L9,6c0,-0.55 -0.45,-1 -1,-1L5,5c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1zM5,18h3c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1L5,12c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1zM11,18h3c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1h-3c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1zM17,18h3c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1h-3c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1zM11,11h3c0.55,0 1,-0.45 1,-1L15,6c0,-0.55 -0.45,-1 -1,-1h-3c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1zM16,6v4c0,0.55 0.45,1 1,1h3c0.55,0 1,-0.45 1,-1L21,6c0,-0.55 -0.45,-1 -1,-1h-3c-0.55,0 -1,0.45 -1,1z"/>
</vector>

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="13dp"
android:paddingBottom="13dp"
tools:background="@color/white">
<LinearLayout
android:id="@+id/achievement_container"
android:layout_width="156dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/layout_rounded_bg_gray_700"
android:clipChildren="true"
android:layout_centerHorizontal="true"
android:layout_alignParentTop="true"
android:gravity="center_horizontal"
android:layout_margin="4dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="66dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/achievement_icon"
android:layout_width="48dp"
android:layout_height="52dp"
android:layout_gravity="center"/>
</FrameLayout>
<TextView
android:id="@+id/achievement_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="40dp"
style="@style/Body1"
android:gravity="center"
android:textColor="@color/gray_100"
android:background="@color/gray_600"
android:paddingStart="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_medium"
android:paddingTop="@dimen/spacing_small"
android:paddingBottom="@dimen/spacing_small"/>
</LinearLayout>
<TextView
android:id="@+id/achievement_count_label"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:minWidth="24dp"
android:background="@drawable/achievement_badge_bg"
android:gravity="center"
tools:text="1"
android:paddingStart="2dp"
android:paddingEnd="2dp"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold"
android:layout_alignParentTop="true"
android:layout_alignStart="@id/achievement_container"
android:layout_marginStart="-4dp"/>
</RelativeLayout>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="13dp"
android:paddingBottom="13dp">
<RelativeLayout
android:layout_width="100dp"
android:layout_height="86dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/achievement_icon"
android:layout_width="48dp"
android:layout_height="52dp"
android:layout_centerInParent="true"/>
<TextView
android:id="@+id/achievement_count_label"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:minWidth="24dp"
android:background="@drawable/achievement_badge_bg"
android:gravity="center"
tools:text="1"
android:paddingStart="2dp"
android:paddingEnd="2dp"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_marginStart="13dp"
android:layout_marginTop="0dp"/>
</RelativeLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/achievement_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Body1"
android:textColor="@color/gray_100"/>
<TextView
android:id="@+id/achievement_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Caption3"
android:textColor="@color/gray_100"/>
</LinearLayout>r
</LinearLayout>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="13dp"
android:paddingBottom="13dp">
<TextView
android:id="@+id/achievement_count_label"
android:layout_width="40dp"
android:layout_height="40dp"
android:gravity="center"
android:textColor="@color/gray_200"
style="@style/Body1"
android:background="@drawable/circle_gray600"
tools:text="12"
android:layout_marginStart="35dp"
android:layout_marginEnd="25dp"/>
<TextView
android:id="@+id/achievement_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Body1"
android:textColor="@color/gray_100"
tools:text="This is the quest title"/>
</LinearLayout>

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_marginTop="@dimen/spacing_medium"
android:paddingLeft="@dimen/spacing_large"
android:paddingRight="@dimen/spacing_large">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
style="@style/Overline"
tools:text="Title"/>
<TextView
android:id="@+id/count_label"
android:layout_width="wrap_content"
android:minWidth="24dp"
android:layout_height="24dp"
android:gravity="center"
android:background="@drawable/achievement_section_badge_bg"
android:paddingStart="2dp"
android:paddingEnd="2dp"
android:textColor="@color/gray_200"
style="@style/Overline"
tools:text="1"/>
</LinearLayout>

View file

@ -289,4 +289,13 @@
<action
android:id="@+id/openProfileActivity"
app:destination="@id/fullProfileActivity" />
<fragment
android:id="@+id/achievementsFragment"
android:name="com.habitrpg.android.habitica.ui.fragments.AchievementsFragment"
android:label="@string/sidebar_achievements" >
<argument
android:name="userID"
app:argType="string"
app:nullable="true" />
</fragment>
</navigation>

View file

@ -919,4 +919,10 @@
<string name="pin">Pin</string>
<string name="unpin">Unpin</string>
<string name="take_me_back">Take me Back</string>
<string name="sidebar_achievements">Achievements</string>
<string name="basic_achievements">Basic Achievements</string>
<string name="seasonal_achievements">Seasonal Achievements</string>
<string name="special_achievements">Special Achievements</string>
<string name="switch_to_list_view">Switch to list view</string>
<string name="switch_to_grid_view">Switch to grid view</string>
</resources>

View file

@ -121,4 +121,9 @@
<item name="android:textSize">12sp</item>
<item name="android:letterSpacing">0.035</item>
</style>
<style name="Overline">
<item name="android:fontFamily">@string/font_family_regular</item>
<item name="android:textSize">10sp</item>
<item name="android:textAllCaps">true</item>
</style>
</resources>

View file

@ -1,6 +1,6 @@
package com.habitrpg.android.habitica.api;
import com.habitrpg.android.habitica.models.AchievementResult;
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.PurchaseValidationRequest;
@ -284,7 +284,7 @@ public interface ApiService {
Flowable<HabitResponse<Member>> getMemberWithUsername(@Path("username") String username);
@GET("members/{mid}/achievements")
Flowable<HabitResponse<AchievementResult>> getMemberAchievements(@Path("mid") String memberId);
Flowable<HabitResponse<List<Achievement>>> getMemberAchievements(@Path("mid") String memberId);
@POST("members/send-private-message")
Flowable<HabitResponse<PostChatMessageResult>> postPrivateMessage(@Body Map<String, String> messageDetails);

View file

@ -3,6 +3,7 @@ package com.habitrpg.android.habitica.api;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.habitrpg.android.habitica.models.Achievement;
import com.habitrpg.android.habitica.models.ContentResult;
import com.habitrpg.android.habitica.models.FAQArticle;
import com.habitrpg.android.habitica.models.Skill;
@ -11,8 +12,6 @@ import com.habitrpg.android.habitica.models.TutorialStep;
import com.habitrpg.android.habitica.models.WorldState;
import com.habitrpg.android.habitica.models.inventory.Customization;
import com.habitrpg.android.habitica.models.inventory.Equipment;
import com.habitrpg.android.habitica.models.inventory.Mount;
import com.habitrpg.android.habitica.models.inventory.Pet;
import com.habitrpg.android.habitica.models.inventory.Quest;
import com.habitrpg.android.habitica.models.inventory.QuestCollect;
import com.habitrpg.android.habitica.models.inventory.QuestDropItem;
@ -29,6 +28,7 @@ import com.habitrpg.android.habitica.models.user.OwnedMount;
import com.habitrpg.android.habitica.models.user.OwnedPet;
import com.habitrpg.android.habitica.models.user.Purchases;
import com.habitrpg.android.habitica.models.user.User;
import com.habitrpg.android.habitica.utils.AchievementListDeserializer;
import com.habitrpg.android.habitica.utils.BooleanAsIntAdapter;
import com.habitrpg.android.habitica.utils.ChallengeDeserializer;
import com.habitrpg.android.habitica.utils.ChallengeListDeserializer;
@ -53,15 +53,14 @@ import com.habitrpg.android.habitica.utils.QuestDropItemsListSerialization;
import com.habitrpg.android.habitica.utils.SkillDeserializer;
import com.habitrpg.android.habitica.utils.TaskListDeserializer;
import com.habitrpg.android.habitica.utils.TaskSerializer;
import com.habitrpg.android.habitica.utils.TutorialStepListDeserializer;
import com.habitrpg.android.habitica.utils.TaskTagDeserializer;
import com.habitrpg.android.habitica.utils.TutorialStepListDeserializer;
import com.habitrpg.android.habitica.utils.UserDeserializer;
import com.habitrpg.android.habitica.utils.WorldStateSerialization;
import java.lang.reflect.Type;
import java.util.Date;
import java.util.List;
import java.util.Map;
import io.realm.RealmList;
import retrofit2.converter.gson.GsonConverterFactory;
@ -83,6 +82,7 @@ public class GSonFactoryCreator {
Type ownedItemListType = new TypeToken<RealmList<OwnedItem>>() {}.getType();
Type ownedPetListType = new TypeToken<RealmList<OwnedPet>>() {}.getType();
Type ownedMountListType = new TypeToken<RealmList<OwnedMount>>() {}.getType();
Type achievementsListType = new TypeToken<List<Achievement>>() {}.getType();
//Exclusion strategy needed for DBFlow https://github.com/Raizlabs/DBFlow/issues/121
@ -113,6 +113,7 @@ public class GSonFactoryCreator {
.registerTypeAdapter(ownedItemListType, new OwnedItemListDeserializer())
.registerTypeAdapter(ownedPetListType, new OwnedPetListDeserializer())
.registerTypeAdapter(ownedMountListType, new OwnedMountListDeserializer())
.registerTypeAdapter(achievementsListType, new AchievementListDeserializer())
.registerTypeAdapter(Quest.class, new QuestDeserializer())
.registerTypeAdapter(Member.class, new MemberSerialization())
.registerTypeAdapter(WorldState.class, new WorldStateSerialization())

View file

@ -41,6 +41,7 @@ import com.habitrpg.android.habitica.ui.adapter.tasks.HabitsRecyclerViewAdapter;
import com.habitrpg.android.habitica.ui.adapter.tasks.RewardsRecyclerViewAdapter;
import com.habitrpg.android.habitica.ui.adapter.tasks.TodosRecyclerViewAdapter;
import com.habitrpg.android.habitica.ui.fragments.AboutFragment;
import com.habitrpg.android.habitica.ui.fragments.AchievementsFragment;
import com.habitrpg.android.habitica.ui.fragments.GemsPurchaseFragment;
import com.habitrpg.android.habitica.ui.fragments.NavigationDrawerFragment;
import com.habitrpg.android.habitica.ui.fragments.NewsFragment;
@ -314,4 +315,6 @@ public interface AppComponent {
void inject(@NotNull ReportMessageActivity reportMessageActivity);
void inject(@NotNull GuildDetailFragment guildDetailFragment);
void inject(@NotNull AchievementsFragment achievementsFragment);
}

View file

@ -179,7 +179,7 @@ interface ApiClient {
fun getMember(memberId: String): Flowable<Member>
fun getMemberWithUsername(username: String): Flowable<Member>
fun getMemberAchievements(memberId: String): Flowable<AchievementResult>
fun getMemberAchievements(memberId: String): Flowable<List<Achievement>>
fun postPrivateMessage(messageDetails: Map<String, String>): Flowable<PostChatMessageResult>

View file

@ -1,20 +1,11 @@
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.inventory.Egg
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Food
import com.habitrpg.android.habitica.models.inventory.HatchingPotion
import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.inventory.Mount
import com.habitrpg.android.habitica.models.inventory.Pet
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.inventory.*
import com.habitrpg.android.habitica.models.responses.BuyResponse
import com.habitrpg.android.habitica.models.responses.FeedResponse
import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.*
import io.reactivex.Flowable
import io.realm.RealmResults
@ -34,6 +25,7 @@ interface InventoryRepository : ContentRepository {
fun getOwnedPets(): Flowable<RealmResults<OwnedPet>>
fun getQuestContent(key: String): Flowable<QuestContent>
fun getQuestContent(keys: List<String>): Flowable<RealmResults<QuestContent>>
fun getEquipment(searchedKeys: List<String>): Flowable<RealmResults<Equipment>>
fun retrieveInAppRewards(): Flowable<List<ShopItem>>

View file

@ -1,10 +1,13 @@
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.AchievementResult
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
import com.habitrpg.android.habitica.models.social.*
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.GroupMembership
import com.habitrpg.android.habitica.models.user.User
import io.reactivex.Flowable
import io.reactivex.Single
@ -75,7 +78,7 @@ interface SocialRepository : BaseRepository {
fun forceStartQuest(party: Group): Flowable<Quest>
fun getMemberAchievements(userId: String?): Flowable<AchievementResult>
fun getMemberAchievements(userId: String?): Flowable<List<Achievement>>
fun getGroupMembership(id: String): Flowable<GroupMembership>
fun getGroupMemberships(): Flowable<RealmResults<GroupMembership>>

View file

@ -1,5 +1,7 @@
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Skill
import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.models.inventory.CustomizationSet
@ -69,4 +71,7 @@ interface UserRepository : BaseRepository {
fun bulkAllocatePoints(user: User?, strength: Int, intelligence: Int, constitution: Int, perception: Int): Flowable<Stats>
fun useCustomization(user: User?, type: String, category: String?, identifier: String): Flowable<User>
fun retrieveAchievements(): Flowable<List<Achievement>>
fun getAchievements(): Flowable<RealmResults<Achievement>>
fun getQuestAchievements(): Flowable<RealmResults<QuestAchievement>>
}

View file

@ -589,7 +589,7 @@ class ApiClientImpl//private OnHabitsAPIResult mResultListener;
return apiService.getMemberWithUsername(username).compose(configureApiCallObserver())
}
override fun getMemberAchievements(memberId: String): Flowable<AchievementResult> {
override fun getMemberAchievements(memberId: String): Flowable<List<Achievement>> {
return apiService.getMemberAchievements(memberId).compose(configureApiCallObserver())
}

View file

@ -11,9 +11,13 @@ import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.*
import io.reactivex.Flowable
import io.realm.RealmList
import io.realm.RealmResults
class InventoryRepositoryImpl(localRepository: InventoryLocalRepository, apiClient: ApiClient, userID: String, var appConfigManager: AppConfigManager) : ContentRepositoryImpl<InventoryLocalRepository>(localRepository, apiClient, userID), InventoryRepository {
override fun getQuestContent(keys: List<String>): Flowable<RealmResults<QuestContent>> {
return localRepository.getQuestContent(keys)
}
override fun getQuestContent(key: String): Flowable<QuestContent> {
return localRepository.getQuestContent(key)

View file

@ -5,11 +5,14 @@ import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.data.local.SocialLocalRepository
import com.habitrpg.android.habitica.extensions.notNull
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.AchievementResult
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
import com.habitrpg.android.habitica.models.social.*
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.GroupMembership
import com.habitrpg.android.habitica.models.user.User
import io.reactivex.Flowable
import io.reactivex.Single
@ -284,7 +287,7 @@ class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: Ap
.doOnNext { localRepository.setQuestActivity(party, true) }
}
override fun getMemberAchievements(userId: String?): Flowable<AchievementResult> {
override fun getMemberAchievements(userId: String?): Flowable<List<Achievement>> {
return if (userId == null) {
Flowable.empty()
} else apiClient.getMemberAchievements(userId)

View file

@ -6,6 +6,8 @@ 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.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Skill
import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.models.inventory.CustomizationSet
@ -303,6 +305,20 @@ class UserRepositoryImpl(localRepository: UserLocalRepository, apiClient: ApiCli
return updateUser(user, updatePath, identifier)
}
override fun retrieveAchievements(): Flowable<List<Achievement>> {
return apiClient.getMemberAchievements(userID).doOnNext {
localRepository.save(it)
}
}
override fun getAchievements(): Flowable<RealmResults<Achievement>> {
return localRepository.getAchievements()
}
override fun getQuestAchievements(): Flowable<RealmResults<QuestAchievement>> {
return localRepository.getQuestAchievements(userID)
}
private fun mergeUser(oldUser: User?, newUser: User): User {
if (oldUser == null || !oldUser.isValid) {
return oldUser ?: newUser

View file

@ -24,6 +24,7 @@ interface InventoryLocalRepository : ContentLocalRepository {
fun getInAppRewards(): Flowable<RealmResults<ShopItem>>
fun getQuestContent(key: String): Flowable<QuestContent>
fun getQuestContent(keys: List<String>): Flowable<RealmResults<QuestContent>>
fun getEquipment(searchedKeys: List<String>): Flowable<RealmResults<Equipment>>

View file

@ -1,5 +1,7 @@
package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Skill
import com.habitrpg.android.habitica.models.TutorialStep
import com.habitrpg.android.habitica.models.social.ChatMessage
@ -20,4 +22,6 @@ interface UserLocalRepository : BaseLocalRepository {
fun getSkills(user: User): Flowable<RealmResults<Skill>>
fun getSpecialItems(user: User): Flowable<RealmResults<Skill>>
fun getAchievements(): Flowable<RealmResults<Achievement>>
fun getQuestAchievements(userID: String): Flowable<RealmResults<QuestAchievement>>
}

View file

@ -19,6 +19,13 @@ import io.realm.Sort
class RealmInventoryLocalRepository(realm: Realm, private val context: Context) : RealmContentLocalRepository(realm), InventoryLocalRepository {
override fun getQuestContent(keys: List<String>): Flowable<RealmResults<QuestContent>> {
return realm.where(QuestContent::class.java)
.`in`("key", keys.toTypedArray())
.findAll()
.asFlowable()
.filter { it.isLoaded }
}
override fun getQuestContent(key: String): Flowable<QuestContent> {
return realm.where(QuestContent::class.java).equalTo("key", key)

View file

@ -1,9 +1,7 @@
package com.habitrpg.android.habitica.data.local.implementation
import com.habitrpg.android.habitica.data.local.UserLocalRepository
import com.habitrpg.android.habitica.models.Skill
import com.habitrpg.android.habitica.models.Tag
import com.habitrpg.android.habitica.models.TutorialStep
import com.habitrpg.android.habitica.models.*
import com.habitrpg.android.habitica.models.social.ChallengeMembership
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.user.User
@ -12,6 +10,21 @@ import io.realm.Realm
import io.realm.RealmResults
class RealmUserLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), UserLocalRepository {
override fun getAchievements(): Flowable<RealmResults<Achievement>> {
return realm.where(Achievement::class.java)
.sort("index")
.findAll()
.asFlowable()
.filter { it.isLoaded }
}
override fun getQuestAchievements(userID: String): Flowable<RealmResults<QuestAchievement>> {
return realm.where(QuestAchievement::class.java)
.equalTo("userID", userID)
.findAll()
.asFlowable()
.filter { it.isLoaded }
}
override fun getTutorialSteps(): Flowable<RealmResults<TutorialStep>> = realm.where(TutorialStep::class.java).findAll().asFlowable()
.filter { it.isLoaded }

View file

@ -1,14 +0,0 @@
package com.habitrpg.android.habitica.models;
public class Achievement {
public String type;
public String title;
public String text;
public String icon;
public String category;
public String key;
public String value;
public boolean earned;
public int index;
public Integer optionalCount;
}

View file

@ -0,0 +1,17 @@
package com.habitrpg.android.habitica.models
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class Achievement : RealmObject() {
@PrimaryKey
var key: String? = null
var type: String? = null
var title: String? = null
var text: String? = null
var icon: String? = null
var category: String? = null
var earned: Boolean = false
var index: Int = 0
var optionalCount: Int? = null
}

View file

@ -0,0 +1,25 @@
package com.habitrpg.android.habitica.models
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
open class QuestAchievement: RealmObject() {
@PrimaryKey
var combinedKey: String? = null
var questKey: String? = null
set(value) {
field = value
combinedKey = userID + questKey
}
var userID: String? = null
set(value) {
field = value
combinedKey = userID + questKey
}
var count: Int = 0
@Ignore
var title: String? = null
}

View file

@ -3,6 +3,7 @@ package com.habitrpg.android.habitica.models.user
import com.google.gson.annotations.SerializedName
import com.habitrpg.android.habitica.models.Avatar
import com.habitrpg.android.habitica.models.PushDevice
import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Tag
import com.habitrpg.android.habitica.models.invitations.Invitations
import com.habitrpg.android.habitica.models.social.ChallengeMembership
@ -118,6 +119,11 @@ open class User : RealmObject(), Avatar {
}
var tags = RealmList<Tag>()
var questAchievements = RealmList<QuestAchievement>()
set(value) {
field = value
field.forEach { it.userID = id }
}
@Ignore
var pushDevices: List<PushDevice>? = null

View file

@ -20,15 +20,14 @@ import com.habitrpg.android.habitica.extensions.notNull
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.UserStatComputer
import com.habitrpg.android.habitica.models.AchievementGroup
import com.habitrpg.android.habitica.models.AchievementResult
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.user.Outfit
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.ui.AvatarView
import com.habitrpg.android.habitica.ui.AvatarWithBarsViewModel
import com.habitrpg.android.habitica.ui.adapter.social.AchievementAdapter
import com.habitrpg.android.habitica.ui.adapter.social.AchievementProfileAdapter
import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
import com.habitrpg.android.habitica.ui.helpers.MarkdownParser
import com.habitrpg.android.habitica.ui.helpers.bindView
@ -208,7 +207,7 @@ class FullProfileActivity : BaseActivity() {
// Load the members achievements now
compositeSubscription.add(socialRepository.getMemberAchievements(this.userID).subscribe(Consumer<AchievementResult> { this.fillAchievements(it) }, RxErrorHandler.handleEmptyError()))
compositeSubscription.add(socialRepository.getMemberAchievements(this.userID).subscribe(Consumer { this.fillAchievements(it) }, RxErrorHandler.handleEmptyError()))
}
private fun updatePetsMountsView(user: Member) {
@ -223,17 +222,17 @@ class FullProfileActivity : BaseActivity() {
// region Attributes
private fun fillAchievements(achievements: AchievementResult?) {
private fun fillAchievements(achievements: List<Achievement>?) {
if (achievements == null) {
return
}
val items = ArrayList<Any>()
fillAchievements(achievements.basic, items)
fillAchievements(achievements.seasonal, items)
fillAchievements(achievements.special, items)
fillAchievements(R.string.basic_achievements, achievements.filter { it.category == "basic" }, items)
fillAchievements(R.string.seasonal_achievements, achievements.filter { it.category == "seasonal" }, items)
fillAchievements(R.string.special_achievements, achievements.filter { it.category == "special" }, items)
val adapter = AchievementAdapter()
val adapter = AchievementProfileAdapter()
adapter.setItemList(items)
val layoutManager = androidx.recyclerview.widget.GridLayoutManager(this, 3)
@ -252,12 +251,12 @@ class FullProfileActivity : BaseActivity() {
stopAndHideProgress(achievementProgress)
}
private fun fillAchievements(achievementGroup: AchievementGroup, targetList: MutableList<Any>) {
private fun fillAchievements(labelID: Int, achievements: List<Achievement>, targetList: MutableList<Any>) {
// Order by ID first
val achievementList = ArrayList(achievementGroup.achievements.values)
val achievementList = ArrayList(achievements)
achievementList.sortWith(Comparator { achievement, t1 -> java.lang.Double.compare(achievement.index.toDouble(), t1.index.toDouble()) })
targetList.add(achievementGroup.label)
targetList.add(getString(labelID))
targetList.addAll(achievementList)
}

View file

@ -0,0 +1,115 @@
package com.habitrpg.android.habitica.ui.adapter
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.facebook.drawee.view.SimpleDraweeView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.extensions.inflate
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
import com.habitrpg.android.habitica.ui.helpers.bindOptionalView
import com.habitrpg.android.habitica.ui.helpers.bindView
class AchievementsAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var useGridLayout: Boolean = false
var entries = listOf<Any>()
var questAchievements = listOf<QuestAchievement>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
0 -> SectionViewHolder(parent.inflate(R.layout.achievement_section_header))
3 -> QuestAchievementViewHolder(parent.inflate(R.layout.achievement_quest_item))
else -> AchievementViewHolder(if (useGridLayout) {
parent.inflate(R.layout.achievement_grid_item)
} else {
parent.inflate(R.layout.achievement_list_item)
})
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when {
entries.size > position -> when (val entry = entries[position]) {
is Achievement -> (holder as? AchievementViewHolder)?.bind(entry)
is Pair<*, *> -> (holder as? SectionViewHolder)?.bind(entry)
}
entries.size == position -> (holder as? SectionViewHolder)?.bind(Pair("Quests completed", questAchievements.size))
else -> (holder as? QuestAchievementViewHolder)?.bind(questAchievements[position - 1 - entries.size])
}
}
override fun getItemCount(): Int {
return entries.size + questAchievements.size + 1
}
override fun getItemViewType(position: Int): Int {
return when {
entries.size > position -> {
val entry = entries[position]
if (entry is Pair<*, *>) {
0
} else {
if (useGridLayout) 1 else 2
}
}
entries.size == position -> 0
else -> 3
}
}
class SectionViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
private val titleView: TextView by bindView(R.id.title)
private val countView: TextView by bindView(R.id.count_label)
fun bind(category: Pair<*, *>) {
titleView.text = category.first as? String
countView.text = category.second.toString()
}
}
class AchievementViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
private var achievement: Achievement? = null
private val achievementContainer: ViewGroup? by bindOptionalView(R.id.achievement_container)
private val achievementIconView: SimpleDraweeView by bindView(R.id.achievement_icon)
private val achievementCountView: TextView by bindView(R.id.achievement_count_label)
private val achievementTitleView: TextView by bindView(R.id.achievement_title)
private val achievementDescriptionView: TextView? by bindOptionalView(R.id.achievement_description)
fun bind(achievement: Achievement) {
this.achievement = achievement
val iconName = if (achievement.earned) {
achievement.icon + "2x"
} else {
"achievement-unearned2x"
}
DataBindingUtils.loadImage(achievementIconView, iconName)
achievementTitleView.text = achievement.title
achievementDescriptionView?.text = achievement.text
if (achievement.optionalCount ?: 0 > 0) {
achievementCountView.visibility = View.VISIBLE
achievementCountView.text = achievement.optionalCount.toString()
} else {
achievementCountView.visibility = View.GONE
}
achievementContainer?.clipToOutline = true
}
}
class QuestAchievementViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
private var achievement: QuestAchievement? = null
private val achievementCountView: TextView by bindView(R.id.achievement_count_label)
private val achievementTitleView: TextView by bindView(R.id.achievement_title)
fun bind(achievement: QuestAchievement) {
this.achievement = achievement
achievementTitleView.text = achievement.title
achievementCountView.text = achievement.count.toString()
}
}
}

View file

@ -20,7 +20,7 @@ import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.viewHolders.SectionViewHolder
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
class AchievementAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class AchievementProfileAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var itemType: String? = null
var activity: MainActivity? = null

View file

@ -0,0 +1,136 @@
package com.habitrpg.android.habitica.ui.fragments
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.*
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.AppComponent
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.ui.adapter.AchievementsAdapter
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.helpers.resetViews
import io.reactivex.functions.Action
import io.reactivex.functions.Consumer
import io.reactivex.rxkotlin.combineLatest
import io.realm.RealmResults
import javax.inject.Inject
class AchievementsFragment: BaseMainFragment(), SwipeRefreshLayout.OnRefreshListener {
@Inject
lateinit var inventoryRepository: InventoryRepository
private var menuID: Int = 0
private lateinit var adapter: AchievementsAdapter
private val layoutManager = GridLayoutManager(activity, 2)
private var useGridLayout = true
set(value) {
field = value
adapter.useGridLayout = value
adapter.notifyDataSetChanged()
}
private val recyclerView: RecyclerView by bindView(R.id.recyclerView)
private val refreshLayout: SwipeRefreshLayout by bindView(R.id.refreshLayout)
override fun injectFragment(component: AppComponent) {
component.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
hidesToolbar = true
super.onCreateView(inflater, container, savedInstanceState)
adapter = AchievementsAdapter()
return inflater.inflate(R.layout.fragment_refresh_recyclerview, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
resetViews()
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
adapter.useGridLayout = useGridLayout
context?.let { recyclerView.background = ColorDrawable(ContextCompat.getColor(it, R.color.white)) }
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (adapter.getItemViewType(position) == 1) {
1
} else {
2
}
}
}
refreshLayout.setOnRefreshListener(this)
compositeSubscription.add(userRepository.getAchievements().subscribe(Consumer<RealmResults<Achievement>> {
val entries = mutableListOf<Any>()
var lastCategory = ""
it.forEach { achievement ->
val categoryIdentifier = achievement.category ?: ""
if (categoryIdentifier != lastCategory) {
val category = Pair(categoryIdentifier, it.count { check ->
check.category == categoryIdentifier && check.earned
})
entries.add(category)
lastCategory = categoryIdentifier
}
entries.add(achievement)
}
adapter.entries = entries
adapter.notifyDataSetChanged()
}, RxErrorHandler.handleEmptyError()))
compositeSubscription.add(userRepository.getQuestAchievements()
.combineLatest(userRepository.getQuestAchievements()
.map { it.mapNotNull { achievement -> achievement.questKey } }
.flatMap { inventoryRepository.getQuestContent(it) })
.subscribeWithErrorHandler(Consumer { result ->
val achievements = result.first.map {achievement ->
val questContent = result.second.firstOrNull { achievement.questKey == it.key }
achievement.title = questContent?.text
achievement
}
adapter.questAchievements = achievements
adapter.notifyDataSetChanged()
}))
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
if (useGridLayout) {
val menuItem = menu?.add(R.string.switch_to_list_view)
menuID = menuItem?.itemId ?: 0
menuItem?.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
menuItem?.setIcon(R.drawable.ic_round_view_list_24px)
} else {
val menuItem = menu?.add(R.string.switch_to_grid_view)
menuID = menuItem?.itemId ?: 0
menuItem?.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
menuItem?.setIcon(R.drawable.ic_round_view_module_24px)
}
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
if (item?.itemId == menuID) {
useGridLayout = !useGridLayout
activity?.invalidateOptionsMenu()
}
return super.onOptionsItemSelected(item)
}
override fun onRefresh() {
compositeSubscription.add(userRepository.retrieveAchievements().subscribe(Consumer {
}, RxErrorHandler.handleEmptyError(), Action { refreshLayout.isRefreshing = false }))
}
}

View file

@ -208,6 +208,7 @@ class NavigationDrawerFragment : DialogFragment() {
items.add(HabiticaDrawerItem(R.id.tasksFragment, SIDEBAR_TASKS, context.getString(R.string.sidebar_tasks)))
items.add(HabiticaDrawerItem(R.id.skillsFragment, SIDEBAR_SKILLS, context.getString(R.string.sidebar_skills)))
items.add(HabiticaDrawerItem(R.id.statsFragment, SIDEBAR_STATS, context.getString(R.string.sidebar_stats)))
items.add(HabiticaDrawerItem(R.id.achievementsFragment, SIDEBAR_ACHIEVEMENTS, context.getString(R.string.sidebar_achievements)))
items.add(HabiticaDrawerItem(0, SIDEBAR_SOCIAL, context.getString(R.string.sidebar_section_social), true))
items.add(HabiticaDrawerItem(R.id.tavernFragment, SIDEBAR_TAVERN, context.getString(R.string.sidebar_tavern), false, false))
items.add(HabiticaDrawerItem(R.id.partyFragment, SIDEBAR_PARTY, context.getString(R.string.sidebar_party)))
@ -327,6 +328,7 @@ class NavigationDrawerFragment : DialogFragment() {
const val SIDEBAR_TASKS = "tasks"
const val SIDEBAR_SKILLS = "skills"
const val SIDEBAR_STATS = "stats"
const val SIDEBAR_ACHIEVEMENTS = "achievements"
const val SIDEBAR_SOCIAL = "social"
const val SIDEBAR_INBOX = "inbox"
const val SIDEBAR_TAVERN = "tavern"

View file

@ -232,15 +232,17 @@ class QuestDetailFragment : BaseMainFragment() {
}
private fun onQuestBegin() {
context?.let {
val alert = HabiticaAlertDialog(it)
val context = context
if (context != null) {
val alert = HabiticaAlertDialog(context)
alert.setMessage(beginQuestMessage)
alert.addButton(R.string.yes, true) { _, _ ->
party.notNull { party ->
socialRepository.forceStartQuest(party)
.subscribe(Consumer { }, RxErrorHandler.handleEmptyError())
}
}
val party = party
if (party != null) {
socialRepository.forceStartQuest(party)
.subscribe(Consumer { }, RxErrorHandler.handleEmptyError())
}
}
alert.addButton(R.string.no, false)
alert.show()
}

View file

@ -183,8 +183,9 @@ class PartyDetailFragment : BaseFragment() {
}
private fun leaveParty() {
activity?.let {
val alert = HabiticaAlertDialog(it)
val context = context
if (context != null) {
val alert = HabiticaAlertDialog(context)
alert.setMessage(R.string.leave_party_confirmation)
alert.addButton(R.string.yes, true) { _, _ ->
viewModel?.leaveGroup { }

View file

@ -166,7 +166,9 @@ open class HabiticaAlertDialog(context: Context) : AlertDialog(context, R.style.
val buttonIndex = buttonsWrapper.childCount
buttonView.setOnClickListener {
weakThis.get()?.let { it1 ->
function?.invoke(it1, buttonIndex)
if (function != null) {
function(it1, buttonIndex)
}
dismiss()
}
}

View file

@ -0,0 +1,33 @@
package com.habitrpg.android.habitica.utils
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.habitrpg.android.habitica.extensions.getAsString
import com.habitrpg.android.habitica.models.Achievement
import java.lang.reflect.Type
class AchievementListDeserializer: JsonDeserializer<List<Achievement>> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): List<Achievement> {
val achievements = mutableListOf<Achievement>()
for (categoryEntry in json?.asJsonObject?.entrySet() ?: emptySet()) {
val categoryIdentifier = categoryEntry.key
for (entry in categoryEntry.value.asJsonObject.getAsJsonObject("achievements").entrySet()) {
var obj = entry.value.asJsonObject
val achievement = Achievement()
achievement.key = entry.key
achievement.category = categoryIdentifier
achievement.earned = obj.get("earned").asBoolean
achievement.title = obj.getAsString("title")
achievement.text = obj.getAsString("text")
achievement.icon = obj.getAsString("icon")
achievement.index = if (obj.has("index")) obj["index"].asInt else 0
achievement.optionalCount = if (obj.has("optionalCount")) obj["optionalCount"].asInt else 0
achievements.add(achievement)
}
}
return achievements
}
}

View file

@ -1,6 +1,5 @@
package com.habitrpg.android.habitica.utils
import android.os.Trace
import com.google.firebase.perf.FirebasePerformance
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
@ -8,6 +7,7 @@ import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import com.google.gson.reflect.TypeToken
import com.habitrpg.android.habitica.models.PushDevice
import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Tag
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.invitations.Invitations
@ -118,12 +118,22 @@ class UserDeserializer : JsonDeserializer<User> {
}
if (obj.has("achievements")) {
if (obj.getAsJsonObject("achievements").has("streak")) {
val achievements = obj.getAsJsonObject("achievements")
if (achievements.has("streak")) {
try {
user.streakCount = obj.getAsJsonObject("achievements").get("streak").asInt
} catch (ignored: UnsupportedOperationException) {
}
}
if (achievements.has("quests")) {
val questAchievements = RealmList<QuestAchievement>()
for (entry in achievements.getAsJsonObject("quests").entrySet()) {
val questAchievement = QuestAchievement()
questAchievement.questKey = entry.key
questAchievement.count = entry.value.asInt
questAchievements.add(questAchievement)
}
user.questAchievements = questAchievements
}
}