mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-05-20 12:49:02 +00:00
Merge branch 'main' of github.com:HabitRPG/habitica-android into main
This commit is contained in:
commit
5f421dc03d
46 changed files with 3563 additions and 2204 deletions
|
|
@ -4,18 +4,37 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chat_empty_textview"
|
||||
<LinearLayout
|
||||
android:id="@+id/chat_empty_container"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:padding="16dp"
|
||||
android:visibility="invisible"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/chat_empty_state"
|
||||
android:textColor="@color/gray200_gray400"
|
||||
android:textSize="16sp" />
|
||||
android:paddingHorizontal="@dimen/spacing_large"
|
||||
android:paddingVertical="@dimen/spacing_medium">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chat_empty_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/chat_empty_state_title"
|
||||
android:textColor="@color/gray100_gray400"
|
||||
android:textSize="17sp"
|
||||
android:fontFamily="sans-serif-medium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chat_empty_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/chat_empty_state_description"
|
||||
android:textColor="@color/gray200_gray400"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.habitrpg.android.habitica.ui.helpers.RecyclerViewEmptySupport
|
||||
android:id="@+id/recyclerView"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/skill_item_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/layout_rounded_bg_window"
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
<color name="purple400_purple500">@color/brand_500</color>
|
||||
<color name="text_green10_green500">@color/green_500</color>
|
||||
<color name="gray100_gray400">@color/gray_400</color>
|
||||
<color name="gray100_gray500">@color/gray_500</color>
|
||||
<color name="gray200_gray400">@color/gray_400</color>
|
||||
<color name="gray600_gray10">@color/gray_10</color>
|
||||
<color name="gray600_gray50">@color/gray_50</color>
|
||||
|
|
@ -70,4 +71,5 @@
|
|||
<color name="maroon100_red100">@color/red_100</color>
|
||||
<color name="brand_button">@color/brand_600</color>
|
||||
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -129,7 +129,8 @@
|
|||
|
||||
<color name="purple400_purple500">@color/brand_400</color>
|
||||
<color name="text_green10_green500">@color/green_10</color>
|
||||
<color name="gray100_gray400">@color/gray_10</color>
|
||||
<color name="gray100_gray400">@color/gray_100</color>
|
||||
<color name="gray100_gray500">@color/gray_100</color>
|
||||
<color name="gray200_gray400">@color/gray_200</color>
|
||||
<color name="gray600_gray10">@color/gray_600</color>
|
||||
<color name="gray600_gray50">@color/gray_600</color>
|
||||
|
|
|
|||
|
|
@ -628,6 +628,8 @@
|
|||
<string name="profile_summary">Edit your public profile.</string>
|
||||
<string name="display_name">Display Name</string>
|
||||
<string name="photo_url">Photo URL</string>
|
||||
<string name="photo_url_description">You can display an image on your Habitica profile for others to see by adding a link to the image here.</string>
|
||||
<string name="save_photo_url">Save Photo URL</string>
|
||||
<string name="login_name">Login Name</string>
|
||||
<string name="about">About</string>
|
||||
<string name="app_settings">App Settings</string>
|
||||
|
|
@ -635,7 +637,9 @@
|
|||
<string name="authentication_summary">Change your authentication options.</string>
|
||||
<string name="change_password">Change Password</string>
|
||||
<string name="change_email">Change Email Address</string>
|
||||
<string name="change_email_description">This is the email address that you use to log in to Habitica, as well as receive notifications.</string>
|
||||
<string name="change_username">Change Username</string>
|
||||
<string name="change_username_description">Usernames must be 1 to 20 characters, containing only letters a to z, numbers 0 to 9, hyphens, or underscores.</string>
|
||||
<string name="change">Change</string>
|
||||
<string name="character_level">Character Level</string>
|
||||
<string name="auto_allocate_points">Auto Allocate Points</string>
|
||||
|
|
@ -763,6 +767,8 @@
|
|||
<string name="username_copied">Username copied to clipboard</string>
|
||||
<string name="verification_pet">One of these Veteran Pets will be waiting for you after you’ve finished confirming!</string>
|
||||
<string name="welcomeNameTitle">What should we call you?</string>
|
||||
<string name="change_display_name">Change Display Name</string>
|
||||
<string name="display_name_description">This is the name that will be displayed for your avatar in Habitica. Unlike username, it does not have to be a unique identifier.</string>
|
||||
<string name="display_name_length_error">Display names must be between 1 and 30 characters</string>
|
||||
<string name="setup_task_join_habitica">Join Habitica (Check me off!)</string>
|
||||
<string name="setup_task_join_habitica_notes">You can either complete this To Do, edit it, or remove it.</string>
|
||||
|
|
@ -813,6 +819,8 @@
|
|||
<string name="old_password">Old Password</string>
|
||||
<string name="new_password">New Password</string>
|
||||
<string name="new_password_repeat">Repeat new Password</string>
|
||||
<string name="password_change_info">Passwords must be 8 characters or more. Changing your password will log you out of any other devices and third-party tools you may use.</string>
|
||||
<string name="confirm_new_password">Confirm new Password</string>
|
||||
<string name="adjust_streak">Adjust Streak</string>
|
||||
<string name="adjust_counter">Adjust Counter</string>
|
||||
<string name="password_changed">Password successfully changed</string>
|
||||
|
|
@ -1041,6 +1049,7 @@
|
|||
<string name="stat_allocation">Stat Allocation</string>
|
||||
<string name="stat_description">All Habitica characters have four stats that affect the gameplay aspects of Habitica.\n\n**Strength (STR)** affects critical hits and raises damage done to a Quest Boss. Warriors and Rogues gain STR from their class equipment.\n\n**Constitution (CON)** raises your HP and makes you take less damage. Healers and Warriors gain CON from their class equipment.\n\n**Intelligence (INT)** raises the amount of EXP you earn and gives you more Mana. Mages and Healers gain INT from their class equipment.\n\n**Perception (PER)** increases the gold you earn and the rate of finding dropped items. Rogues and Mages gain PER from their class equipment.\n\nAfter level 10, you earn 1 Stat Point every level you gain that you can put into any stat you’d like. You can also equip gear that has different combinations of stat boosts.</string>
|
||||
<string name="use_skill">Use Skill</string>
|
||||
<string name="use_on_party">Use on Party</string>
|
||||
|
||||
<string name="standard"> Standard </string>
|
||||
<string name="premium_currency">Premium Currency</string>
|
||||
|
|
@ -1220,6 +1229,8 @@
|
|||
<string name="my_account">My Account</string>
|
||||
<string name="public_profile">Public Profile</string>
|
||||
<string name="about_me">About Me</string>
|
||||
<string name="about_me_description">Add a small blurb about yourself that will appear on your Habitica profile when others view it.</string>
|
||||
<string name="save_about_me">Save About Me</string>
|
||||
<string name="api" translatable="false">API</string>
|
||||
<string name="account_info">Account Info</string>
|
||||
<string name="login_methods">Login Methods</string>
|
||||
|
|
@ -1230,7 +1241,7 @@
|
|||
<string name="connect">Connect</string>
|
||||
<string name="disconnect">Disconnect</string>
|
||||
<string name="add">Add</string>
|
||||
<string name="apitoken_summary">Copy Token. Be careful, this is a password!</string>
|
||||
<string name="apitoken_summary">Password token for developers and third-party tools.</string>
|
||||
<string name="added_social_auth">Added %s authentication</string>
|
||||
<string name="copied_to_clipboard">Copied %s to clipboard</string>
|
||||
<string name="removed_social_auth">Disconnected %s</string>
|
||||
|
|
@ -1565,7 +1576,8 @@
|
|||
<string name="max_gem_cap">Max Gem Cap</string>
|
||||
<string name="max_gem_cap_text">Instantly start at the max Gem Cap</string>
|
||||
<string name="max_gem_cap_text_gift">They instantly start at the Max Gem Cap</string>
|
||||
|
||||
<string name="chat_empty_state_title">Start chatting!</string>
|
||||
<string name="chat_empty_state_description">Remember to be friendly and follow the Community Guidelines.</string>
|
||||
<string name="gem_cap">Gem Cap</string>
|
||||
<string name="get_12_mystic_hourglasses">Get 12 Mystic Hourglasses immediately after your first 12 month subscription!</string>
|
||||
<string name="twelve_mystic_hourglasses">12 Mystic Hourglasses</string>
|
||||
|
|
@ -1582,7 +1594,16 @@
|
|||
<string name="auth_get_credentials_error">Error getting credentials for authentication.</string>
|
||||
<string name="auth_invalid_credentials">Received invalid credentials.</string>
|
||||
<string name="auth_unknown_error">Unknown error during authentication.</string>
|
||||
<string name="chat_empty_state"><b>Start chatting!</b>\nRemember to be friendly and follow the Community Guidelines.</string>
|
||||
<string name="api_token_title">API Token</string>
|
||||
<string name="api_token_is_password">Your API Token is like a password</string>
|
||||
<string name="api_token_is_password_info">
|
||||
Do not share it publicly. You may occasionally be asked for your User ID, but never post your API Token where others can see it, including on Github.
|
||||
</string>
|
||||
<string name="api_token_reset_title">If you need a new API Token</string>
|
||||
<string name="api_token_reset_info">
|
||||
You can change your password to reset it. Once it is reset, you will need to log back in to any other devices you use Habitica on and provide the new API Token to third-party tools you may use.
|
||||
</string>
|
||||
<string name="copy_token">Copy Token</string>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -952,6 +952,21 @@
|
|||
<item name="hintTextAppearance">@style/TaskFormHintTextAppearance</item>
|
||||
</style>
|
||||
|
||||
<style name="SettingTextInputLayoutAppearance" parent="Widget.MaterialComponents.TextInputLayout.FilledBox">
|
||||
<item name="boxBackgroundColor">?colorTintedBackground</item>
|
||||
<item name="boxStrokeColor">@color/gray200_gray400</item>
|
||||
<item name="boxStrokeWidth">2dp</item>
|
||||
<item name="boxStrokeWidthFocused">2dp</item>
|
||||
<item name="android:textColor">?attr/colorPrimaryText</item>
|
||||
<item name="android:textColorHint">?colorPrimaryText</item>
|
||||
<item name="colorControlNormal">?attr/colorPrimary</item>
|
||||
<item name="colorControlActivated">?attr/colorPrimary</item>
|
||||
<item name="colorControlHighlight">?attr/colorPrimary</item>
|
||||
<item name="colorAccent">?attr/colorPrimaryText</item>
|
||||
<item name="colorPrimary">?attr/colorPrimaryText</item>
|
||||
<item name="hintTextAppearance">@style/TaskFormHintTextAppearance</item>
|
||||
</style>
|
||||
|
||||
<style name="TaskFormHintTextAppearance">
|
||||
<item name="android:colorPrimary">?attr/colorPrimaryText</item>
|
||||
<item name="colorPrimary">?attr/colorPrimaryText</item>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ import android.content.res.Configuration
|
|||
import android.content.res.Resources
|
||||
import android.database.DatabaseErrorHandler
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
|
|
@ -33,6 +31,8 @@ import com.habitrpg.android.habitica.extensions.DateUtils
|
|||
import com.habitrpg.android.habitica.helpers.AdHandler
|
||||
import com.habitrpg.android.habitica.helpers.Analytics
|
||||
import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager
|
||||
import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager.Companion.DEVICE_TOKEN_PREFERENCE_KEY
|
||||
import com.habitrpg.android.habitica.models.user.User
|
||||
import com.habitrpg.android.habitica.modules.AuthenticationHandler
|
||||
import com.habitrpg.android.habitica.ui.activities.BaseActivity
|
||||
import com.habitrpg.android.habitica.ui.activities.OnboardingActivity
|
||||
|
|
@ -318,15 +318,26 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
|
|||
realm.close()
|
||||
}
|
||||
|
||||
fun logout(context: Context) {
|
||||
fun logout(context: Context, user: User? = null) {
|
||||
MainScope().launchCatching {
|
||||
getInstance(context)?.pushNotificationManager?.removePushDeviceUsingStoredToken()
|
||||
deleteDatabase(context)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val instance = getInstance(context)
|
||||
val pushManager = instance?.pushNotificationManager
|
||||
val deviceToken = preferences.getString(DEVICE_TOKEN_PREFERENCE_KEY, "") ?: ""
|
||||
|
||||
val useReminder = preferences.getBoolean("use_reminder", false)
|
||||
val reminderTime = preferences.getString("reminder_time", "19:00")
|
||||
val lightMode = preferences.getString("theme_mode", "system")
|
||||
val launchScreen = preferences.getString("launch_screen", "")
|
||||
|
||||
// set the user and refreshed token in the push manager, so we can remove the push device
|
||||
if (deviceToken.isNotEmpty() && user != null) {
|
||||
pushManager?.setUser(user)
|
||||
pushManager?.refreshedToken = deviceToken
|
||||
pushManager?.removePushDeviceUsingStoredToken()
|
||||
}
|
||||
|
||||
deleteDatabase(context)
|
||||
preferences.edit {
|
||||
clear()
|
||||
putBoolean("use_reminder", useReminder)
|
||||
|
|
@ -334,7 +345,9 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
|
|||
putString("theme_mode", lightMode)
|
||||
putString("launch_screen", launchScreen)
|
||||
}
|
||||
getInstance(context)?.lazyApiHelper?.updateAuthenticationCredentials(null, null)
|
||||
pushManager?.clearUser()
|
||||
|
||||
instance?.lazyApiHelper?.updateAuthenticationCredentials(null, null)
|
||||
Wearable.getCapabilityClient(context).removeLocalCapability("provide_auth")
|
||||
startActivity(OnboardingActivity::class.java, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.habitrpg.android.habitica.models.inventory.Equipment;
|
|||
import com.habitrpg.android.habitica.models.inventory.Quest;
|
||||
import com.habitrpg.android.habitica.models.inventory.QuestCollect;
|
||||
import com.habitrpg.android.habitica.models.inventory.QuestDropItem;
|
||||
import com.habitrpg.android.habitica.models.invitations.InviteResponse;
|
||||
import com.habitrpg.android.habitica.models.members.Member;
|
||||
import com.habitrpg.android.habitica.models.social.Challenge;
|
||||
import com.habitrpg.android.habitica.models.social.ChatMessage;
|
||||
|
|
@ -43,6 +44,7 @@ import com.habitrpg.android.habitica.utils.FAQArticleListDeserilializer;
|
|||
import com.habitrpg.android.habitica.utils.FeedResponseDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.FindUsernameResultDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.GroupSerialization;
|
||||
import com.habitrpg.android.habitica.utils.InviteResponseDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.MemberSerialization;
|
||||
import com.habitrpg.android.habitica.utils.NotificationDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.OwnedItemListDeserializer;
|
||||
|
|
@ -136,6 +138,7 @@ public class GSonFactoryCreator {
|
|||
.registerTypeAdapter(assignedDetailsListType, new AssignedDetailsDeserializer())
|
||||
.registerTypeAdapter(Quest.class, new QuestDeserializer())
|
||||
.registerTypeAdapter(Member.class, new MemberSerialization())
|
||||
.registerTypeAdapter(InviteResponse.class, new InviteResponseDeserializer())
|
||||
.registerTypeAdapter(WorldState.class, new WorldStateSerialization())
|
||||
.registerTypeAdapter(FindUsernameResult.class, new FindUsernameResultDeserializer())
|
||||
.registerTypeAdapter(Notification.class, new NotificationDeserializer())
|
||||
|
|
|
|||
|
|
@ -56,12 +56,12 @@ object Analytics {
|
|||
data.putAll(additionalData)
|
||||
}
|
||||
if (eventAction != null) {
|
||||
if (this::amplitude.isInitialized) {
|
||||
executeLambda(AnalyticsTarget.AMPLITUDE) {
|
||||
if (target == null || target == AnalyticsTarget.AMPLITUDE) {
|
||||
amplitude.track(eventAction, data)
|
||||
}
|
||||
}
|
||||
if (this::firebase.isInitialized) {
|
||||
executeLambda(AnalyticsTarget.FIREBASE) {
|
||||
if (target == null || target == AnalyticsTarget.FIREBASE) {
|
||||
firebase.logEvent(eventAction, bundleOf(*data.toList().toTypedArray()))
|
||||
}
|
||||
|
|
@ -94,17 +94,17 @@ object Analytics {
|
|||
sharedPrefs.getString("launch_screen", "")?.let {
|
||||
identify.set("launch_screen", it)
|
||||
}
|
||||
if (this::amplitude.isInitialized) {
|
||||
executeLambda(AnalyticsTarget.AMPLITUDE) {
|
||||
amplitude.identify(identify)
|
||||
}
|
||||
}
|
||||
|
||||
fun setUserID(userID: String) {
|
||||
if (this::amplitude.isInitialized) {
|
||||
executeLambda(AnalyticsTarget.AMPLITUDE) {
|
||||
amplitude.setUserId(userID)
|
||||
}
|
||||
FirebaseCrashlytics.getInstance().setUserId(userID)
|
||||
if (this::firebase.isInitialized) {
|
||||
executeLambda(AnalyticsTarget.FIREBASE) {
|
||||
firebase.setUserId(userID)
|
||||
}
|
||||
}
|
||||
|
|
@ -113,10 +113,10 @@ object Analytics {
|
|||
identifier: String,
|
||||
value: Any?
|
||||
) {
|
||||
if (this::amplitude.isInitialized) {
|
||||
executeLambda(AnalyticsTarget.AMPLITUDE) {
|
||||
amplitude.identify(mapOf(identifier to value))
|
||||
}
|
||||
if (this::firebase.isInitialized) {
|
||||
executeLambda(AnalyticsTarget.FIREBASE) {
|
||||
firebase.setUserProperty(identifier, value?.toString())
|
||||
}
|
||||
}
|
||||
|
|
@ -131,12 +131,21 @@ object Analytics {
|
|||
|
||||
fun setAnalyticsConsent(consents: Boolean?) {
|
||||
val isEnabled = consents == true
|
||||
if (this::firebase.isInitialized) {
|
||||
executeLambda(AnalyticsTarget.FIREBASE) {
|
||||
firebase.setAnalyticsCollectionEnabled(isEnabled)
|
||||
FirebasePerformance.getInstance().isPerformanceCollectionEnabled = isEnabled
|
||||
}
|
||||
if (this::amplitude.isInitialized) {
|
||||
FirebasePerformance.getInstance().isPerformanceCollectionEnabled = isEnabled
|
||||
executeLambda(AnalyticsTarget.AMPLITUDE) {
|
||||
amplitude.configuration.optOut = !isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun executeLambda(analyticsTarget: AnalyticsTarget, action: () -> Unit) {
|
||||
when (analyticsTarget) {
|
||||
AnalyticsTarget.AMPLITUDE -> if (!::amplitude.isInitialized) return
|
||||
AnalyticsTarget.FIREBASE -> if (!::firebase.isInitialized) return
|
||||
}
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,14 @@ class PushNotificationManager(
|
|||
this.user = user
|
||||
}
|
||||
|
||||
fun clearUser() {
|
||||
this.user = null
|
||||
this.refreshedToken = ""
|
||||
sharedPreferences.edit {
|
||||
remove(DEVICE_TOKEN_PREFERENCE_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New installs on Android 13 require
|
||||
* Notification permissions be approved.
|
||||
|
|
@ -131,7 +139,7 @@ class PushNotificationManager(
|
|||
const val GROUP_ACTIVITY_NOTIFICATION_KEY = "groupActivity"
|
||||
const val CONTENT_RELEASE_NOTIFICATION_KEY = "contentRelease"
|
||||
const val G1G1_PROMO_KEY = "g1g1Promo"
|
||||
private const val DEVICE_TOKEN_PREFERENCE_KEY = "device-token-preference"
|
||||
const val DEVICE_TOKEN_PREFERENCE_KEY = "device-token-preference"
|
||||
|
||||
fun displayNotification(
|
||||
remoteMessage: RemoteMessage,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
package com.habitrpg.android.habitica.models.invitations
|
||||
|
||||
class InviteResponse
|
||||
sealed class InviteResponse {
|
||||
data class UserInvite(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val inviter: String
|
||||
) : InviteResponse()
|
||||
|
||||
data class EmailInvite(
|
||||
val email: String
|
||||
) : InviteResponse()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,14 @@ import androidx.activity.viewModels
|
|||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -32,6 +37,7 @@ import androidx.compose.runtime.livedata.observeAsState
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.ViewCompat
|
||||
|
|
@ -186,7 +192,7 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
|
|||
viewModel.updateAllowPushNotifications(false)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !viewModel.sharedPreferences.getBoolean("prompted_exact_scheduling", false)) {
|
||||
val alarmManager = this.getSystemService(Context.ALARM_SERVICE) as? AlarmManager ?: return@registerForActivityResult
|
||||
val alarmManager = this.getSystemService(ALARM_SERVICE) as? AlarmManager ?: return@registerForActivityResult
|
||||
if (!alarmManager.canScheduleExactAlarms()) {
|
||||
val intent = Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
|
||||
intent.setData(Uri.fromParts("package", applicationContext?.packageName, null))
|
||||
|
|
@ -344,69 +350,76 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
|
|||
isMyProfile = true,
|
||||
onAvatarClicked = {
|
||||
showAsBottomSheet { dismiss ->
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(22.dp)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f)
|
||||
.padding(22.dp)
|
||||
) {
|
||||
ComposableAvatarView(
|
||||
avatar = user,
|
||||
configManager = appConfigManager
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(15.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
HabiticaButton(
|
||||
background = HabiticaTheme.colors.tintedUiSub,
|
||||
color = Color.White,
|
||||
modifier = Modifier.height(48.dp),
|
||||
onClick = {
|
||||
dismiss()
|
||||
MainNavigationController.navigate(
|
||||
MainNavDirections.openProfileActivity(
|
||||
user?.id ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
ComposableAvatarView(
|
||||
avatar = user,
|
||||
configManager = appConfigManager
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(15.dp)
|
||||
) {
|
||||
Text(stringResource(id = R.string.open_profile))
|
||||
}
|
||||
|
||||
HabiticaButton(
|
||||
background = HabiticaTheme.colors.tintedUiSub,
|
||||
color = Color.White,
|
||||
modifier = Modifier.height(48.dp),
|
||||
onClick = {
|
||||
dismiss()
|
||||
MainNavigationController.navigate(R.id.avatarOverviewFragment)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(id = R.string.customize_avatar))
|
||||
}
|
||||
|
||||
HabiticaButton(
|
||||
background = HabiticaTheme.colors.tintedUiSub,
|
||||
color = Color.White,
|
||||
modifier = Modifier.height(48.dp),
|
||||
onClick = {
|
||||
dismiss()
|
||||
user?.let {
|
||||
val usecase = ShareAvatarUseCase()
|
||||
lifecycleScope.launchCatching {
|
||||
usecase.callInteractor(
|
||||
ShareAvatarUseCase.RequestValues(
|
||||
this@MainActivity,
|
||||
it,
|
||||
"Check out my avatar on Habitica!",
|
||||
"avatar_bottomsheet"
|
||||
)
|
||||
HabiticaButton(
|
||||
background = HabiticaTheme.colors.tintedUiSub,
|
||||
color = Color.White,
|
||||
modifier = Modifier.height(48.dp),
|
||||
onClick = {
|
||||
dismiss()
|
||||
MainNavigationController.navigate(
|
||||
MainNavDirections.openProfileActivity(
|
||||
user?.id ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(id = R.string.open_profile))
|
||||
}
|
||||
|
||||
HabiticaButton(
|
||||
background = HabiticaTheme.colors.tintedUiSub,
|
||||
color = Color.White,
|
||||
modifier = Modifier.height(48.dp),
|
||||
onClick = {
|
||||
dismiss()
|
||||
MainNavigationController.navigate(R.id.avatarOverviewFragment)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(id = R.string.customize_avatar))
|
||||
}
|
||||
|
||||
HabiticaButton(
|
||||
background = HabiticaTheme.colors.tintedUiSub,
|
||||
color = Color.White,
|
||||
modifier = Modifier.height(48.dp),
|
||||
onClick = {
|
||||
dismiss()
|
||||
user?.let {
|
||||
val usecase = ShareAvatarUseCase()
|
||||
lifecycleScope.launchCatching {
|
||||
usecase.callInteractor(
|
||||
ShareAvatarUseCase.RequestValues(
|
||||
this@MainActivity,
|
||||
it,
|
||||
"Check out my avatar on Habitica!",
|
||||
"avatar_bottomsheet"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(id = R.string.share_avatar))
|
||||
}
|
||||
) {
|
||||
Text(stringResource(id = R.string.share_avatar))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -414,13 +427,18 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
|
|||
},
|
||||
onMemberRowClicked = {
|
||||
showAsBottomSheet { onClose ->
|
||||
val group by viewModel.userViewModel.currentTeamPlanGroup.collectAsState(
|
||||
null
|
||||
)
|
||||
val group by viewModel.userViewModel.currentTeamPlanGroup.collectAsState(null)
|
||||
val members by viewModel.userViewModel.currentTeamPlanMembers.observeAsState()
|
||||
GroupPlanMemberList(members, group, appConfigManager) {
|
||||
onClose()
|
||||
FullProfileActivity.open(it)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
) {
|
||||
GroupPlanMemberList(members, group, appConfigManager) { member ->
|
||||
onClose()
|
||||
FullProfileActivity.open(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -645,8 +663,8 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
|
|||
}
|
||||
preferences?.sound?.let { soundManager.soundTheme = it }
|
||||
|
||||
CrashReporter.setCustomKey("day_start", (user.preferences?.dayStart ?: 0).toString())
|
||||
CrashReporter.setCustomKey("timezone_offset", (user.preferences?.timezoneOffset ?: 0).toString())
|
||||
CrashReporter.setCustomKey("day_start", "${user.preferences?.dayStart ?: 0}")
|
||||
CrashReporter.setCustomKey("timezone_offset", "${user.preferences?.timezoneOffset ?: 0}")
|
||||
Analytics.setAnalyticsConsent(user.preferences?.analyticsConsent)
|
||||
|
||||
displayDeathDialogIfNeeded()
|
||||
|
|
|
|||
|
|
@ -23,10 +23,12 @@ class PrefsActivity :
|
|||
super.onCreate(savedInstanceState)
|
||||
|
||||
setupToolbar(findViewById(R.id.toolbar))
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, PreferencesFragment())
|
||||
.commit()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, PreferencesFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.databinding.SkillListItemBinding
|
||||
import com.habitrpg.android.habitica.databinding.SkillTransformationListItemBinding
|
||||
import com.habitrpg.android.habitica.models.Skill
|
||||
import com.habitrpg.android.habitica.models.user.OwnedItem
|
||||
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
|
||||
|
|
@ -18,7 +19,8 @@ import com.habitrpg.common.habitica.extensions.loadImage
|
|||
import io.realm.RealmList
|
||||
|
||||
class SkillsRecyclerViewAdapter :
|
||||
RecyclerView.Adapter<SkillsRecyclerViewAdapter.SkillViewHolder>() {
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
var onUseSkill: ((Skill) -> Unit)? = null
|
||||
|
||||
var mana: Double = 0.0
|
||||
|
|
@ -26,65 +28,73 @@ class SkillsRecyclerViewAdapter :
|
|||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
var level: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
var specialItems: RealmList<OwnedItem>? = null
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private var skillList: List<Skill> = emptyList()
|
||||
|
||||
fun setSkillList(skillList: List<Skill>) {
|
||||
this.skillList = skillList
|
||||
this.notifyDataSetChanged()
|
||||
companion object {
|
||||
private const val TYPE_NORMAL = 0
|
||||
private const val TYPE_SPECIAL = 1
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): SkillViewHolder {
|
||||
return SkillViewHolder(parent.inflate(R.layout.skill_list_item))
|
||||
fun setSkillList(list: List<Skill>) {
|
||||
skillList = list
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: SkillViewHolder,
|
||||
position: Int
|
||||
) {
|
||||
holder.bind(skillList[position])
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (skillList[position].habitClass == "special") TYPE_SPECIAL
|
||||
else TYPE_NORMAL
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return skillList.size
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return if (viewType == TYPE_SPECIAL) {
|
||||
val view = parent.inflate(R.layout.skill_transformation_list_item)
|
||||
SpecialViewHolder(view)
|
||||
} else {
|
||||
val view = parent.inflate(R.layout.skill_list_item)
|
||||
NormalViewHolder(view)
|
||||
}
|
||||
}
|
||||
|
||||
inner class SkillViewHolder(itemView: View) :
|
||||
RecyclerView.ViewHolder(itemView),
|
||||
View.OnClickListener {
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is SpecialViewHolder -> holder.bind(skillList[position])
|
||||
is NormalViewHolder -> holder.bind(skillList[position])
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = skillList.size
|
||||
|
||||
private inner class NormalViewHolder(itemView: View) :
|
||||
RecyclerView.ViewHolder(itemView), View.OnClickListener {
|
||||
|
||||
private val binding = SkillListItemBinding.bind(itemView)
|
||||
private val magicDrawable: Drawable
|
||||
private val lockDrawable: Drawable
|
||||
|
||||
var skill: Skill? = null
|
||||
|
||||
var context: Context = itemView.context
|
||||
private val context = itemView.context
|
||||
private val magicDrawable: Drawable =
|
||||
BitmapDrawable(context.resources, HabiticaIconsHelper.imageOfMagic())
|
||||
private val lockDrawable: Drawable =
|
||||
BitmapDrawable(
|
||||
context.resources,
|
||||
HabiticaIconsHelper.imageOfLocked(
|
||||
ContextCompat.getColor(context, R.color.text_dimmed)
|
||||
)
|
||||
)
|
||||
private var skill: Skill? = null
|
||||
|
||||
init {
|
||||
binding.buttonWrapper.setOnClickListener(this)
|
||||
magicDrawable = BitmapDrawable(context.resources, HabiticaIconsHelper.imageOfMagic())
|
||||
lockDrawable =
|
||||
BitmapDrawable(
|
||||
context.resources,
|
||||
HabiticaIconsHelper.imageOfLocked(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.text_dimmed
|
||||
)
|
||||
)
|
||||
)
|
||||
binding.skillItemContainer.setOnClickListener(this)
|
||||
}
|
||||
|
||||
fun bind(skill: Skill) {
|
||||
|
|
@ -97,103 +107,79 @@ class SkillsRecyclerViewAdapter :
|
|||
binding.skillNotes.visibility = View.VISIBLE
|
||||
binding.priceLabel.visibility = View.VISIBLE
|
||||
|
||||
if ("special" == skill.habitClass) {
|
||||
binding.countLabel.visibility = View.VISIBLE
|
||||
binding.countLabel.text = getOwnedCount(skill.key).toString()
|
||||
binding.priceLabel.setText(R.string.skill_transformation_use)
|
||||
if (context.isUsingNightModeResources()) {
|
||||
binding.priceLabel.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.brand_500
|
||||
)
|
||||
)
|
||||
} else {
|
||||
binding.priceLabel.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.color_accent
|
||||
)
|
||||
)
|
||||
}
|
||||
binding.buttonIconView.setImageDrawable(null)
|
||||
binding.countLabel.visibility = View.GONE
|
||||
binding.priceLabel.text = skill.mana?.toString()
|
||||
|
||||
val manaColor = if (context.isUsingNightModeResources())
|
||||
R.color.blue_500 else R.color.blue_10
|
||||
binding.priceLabel.setTextColor(ContextCompat.getColor(context, manaColor))
|
||||
|
||||
binding.buttonIconView.setImageDrawable(magicDrawable)
|
||||
|
||||
if ((skill.mana ?: 0) > mana) {
|
||||
binding.buttonWrapper.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.offset_background
|
||||
)
|
||||
ContextCompat.getColor(context, R.color.offset_background)
|
||||
)
|
||||
binding.buttonIconView.alpha = 0.3f
|
||||
binding.priceLabel.alpha = 0.3f
|
||||
} else {
|
||||
binding.buttonWrapper.setBackgroundColor(
|
||||
ContextCompat.getColor(context, R.color.blue_500_24)
|
||||
)
|
||||
binding.buttonIconView.alpha = 1.0f
|
||||
binding.priceLabel.alpha = 1.0f
|
||||
} else {
|
||||
binding.countLabel.visibility = View.GONE
|
||||
binding.priceLabel.text = skill.mana?.toString()
|
||||
if (context.isUsingNightModeResources()) {
|
||||
binding.priceLabel.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.blue_500
|
||||
)
|
||||
)
|
||||
} else {
|
||||
binding.priceLabel.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.blue_10
|
||||
)
|
||||
)
|
||||
}
|
||||
binding.buttonIconView.setImageDrawable(magicDrawable)
|
||||
|
||||
if ((skill.mana ?: 0) > mana) {
|
||||
binding.buttonWrapper.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.offset_background
|
||||
)
|
||||
)
|
||||
binding.buttonIconView.alpha = 0.3f
|
||||
binding.priceLabel.alpha = 0.3f
|
||||
} else {
|
||||
binding.buttonWrapper.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.blue_500_24
|
||||
)
|
||||
)
|
||||
binding.buttonIconView.alpha = 1.0f
|
||||
binding.priceLabel.alpha = 1.0f
|
||||
}
|
||||
if ((skill.lvl ?: 0) > level) {
|
||||
binding.buttonWrapper.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.offset_background
|
||||
)
|
||||
)
|
||||
binding.skillText.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.text_dimmed
|
||||
)
|
||||
)
|
||||
binding.skillText.text = context.getString(R.string.skill_unlocks_at, skill.lvl)
|
||||
binding.skillNotes.visibility = View.GONE
|
||||
binding.buttonIconView.setImageDrawable(lockDrawable)
|
||||
binding.priceLabel.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
if ((skill.lvl ?: 0) > level) {
|
||||
binding.buttonWrapper.setBackgroundColor(
|
||||
ContextCompat.getColor(context, R.color.offset_background)
|
||||
)
|
||||
binding.skillText.setTextColor(
|
||||
ContextCompat.getColor(context, R.color.text_dimmed)
|
||||
)
|
||||
binding.skillText.text = context.getString(
|
||||
R.string.skill_unlocks_at, skill.lvl
|
||||
)
|
||||
binding.skillNotes.visibility = View.GONE
|
||||
binding.buttonIconView.setImageDrawable(lockDrawable)
|
||||
binding.priceLabel.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.skillImage.loadImage("shop_" + skill.key)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if ((skill?.lvl ?: 0) <= level) {
|
||||
skill?.let { onUseSkill?.invoke(it) }
|
||||
skill?.takeIf { (it.lvl ?: 0) <= level }?.also {
|
||||
onUseSkill?.invoke(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOwnedCount(key: String): Int {
|
||||
return specialItems?.firstOrNull { it.key == key }?.numberOwned ?: 0
|
||||
private inner class SpecialViewHolder(itemView: View) :
|
||||
RecyclerView.ViewHolder(itemView), View.OnClickListener {
|
||||
|
||||
private val binding = SkillTransformationListItemBinding.bind(itemView)
|
||||
private val context = itemView.context
|
||||
|
||||
init {
|
||||
binding.specialSkillContainer.setOnClickListener(this)
|
||||
}
|
||||
|
||||
fun bind(skill: Skill) {
|
||||
binding.skillText.text = skill.text
|
||||
binding.skillNotes.text = skill.notes
|
||||
binding.skillText.setTextColor(ContextCompat.getColor(context, R.color.text_primary))
|
||||
binding.skillNotes.setTextColor(ContextCompat.getColor(context, R.color.text_ternary))
|
||||
|
||||
binding.countLabel.text = getOwnedCount(skill.key).toString()
|
||||
binding.skillImage.loadImage("shop_" + skill.key)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
onUseSkill?.invoke(skillList[bindingAdapterPosition])
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOwnedCount(key: String): Int =
|
||||
specialItems?.firstOrNull { it.key == key }?.numberOwned ?: 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
package com.habitrpg.android.habitica.ui.adapter.inventory
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.habitrpg.android.habitica.R
|
||||
|
|
@ -20,10 +24,15 @@ import com.habitrpg.android.habitica.models.inventory.SpecialItem
|
|||
import com.habitrpg.android.habitica.models.user.OwnedItem
|
||||
import com.habitrpg.android.habitica.models.user.OwnedPet
|
||||
import com.habitrpg.android.habitica.models.user.User
|
||||
import com.habitrpg.android.habitica.ui.activities.SkillMemberActivity
|
||||
import com.habitrpg.android.habitica.ui.activities.SkillTasksActivity
|
||||
import com.habitrpg.android.habitica.ui.adapter.BaseRecyclerViewAdapter
|
||||
import com.habitrpg.android.habitica.ui.fragments.skills.SkillDialogBottomSheetFragment
|
||||
import com.habitrpg.android.habitica.ui.menu.BottomSheetMenu
|
||||
import com.habitrpg.android.habitica.ui.menu.BottomSheetMenuItem
|
||||
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
|
||||
import com.habitrpg.android.habitica.ui.views.dialogs.DetailDialog
|
||||
import com.habitrpg.common.habitica.extensions.asPainter
|
||||
import com.habitrpg.common.habitica.extensions.layoutInflater
|
||||
import com.habitrpg.common.habitica.extensions.loadImage
|
||||
import com.habitrpg.common.habitica.extensions.localizedCapitalizeWithSpaces
|
||||
|
|
@ -258,20 +267,19 @@ class ItemRecyclerAdapter(val context: Context) :
|
|||
}
|
||||
} else if (ownedItem?.itemType == "special") {
|
||||
if ((ownedItem?.numberOwned ?: 0) > 0) {
|
||||
menu.addMenuItem(BottomSheetMenuItem(resources.getString(R.string.use_item)))
|
||||
if (item == null && ownedItem != null) {
|
||||
// Special items that are not Mystery Item
|
||||
val specialItem = SpecialItem()
|
||||
ownedItem?.key?.let { key ->
|
||||
specialItem.key = key
|
||||
specialItem.text = key.localizedCapitalizeWithSpaces()
|
||||
}
|
||||
onUseSpecialItem?.invoke(specialItem)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
menu.setSelectionRunnable { index ->
|
||||
if (item == null && ownedItem != null) {
|
||||
// Special items that are not Mystery Item
|
||||
val specialItem = SpecialItem()
|
||||
ownedItem?.key?.let { key ->
|
||||
specialItem.key = key
|
||||
specialItem.text = key.localizedCapitalizeWithSpaces()
|
||||
}
|
||||
onUseSpecialItem?.invoke(specialItem)
|
||||
return@setSelectionRunnable
|
||||
}
|
||||
item?.let { selectedItem ->
|
||||
if (!(selectedItem is QuestContent || selectedItem is SpecialItem || ownedItem?.itemType == "special") && index == 0) {
|
||||
ownedItem?.let { selectedOwnedItem ->
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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.adapter.DiffCallback
|
||||
import com.habitrpg.android.habitica.ui.viewHolders.tasks.BaseTaskViewHolder
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
|
||||
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import com.habitrpg.android.habitica.data.InventoryRepository
|
|||
import com.habitrpg.android.habitica.databinding.FragmentEquipmentDetailBinding
|
||||
import com.habitrpg.android.habitica.helpers.AppConfigManager
|
||||
import com.habitrpg.android.habitica.helpers.ReviewManager
|
||||
import com.habitrpg.android.habitica.models.inventory.Equipment
|
||||
import com.habitrpg.android.habitica.ui.adapter.inventory.EquipmentRecyclerViewAdapter
|
||||
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
|
||||
import com.habitrpg.android.habitica.ui.helpers.KeyboardUtil
|
||||
|
|
@ -80,6 +81,8 @@ class EquipmentDetailFragment :
|
|||
@Inject
|
||||
lateinit var configManager: AppConfigManager
|
||||
|
||||
private var pinnedGearKey: String? = null
|
||||
|
||||
override fun createBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
|
|
@ -133,6 +136,7 @@ class EquipmentDetailFragment :
|
|||
type = args.type
|
||||
isCostume = args.isCostume
|
||||
equippedGear = args.equippedGear
|
||||
pinnedGearKey = equippedGear
|
||||
}
|
||||
binding?.refreshLayout?.setOnRefreshListener(this)
|
||||
binding?.recyclerView?.onRefresh = { onRefresh() }
|
||||
|
|
@ -175,26 +179,29 @@ class EquipmentDetailFragment :
|
|||
binding?.recyclerView?.layoutManager = LinearLayoutManager(mainActivity)
|
||||
binding?.recyclerView?.itemAnimator = SafeDefaultItemAnimator()
|
||||
|
||||
type?.let { type ->
|
||||
type?.let { gearType ->
|
||||
lifecycleScope.launchCatching {
|
||||
inventoryRepository.getOwnedEquipment(type)
|
||||
inventoryRepository.getOwnedEquipment(gearType)
|
||||
.combine(searchedText) { equipment, query ->
|
||||
if (query.isNullOrBlank()) {
|
||||
return@combine equipment
|
||||
}
|
||||
val tokens = query.split(" ")
|
||||
val tokenCount = tokens.size
|
||||
equipment.filter {
|
||||
var matchCount = 0
|
||||
for (token in tokens) {
|
||||
if (it.text.contains(token, true) || it.notes.contains(token, true)) {
|
||||
matchCount += 1
|
||||
if (query.isNullOrBlank()) equipment
|
||||
else {
|
||||
val tokens = query.split(" ")
|
||||
equipment.filter { gear ->
|
||||
tokens.all { token ->
|
||||
gear.text.contains(token, true) || gear.notes.contains(token, true)
|
||||
}
|
||||
}
|
||||
return@filter matchCount == tokenCount
|
||||
}
|
||||
}
|
||||
.map { it.sortedBy { equipment -> equipment.text } }
|
||||
.map { list ->
|
||||
val sorted = list.sortedBy { it.text }
|
||||
pinnedGearKey?.let { key ->
|
||||
sorted.sortedWith(
|
||||
compareBy<Equipment> { it.key != key }
|
||||
.thenBy { it.text }
|
||||
)
|
||||
} ?: sorted
|
||||
}
|
||||
.collect { adapter.data = it }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.habitrpg.android.habitica.helpers.EventCategory
|
|||
import com.habitrpg.android.habitica.helpers.HitType
|
||||
import com.habitrpg.android.habitica.helpers.ReviewManager
|
||||
import com.habitrpg.android.habitica.interactors.HatchPetUseCase
|
||||
import com.habitrpg.android.habitica.models.Skill
|
||||
import com.habitrpg.android.habitica.models.inventory.Egg
|
||||
import com.habitrpg.android.habitica.models.inventory.Food
|
||||
import com.habitrpg.android.habitica.models.inventory.HatchingPotion
|
||||
|
|
@ -38,18 +39,22 @@ import com.habitrpg.android.habitica.ui.activities.MainActivity
|
|||
import com.habitrpg.android.habitica.ui.activities.SkillMemberActivity
|
||||
import com.habitrpg.android.habitica.ui.adapter.inventory.ItemRecyclerAdapter
|
||||
import com.habitrpg.android.habitica.ui.fragments.BaseFragment
|
||||
import com.habitrpg.android.habitica.ui.fragments.skills.SkillDialogBottomSheetFragment
|
||||
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
|
||||
import com.habitrpg.android.habitica.ui.menu.BottomSheetMenuItem
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
|
||||
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
|
||||
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
|
||||
import com.habitrpg.android.habitica.ui.views.dialogs.OpenedMysteryitemDialog
|
||||
import com.habitrpg.common.habitica.extensions.loadImage
|
||||
import com.habitrpg.common.habitica.extensions.localizedCapitalizeWithSpaces
|
||||
import com.habitrpg.common.habitica.extensions.observeOnce
|
||||
import com.habitrpg.common.habitica.helpers.EmptyItem
|
||||
import com.habitrpg.common.habitica.helpers.ExceptionHandler
|
||||
import com.habitrpg.common.habitica.helpers.MainNavigationController
|
||||
import com.habitrpg.common.habitica.helpers.launchCatching
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -87,6 +92,7 @@ class ItemRecyclerFragment :
|
|||
var transformationItems: MutableList<OwnedItem> = mutableListOf()
|
||||
var itemTypeText: String? = null
|
||||
private var selectedSpecialItem: SpecialItem? = null
|
||||
private var specialSkills: MutableList<Skill> = mutableListOf()
|
||||
internal var layoutManager: androidx.recyclerview.widget.LinearLayoutManager? = null
|
||||
|
||||
override var binding: FragmentItemsBinding? = null
|
||||
|
|
@ -113,6 +119,7 @@ class ItemRecyclerFragment :
|
|||
this.itemType = savedInstanceState.getString(ITEM_TYPE_KEY, "")
|
||||
this.itemTypeText = savedInstanceState.getString(ITEM_TYPE_TEXT_KEY, "")
|
||||
}
|
||||
getSpecialSkills()
|
||||
|
||||
binding?.refreshLayout?.setOnRefreshListener(this)
|
||||
val buttonMethod = {
|
||||
|
|
@ -170,6 +177,24 @@ class ItemRecyclerFragment :
|
|||
this.loadItems()
|
||||
}
|
||||
|
||||
private fun getSpecialSkills() {
|
||||
// Get special skills for description of special items
|
||||
lifecycleScope.launchCatching {
|
||||
val user = userViewModel.user.value ?: return@launchCatching
|
||||
userRepository.getSkills(user)
|
||||
.combine(userRepository.getSpecialItems(user)) { skills, items ->
|
||||
val allEntries = mutableListOf<Skill>()
|
||||
for (skill in skills) {
|
||||
allEntries.add(skill)
|
||||
}
|
||||
for (item in items) {
|
||||
allEntries.add(item)
|
||||
}
|
||||
return@combine allEntries
|
||||
}.collect { skills -> specialSkills = skills }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAdapter() {
|
||||
val context = activity
|
||||
|
||||
|
|
@ -180,7 +205,23 @@ class ItemRecyclerFragment :
|
|||
}
|
||||
binding?.recyclerView?.adapter = adapter
|
||||
}
|
||||
adapter?.onUseSpecialItem = { onSpecialItemSelected(it) }
|
||||
adapter?.onUseSpecialItem = { specialItem ->
|
||||
val specialSkill = specialSkills.find { it.key == specialItem.key }
|
||||
if (specialSkill != null) {
|
||||
val skillIdentifier = "shop_"
|
||||
val bottomSheet = SkillDialogBottomSheetFragment.newInstance(
|
||||
skillTitle = specialSkill.text,
|
||||
skillDescription = specialSkill.notes ?: "",
|
||||
skillKey = specialSkill.key,
|
||||
skillPath = skillIdentifier,
|
||||
isTransformationItem = true,
|
||||
onUseSkill = {
|
||||
onSpecialItemSelected(specialItem)
|
||||
}
|
||||
)
|
||||
bottomSheet.show(childFragmentManager, "SkillDialogBottomSheet")
|
||||
}
|
||||
}
|
||||
adapter?.onSellItem = { item, ownedItem ->
|
||||
showSellItemConfirmation(item, ownedItem)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ import android.content.Intent
|
|||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
|
|
@ -23,13 +26,21 @@ import com.habitrpg.android.habitica.R
|
|||
import com.habitrpg.android.habitica.data.ApiClient
|
||||
import com.habitrpg.android.habitica.extensions.addCancelButton
|
||||
import com.habitrpg.android.habitica.extensions.addCloseButton
|
||||
import com.habitrpg.android.habitica.extensions.addOkButton
|
||||
import com.habitrpg.android.habitica.models.user.User
|
||||
import com.habitrpg.android.habitica.ui.activities.FixCharacterValuesActivity
|
||||
import com.habitrpg.android.habitica.ui.fragments.preferences.HabiticaAccountDialog.AccountUpdateConfirmed
|
||||
import com.habitrpg.android.habitica.ui.helpers.KeyboardUtil
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.AuthenticationViewModel
|
||||
import com.habitrpg.android.habitica.ui.views.AboutMeScreen
|
||||
import com.habitrpg.android.habitica.ui.views.ApiTokenBottomSheet
|
||||
import com.habitrpg.android.habitica.ui.views.ChangeDisplayNameScreen
|
||||
import com.habitrpg.android.habitica.ui.views.ChangeEmailScreen
|
||||
import com.habitrpg.android.habitica.ui.views.ChangePasswordScreen
|
||||
import com.habitrpg.android.habitica.ui.views.ChangeUsernameScreen
|
||||
import com.habitrpg.android.habitica.ui.views.ExtraLabelPreference
|
||||
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
|
||||
import com.habitrpg.android.habitica.ui.views.PhotoUrlScreen
|
||||
import com.habitrpg.android.habitica.ui.views.SnackbarActivity
|
||||
import com.habitrpg.android.habitica.ui.views.ValidatingEditText
|
||||
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
|
||||
|
|
@ -155,13 +166,13 @@ class AccountPreferenceFragment :
|
|||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
when (preference.key) {
|
||||
"username" -> showLoginNameDialog()
|
||||
"username" -> showChangeUsernameDialog()
|
||||
"confirm_username" -> showConfirmUsernameDialog()
|
||||
"email" -> {
|
||||
if (user?.authentication?.hasPassword != true && user?.authentication?.localAuthentication?.email?.isNotBlank() != true) {
|
||||
showAddPasswordDialog(true)
|
||||
} else {
|
||||
showEmailDialog()
|
||||
showChangeEmailDialog()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -179,25 +190,15 @@ class AccountPreferenceFragment :
|
|||
}
|
||||
|
||||
"APIToken" -> {
|
||||
copyValue(getString(R.string.SP_APIToken_title), hostConfig.apiKey)
|
||||
ApiTokenBottomSheetFragment.newInstance(hostConfig.apiKey).show(childFragmentManager, ApiTokenBottomSheetFragment.TAG)
|
||||
return true
|
||||
}
|
||||
|
||||
"display_name" ->
|
||||
updateUser(
|
||||
"profile.name",
|
||||
user?.profile?.name,
|
||||
getString(R.string.display_name),
|
||||
)
|
||||
"display_name" -> showChangeDisplayNameDialog()
|
||||
|
||||
"photo_url" ->
|
||||
updateUser(
|
||||
"profile.imageUrl",
|
||||
user?.profile?.imageUrl,
|
||||
getString(R.string.photo_url),
|
||||
)
|
||||
"photo_url" -> showPhotoUrlDialog()
|
||||
|
||||
"about" -> updateUser("profile.blurb", user?.profile?.blurb, getString(R.string.about))
|
||||
"about" -> showAboutMeDialog()
|
||||
"google_auth" -> {
|
||||
if (user?.authentication?.hasGoogleAuth == true) {
|
||||
disconnect("google", "Google")
|
||||
|
|
@ -256,61 +257,173 @@ class AccountPreferenceFragment :
|
|||
)
|
||||
}
|
||||
|
||||
private fun updateUser(
|
||||
path: String,
|
||||
value: String?,
|
||||
title: String,
|
||||
) {
|
||||
showSingleEntryDialog(value, title) {
|
||||
if (value != it) {
|
||||
lifecycleScope.launchCatching {
|
||||
userRepository.updateUser(path, it ?: "")
|
||||
private fun showChangePasswordDialog() {
|
||||
val sheet = SettingsFormBottomSheet()
|
||||
|
||||
sheet.content = {
|
||||
ChangePasswordScreen(
|
||||
onBack = { sheet.dismiss() },
|
||||
onSave = { oldPassword, newPassword ->
|
||||
lifecycleScope.launchCatching {
|
||||
KeyboardUtil.dismissKeyboard(activity)
|
||||
val response = userRepository.updatePassword(
|
||||
oldPassword, newPassword, newPassword
|
||||
)
|
||||
response?.apiToken?.let {
|
||||
viewModel.saveTokens(it, user?.id ?: "")
|
||||
sheet.dismiss()
|
||||
}
|
||||
}
|
||||
},
|
||||
onForgot = {
|
||||
showForgotPasswordDialog()
|
||||
sheet.dismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
sheet.show(childFragmentManager, SettingsFormBottomSheet.TAG)
|
||||
}
|
||||
|
||||
private fun showChangePasswordDialog() {
|
||||
val inflater = context?.layoutInflater
|
||||
val view = inflater?.inflate(R.layout.dialog_edittext_change_pw, null)
|
||||
val oldPasswordEditText =
|
||||
view?.findViewById<ValidatingEditText>(R.id.old_password_edit_text)
|
||||
val passwordEditText = view?.findViewById<ValidatingEditText>(R.id.new_password_edit_text)
|
||||
passwordEditText?.validator = { (it?.length ?: 0) >= 8 }
|
||||
passwordEditText?.errorText = getString(R.string.password_too_short, 8)
|
||||
val passwordRepeatEditText =
|
||||
view?.findViewById<ValidatingEditText>(R.id.new_password_repeat_edit_text)
|
||||
passwordRepeatEditText?.validator = { it == passwordEditText?.text }
|
||||
passwordRepeatEditText?.errorText = getString(R.string.password_not_matching)
|
||||
context?.let { context ->
|
||||
val dialog = HabiticaAlertDialog(context)
|
||||
dialog.setTitle(R.string.change_password)
|
||||
dialog.addButton(R.string.change, true, false, false) { d, _ ->
|
||||
KeyboardUtil.dismissKeyboard(activity)
|
||||
passwordEditText?.showErrorIfNecessary()
|
||||
passwordRepeatEditText?.showErrorIfNecessary()
|
||||
if (passwordEditText?.isValid != true || passwordRepeatEditText?.isValid != true) return@addButton
|
||||
lifecycleScope.launchCatching {
|
||||
val response = userRepository.updatePassword(
|
||||
oldPasswordEditText?.text ?: "",
|
||||
passwordEditText.text ?: "",
|
||||
passwordRepeatEditText.text ?: "",
|
||||
)
|
||||
response?.apiToken?.let {
|
||||
viewModel.saveTokens(it, user?.id ?: "")
|
||||
private fun showChangeEmailDialog() {
|
||||
val sheet = SettingsFormBottomSheet()
|
||||
sheet.content = {
|
||||
ChangeEmailScreen(
|
||||
initialEmail = user?.authentication?.localAuthentication?.email ?: "",
|
||||
onBack = { sheet.dismiss() },
|
||||
onSave = { newEmail, password ->
|
||||
lifecycleScope.launchCatching {
|
||||
KeyboardUtil.dismissKeyboard(activity)
|
||||
userRepository.updateEmail(
|
||||
newEmail,
|
||||
password,
|
||||
)
|
||||
lifecycleScope.launch(ExceptionHandler.coroutine()) {
|
||||
userRepository.retrieveUser(true, true)
|
||||
}
|
||||
configurePreference(findPreference("email"), newEmail)
|
||||
sheet.dismiss()
|
||||
}
|
||||
(activity as? SnackbarActivity)?.showSnackbar(
|
||||
content = context.getString(R.string.password_changed),
|
||||
displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS,
|
||||
)
|
||||
},
|
||||
onForgotPassword = {
|
||||
showForgotPasswordDialog()
|
||||
sheet.dismiss()
|
||||
}
|
||||
d.dismiss()
|
||||
}
|
||||
dialog.addCancelButton()
|
||||
dialog.setAdditionalContentView(view)
|
||||
dialog.setAdditionalContentSidePadding(12)
|
||||
dialog.show()
|
||||
)
|
||||
}
|
||||
sheet.show(childFragmentManager, SettingsFormBottomSheet.TAG)
|
||||
}
|
||||
|
||||
private fun showChangeUsernameDialog() {
|
||||
val sheet = SettingsFormBottomSheet()
|
||||
sheet.content = {
|
||||
ChangeUsernameScreen(
|
||||
initial = user?.username ?: "",
|
||||
onBack = { sheet.dismiss() },
|
||||
onSave = { newUsername ->
|
||||
lifecycleScope.launchCatching {
|
||||
KeyboardUtil.dismissKeyboard(activity)
|
||||
if (!newUsername.contains(" ") && newUsername.length > 1 && newUsername.length < 20 && !newUsername.contains(regex)) {
|
||||
val user = userRepository.updateLoginName(newUsername ?: "")
|
||||
if (user == null || user.username != newUsername) {
|
||||
userRepository.retrieveUser(false, forced = true)
|
||||
}
|
||||
}
|
||||
sheet.dismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
sheet.show(childFragmentManager, SettingsFormBottomSheet.TAG)
|
||||
}
|
||||
|
||||
private fun showChangeDisplayNameDialog() {
|
||||
val sheet = SettingsFormBottomSheet()
|
||||
sheet.content = {
|
||||
ChangeDisplayNameScreen(
|
||||
initial = user?.profile?.name ?: "",
|
||||
onBack = { sheet.dismiss() },
|
||||
onSave = { newDisplayName ->
|
||||
lifecycleScope.launchCatching {
|
||||
KeyboardUtil.dismissKeyboard(activity)
|
||||
userRepository.updateUser("profile.name", newDisplayName)
|
||||
sheet.dismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
sheet.show(childFragmentManager, SettingsFormBottomSheet.TAG)
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun showForgotPasswordDialog() {
|
||||
val input = EditText(requireContext())
|
||||
input.setAutofillHints(EditText.AUTOFILL_HINT_EMAIL_ADDRESS)
|
||||
input.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
||||
input.hint = getString(R.string.forgot_password_hint_example)
|
||||
input.textSize = 16f
|
||||
val lp =
|
||||
LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
input.layoutParams = lp
|
||||
val alertDialog = HabiticaAlertDialog(requireContext())
|
||||
alertDialog.setTitle(R.string.forgot_password_title)
|
||||
alertDialog.setMessage(R.string.forgot_password_description)
|
||||
alertDialog.setAdditionalContentView(input)
|
||||
alertDialog.addButton(R.string.send, true) { _, _ ->
|
||||
lifecycleScope.launchCatching {
|
||||
userRepository.sendPasswordResetEmail(input.text.toString())
|
||||
showPasswordEmailConfirmation()
|
||||
}
|
||||
}
|
||||
alertDialog.addCancelButton()
|
||||
alertDialog.show()
|
||||
}
|
||||
|
||||
private fun showAboutMeDialog() {
|
||||
val sheet = SettingsFormBottomSheet()
|
||||
sheet.content = {
|
||||
AboutMeScreen(
|
||||
initial = user?.profile?.blurb.orEmpty(),
|
||||
onBack = { sheet.dismiss() },
|
||||
onSave = { about ->
|
||||
lifecycleScope.launchCatching {
|
||||
userRepository.updateUser("profile.blurb", about)
|
||||
sheet.dismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
sheet.show(childFragmentManager, SettingsFormBottomSheet.TAG)
|
||||
}
|
||||
|
||||
|
||||
private fun showPhotoUrlDialog() {
|
||||
val sheet = SettingsFormBottomSheet()
|
||||
sheet.content = {
|
||||
PhotoUrlScreen(
|
||||
initial = user?.profile?.imageUrl ?: "",
|
||||
onBack = { sheet.dismiss() },
|
||||
onSave = { photoUrl ->
|
||||
lifecycleScope.launchCatching {
|
||||
KeyboardUtil.dismissKeyboard(activity)
|
||||
userRepository.updateUser("profile.imageUrl", photoUrl)
|
||||
sheet.dismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
sheet.show(childFragmentManager, SettingsFormBottomSheet.TAG)
|
||||
}
|
||||
|
||||
private fun showPasswordEmailConfirmation() {
|
||||
val alert = HabiticaAlertDialog(requireContext())
|
||||
alert.setMessage(R.string.forgot_password_confirmation)
|
||||
alert.addOkButton()
|
||||
alert.show()
|
||||
}
|
||||
|
||||
private fun showAddPasswordDialog(showEmail: Boolean) {
|
||||
|
|
@ -363,44 +476,6 @@ class AccountPreferenceFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun showEmailDialog() {
|
||||
val inflater = context?.layoutInflater
|
||||
val view = inflater?.inflate(R.layout.dialog_edittext_confirm_pw, null)
|
||||
val emailEditText = view?.findViewById<ValidatingEditText>(R.id.email_edit_text)
|
||||
emailEditText?.text = user?.authentication?.localAuthentication?.email
|
||||
emailEditText?.validator = { PatternsCompat.EMAIL_ADDRESS.matcher(it ?: "").matches() }
|
||||
emailEditText?.errorText = getString(R.string.email_invalid)
|
||||
emailEditText?.hint = context?.getString(R.string.email)
|
||||
val passwordEditText = view?.findViewById<ValidatingEditText>(R.id.password_edit_text)
|
||||
if (user?.authentication?.hasPassword != true) {
|
||||
passwordEditText?.isVisible = false
|
||||
}
|
||||
context?.let { context ->
|
||||
val dialog = HabiticaAlertDialog(context)
|
||||
dialog.setTitle(R.string.change_email)
|
||||
dialog.addButton(R.string.change, true, false, false) { _, _ ->
|
||||
KeyboardUtil.dismissKeyboard(activity)
|
||||
emailEditText?.showErrorIfNecessary()
|
||||
if (emailEditText?.isValid != true) return@addButton
|
||||
lifecycleScope.launchCatching {
|
||||
userRepository.updateEmail(
|
||||
emailEditText.text.toString(),
|
||||
passwordEditText?.text.toString(),
|
||||
)
|
||||
lifecycleScope.launch(ExceptionHandler.coroutine()) {
|
||||
userRepository.retrieveUser(true, true)
|
||||
}
|
||||
configurePreference(findPreference("email"), emailEditText.text.toString())
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
dialog.addCancelButton()
|
||||
dialog.setAdditionalContentView(view)
|
||||
dialog.setAdditionalContentSidePadding(12.dpToPx(context))
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
private val regex = "[^a-zA-Z0-9_-]".toRegex()
|
||||
|
||||
private fun showLoginNameDialog() {
|
||||
|
|
@ -486,7 +561,10 @@ class AccountPreferenceFragment :
|
|||
userRepository.deleteAccount(password)
|
||||
dialog?.dismiss()
|
||||
accountDialog.dismiss()
|
||||
context?.let { HabiticaBaseApplication.logout(it) }
|
||||
context?.let {
|
||||
val user = userViewModel.user.value
|
||||
HabiticaBaseApplication.logout(it, user)
|
||||
}
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package com.habitrpg.android.habitica.ui.fragments.preferences
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.ui.views.ApiTokenBottomSheet
|
||||
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
|
||||
import com.habitrpg.android.habitica.ui.views.SnackbarActivity
|
||||
|
||||
class ApiTokenBottomSheetFragment : BottomSheetDialogFragment() {
|
||||
private lateinit var apiToken: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
apiToken = arguments?.getString(ARG_API_TOKEN) ?: ""
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
val nightModeFlags = requireContext()
|
||||
.resources
|
||||
.configuration
|
||||
.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
|
||||
if (nightModeFlags == Configuration.UI_MODE_NIGHT_NO) {
|
||||
dialog?.window?.let { window ->
|
||||
window.statusBarColor = ContextCompat.getColor(
|
||||
requireContext(),
|
||||
android.R.color.transparent
|
||||
)
|
||||
window.navigationBarColor = ContextCompat.getColor(
|
||||
requireContext(),
|
||||
android.R.color.transparent
|
||||
)
|
||||
|
||||
WindowInsetsControllerCompat(window, window.decorView).apply {
|
||||
isAppearanceLightStatusBars = true
|
||||
isAppearanceLightNavigationBars = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
ApiTokenBottomSheet(apiToken = apiToken, onCopyToken = { copiedToken ->
|
||||
(activity as? SnackbarActivity)?.showSnackbar(
|
||||
content = getString(R.string.copied_to_clipboard, copiedToken),
|
||||
displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS,
|
||||
)
|
||||
dismiss()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_API_TOKEN = "arg_api_token"
|
||||
fun newInstance(apiToken: String): ApiTokenBottomSheetFragment =
|
||||
ApiTokenBottomSheetFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(ARG_API_TOKEN, apiToken)
|
||||
}
|
||||
}
|
||||
|
||||
const val TAG = "ApiTokenBottomSheet"
|
||||
}
|
||||
}
|
||||
|
|
@ -251,7 +251,7 @@ class PreferencesFragment :
|
|||
val dialog = HabiticaAlertDialog(context)
|
||||
dialog.setTitle(R.string.are_you_sure)
|
||||
dialog.addButton(R.string.logout, true) { _, _ ->
|
||||
HabiticaBaseApplication.logout(context)
|
||||
HabiticaBaseApplication.logout(context, user)
|
||||
activity?.finish()
|
||||
}
|
||||
dialog.addCancelButton()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
package com.habitrpg.android.habitica.ui.fragments.preferences
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.habitrpg.common.habitica.theme.HabiticaTheme
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
|
||||
class SettingsFormBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
var content: @Composable () -> Unit = {}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// this is a workaround to prevent the screen from appearing blank during config changes (Light/Dark mode change for example)
|
||||
retainInstance = true
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val nightModeFlags = requireContext()
|
||||
.resources
|
||||
.configuration
|
||||
.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
|
||||
if (nightModeFlags == Configuration.UI_MODE_NIGHT_NO) {
|
||||
dialog?.window?.apply {
|
||||
statusBarColor = Color.TRANSPARENT
|
||||
navigationBarColor = Color.TRANSPARENT
|
||||
WindowInsetsControllerCompat(this, decorView).apply {
|
||||
isAppearanceLightStatusBars = true
|
||||
isAppearanceLightNavigationBars = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog?.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
|
||||
?.let { sheet ->
|
||||
sheet.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
BottomSheetBehavior.from(sheet).apply {
|
||||
state = BottomSheetBehavior.STATE_EXPANDED
|
||||
isDraggable = false
|
||||
skipCollapsed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
HabiticaTheme {
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) { visible = true }
|
||||
AnimatedVisibility(visible = visible, enter = fadeIn()) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "SettingsFormBottomSheet"
|
||||
}
|
||||
}
|
||||
|
|
@ -21,12 +21,13 @@ class SkillDialogBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
private const val ARG_SKILL_MP_COST = "skill_mp_cost"
|
||||
|
||||
fun newInstance(
|
||||
skillTitle: String,
|
||||
skillDescription: String,
|
||||
skillMpCost: String,
|
||||
skillPath: String,
|
||||
skillKey: String,
|
||||
resourceIcon: Drawable,
|
||||
skillTitle: String? = "",
|
||||
skillDescription: String? = "",
|
||||
skillMpCost: String? = "",
|
||||
skillPath: String? = "",
|
||||
skillKey: String? = "",
|
||||
resourceIcon: Drawable? = null,
|
||||
isTransformationItem: Boolean = false,
|
||||
onUseSkill: () -> Unit
|
||||
): SkillDialogBottomSheetFragment {
|
||||
return SkillDialogBottomSheetFragment().apply {
|
||||
|
|
@ -36,9 +37,10 @@ class SkillDialogBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
putString(ARG_SKILL_MP_COST, skillMpCost)
|
||||
}
|
||||
this.resourceIcon = resourceIcon
|
||||
this.skillKey = skillKey
|
||||
this.skillPath = skillPath
|
||||
this.skillKey = skillKey ?: ""
|
||||
this.skillPath = skillPath ?: ""
|
||||
this.onUseSkill = onUseSkill
|
||||
this.isTransformationItem = isTransformationItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +49,7 @@ class SkillDialogBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
private var resourceIcon: Drawable? = null
|
||||
var skillKey = ""
|
||||
var skillPath = ""
|
||||
var isTransformationItem: Boolean = false
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
|
@ -62,6 +65,7 @@ class SkillDialogBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
title = requireArguments().getString(ARG_SKILL_TITLE) ?: "",
|
||||
description = requireArguments().getString(ARG_SKILL_DESCRIPTION) ?: "",
|
||||
mpCost = requireArguments().getString(ARG_SKILL_MP_COST) ?: "",
|
||||
isTransformationItem = isTransformationItem,
|
||||
onUseSkill = {
|
||||
onUseSkill?.invoke()
|
||||
dismiss()
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ class SkillsFragment : BaseMainFragment<FragmentRecyclerviewBinding>() {
|
|||
val context = context ?: return
|
||||
val resourceIconDrawable: Drawable = HabiticaIconsHelper.imageOfMagic().toDrawable(context.resources)
|
||||
val skillIdentifier = "shop_"
|
||||
val isTransformationItem = skill.habitClass == "special"
|
||||
|
||||
val bottomSheet = SkillDialogBottomSheetFragment.newInstance(
|
||||
skillTitle = skill.text,
|
||||
|
|
@ -114,6 +115,7 @@ class SkillsFragment : BaseMainFragment<FragmentRecyclerviewBinding>() {
|
|||
skillPath = skillIdentifier,
|
||||
skillMpCost = "${skill.mana?.toInt() ?: 0} MP",
|
||||
resourceIcon = resourceIconDrawable,
|
||||
isTransformationItem = isTransformationItem,
|
||||
onUseSkill = {
|
||||
when {
|
||||
"special" == skill.habitClass -> {
|
||||
|
|
@ -133,8 +135,6 @@ class SkillsFragment : BaseMainFragment<FragmentRecyclerviewBinding>() {
|
|||
}
|
||||
)
|
||||
bottomSheet.show(childFragmentManager, "SkillDialogBottomSheet")
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun displaySkillResult(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
|
|
@ -140,11 +142,41 @@ open class ChatFragment : BaseFragment<FragmentChatBinding>() {
|
|||
}
|
||||
}
|
||||
}
|
||||
binding?.chatBarView?.let { applyScrollContentWindowInsets(it) }
|
||||
|
||||
binding?.root.apply {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this!!) { _, insets ->
|
||||
val ime = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||
val nav = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
||||
|
||||
binding?.chatBarView?.translationY = -ime.toFloat()
|
||||
binding?.chatBarView?.setPadding(
|
||||
binding?.chatBarView!!.paddingLeft,
|
||||
binding?.chatBarView!!.paddingTop,
|
||||
binding?.chatBarView!!.paddingRight,
|
||||
nav
|
||||
)
|
||||
|
||||
|
||||
binding?.recyclerView?.translationY = -ime.toFloat()
|
||||
|
||||
|
||||
binding?.recyclerView?.setPadding(
|
||||
binding?.recyclerView!!.paddingLeft,
|
||||
binding?.recyclerView!!.paddingTop,
|
||||
binding?.recyclerView!!.paddingRight,
|
||||
ime + nav
|
||||
)
|
||||
|
||||
insets
|
||||
}
|
||||
ViewCompat.requestApplyInsets(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding?.root?.let { ViewCompat.requestApplyInsets(it) }
|
||||
setNavigatedToFragment()
|
||||
}
|
||||
|
||||
|
|
@ -224,10 +256,10 @@ open class ChatFragment : BaseFragment<FragmentChatBinding>() {
|
|||
|
||||
if (chatMessages.isEmpty()) {
|
||||
binding?.recyclerView?.state = RecyclerViewState.EMPTY
|
||||
binding?.chatEmptyTextview?.fadeInAnimation()
|
||||
binding?.chatEmptyContainer?.fadeInAnimation()
|
||||
} else {
|
||||
binding?.recyclerView?.state = RecyclerViewState.DISPLAYING_DATA
|
||||
binding?.chatEmptyTextview?.isGone = true
|
||||
binding?.chatEmptyContainer?.isGone = true
|
||||
}
|
||||
|
||||
viewModel.gotNewMessages = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.habitrpg.android.habitica.ui.fragments.social.party
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
|
@ -72,6 +73,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import kotlinx.coroutines.delay
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
|
|
@ -279,14 +281,25 @@ fun PartyInviteView(
|
|||
inviteButtonState = LoadingButtonState.CONTENT
|
||||
}
|
||||
}) {
|
||||
val responses = viewModel.sendInvites()
|
||||
if ((responses?.size ?: 0) > 0) {
|
||||
val responses: List<InviteResponse>? = viewModel.sendInvites()
|
||||
if (!responses.isNullOrEmpty()) {
|
||||
inviteButtonState = LoadingButtonState.SUCCESS
|
||||
delay(2.toDuration(DurationUnit.SECONDS))
|
||||
// we are not differentiating between user and email invites here, however in the event we do - we can handle it
|
||||
responses.forEach { resp ->
|
||||
when (resp) {
|
||||
is InviteResponse.UserInvite -> {
|
||||
// UserInvite is a UUID
|
||||
}
|
||||
is InviteResponse.EmailInvite -> {
|
||||
// EmailInvite is an email address
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(2.seconds)
|
||||
dismiss()
|
||||
} else {
|
||||
inviteButtonState = LoadingButtonState.FAILED
|
||||
delay(2.toDuration(DurationUnit.SECONDS))
|
||||
delay(2.seconds)
|
||||
inviteButtonState = LoadingButtonState.CONTENT
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -581,12 +581,19 @@ open class TaskRecyclerViewFragment :
|
|||
if (it != null) {
|
||||
when (taskType) {
|
||||
TaskType.TODO -> {
|
||||
viewModel.setActiveFilter(
|
||||
TaskType.TODO,
|
||||
viewModel.getTaskFilterPreference(TaskType.TODO)
|
||||
)
|
||||
// Handle case where a initial filter preference for to-dos were set for FILTER_ALL
|
||||
if (viewModel.getTaskFilterPreference(TaskType.TODO) == Task.FILTER_ALL) {
|
||||
viewModel.setActiveFilter(
|
||||
TaskType.TODO,
|
||||
Task.FILTER_ACTIVE
|
||||
)
|
||||
} else {
|
||||
viewModel.setActiveFilter(
|
||||
TaskType.TODO,
|
||||
viewModel.getTaskFilterPreference(TaskType.TODO)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TaskType.DAILY -> {
|
||||
if (!viewModel.initialPreferenceFilterSet) {
|
||||
viewModel.initialPreferenceFilterSet = true
|
||||
|
|
|
|||
|
|
@ -249,7 +249,9 @@ class TasksFragment :
|
|||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
bottomNavigation?.selectedPosition = position
|
||||
updateFilterIcon(getTaskTypeFromTabPosition(position))
|
||||
binding?.viewPager?.post {
|
||||
updateFilterIcon(getTaskTypeFromTabPosition(position))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.habitrpg.android.habitica.ui.viewHolders.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Spanned
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
|
|
@ -38,8 +37,6 @@ abstract class BaseTaskViewHolder(
|
|||
private val scope = MainScope()
|
||||
|
||||
var task: Task? = null
|
||||
var existingMarkdownText: Spanned? = null
|
||||
var existingMarkdownNotes: Spanned? = null
|
||||
var movingFromPosition: Int? = null
|
||||
var errorButtonClicked: (() -> Unit)? = null
|
||||
var userID: String? = null
|
||||
|
|
@ -172,46 +169,23 @@ abstract class BaseTaskViewHolder(
|
|||
notesTextView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
val text = data.text ?: ""
|
||||
if (!MarkdownParser.containsMarkdown(text)) {
|
||||
titleTextView.text = text
|
||||
existingMarkdownText = null
|
||||
val titleText = data.text ?: ""
|
||||
if (!MarkdownParser.containsMarkdown(titleText)) {
|
||||
titleTextView.text = titleText
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (text.isNotEmpty() && MarkdownParser.containsMarkdown(text)) {
|
||||
val parsedText = MarkdownParser.parseMarkdown(text)
|
||||
if (existingMarkdownText != null && existingMarkdownText == parsedText) {
|
||||
return@launch
|
||||
}
|
||||
existingMarkdownText = parsedText
|
||||
withContext(Dispatchers.Main) {
|
||||
data.parsedText = parsedText
|
||||
titleTextView.setParsedMarkdown(parsedText)
|
||||
}
|
||||
}
|
||||
}
|
||||
val parsedText = MarkdownParser.parseMarkdown(titleText)
|
||||
data.parsedText = parsedText
|
||||
titleTextView.setParsedMarkdown(parsedText)
|
||||
}
|
||||
|
||||
if (displayMode != "minimal") {
|
||||
val notes = data.notes ?: ""
|
||||
if (!MarkdownParser.containsMarkdown(notes)) {
|
||||
notesTextView?.text = data.notes
|
||||
existingMarkdownNotes = null
|
||||
notesTextView?.text = notes
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (notes.isEmpty() || !MarkdownParser.containsMarkdown(notes)) {
|
||||
return@launch
|
||||
}
|
||||
val parsedNotes = MarkdownParser.parseMarkdown(notes)
|
||||
if (existingMarkdownNotes != null && existingMarkdownNotes == parsedNotes) {
|
||||
return@launch
|
||||
}
|
||||
existingMarkdownNotes = parsedNotes
|
||||
withContext(Dispatchers.Main) {
|
||||
data.parsedNotes = parsedNotes
|
||||
notesTextView?.setParsedMarkdown(parsedNotes)
|
||||
}
|
||||
}
|
||||
val parsedNotes = MarkdownParser.parseMarkdown(notes)
|
||||
data.parsedNotes = parsedNotes
|
||||
notesTextView?.setParsedMarkdown(parsedNotes)
|
||||
}
|
||||
} else {
|
||||
notesTextView?.visibility = View.GONE
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ constructor(
|
|||
val partyID: String?
|
||||
get() = validatedUser?.party?.id
|
||||
val isUserFainted: Boolean
|
||||
get() = (validatedUser?.stats?.hp ?: 1.0) == 0.0
|
||||
get() = (validatedUser?.stats?.hp ?: 1.0) <= 0.0
|
||||
val isUserInParty: Boolean
|
||||
get() = validatedUser?.hasParty == true
|
||||
val mirrorGroupTasks: List<String>
|
||||
|
|
|
|||
|
|
@ -199,9 +199,12 @@ constructor(
|
|||
if (activeFilters[type] == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return if (TaskType.TODO == type) {
|
||||
Task.FILTER_ACTIVE != activeFilters[type]
|
||||
when(activeFilters[type]) {
|
||||
Task.FILTER_ACTIVE -> false
|
||||
Task.FILTER_ALL -> false
|
||||
else -> true
|
||||
}
|
||||
} else {
|
||||
Task.FILTER_ALL != activeFilters[type]
|
||||
}
|
||||
|
|
@ -289,7 +292,7 @@ constructor(
|
|||
fun getTaskFilterPreference(
|
||||
type: TaskType
|
||||
): String {
|
||||
return sharedPreferences.getString("filter_${type.value}", Task.FILTER_ALL) ?: Task.FILTER_ALL
|
||||
return sharedPreferences.getString("filter_${type.value}", Task.FILTER_ALL) ?: if (TaskType.TODO == type) Task.FILTER_ACTIVE else Task.FILTER_ALL
|
||||
}
|
||||
|
||||
fun createQuery(unfilteredData: OrderedRealmCollection<Task>): RealmQuery<Task>? {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
package com.habitrpg.android.habitica.ui.views
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.*
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.ui.theme.colors
|
||||
import com.habitrpg.common.habitica.theme.HabiticaTheme
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
||||
@Composable
|
||||
fun ApiTokenBottomSheet(
|
||||
apiToken: String,
|
||||
onCopyToken: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val colors = HabiticaTheme.colors
|
||||
|
||||
val background = colors.windowBackground
|
||||
val fieldBackground = colorResource(id = R.color.gray600_gray10)
|
||||
val mainTextColor = colors.textPrimary
|
||||
val secondaryText = colors.textSecondary
|
||||
val tokenTextColor = colorResource(id = R.color.gray200_gray400)
|
||||
val buttonBg = colorResource(id = R.color.yellow_100)
|
||||
val buttonText = colorResource(id = R.color.yellow_1)
|
||||
val lockIconColor = colors.textSecondary
|
||||
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.background(background, RoundedCornerShape(22.dp))
|
||||
.padding(horizontal = 20.dp, vertical = 8.dp)
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.background(colorResource(R.color.content_background_offset))
|
||||
.size(24.dp, 3.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
stringResource(id = R.string.api_token_title),
|
||||
color = mainTextColor,
|
||||
fontSize = 21.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
Text(
|
||||
stringResource(id = R.string.api_token_is_password),
|
||||
color = mainTextColor,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
stringResource(id = R.string.api_token_is_password_info),
|
||||
color = secondaryText,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
lineHeight = 20.sp,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
Text(
|
||||
stringResource(id = R.string.api_token_reset_title),
|
||||
color = mainTextColor,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
stringResource(id = R.string.api_token_reset_info),
|
||||
color = secondaryText,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
lineHeight = 20.sp,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(fieldBackground)
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.icon_lock),
|
||||
contentDescription = stringResource(R.string.locked),
|
||||
tint = lockIconColor,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
apiToken,
|
||||
color = tokenTextColor,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
HabiticaButton(
|
||||
background = buttonBg,
|
||||
color = buttonText,
|
||||
onClick = { onCopyToken(apiToken) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
fontSize = 16.sp
|
||||
) {
|
||||
Text(stringResource(id = R.string.copy_token))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 380, heightDp = 550, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
fun ApiTokenBottomSheetPreview() {
|
||||
HabiticaTheme {
|
||||
ApiTokenBottomSheet(
|
||||
apiToken = "sample_api_token_1234567890",
|
||||
onCopyToken = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,623 @@
|
|||
package com.habitrpg.android.habitica.ui.views
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.text.InputType
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.ui.theme.colors
|
||||
import com.habitrpg.common.habitica.extensions.dpToPx
|
||||
import com.habitrpg.common.habitica.theme.HabiticaTheme
|
||||
|
||||
@Composable
|
||||
fun ConfigurableFormScreen(cfg: FormScreenConfig) {
|
||||
val colors = HabiticaTheme.colors
|
||||
|
||||
var values by remember { mutableStateOf(cfg.fields.associate { it.key to it.initialValue }) }
|
||||
var attempted by remember { mutableStateOf(false) }
|
||||
val touchedFields = remember { mutableStateMapOf<String, Boolean>() }
|
||||
|
||||
val errorRes: Map<String, Int?> = remember(values, attempted, touchedFields) {
|
||||
cfg.fields.associate { f ->
|
||||
val show = attempted || (touchedFields[f.key] == true)
|
||||
val rawError: Int? = when (f.key) {
|
||||
"confirmPw" -> {
|
||||
if (show && values["confirmPw"].orEmpty() != values["newPw"].orEmpty()) {
|
||||
R.string.password_not_matching
|
||||
} else {
|
||||
f.validator(values[f.key].orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
else -> f.validator(values[f.key].orEmpty())
|
||||
}
|
||||
f.key to if (show) rawError else null
|
||||
}
|
||||
}
|
||||
|
||||
val errors: Map<String, String?> = errorRes.mapValues { (key, resId) ->
|
||||
resId?.let {
|
||||
if (it == R.string.password_too_short) {
|
||||
stringResource(it, 8)
|
||||
} else {
|
||||
stringResource(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = colors.windowBackground
|
||||
) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
IconButton(
|
||||
onClick = cfg.onBack,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.align(Alignment.TopStart)
|
||||
.padding(start = 22.dp, top = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.arrow_back),
|
||||
contentDescription = stringResource(R.string.action_back),
|
||||
tint = colors.textPrimary
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = 16.dp, end = 16.dp, top = (16.dp + 40.dp + 8.dp))
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(cfg.titleRes),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 28.sp,
|
||||
color = colors.textPrimary,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Start)
|
||||
.padding(start = 6.dp, bottom = 12.dp)
|
||||
)
|
||||
|
||||
cfg.descriptionRes?.let {
|
||||
Text(
|
||||
text = stringResource(it),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
lineHeight = 20.sp,
|
||||
color = colors.textPrimary,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(start = 6.dp, bottom = 22.dp)
|
||||
)
|
||||
}
|
||||
|
||||
cfg.fields.forEach { f ->
|
||||
ComponentTextInput(
|
||||
hintRes = f.labelRes,
|
||||
value = values[f.key].orEmpty(),
|
||||
onValueChange = { v -> values = values.toMutableMap().also { it[f.key] = v } },
|
||||
kind = f.kind,
|
||||
isError = errors[f.key] != null,
|
||||
errorMessage = errors[f.key],
|
||||
onFocusChanged = { focused ->
|
||||
if (!focused) touchedFields[f.key] = true
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
attempted = true
|
||||
if (cfg.canSubmit(values)) cfg.onSubmit(values)
|
||||
},
|
||||
enabled = true,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = colorResource(id = R.color.brand_400),
|
||||
disabledContainerColor = colorResource(id = R.color.brand_400),
|
||||
contentColor = Color.White,
|
||||
disabledContentColor = Color.White
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(60.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(cfg.submitButtonRes),
|
||||
color = Color.White,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
|
||||
cfg.textButtonRes?.let { btnRes ->
|
||||
TextButton(
|
||||
onClick = cfg.onTextButton,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(btnRes),
|
||||
color = colorResource(id = R.color.purple400_purple500),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ComponentTextInput(
|
||||
@StringRes hintRes: Int,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
kind: FieldKind,
|
||||
isError: Boolean,
|
||||
errorMessage: String?,
|
||||
onFocusChanged: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val onTextChangedColor = if (value.isNotBlank())
|
||||
colorResource(id = R.color.purple400_purple500)
|
||||
else
|
||||
colorResource(id = R.color.gray200_gray400)
|
||||
|
||||
val activeNotFilledColor = colorResource(id = R.color.purple400_purple500)
|
||||
val filledNotActiveColor = colorResource(id = R.color.gray_400)
|
||||
val labelColor = colorResource(id = R.color.gray200_gray400)
|
||||
val textColorArgb = HabiticaTheme.colors.textPrimary.toArgb()
|
||||
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
LayoutInflater.from(ctx)
|
||||
.inflate(R.layout.component_text_input, null, false)
|
||||
.apply {
|
||||
findViewById<TextInputLayout>(R.id.text_input_layout)
|
||||
.setBoxBackgroundColorResource(R.color.gray600_gray10)
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
val til = view.findViewById<TextInputLayout>(R.id.text_input_layout)
|
||||
val edit = view.findViewById<AppCompatEditText>(R.id.text_edit_text)
|
||||
|
||||
til.hint = view.context.getString(hintRes)
|
||||
|
||||
edit.inputType = when (kind) {
|
||||
FieldKind.EMAIL -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
||||
FieldKind.URI -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
|
||||
FieldKind.MULTILINE -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
else ->
|
||||
InputType.TYPE_CLASS_TEXT or
|
||||
if (kind == FieldKind.PASSWORD) InputType.TYPE_TEXT_VARIATION_PASSWORD else 0
|
||||
}
|
||||
edit.transformationMethod =
|
||||
if (kind == FieldKind.PASSWORD) PasswordTransformationMethod.getInstance() else null
|
||||
|
||||
if (kind == FieldKind.MULTILINE) {
|
||||
edit.inputType = edit.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
edit.height = 115.dpToPx(context = view.context)
|
||||
edit.gravity = Gravity.TOP and Gravity.START
|
||||
edit.isVerticalScrollBarEnabled = true
|
||||
}
|
||||
|
||||
fun syncColors(focused: Boolean) {
|
||||
val active = focused || edit.text?.isNotBlank() == true
|
||||
val filledNotActive = !focused && edit.text?.isNotBlank() == true
|
||||
val activeNotFilled = focused && edit.text?.isBlank() == true
|
||||
val strokeColor = if (active) onTextChangedColor else labelColor
|
||||
|
||||
til.defaultHintTextColor = ColorStateList.valueOf(strokeColor.toArgb())
|
||||
til.setBoxStrokeColorStateList(ColorStateList.valueOf(strokeColor.toArgb()))
|
||||
|
||||
if (activeNotFilled) {
|
||||
til.defaultHintTextColor = ColorStateList.valueOf(activeNotFilledColor.toArgb())
|
||||
til.setBoxStrokeColorStateList(ColorStateList.valueOf(activeNotFilledColor.toArgb()))
|
||||
}
|
||||
if (filledNotActive) {
|
||||
til.defaultHintTextColor = ColorStateList.valueOf(filledNotActiveColor.toArgb())
|
||||
til.setBoxStrokeColorStateList(ColorStateList.valueOf(filledNotActiveColor.toArgb()))
|
||||
}
|
||||
}
|
||||
|
||||
syncColors(edit.isFocused)
|
||||
|
||||
edit.setOnFocusChangeListener { _, focused ->
|
||||
syncColors(focused)
|
||||
onFocusChanged(focused)
|
||||
}
|
||||
|
||||
edit.doAfterTextChanged {
|
||||
syncColors(edit.isFocused)
|
||||
onValueChange(it.toString())
|
||||
}
|
||||
|
||||
if (edit.text.toString() != value) {
|
||||
edit.setText(value)
|
||||
edit.setSelection(value.length)
|
||||
}
|
||||
|
||||
til.isErrorEnabled = isError
|
||||
til.error = errorMessage
|
||||
til.setBoxStrokeWidth(2)
|
||||
edit.setTextColor(textColorArgb)
|
||||
},
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
enum class FieldKind { TEXT, PASSWORD, MULTILINE, URI, EMAIL }
|
||||
|
||||
data class FieldConfig(
|
||||
val key: String,
|
||||
@StringRes val labelRes: Int,
|
||||
val kind: FieldKind = FieldKind.TEXT,
|
||||
val initialValue: String = "",
|
||||
val validator: (String) -> Int? = { null }
|
||||
)
|
||||
|
||||
data class FormScreenConfig(
|
||||
@StringRes val titleRes: Int,
|
||||
@StringRes val descriptionRes: Int? = null,
|
||||
val fields: List<FieldConfig>,
|
||||
@StringRes val submitButtonRes: Int,
|
||||
val onSubmit: (Map<String, String>) -> Unit,
|
||||
val canSubmit: (Map<String, String>) -> Boolean,
|
||||
@StringRes val textButtonRes: Int? = null,
|
||||
val onTextButton: () -> Unit = {},
|
||||
val onBack: () -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ChangePasswordScreen(
|
||||
onBack: () -> Unit,
|
||||
onSave: (old: String, new: String) -> Unit,
|
||||
onForgot: () -> Unit
|
||||
) {
|
||||
val fields = listOf(
|
||||
FieldConfig(
|
||||
key = "oldPw",
|
||||
labelRes = R.string.old_password,
|
||||
kind = FieldKind.PASSWORD,
|
||||
validator = { if (it.length < 8) R.string.password_too_short else null }
|
||||
),
|
||||
FieldConfig(
|
||||
key = "newPw",
|
||||
labelRes = R.string.new_password,
|
||||
kind = FieldKind.PASSWORD,
|
||||
validator = { if (it.length < 8) R.string.password_too_short else null }
|
||||
),
|
||||
FieldConfig(
|
||||
key = "confirmPw",
|
||||
labelRes = R.string.confirm_new_password,
|
||||
kind = FieldKind.PASSWORD,
|
||||
validator = { if (it.isBlank()) R.string.password_not_matching else null }
|
||||
)
|
||||
)
|
||||
|
||||
ConfigurableFormScreen(
|
||||
FormScreenConfig(
|
||||
titleRes = R.string.change_password,
|
||||
descriptionRes = R.string.password_change_info,
|
||||
fields = fields,
|
||||
submitButtonRes = R.string.change_password,
|
||||
canSubmit = { vals ->
|
||||
fields.all { it.validator(vals[it.key].orEmpty()) == null }
|
||||
&& vals["newPw"] == vals["confirmPw"]
|
||||
},
|
||||
onSubmit = { v -> onSave(v["oldPw"]!!, v["newPw"]!!) },
|
||||
textButtonRes = R.string.forgot_pw_btn,
|
||||
onTextButton = onForgot,
|
||||
onBack = onBack
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangeUsernameScreen(
|
||||
initial: String,
|
||||
onBack: () -> Unit,
|
||||
onSave: (newUsername: String) -> Unit
|
||||
) {
|
||||
val fields = listOf(
|
||||
FieldConfig(
|
||||
key = "username",
|
||||
labelRes = R.string.username,
|
||||
kind = FieldKind.TEXT,
|
||||
initialValue = initial,
|
||||
validator = {
|
||||
when {
|
||||
it.isBlank() -> R.string.username_requirements
|
||||
it.length > 20 -> R.string.username_requirements
|
||||
!Regex("^[A-Za-z0-9_-]+$").matches(it) -> R.string.username_requirements
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
ConfigurableFormScreen(
|
||||
FormScreenConfig(
|
||||
titleRes = R.string.change_username,
|
||||
descriptionRes = R.string.change_username_description,
|
||||
fields = fields,
|
||||
submitButtonRes = R.string.change_username,
|
||||
canSubmit = { vals -> fields.all { f -> f.validator(vals[f.key].orEmpty()) == null } },
|
||||
onSubmit = { vals -> onSave(vals["username"]!!.trim()) },
|
||||
onBack = onBack
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangeEmailScreen(
|
||||
initialEmail: String,
|
||||
onBack: () -> Unit,
|
||||
onSave: (newEmail: String, password: String) -> Unit,
|
||||
onForgotPassword: () -> Unit
|
||||
) {
|
||||
val fields = listOf(
|
||||
FieldConfig(
|
||||
key = "email",
|
||||
labelRes = R.string.email,
|
||||
kind = FieldKind.EMAIL,
|
||||
initialValue = initialEmail,
|
||||
validator = { if (it.isBlank()) R.string.email_invalid else null }
|
||||
),
|
||||
FieldConfig(
|
||||
key = "password",
|
||||
labelRes = R.string.password,
|
||||
kind = FieldKind.PASSWORD,
|
||||
validator = { if (it.length < 8) R.string.password_too_short else null }
|
||||
)
|
||||
)
|
||||
|
||||
ConfigurableFormScreen(
|
||||
FormScreenConfig(
|
||||
titleRes = R.string.change_email,
|
||||
descriptionRes = R.string.change_email_description,
|
||||
fields = fields,
|
||||
submitButtonRes = R.string.change_email,
|
||||
canSubmit = { vals -> fields.all { f -> f.validator(vals[f.key].orEmpty()) == null } },
|
||||
onSubmit = { vals -> onSave(vals["email"]!!, vals["password"]!!) },
|
||||
textButtonRes = R.string.forgot_pw_btn,
|
||||
onTextButton = onForgotPassword,
|
||||
onBack = onBack
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangeDisplayNameScreen(
|
||||
initial: String,
|
||||
onBack: () -> Unit,
|
||||
onSave: (newDisplayName: String) -> Unit
|
||||
) {
|
||||
val fields = listOf(
|
||||
FieldConfig(
|
||||
key = "displayName",
|
||||
labelRes = R.string.display_name,
|
||||
kind = FieldKind.TEXT,
|
||||
validator = { if (it.isBlank()) R.string.display_name_length_error else null },
|
||||
initialValue = initial
|
||||
)
|
||||
)
|
||||
|
||||
ConfigurableFormScreen(
|
||||
FormScreenConfig(
|
||||
titleRes = R.string.change_display_name,
|
||||
descriptionRes = R.string.display_name_description,
|
||||
fields = fields,
|
||||
submitButtonRes = R.string.change_display_name,
|
||||
canSubmit = { vals -> vals["displayName"]!!.isNotBlank() },
|
||||
onSubmit = { vals -> onSave(vals["displayName"]!!) },
|
||||
onBack = onBack
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AboutMeScreen(
|
||||
initial: String,
|
||||
onBack: () -> Unit,
|
||||
onSave: (aboutText: String) -> Unit
|
||||
) {
|
||||
val fields = listOf(
|
||||
FieldConfig(
|
||||
key = "about",
|
||||
labelRes = R.string.about_me,
|
||||
kind = FieldKind.MULTILINE,
|
||||
initialValue = initial
|
||||
)
|
||||
)
|
||||
|
||||
ConfigurableFormScreen(
|
||||
FormScreenConfig(
|
||||
titleRes = R.string.about_me,
|
||||
descriptionRes = R.string.about_me_description,
|
||||
fields = fields,
|
||||
submitButtonRes = R.string.save_about_me,
|
||||
canSubmit = { true },
|
||||
onSubmit = { vals -> onSave(vals["about"]!!.trim()) },
|
||||
onBack = onBack
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhotoUrlScreen(
|
||||
initial: String,
|
||||
onBack: () -> Unit,
|
||||
onSave: (photoUrl: String) -> Unit
|
||||
) {
|
||||
val fields = listOf(
|
||||
FieldConfig(
|
||||
key = "photoUrl",
|
||||
labelRes = R.string.photo_url,
|
||||
kind = FieldKind.URI,
|
||||
initialValue = initial
|
||||
)
|
||||
)
|
||||
|
||||
ConfigurableFormScreen(
|
||||
FormScreenConfig(
|
||||
titleRes = R.string.photo_url,
|
||||
descriptionRes = R.string.photo_url_description,
|
||||
fields = fields,
|
||||
submitButtonRes = R.string.save_photo_url,
|
||||
canSubmit = { vals -> vals["photoUrl"]!!.isNotBlank() },
|
||||
onSubmit = { vals -> onSave(vals["photoUrl"]!!.trim()) },
|
||||
onBack = onBack
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(
|
||||
name = "ChangeUsername – Dark",
|
||||
showBackground = true,
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES
|
||||
)
|
||||
@Composable
|
||||
fun PreviewChangeUsernameScreenDark() {
|
||||
HabiticaTheme {
|
||||
ChangeUsernameScreen(
|
||||
initial = "",
|
||||
onBack = {},
|
||||
onSave = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(
|
||||
name = "ChangeEmail – Dark",
|
||||
showBackground = true,
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES
|
||||
)
|
||||
@Composable
|
||||
fun PreviewChangeEmailScreenDark() {
|
||||
HabiticaTheme {
|
||||
ChangeEmailScreen(
|
||||
onBack = {},
|
||||
initialEmail = "",
|
||||
onSave = { newEmail, password -> },
|
||||
onForgotPassword = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(
|
||||
name = "ChangeDisplayName – Dark",
|
||||
showBackground = true,
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES
|
||||
)
|
||||
@Composable
|
||||
fun PreviewChangeDisplayNameScreenDark() {
|
||||
HabiticaTheme {
|
||||
ChangeDisplayNameScreen(
|
||||
initial = "displayName",
|
||||
onBack = {},
|
||||
onSave = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(
|
||||
name = "AboutMe – Dark",
|
||||
showBackground = true,
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES
|
||||
)
|
||||
@Composable
|
||||
fun PreviewAboutMeScreenDark() {
|
||||
HabiticaTheme {
|
||||
AboutMeScreen(
|
||||
initial = "",
|
||||
onBack = {},
|
||||
onSave = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(
|
||||
name = "PhotoURL – Dark",
|
||||
showBackground = true,
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES
|
||||
)
|
||||
@Composable
|
||||
fun PreviewPhotoUrlScreenDark() {
|
||||
HabiticaTheme {
|
||||
PhotoUrlScreen(
|
||||
initial = "",
|
||||
onBack = {},
|
||||
onSave = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview(
|
||||
name = "ChangePasswordScreen – Dark",
|
||||
showBackground = true,
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES
|
||||
)
|
||||
@Composable
|
||||
fun PreviewChangePasswordScreenDark() {
|
||||
HabiticaTheme {
|
||||
ChangePasswordScreen(
|
||||
onBack = {},
|
||||
onSave = { old, new -> },
|
||||
onForgot = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -40,9 +40,10 @@ fun SkillDialog(
|
|||
skillPath: String = "",
|
||||
skillKey: String = "",
|
||||
resourceIconPainter: Painter,
|
||||
title: String,
|
||||
description: String,
|
||||
mpCost: String,
|
||||
title: String = "",
|
||||
description: String = "",
|
||||
mpCost: String = "",
|
||||
isTransformationItem: Boolean = false,
|
||||
onUseSkill: () -> Unit,
|
||||
) {
|
||||
val colors = HabiticaTheme.colors
|
||||
|
|
@ -61,11 +62,10 @@ fun SkillDialog(
|
|||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.background(colorResource(R.color.content_background_offset))
|
||||
.size(24.dp, 3.dp)
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.background(colorResource(R.color.content_background_offset))
|
||||
.size(24.dp, 3.dp)
|
||||
)
|
||||
|
||||
Box(
|
||||
|
|
@ -75,14 +75,12 @@ fun SkillDialog(
|
|||
.background(colors.pixelArtBackground(hasIcon = true)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
PixelArtView (
|
||||
imageName = skillPath + skillKey,
|
||||
PixelArtView(
|
||||
imageName = "$skillPath$skillKey",
|
||||
modifier = Modifier.size(62.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 16.sp,
|
||||
|
|
@ -96,58 +94,63 @@ fun SkillDialog(
|
|||
fontSize = 14.sp,
|
||||
color = colors.textSecondary,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
fontWeight = FontWeight.Normal,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(18.dp))
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(chipBg)
|
||||
.padding(horizontal = 20.dp, vertical = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = resourceIconPainter,
|
||||
contentDescription = null,
|
||||
tint = Color.Unspecified,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = mpCost,
|
||||
color = chipTextColor,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
if (!isTransformationItem) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(chipBg)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = resourceIconPainter,
|
||||
contentDescription = null,
|
||||
tint = Color.Unspecified,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = mpCost,
|
||||
color = chipTextColor,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(28.dp))
|
||||
} else {
|
||||
Spacer(Modifier.height(18.dp))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(28.dp))
|
||||
|
||||
Button(
|
||||
onClick = onUseSkill,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = colorResource(R.color.brand_400),
|
||||
contentColor = Color.White
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.height(43.dp)
|
||||
) {
|
||||
val label = if (isTransformationItem)
|
||||
stringResource(R.string.use_on_party)
|
||||
else
|
||||
stringResource(R.string.use_skill)
|
||||
Text(
|
||||
text = stringResource(R.string.use_skill),
|
||||
fontWeight = FontWeight.Normal,
|
||||
text = label,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 17.sp
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Preview(showBackground = true, backgroundColor = 0xFF232136)
|
||||
@Composable
|
||||
fun PreviewSkillDialog() {
|
||||
|
|
@ -158,6 +161,7 @@ fun PreviewSkillDialog() {
|
|||
title = "Title Skill",
|
||||
description = "Skill Description",
|
||||
mpCost = "10 MP",
|
||||
isTransformationItem = true,
|
||||
onUseSkill = {
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.habitrpg.android.habitica.ui.views.preferences
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -11,6 +13,7 @@ import androidx.compose.ui.res.colorResource
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.habitrpg.android.habitica.R
|
||||
|
|
@ -24,6 +27,8 @@ fun PauseResumeDamageView(
|
|||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val colors = HabiticaTheme.colors
|
||||
val mainTextColor = colors.textPrimary
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier =
|
||||
|
|
@ -78,6 +83,9 @@ fun PauseResumeDamageView(
|
|||
HabiticaButton(
|
||||
background = colorResource(R.color.yellow_100),
|
||||
color = colorResource(R.color.yellow_1),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
onClick = { onClick() }
|
||||
) {
|
||||
Text(stringResource(R.string.resume_damage))
|
||||
|
|
@ -85,8 +93,9 @@ fun PauseResumeDamageView(
|
|||
} else {
|
||||
Text(
|
||||
stringResource(R.string.pause_damage),
|
||||
color = HabiticaTheme.colors.textSecondary,
|
||||
fontSize = 16.sp,
|
||||
fontSize = 21.sp,
|
||||
color = mainTextColor,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
|
|
@ -97,12 +106,16 @@ fun PauseResumeDamageView(
|
|||
stringResource(R.string.pause_damage_1_title),
|
||||
color = HabiticaTheme.colors.textPrimary,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.pause_damage_1_description),
|
||||
color = HabiticaTheme.colors.textSecondary,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
|
|
@ -110,31 +123,44 @@ fun PauseResumeDamageView(
|
|||
stringResource(R.string.pause_damage_2_title),
|
||||
color = HabiticaTheme.colors.textPrimary,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.pause_damage_2_description),
|
||||
color = HabiticaTheme.colors.textSecondary,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.pause_damage_3_title),
|
||||
color = HabiticaTheme.colors.textPrimary,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.pause_damage_3_description),
|
||||
color = HabiticaTheme.colors.textSecondary,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
modifier = Modifier.padding(bottom = 18.dp)
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
HabiticaButton(
|
||||
background = colorResource(R.color.yellow_100),
|
||||
color = colorResource(R.color.yellow_1),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
onClick = { onClick() }
|
||||
) {
|
||||
Text(stringResource(R.string.pause_damage))
|
||||
|
|
@ -142,3 +168,19 @@ fun PauseResumeDamageView(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(
|
||||
showBackground = true,
|
||||
widthDp = 360,
|
||||
name = "Pause Damage (isPaused = false)",
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES
|
||||
)
|
||||
@Composable
|
||||
private fun PauseDamagePreview() {
|
||||
HabiticaTheme {
|
||||
PauseResumeDamageView(
|
||||
isPaused = false,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,20 +163,16 @@ class ChatBarView : LinearLayout, OnImeVisibilityChangedListener {
|
|||
}
|
||||
|
||||
override fun onImeVisibilityChanged(visible: Boolean, height: Int, safeInsets: Insets) {
|
||||
this.safeInsets = safeInsets
|
||||
imeHeight = if (visible) {
|
||||
height
|
||||
} else {
|
||||
0
|
||||
}
|
||||
applyAllPadding()
|
||||
val navInset = safeInsets.bottom
|
||||
val imeOffset = if (visible) (height - navInset).coerceAtLeast(0) else 0
|
||||
|
||||
updatePadding(
|
||||
left = safeInsets.left,
|
||||
right = safeInsets.right,
|
||||
bottom = navInset
|
||||
)
|
||||
|
||||
translationY = -imeOffset.toFloat()
|
||||
}
|
||||
|
||||
private fun applyAllPadding() {
|
||||
Log.e("ChatBarView", "applyAllPadding: safeInsets = $safeInsets, imeHeight = $imeHeight")
|
||||
updatePadding(
|
||||
left = safeInsets.left,
|
||||
right = safeInsets.right,
|
||||
bottom = if (imeHeight > 0) imeHeight else safeInsets.bottom)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package com.habitrpg.android.habitica.utils
|
||||
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.habitrpg.android.habitica.models.invitations.InviteResponse
|
||||
import io.realm.RealmList
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class InviteResponseDeserializer : JsonDeserializer<InviteResponse> {
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: Type,
|
||||
context: JsonDeserializationContext
|
||||
): InviteResponse {
|
||||
return when {
|
||||
json.isJsonPrimitive && json.asJsonPrimitive.isString -> {
|
||||
InviteResponse.EmailInvite(json.asString)
|
||||
}
|
||||
json.isJsonObject -> {
|
||||
val obj = json.asJsonObject
|
||||
InviteResponse.UserInvite(
|
||||
id = obj["id"].asString,
|
||||
name = obj["name"].asString,
|
||||
inviter = obj["inviter"].asString
|
||||
)
|
||||
}
|
||||
else -> throw JsonParseException("Unexpected InviteResponse: $json")
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Habitica/src/main/res/layout/component_text_input.xml
Normal file
29
Habitica/src/main/res/layout/component_text_input.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/text_input_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/SettingTextInputLayoutAppearance"
|
||||
app:boxStrokeWidth="2dp"
|
||||
app:boxStrokeWidthFocused="2dp"
|
||||
app:boxBackgroundColor="@color/gray600_gray10"
|
||||
app:hintTextColor="@color/gray100_gray500"
|
||||
app:boxStrokeColor="@color/gray200_gray400"
|
||||
android:backgroundTint="?attr/colorPrimaryText"
|
||||
android:hint="@string/task_title"
|
||||
android:alpha="0.75">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/text_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/colorPrimaryText"
|
||||
android:inputType="textCapSentences|textAutoCorrect" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<?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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/special_skill_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/layout_rounded_bg_window"
|
||||
android:clipToPadding="true"
|
||||
android:clipChildren="true"
|
||||
android:clipToOutline="true"
|
||||
android:clickable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:padding="12dp">
|
||||
|
||||
<com.habitrpg.common.habitica.views.PixelArtView
|
||||
android:id="@+id/skill_image"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_vertical" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/skill_text"
|
||||
style="@style/SubHeader1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Seafoam" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/skill_notes"
|
||||
style="@style/Caption2.Regular"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
tools:text="Turn a friend into a sea creature!" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/count_label"
|
||||
style="@style/CountLabel"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:textSize="14sp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="12dp"
|
||||
tools:text="6" />
|
||||
</RelativeLayout>
|
||||
|
|
@ -215,6 +215,9 @@ object DataBindingUtils {
|
|||
tempMap["Pet_HatchingPotion_Fungi"] = "gif"
|
||||
tempMap["shop_armoire"] = "gif"
|
||||
tempMap["Pet_HatchingPotion_Cryptid"] = "gif"
|
||||
tempMap["Mount_Head_Dragon-Hydra"] = "gif"
|
||||
tempMap["Mount_Body_Dragon-Hydra"] = "gif"
|
||||
tempMap["stable_Mount_Icon_Dragon-Hydra"] = "gif"
|
||||
FILEFORMAT_MAP = tempMap
|
||||
|
||||
val tempNameMap = mutableMapOf<String, String>()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -3,6 +3,8 @@ package com.habitrpg.common.habitica.helpers
|
|||
import java.util.regex.Pattern
|
||||
|
||||
object EmojiParser {
|
||||
private val pattern: Pattern = Pattern.compile("(:[^:\\s]+:)")
|
||||
|
||||
/**
|
||||
* Converts Cheat Sheet emoji-codes into unicode characters
|
||||
*
|
||||
|
|
@ -14,12 +16,10 @@ object EmojiParser {
|
|||
return text
|
||||
}
|
||||
var returnString: String = text
|
||||
val pattern = Pattern.compile("(:[^:]+:)")
|
||||
val matcher = pattern.matcher(text)
|
||||
while (matcher.find()) {
|
||||
val found = matcher.group()
|
||||
if (EmojiMap.invertedEmojiMap[found] == null) continue
|
||||
val hexInt = EmojiMap.invertedEmojiMap[found]!!
|
||||
val hexInt = EmojiMap.invertedEmojiMap[found] ?: continue
|
||||
val replacement = String(Character.toChars(hexInt))
|
||||
returnString = returnString.replace(found, replacement)
|
||||
}
|
||||
|
|
@ -41,9 +41,8 @@ object EmojiParser {
|
|||
for (i in 0..charArray.size - 2) {
|
||||
val testString = String(charArray.copyOfRange(i, i + 2))
|
||||
val test = testString.codePointAt(0)
|
||||
if (EmojiMap.emojiMap.containsKey(test)) {
|
||||
returnString = returnString.replace(testString, EmojiMap.emojiMap[test]!!)
|
||||
}
|
||||
val cheatCode = EmojiMap.emojiMap[test] ?: continue
|
||||
returnString = returnString.replace(testString, cheatCode)
|
||||
}
|
||||
return returnString
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
New in 4.7.4
|
||||
- Hungarian language support
|
||||
- Upgraded to the latest Google Sign In authentication standards
|
||||
- Implemented full edge-to-edge display functionality on Android 11+ devices
|
||||
- Fixed some issues where the text box in chat wasn't adjusting properly
|
||||
- More support for landscape mode
|
||||
- Various other bug fixes and improvements
|
||||
- Support for future events
|
||||
New in 4.7.6
|
||||
- Monthly Dailies should repeat more accurately
|
||||
- Reminders update correctly after being deleted
|
||||
- More accessible designs for the Skill section
|
||||
- Scheduled To Do filter will stay applied on app reopen
|
||||
- Party description box no longer gets hidden by keyboard
|
||||
- Fixed a crash with certain languages in Avatar Customization
|
||||
- Fixed a crash with Group Plan member lists
|
||||
- Animated backgrounds move again
|
||||
- Markdown formatting no longer flashes when refreshing
|
||||
- Various other bug fixes
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ androidTest = "1.6.1"
|
|||
androidTestMonitor = "1.7.2"
|
||||
androidTestRunner = "1.6.2"
|
||||
annotationApi = "1.3.2"
|
||||
appcompat = "1.7.0"
|
||||
appcompat = "1.7.1"
|
||||
billing = "7.1.1"
|
||||
coil = "3.1.0"
|
||||
compose = "1.8.1"
|
||||
|
|
@ -21,7 +21,7 @@ coordinatorlayout = "1.3.0"
|
|||
coreSplashscreen = "1.1.0-rc01"
|
||||
core_ktx = "1.16.0"
|
||||
coroutines = "1.10.2"
|
||||
crashlytics = "3.0.3"
|
||||
crashlytics = "3.0.4"
|
||||
credentials = "1.5.0"
|
||||
daggerhilt = "2.56.2"
|
||||
desuggar = "2.1.5"
|
||||
|
|
@ -29,8 +29,8 @@ detekt = "1.23.8"
|
|||
firebase-perf = "1.4.2"
|
||||
firebase_bom = "33.13.0"
|
||||
flexbox = "3.0.0"
|
||||
fragmentKtx = "1.8.6"
|
||||
fragmentTesting = "1.8.6"
|
||||
fragmentKtx = "1.8.8"
|
||||
fragmentTesting = "1.8.8"
|
||||
google-service = "4.4.2"
|
||||
googleid = "1.1.1"
|
||||
gson = "2.13.1"
|
||||
|
|
@ -39,12 +39,12 @@ inAppReview = "2.0.2"
|
|||
junitKtx = "1.2.1"
|
||||
kaspresso = "1.6.0"
|
||||
kotest = "5.9.1"
|
||||
kotlin = "2.1.20"
|
||||
ksp = "2.1.20-2.0.1"
|
||||
ktlintPlugin = "12.2.0"
|
||||
kotlin = "2.1.21"
|
||||
ksp = "2.1.21-2.0.2"
|
||||
ktlintPlugin = "12.3.0"
|
||||
leakCanary = "2.14"
|
||||
lifecycle = "2.9.0"
|
||||
lifecycleRuntimeKtx = "2.9.0"
|
||||
lifecycle = "2.9.1"
|
||||
lifecycleRuntimeKtx = "2.9.1"
|
||||
markwon = "4.6.2"
|
||||
material = "1.12.0"
|
||||
material3 = "1.3.2"
|
||||
|
|
@ -59,10 +59,10 @@ play_wearables = "19.0.0"
|
|||
preferences = "1.2.1"
|
||||
realmPlugin = "10.19.0"
|
||||
recyclerview = "1.4.0"
|
||||
retrofit = "2.11.0"
|
||||
retrofit = "3.0.0"
|
||||
shimmer = "0.5.0"
|
||||
swipeRefresh = "1.1.0"
|
||||
turbine = "1.2.0"
|
||||
turbine = "1.2.1"
|
||||
wear = "1.3.0"
|
||||
wearInput = "1.1.0"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
NAME=4.7.5
|
||||
CODE=12981
|
||||
NAME=4.7.6
|
||||
CODE=13431
|
||||
Loading…
Reference in a new issue