mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-04-14 19:56:32 +00:00
Notification scheduling improvements
This commit is contained in:
parent
9fe345f72e
commit
03de0bbf91
21 changed files with 289 additions and 732 deletions
|
|
@ -3,7 +3,6 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.habitrpg.android.habitica"
|
||||
android:screenOrientation="portrait"
|
||||
android:installLocation="auto" >
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
|
@ -239,10 +238,12 @@
|
|||
<action android:name="REJECT_QUEST_INVITE"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".receivers.TaskAlarmBootReceiver" android:permission="android.permission.RECEIVE_BOOT_COMPLETED"
|
||||
<receiver android:name=".receivers.TaskAlarmBootReceiver"
|
||||
android:permission="android.permission.RECEIVE_BOOT_COMPLETED"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
|
|
|||
|
|
@ -227,7 +227,6 @@
|
|||
android:text="@string/push_notification_system_settings_reminders"
|
||||
android:textColor="@color/text_quad"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<com.habitrpg.android.habitica.ui.views.tasks.form.ReminderContainer
|
||||
|
|
@ -235,6 +234,22 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/exact_alarm_disabled_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/spacing_medium"
|
||||
android:background="@drawable/layout_rounded_bg_yellow_10">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/spacing_medium"
|
||||
android:text="@string/exact_alarm_system_settings_reminders"
|
||||
android:textColor="@color/yellow_0"
|
||||
android:textSize="12sp" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/stat_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
26
Habitica/res/layout/fragment_equipment_detail.xml
Normal file
26
Habitica/res/layout/fragment_equipment_detail.xml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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"
|
||||
android:orientation="vertical">
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/avatar_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
<com.habitrpg.android.habitica.ui.helpers.RecyclerViewEmptySupport
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:scrollbarSize="3dp"
|
||||
android:scrollbarThumbVertical="@color/scrollbarThumb"
|
||||
android:layout_gravity="center"
|
||||
android:scrollbars="vertical" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</LinearLayout>
|
||||
|
|
@ -108,29 +108,12 @@
|
|||
<action
|
||||
android:id="@+id/openComposeAvatarDetail"
|
||||
app:destination="@id/ComposeAvatarCustomizationFragment" />
|
||||
<action
|
||||
android:id="@+id/openAvatarDetail"
|
||||
app:destination="@id/avatarCustomizationFragment" />
|
||||
<action
|
||||
android:id="@+id/openEquipmentDetail"
|
||||
app:destination="@id/equipmentDetailFragment" />
|
||||
<action
|
||||
android:id="@+id/openComposeAvatarEquipment"
|
||||
app:destination="@id/composeAvatarEquipmentFragment" />
|
||||
<action
|
||||
android:id="@+id/openAvatarEquipment"
|
||||
app:destination="@id/avatarEquipmentFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/avatarEquipmentFragment"
|
||||
android:name="com.habitrpg.android.habitica.ui.fragments.inventory.customization.AvatarEquipmentFragment"
|
||||
android:label="@string/sidebar_avatar" >
|
||||
<argument
|
||||
android:name="type"
|
||||
app:argType="string" />
|
||||
<argument
|
||||
android:name="category"
|
||||
app:argType="string" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/composeAvatarEquipmentFragment"
|
||||
|
|
@ -326,17 +309,6 @@
|
|||
android:name="category"
|
||||
app:argType="string" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/avatarCustomizationFragment"
|
||||
android:name="com.habitrpg.android.habitica.ui.fragments.inventory.customization.AvatarCustomizationFragment"
|
||||
android:label="@string/sidebar_avatar" >
|
||||
<argument
|
||||
android:name="type"
|
||||
app:argType="string" />
|
||||
<argument
|
||||
android:name="category"
|
||||
app:argType="string" />
|
||||
</fragment>
|
||||
<activity
|
||||
android:id="@+id/prefsActivity"
|
||||
android:name="com.habitrpg.android.habitica.ui.activities.PrefsActivity"
|
||||
|
|
|
|||
|
|
@ -30,8 +30,11 @@
|
|||
<string name="enable_notifications">Enable Notifications</string>
|
||||
<string name="push_notification_system_settings_description">Allow Habitica notifications in the Settings app to receive push notifications</string>
|
||||
<string name="push_notification_system_settings_reminders">Allow Habitica notifications in the Settings app to receive reminders</string>
|
||||
<string name="exact_alarm_system_settings_reminders">Habitica does not have the `Alarms & Reminders` permission in the Settings app. Reminders might not appear at the exact time.</string>
|
||||
<string name="push_notification_system_settings_title">Notifications Disabled</string>
|
||||
<string name="push_notifications_sum">Set your push notifications settings</string>
|
||||
<string name="exact_alarm_system_settings_title">`Alarm & Reminders` disabled`</string>
|
||||
<string name="exact_alarm_system_settings_description">Allow `Alarms & Reminders` in the Settings app to ensure reminders appear at the scheduled time exactly</string>
|
||||
<string name="preference_push_you_won_challenge">You won a Challenge!</string>
|
||||
<string name="preference_push_received_a_private_message">Received a Private Message</string>
|
||||
<string name="preference_push_gifted_gems">Gifted Gems</string>
|
||||
|
|
@ -1539,8 +1542,8 @@
|
|||
<string name="clear_database">Clear Database</string>
|
||||
<string name="clear_cache_settings">Clear Cache</string>
|
||||
<string name="best_deal">Best Deal</string>
|
||||
<string name="unlock_x_gems_per_month">Unlock %d Gems per month instantly</string>
|
||||
<string name="unlocks_x_gems_per_month">Unlocks %d Gems per month instantly</string>
|
||||
<string name="unlock_x_gems_per_month">Unlock %d Gems per month in the Market</string>
|
||||
<string name="unlocks_x_gems_per_month">Unlocks %d Gems per month in the Market</string>
|
||||
<string name="x_gems">%d Gems</string>
|
||||
<string name="two_gems_per_month">Earn +2 Gems every month you\'re subscribed</string>
|
||||
<string name="two_gems_per_month_gift">Earns +2 Gems every month they\'re subscribed</string>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import android.app.PendingIntent
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.habitrpg.android.habitica.data.TaskRepository
|
||||
import com.habitrpg.android.habitica.extensions.withImmutableFlag
|
||||
|
|
@ -283,26 +282,37 @@ class TaskAlarmManager(
|
|||
time: Long,
|
||||
pendingIntent: PendingIntent?,
|
||||
) {
|
||||
HLogger.log(LogLevel.INFO, "TaskAlarmManager", "Scheduling for $time")
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
|
||||
|
||||
if (pendingIntent == null) {
|
||||
if (pendingIntent == null || alarmManager == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val notificationType = AlarmManager.RTC_WAKEUP
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// For SDK >= Android 12, allows batching of reminders
|
||||
try {
|
||||
alarmManager?.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pendingIntent)
|
||||
Log.d(
|
||||
"TaskAlarmManager",
|
||||
"setAlarm: Scheduling for $time using setAndAllowWhileIdle",
|
||||
)
|
||||
var canScheduleExact = true
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
canScheduleExact = alarmManager.canScheduleExactAlarms()
|
||||
}
|
||||
if (canScheduleExact) {
|
||||
alarmManager.setExactAndAllowWhileIdle(notificationType, time, pendingIntent)
|
||||
HLogger.log(LogLevel.DEBUG,
|
||||
"TaskAlarmManager",
|
||||
"setAlarm: Scheduling for $time using setExact",
|
||||
)
|
||||
} else {
|
||||
alarmManager.setAndAllowWhileIdle(notificationType, time, pendingIntent)
|
||||
HLogger.log(LogLevel.DEBUG,
|
||||
"TaskAlarmManager",
|
||||
"setAlarm: Scheduling for $time using setAndAllowWhileIdle",
|
||||
)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
when (ex) {
|
||||
is IllegalStateException, is SecurityException -> {
|
||||
alarmManager?.setWindow(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
alarmManager.setWindow(
|
||||
notificationType,
|
||||
time,
|
||||
600000,
|
||||
pendingIntent,
|
||||
|
|
@ -315,7 +325,7 @@ class TaskAlarmManager(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
alarmManager?.setWindow(AlarmManager.RTC_WAKEUP, time, 600000, pendingIntent)
|
||||
alarmManager.set(notificationType, time, pendingIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,9 +87,12 @@ class NotificationPublisher : BroadcastReceiver() {
|
|||
intent: Intent,
|
||||
notification: Notification?,
|
||||
) {
|
||||
val notificationManager = context?.let { NotificationManagerCompat.from(it) }
|
||||
val context = context ?: return
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
val id = intent.getIntExtra(NOTIFICATION_ID, 0)
|
||||
notification?.let { notificationManager?.notify(id, it) }
|
||||
notification?.let {
|
||||
notificationManager.safeNotify(context, id, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.habitrpg.android.habitica.receivers
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
|
@ -25,7 +26,8 @@ class TaskAlarmBootReceiver : BroadcastReceiver() {
|
|||
context: Context,
|
||||
intent: Intent,
|
||||
) {
|
||||
if (intent.action != Intent.ACTION_BOOT_COMPLETED) {
|
||||
if (intent.action != Intent.ACTION_BOOT_COMPLETED
|
||||
&& intent.action != AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED) {
|
||||
return
|
||||
}
|
||||
MainScope().launch(ExceptionHandler.coroutine()) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package com.habitrpg.android.habitica.receivers
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.RingtoneManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
|
@ -113,6 +116,13 @@ class TaskReceiver : BroadcastReceiver() {
|
|||
)
|
||||
}
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
notificationManager.notify(task.id.hashCode(), notificationBuilder.build())
|
||||
notificationManager.safeNotify(context, task.id.hashCode(), notificationBuilder.build())
|
||||
}
|
||||
}
|
||||
|
||||
fun NotificationManagerCompat.safeNotify(context: Context, code: Int, notification: Notification) {
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
return
|
||||
}
|
||||
notify(code, notification)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
package com.habitrpg.android.habitica.ui.activities
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
|
||||
import android.view.KeyEvent
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewParent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
|
|
@ -31,6 +35,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
|
|
@ -179,6 +184,14 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
|
|||
} else {
|
||||
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
|
||||
if (!alarmManager.canScheduleExactAlarms()) {
|
||||
val intent =Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
|
||||
intent.setData(Uri.fromParts("package", applicationContext?.packageName, null));
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val classSelectionResult =
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
package com.habitrpg.android.habitica.ui.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlarmManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.provider.Settings
|
||||
import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.Menu
|
||||
|
|
@ -891,6 +895,26 @@ class TaskFormActivity : BaseActivity() {
|
|||
binding.remindersContainer.shouldShowNotifPermission = false
|
||||
binding.notificationsDisabledLayout.visibility = View.GONE
|
||||
}
|
||||
|
||||
val alarmManager = this.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
|
||||
var warnAboutInexact = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (alarmManager?.canScheduleExactAlarms() == false) {
|
||||
warnAboutInexact = true
|
||||
}
|
||||
}
|
||||
if (warnAboutInexact) {
|
||||
binding.exactAlarmDisabledContainer.visibility = View.VISIBLE
|
||||
binding.exactAlarmDisabledContainer.setOnClickListener {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val intent =Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
|
||||
intent.setData(Uri.fromParts("package", applicationContext?.packageName, null));
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.exactAlarmDisabledContainer.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun showChallengeDeleteTask() {
|
||||
|
|
|
|||
|
|
@ -1,375 +0,0 @@
|
|||
package com.habitrpg.android.habitica.ui.fragments.inventory.customization
|
||||
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.flexbox.AlignItems
|
||||
import com.google.android.flexbox.FlexDirection.ROW
|
||||
import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.data.CustomizationRepository
|
||||
import com.habitrpg.android.habitica.data.InventoryRepository
|
||||
import com.habitrpg.android.habitica.databinding.BottomSheetBackgroundsFilterBinding
|
||||
import com.habitrpg.android.habitica.databinding.FragmentRefreshRecyclerviewBinding
|
||||
import com.habitrpg.android.habitica.helpers.Analytics
|
||||
import com.habitrpg.android.habitica.models.CustomizationFilter
|
||||
import com.habitrpg.android.habitica.models.inventory.Customization
|
||||
import com.habitrpg.android.habitica.models.user.OwnedCustomization
|
||||
import com.habitrpg.android.habitica.models.user.User
|
||||
import com.habitrpg.android.habitica.ui.adapter.CustomizationRecyclerViewAdapter
|
||||
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
|
||||
import com.habitrpg.android.habitica.ui.helpers.MarginDecoration
|
||||
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
|
||||
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaBottomSheetDialog
|
||||
import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog
|
||||
import com.habitrpg.common.habitica.extensions.dpToPx
|
||||
import com.habitrpg.common.habitica.extensions.getThemeColor
|
||||
import com.habitrpg.common.habitica.extensions.setTintWith
|
||||
import com.habitrpg.common.habitica.helpers.ExceptionHandler
|
||||
import com.habitrpg.common.habitica.helpers.launchCatching
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AvatarCustomizationFragment :
|
||||
BaseMainFragment<FragmentRefreshRecyclerviewBinding>(),
|
||||
SwipeRefreshLayout.OnRefreshListener {
|
||||
private var filterMenuItem: MenuItem? = null
|
||||
override var binding: FragmentRefreshRecyclerviewBinding? = null
|
||||
|
||||
override fun createBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
): FragmentRefreshRecyclerviewBinding {
|
||||
return FragmentRefreshRecyclerviewBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var customizationRepository: CustomizationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var inventoryRepository: InventoryRepository
|
||||
|
||||
@Inject
|
||||
lateinit var userViewModel: MainUserViewModel
|
||||
|
||||
var type: String? = null
|
||||
var category: String? = null
|
||||
private var activeCustomization: String? = null
|
||||
|
||||
internal var adapter: CustomizationRecyclerViewAdapter = CustomizationRecyclerViewAdapter()
|
||||
internal var layoutManager: FlexboxLayoutManager = FlexboxLayoutManager(mainActivity, ROW)
|
||||
|
||||
private val currentFilter = MutableStateFlow(CustomizationFilter(false, true))
|
||||
private val ownedCustomizations = MutableStateFlow<List<OwnedCustomization>>(emptyList())
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
showsBackButton = true
|
||||
adapter.onCustomizationSelected = { customization ->
|
||||
lifecycleScope.launchCatching {
|
||||
if (customization.identifier?.isNotBlank() != true) {
|
||||
userRepository.useCustomization(customization.type ?: "", customization.category, activeCustomization ?: "")
|
||||
} else if (customization.type == "background" && ownedCustomizations.value.firstOrNull { it.key == customization.identifier } == null) {
|
||||
userRepository.unlockPath(customization)
|
||||
userRepository.retrieveUser(false, true, true)
|
||||
} else {
|
||||
userRepository.useCustomization(
|
||||
customization.type ?: "",
|
||||
customization.category,
|
||||
customization.identifier ?: "",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.onShowPurchaseDialog = { item ->
|
||||
val dialog = PurchaseDialog(requireContext(), item, mainActivity)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
lifecycleScope.launchCatching {
|
||||
inventoryRepository.getInAppRewards()
|
||||
.map { rewards -> rewards.map { it.key } }
|
||||
.collect { adapter.setPinnedItemKeys(it) }
|
||||
}
|
||||
|
||||
return super.onCreateView(inflater, container, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onViewCreated(
|
||||
view: View,
|
||||
savedInstanceState: Bundle?,
|
||||
) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
arguments?.let {
|
||||
val args = AvatarCustomizationFragmentArgs.fromBundle(it)
|
||||
type = args.type
|
||||
if (args.category.isNotEmpty()) {
|
||||
category = args.category
|
||||
}
|
||||
currentFilter.value.ascending = type != "background"
|
||||
}
|
||||
adapter.customizationType = type
|
||||
binding?.refreshLayout?.setOnRefreshListener(this)
|
||||
layoutManager = FlexboxLayoutManager(mainActivity, ROW)
|
||||
layoutManager.justifyContent = JustifyContent.CENTER
|
||||
layoutManager.alignItems = AlignItems.FLEX_START
|
||||
binding?.recyclerView?.layoutManager = layoutManager
|
||||
|
||||
binding?.recyclerView?.addItemDecoration(MarginDecoration(context))
|
||||
|
||||
binding?.recyclerView?.adapter = adapter
|
||||
binding?.recyclerView?.itemAnimator = SafeDefaultItemAnimator()
|
||||
this.loadCustomizations()
|
||||
|
||||
userViewModel.user.observe(viewLifecycleOwner) { updateUser(it) }
|
||||
|
||||
binding?.recyclerView?.doOnLayout {
|
||||
adapter.columnCount = it.width / (80.dpToPx(context))
|
||||
}
|
||||
|
||||
lifecycleScope.launchCatching {
|
||||
currentFilter.collect {
|
||||
Log.e("NewFilter", it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
Analytics.sendNavigationEvent("$type screen")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
customizationRepository.close()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(
|
||||
menu: Menu,
|
||||
inflater: MenuInflater,
|
||||
) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.menu_list_customizations, menu)
|
||||
|
||||
filterMenuItem = menu.findItem(R.id.action_filter)
|
||||
updateFilterIcon()
|
||||
}
|
||||
|
||||
private fun updateFilterIcon() {
|
||||
if (!currentFilter.value.isFiltering) {
|
||||
filterMenuItem?.setIcon(R.drawable.ic_action_filter_list)
|
||||
context?.let {
|
||||
val filterIcon = ContextCompat.getDrawable(it, R.drawable.ic_action_filter_list)
|
||||
filterIcon?.setTintWith(it.getThemeColor(R.attr.headerTextColor), PorterDuff.Mode.MULTIPLY)
|
||||
filterMenuItem?.setIcon(filterIcon)
|
||||
}
|
||||
} else {
|
||||
context?.let {
|
||||
val filterIcon = ContextCompat.getDrawable(it, R.drawable.ic_filters_active)
|
||||
filterIcon?.setTintWith(it.getThemeColor(R.attr.textColorPrimaryDark), PorterDuff.Mode.MULTIPLY)
|
||||
filterMenuItem?.setIcon(filterIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_filter -> {
|
||||
showFilterDialog()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun loadCustomizations() {
|
||||
val type = this.type ?: return
|
||||
lifecycleScope.launchCatching {
|
||||
customizationRepository.getCustomizations(type, category, false)
|
||||
.combine(currentFilter) { customizations, filter -> Pair(customizations, filter) }
|
||||
.combine(ownedCustomizations) { pair, ownedCustomizations -> Triple(pair.first, pair.second, ownedCustomizations) }
|
||||
.collect { (customizations, filter, ownedCustomizations) ->
|
||||
adapter.ownedCustomizations =
|
||||
ownedCustomizations.map { it.key + "_" + it.type + "_" + it.category }
|
||||
if (filter.isFiltering) {
|
||||
val displayedCustomizations = mutableListOf<Customization>()
|
||||
for (customization in customizations) {
|
||||
if (shouldSkip(filter, ownedCustomizations, customization)) continue
|
||||
displayedCustomizations.add(customization)
|
||||
}
|
||||
adapter.setCustomizations(
|
||||
if (!filter.ascending) {
|
||||
displayedCustomizations.reversed()
|
||||
} else {
|
||||
displayedCustomizations
|
||||
},
|
||||
)
|
||||
} else {
|
||||
adapter.setCustomizations(
|
||||
if (!filter.ascending) {
|
||||
customizations.reversed()
|
||||
} else {
|
||||
customizations
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type == "hair" && (category == "beard" || category == "mustache")) {
|
||||
val otherCategory = if (category == "mustache") "beard" else "mustache"
|
||||
lifecycleScope.launchCatching {
|
||||
customizationRepository.getCustomizations(type, otherCategory, true).collect {
|
||||
adapter.additionalSetItems = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldSkip(
|
||||
filter: CustomizationFilter,
|
||||
ownedCustomizations: List<OwnedCustomization>,
|
||||
customization: Customization,
|
||||
): Boolean {
|
||||
return if (filter.onlyPurchased && ownedCustomizations.find { it.key == customization.identifier } == null) {
|
||||
true
|
||||
} else {
|
||||
filter.months.isNotEmpty() && !filter.months.contains(customization.customizationSet?.substringAfter('.'))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUser(user: User?) {
|
||||
if (user == null) return
|
||||
this.updateActiveCustomization(user)
|
||||
ownedCustomizations.value = user.purchased?.customizations?.filter { it.type == this.type && it.purchased } ?: emptyList()
|
||||
this.adapter.userSize = user.preferences?.size
|
||||
this.adapter.hairColor = user.preferences?.hair?.color
|
||||
this.adapter.gemBalance = user.gemCount
|
||||
this.adapter.avatar = user
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun updateActiveCustomization(user: User) {
|
||||
if (this.type == null || user.preferences == null) {
|
||||
return
|
||||
}
|
||||
val prefs = user.preferences
|
||||
val activeCustomization =
|
||||
when (this.type) {
|
||||
"skin" -> prefs?.skin
|
||||
"shirt" -> prefs?.shirt
|
||||
"background" -> prefs?.background
|
||||
"chair" -> prefs?.chair
|
||||
"hair" ->
|
||||
when (this.category) {
|
||||
"bangs" -> prefs?.hair?.bangs.toString()
|
||||
"base" -> prefs?.hair?.base.toString()
|
||||
"color" -> prefs?.hair?.color
|
||||
"flower" -> prefs?.hair?.flower.toString()
|
||||
"beard" -> prefs?.hair?.beard.toString()
|
||||
"mustache" -> prefs?.hair?.mustache.toString()
|
||||
else -> ""
|
||||
}
|
||||
else -> ""
|
||||
}
|
||||
if (activeCustomization != null) {
|
||||
this.activeCustomization = activeCustomization
|
||||
this.adapter.activeCustomization = activeCustomization
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
lifecycleScope.launch(ExceptionHandler.coroutine()) {
|
||||
userRepository.retrieveUser(true, true)
|
||||
binding?.refreshLayout?.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
fun showFilterDialog() {
|
||||
val filter = currentFilter.value
|
||||
val context = context ?: return
|
||||
val dialog = HabiticaBottomSheetDialog(context)
|
||||
val binding = BottomSheetBackgroundsFilterBinding.inflate(layoutInflater)
|
||||
binding.showMeWrapper.check(if (filter.onlyPurchased) R.id.show_purchased_button else R.id.show_all_button)
|
||||
binding.showMeWrapper.setOnCheckedChangeListener { _, checkedId ->
|
||||
val newFilter = filter.copy()
|
||||
newFilter.onlyPurchased = checkedId == R.id.show_purchased_button
|
||||
currentFilter.value = newFilter
|
||||
}
|
||||
binding.clearButton.setOnClickListener {
|
||||
currentFilter.value = CustomizationFilter(false, type != "background")
|
||||
dialog.dismiss()
|
||||
}
|
||||
if (type == "background") {
|
||||
binding.sortByWrapper.check(if (filter.ascending) R.id.oldest_button else R.id.newest_button)
|
||||
binding.sortByWrapper.setOnCheckedChangeListener { _, checkedId ->
|
||||
val newFilter = filter.copy()
|
||||
newFilter.ascending = checkedId == R.id.oldest_button
|
||||
currentFilter.value = newFilter
|
||||
}
|
||||
configureMonthFilterButton(binding.januaryButton, 1, filter)
|
||||
configureMonthFilterButton(binding.febuaryButton, 2, filter)
|
||||
configureMonthFilterButton(binding.marchButton, 3, filter)
|
||||
configureMonthFilterButton(binding.aprilButton, 4, filter)
|
||||
configureMonthFilterButton(binding.mayButton, 5, filter)
|
||||
configureMonthFilterButton(binding.juneButton, 6, filter)
|
||||
configureMonthFilterButton(binding.julyButton, 7, filter)
|
||||
configureMonthFilterButton(binding.augustButton, 8, filter)
|
||||
configureMonthFilterButton(binding.septemberButton, 9, filter)
|
||||
configureMonthFilterButton(binding.octoberButton, 10, filter)
|
||||
configureMonthFilterButton(binding.novemberButton, 11, filter)
|
||||
configureMonthFilterButton(binding.decemberButton, 12, filter)
|
||||
} else {
|
||||
binding.sortByTitle.visibility = View.GONE
|
||||
binding.sortByWrapper.visibility = View.GONE
|
||||
binding.monthReleasedTitle.visibility = View.GONE
|
||||
binding.monthReleasedWrapper.visibility = View.GONE
|
||||
}
|
||||
dialog.setContentView(binding.root)
|
||||
dialog.setOnDismissListener { updateFilterIcon() }
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun configureMonthFilterButton(
|
||||
button: CheckBox,
|
||||
value: Int,
|
||||
filter: CustomizationFilter,
|
||||
) {
|
||||
val identifier = value.toString().padStart(2, '0')
|
||||
button.isChecked = filter.months.contains(identifier)
|
||||
button.text
|
||||
button.setOnCheckedChangeListener { _, isChecked ->
|
||||
val newFilter = filter.copy()
|
||||
newFilter.months = mutableListOf()
|
||||
newFilter.months.addAll(currentFilter.value.months)
|
||||
if (!isChecked && newFilter.months.contains(identifier)) {
|
||||
button.typeface = Typeface.create("sans-serif", Typeface.NORMAL)
|
||||
newFilter.months.remove(identifier)
|
||||
} else if (isChecked && !newFilter.months.contains(identifier)) {
|
||||
button.typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL)
|
||||
newFilter.months.add(identifier)
|
||||
}
|
||||
currentFilter.value = newFilter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
package com.habitrpg.android.habitica.ui.fragments.inventory.customization
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.data.InventoryRepository
|
||||
import com.habitrpg.android.habitica.databinding.FragmentRefreshRecyclerviewBinding
|
||||
import com.habitrpg.android.habitica.helpers.Analytics
|
||||
import com.habitrpg.android.habitica.models.user.User
|
||||
import com.habitrpg.android.habitica.ui.adapter.CustomizationEquipmentRecyclerViewAdapter
|
||||
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
|
||||
import com.habitrpg.android.habitica.ui.helpers.MarginDecoration
|
||||
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
|
||||
import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog
|
||||
import com.habitrpg.common.habitica.helpers.ExceptionHandler
|
||||
import com.habitrpg.common.habitica.helpers.launchCatching
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AvatarEquipmentFragment :
|
||||
BaseMainFragment<FragmentRefreshRecyclerviewBinding>(),
|
||||
SwipeRefreshLayout.OnRefreshListener {
|
||||
@Inject
|
||||
lateinit var inventoryRepository: InventoryRepository
|
||||
|
||||
@Inject
|
||||
lateinit var userViewModel: MainUserViewModel
|
||||
|
||||
override var binding: FragmentRefreshRecyclerviewBinding? = null
|
||||
|
||||
override fun createBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
): FragmentRefreshRecyclerviewBinding {
|
||||
return FragmentRefreshRecyclerviewBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
var type: String? = null
|
||||
var category: String? = null
|
||||
private var activeEquipment: String? = null
|
||||
|
||||
internal var adapter: CustomizationEquipmentRecyclerViewAdapter =
|
||||
CustomizationEquipmentRecyclerViewAdapter()
|
||||
internal var layoutManager: GridLayoutManager = GridLayoutManager(mainActivity, 2)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
showsBackButton = true
|
||||
adapter.onSelect = { equipment ->
|
||||
val key =
|
||||
(if (equipment.key?.isNotBlank() != true) activeEquipment else equipment.key) ?: ""
|
||||
lifecycleScope.launchCatching {
|
||||
inventoryRepository.equip(
|
||||
if (userViewModel.user.value?.preferences?.costume == true) "costume" else "equipped",
|
||||
key,
|
||||
)
|
||||
}
|
||||
}
|
||||
adapter.onUnlock = { equipment ->
|
||||
lifecycleScope.launchCatching {
|
||||
inventoryRepository.purchaseItem("gear", equipment.key ?: "", 1)
|
||||
userRepository.retrieveUser(forced = true)
|
||||
}
|
||||
}
|
||||
adapter.onShowPurchaseDialog = { item ->
|
||||
val dialog = PurchaseDialog(requireContext(), item, mainActivity)
|
||||
dialog.show()
|
||||
}
|
||||
return super.onCreateView(inflater, container, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onViewCreated(
|
||||
view: View,
|
||||
savedInstanceState: Bundle?,
|
||||
) {
|
||||
showsBackButton = true
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
arguments?.let {
|
||||
val args = AvatarEquipmentFragmentArgs.fromBundle(it)
|
||||
type = args.type
|
||||
if (args.category.isNotEmpty()) {
|
||||
category = args.category
|
||||
}
|
||||
}
|
||||
binding?.refreshLayout?.setOnRefreshListener(this)
|
||||
setGridSpanCount(view.width)
|
||||
val layoutManager = GridLayoutManager(mainActivity, 4)
|
||||
layoutManager.spanSizeLookup =
|
||||
object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return if (adapter.getItemViewType(position) == 0) {
|
||||
layoutManager.spanCount
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
binding?.recyclerView?.layoutManager = layoutManager
|
||||
binding?.recyclerView?.addItemDecoration(MarginDecoration(context))
|
||||
|
||||
binding?.recyclerView?.adapter = adapter
|
||||
binding?.recyclerView?.itemAnimator = SafeDefaultItemAnimator()
|
||||
this.loadEquipment()
|
||||
|
||||
userViewModel.user.observe(viewLifecycleOwner) { updateUser(it) }
|
||||
|
||||
Analytics.sendNavigationEvent("$type screen")
|
||||
}
|
||||
|
||||
private fun loadEquipment() {
|
||||
val type = this.type ?: return
|
||||
lifecycleScope.launchCatching {
|
||||
inventoryRepository.getEquipmentType(type, category ?: "").collect {
|
||||
adapter.setEquipment(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setGridSpanCount(width: Int) {
|
||||
val itemWidth = context?.resources?.getDimension(R.dimen.customization_width) ?: 0F
|
||||
var spanCount = (width / itemWidth).toInt()
|
||||
if (spanCount == 0) {
|
||||
spanCount = 1
|
||||
}
|
||||
layoutManager.spanCount = spanCount
|
||||
}
|
||||
|
||||
fun updateUser(user: User?) {
|
||||
this.updateActiveCustomization(user)
|
||||
this.adapter.gemBalance = user?.gemCount ?: 0
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun updateActiveCustomization(user: User?) {
|
||||
if (this.type == null || user?.preferences == null) {
|
||||
return
|
||||
}
|
||||
val outfit =
|
||||
if (user.preferences?.costume == true) user.items?.gear?.costume else user.items?.gear?.equipped
|
||||
val activeEquipment =
|
||||
when (this.type) {
|
||||
"headAccessory" -> outfit?.headAccessory
|
||||
"back" -> outfit?.back
|
||||
"eyewear" -> outfit?.eyeWear
|
||||
else -> ""
|
||||
}
|
||||
if (activeEquipment != null) {
|
||||
this.activeEquipment = activeEquipment
|
||||
this.adapter.activeEquipment = activeEquipment
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
lifecycleScope.launch(ExceptionHandler.coroutine()) {
|
||||
userRepository.retrieveUser(true, true)
|
||||
binding?.refreshLayout?.isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,7 +52,6 @@ import com.habitrpg.common.habitica.helpers.MainNavigationController
|
|||
import com.habitrpg.common.habitica.helpers.launchCatching
|
||||
import com.habitrpg.common.habitica.theme.HabiticaTheme
|
||||
import com.habitrpg.common.habitica.views.ComposableAvatarView
|
||||
import com.habitrpg.shared.habitica.models.Avatar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import javax.inject.Inject
|
||||
|
|
@ -97,14 +96,13 @@ open class AvatarOverviewFragment :
|
|||
HabiticaTheme {
|
||||
val avatar by userViewModel.user.observeAsState()
|
||||
Column {
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.background(colorResource(R.color.window_background))) {
|
||||
ComposableAvatarView(
|
||||
avatar = avatar,
|
||||
configManager = appConfigManager,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 24.dp)
|
||||
.padding(top = 6.dp, bottom = 24.dp)
|
||||
.size(140.dp, 147.dp),
|
||||
)
|
||||
Box(
|
||||
|
|
@ -159,32 +157,19 @@ open class AvatarOverviewFragment :
|
|||
type: String,
|
||||
category: String?,
|
||||
) {
|
||||
if (appConfigManager.enableCustomizationShop()) {
|
||||
MainNavigationController.navigate(
|
||||
AvatarOverviewFragmentDirections.openComposeAvatarDetail(
|
||||
type,
|
||||
category ?: "",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
MainNavigationController.navigate(
|
||||
AvatarOverviewFragmentDirections.openAvatarDetail(
|
||||
type,
|
||||
category ?: "",
|
||||
),
|
||||
)
|
||||
}
|
||||
MainNavigationController.navigate(
|
||||
AvatarOverviewFragmentDirections.openComposeAvatarDetail(
|
||||
type,
|
||||
category ?: "",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun displayAvatarEquipmentFragment(
|
||||
type: String,
|
||||
category: String?,
|
||||
) {
|
||||
if (appConfigManager.enableCustomizationShop()) {
|
||||
MainNavigationController.navigate(AvatarOverviewFragmentDirections.openComposeAvatarEquipment(type, category ?: ""))
|
||||
} else {
|
||||
MainNavigationController.navigate(AvatarOverviewFragmentDirections.openAvatarEquipment(type, category ?: ""))
|
||||
}
|
||||
MainNavigationController.navigate(AvatarOverviewFragmentDirections.openComposeAvatarEquipment(type, category ?: ""))
|
||||
}
|
||||
|
||||
private fun displayEquipmentFragment(
|
||||
|
|
|
|||
|
|
@ -478,7 +478,7 @@ private fun AvatarCustomizationView(
|
|||
configManager = configManager,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 24.dp)
|
||||
.padding(top = 6.dp, bottom = 24.dp)
|
||||
.size(140.dp, 147.dp),
|
||||
)
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan
|
|||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -53,17 +52,13 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.data.InventoryRepository
|
||||
import com.habitrpg.android.habitica.databinding.FragmentComposeBinding
|
||||
import com.habitrpg.android.habitica.helpers.Analytics
|
||||
import com.habitrpg.android.habitica.helpers.AppConfigManager
|
||||
import com.habitrpg.android.habitica.models.inventory.Customization
|
||||
import com.habitrpg.android.habitica.models.inventory.Equipment
|
||||
import com.habitrpg.android.habitica.models.user.Gear
|
||||
import com.habitrpg.android.habitica.models.user.User
|
||||
import com.habitrpg.android.habitica.ui.adapter.CustomizationEquipmentRecyclerViewAdapter
|
||||
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
|
||||
import com.habitrpg.android.habitica.ui.theme.colors
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
|
||||
|
|
@ -229,7 +224,7 @@ private fun AvatarEquipmentView(
|
|||
configManager = configManager,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 24.dp)
|
||||
.padding(top = 6.dp, bottom = 24.dp)
|
||||
.size(140.dp, 147.dp),
|
||||
)
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,21 @@ import android.view.ViewGroup
|
|||
import android.widget.AutoCompleteTextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.compose.foundation.background
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.cursoradapter.widget.SimpleCursorAdapter
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -22,12 +37,14 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.data.InventoryRepository
|
||||
import com.habitrpg.android.habitica.databinding.FragmentRefreshRecyclerviewBinding
|
||||
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.ui.adapter.inventory.EquipmentRecyclerViewAdapter
|
||||
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
|
||||
import com.habitrpg.android.habitica.ui.helpers.KeyboardUtil
|
||||
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
|
||||
import com.habitrpg.android.habitica.ui.helpers.ToolbarColorHelper
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
|
||||
import com.habitrpg.common.habitica.extensions.dpToPx
|
||||
import com.habitrpg.common.habitica.extensions.observeOnce
|
||||
|
|
@ -35,6 +52,8 @@ 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 com.habitrpg.common.habitica.theme.HabiticaTheme
|
||||
import com.habitrpg.common.habitica.views.ComposableAvatarView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
|
@ -44,12 +63,12 @@ import javax.inject.Inject
|
|||
|
||||
@AndroidEntryPoint
|
||||
class EquipmentDetailFragment :
|
||||
BaseMainFragment<FragmentRefreshRecyclerviewBinding>(),
|
||||
BaseMainFragment<FragmentEquipmentDetailBinding>(),
|
||||
SwipeRefreshLayout.OnRefreshListener, MenuProvider {
|
||||
@Inject
|
||||
lateinit var inventoryRepository: InventoryRepository
|
||||
|
||||
override var binding: FragmentRefreshRecyclerviewBinding? = null
|
||||
override var binding: FragmentEquipmentDetailBinding? = null
|
||||
|
||||
@Inject
|
||||
lateinit var userViewModel: MainUserViewModel
|
||||
|
|
@ -57,11 +76,14 @@ class EquipmentDetailFragment :
|
|||
@Inject
|
||||
lateinit var reviewManager: ReviewManager
|
||||
|
||||
@Inject
|
||||
lateinit var configManager: AppConfigManager
|
||||
|
||||
override fun createBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
): FragmentRefreshRecyclerviewBinding {
|
||||
return FragmentRefreshRecyclerviewBinding.inflate(inflater, container, false)
|
||||
): FragmentEquipmentDetailBinding {
|
||||
return FragmentEquipmentDetailBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
var type: String? = null
|
||||
|
|
@ -77,6 +99,9 @@ class EquipmentDetailFragment :
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
showsBackButton = true
|
||||
hidesToolbar = true
|
||||
|
||||
adapter.onEquip = {
|
||||
lifecycleScope.launchCatching {
|
||||
inventoryRepository.equipGear(it, isCostume ?: false)
|
||||
|
|
@ -100,7 +125,6 @@ class EquipmentDetailFragment :
|
|||
view: View,
|
||||
savedInstanceState: Bundle?,
|
||||
) {
|
||||
showsBackButton = true
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
arguments?.let {
|
||||
|
|
@ -120,6 +144,28 @@ class EquipmentDetailFragment :
|
|||
MainNavigationController.navigate(R.id.marketFragment)
|
||||
}
|
||||
|
||||
binding?.avatarHeader?.setContent {
|
||||
HabiticaTheme {
|
||||
val avatar by userViewModel.user.observeAsState()
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.background(colorResource(R.color.window_background))) {
|
||||
ComposableAvatarView(
|
||||
avatar = avatar,
|
||||
configManager = configManager,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 6.dp, bottom = 24.dp)
|
||||
.size(140.dp, 147.dp),
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.background(colorResource(R.color.content_background), RoundedCornerShape(topStart = 22.dp, topEnd = 22.dp))
|
||||
.fillMaxWidth()
|
||||
.height(22.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.adapter.equippedGear = this.equippedGear
|
||||
this.adapter.isCostume = this.isCostume
|
||||
this.adapter.type = this.type
|
||||
|
|
@ -230,6 +276,12 @@ class EquipmentDetailFragment :
|
|||
return false
|
||||
}
|
||||
})
|
||||
|
||||
mainActivity?.toolbar?.let {
|
||||
val color = ContextCompat.getColor(requireContext(), R.color.window_background)
|
||||
ToolbarColorHelper.colorizeToolbar(it, mainActivity, backgroundColor = color)
|
||||
requireActivity().window.statusBarColor = color
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ open class NotificationsViewModel
|
|||
}
|
||||
when (data?.destination) {
|
||||
"equipment" -> navController.navigate(R.id.equipmentOverviewFragment)
|
||||
"customization" -> navController.navigate(R.id.avatarCustomizationFragment)
|
||||
"customization" -> navController.navigate(R.id.composeAvatarEquipmentFragment)
|
||||
"stable" -> navController.navigate(R.id.stableFragment)
|
||||
"pets" -> navController.navigate(R.id.stableFragment)
|
||||
"mounts" -> navController.navigate(R.id.stableFragment)
|
||||
|
|
|
|||
|
|
@ -12,106 +12,105 @@ import com.habitrpg.common.habitica.extensions.dpToPx
|
|||
import com.habitrpg.shared.habitica.models.tasks.TaskType
|
||||
|
||||
class ReminderContainer
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
var taskType = TaskType.DAILY
|
||||
set(value) {
|
||||
field = value
|
||||
for (view in children) {
|
||||
if (view is ReminderItemFormView) {
|
||||
view.taskType = taskType
|
||||
}
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
var taskType = TaskType.DAILY
|
||||
set(value) {
|
||||
field = value
|
||||
for (view in children) {
|
||||
if (view is ReminderItemFormView) {
|
||||
view.taskType = taskType
|
||||
}
|
||||
}
|
||||
var reminders: List<RemindersItem>
|
||||
get() {
|
||||
val list = mutableListOf<RemindersItem>()
|
||||
for (child in children) {
|
||||
val view = child as? ReminderItemFormView ?: continue
|
||||
if (view.item.time != null) {
|
||||
list.add(view.item)
|
||||
}
|
||||
}
|
||||
var reminders: List<RemindersItem>
|
||||
get() {
|
||||
val list = mutableListOf<RemindersItem>()
|
||||
for (child in children) {
|
||||
val view = child as? ReminderItemFormView ?: continue
|
||||
if (view.item.time != null) {
|
||||
list.add(view.item)
|
||||
}
|
||||
return list
|
||||
}
|
||||
set(value) {
|
||||
val unAnimatedTransitions = LayoutTransition()
|
||||
unAnimatedTransitions.disableTransitionType(LayoutTransition.APPEARING)
|
||||
unAnimatedTransitions.disableTransitionType(LayoutTransition.CHANGING)
|
||||
unAnimatedTransitions.disableTransitionType(LayoutTransition.DISAPPEARING)
|
||||
layoutTransition = unAnimatedTransitions
|
||||
if (childCount > 1) {
|
||||
for (child in children.take(childCount - 1)) {
|
||||
removeView(child)
|
||||
}
|
||||
return list
|
||||
}
|
||||
set(value) {
|
||||
val unAnimatedTransitions = LayoutTransition()
|
||||
unAnimatedTransitions.disableTransitionType(LayoutTransition.APPEARING)
|
||||
unAnimatedTransitions.disableTransitionType(LayoutTransition.CHANGING)
|
||||
unAnimatedTransitions.disableTransitionType(LayoutTransition.DISAPPEARING)
|
||||
layoutTransition = unAnimatedTransitions
|
||||
if (childCount > 1) {
|
||||
for (child in children.take(childCount - 1)) {
|
||||
removeView(child)
|
||||
}
|
||||
for (item in value) {
|
||||
addReminderViewAt(childCount - 1, item)
|
||||
}
|
||||
val animatedTransitions = LayoutTransition()
|
||||
layoutTransition = animatedTransitions
|
||||
}
|
||||
|
||||
var firstDayOfWeek: Int? = null
|
||||
set(value) {
|
||||
children
|
||||
.filterIsInstance<ReminderItemFormView>()
|
||||
.forEach { it.firstDayOfWeek = value }
|
||||
field = value
|
||||
for (item in value) {
|
||||
addReminderViewAt(childCount - 1, item)
|
||||
}
|
||||
|
||||
var showNotifPermission: ((Boolean) -> Unit)? = null
|
||||
var shouldShowNotifPermission = false
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
|
||||
addReminderViewAt(0)
|
||||
val animatedTransitions = LayoutTransition()
|
||||
layoutTransition = animatedTransitions
|
||||
}
|
||||
|
||||
private fun addReminderViewAt(
|
||||
index: Int,
|
||||
item: RemindersItem? = null,
|
||||
) {
|
||||
val view = ReminderItemFormView(context)
|
||||
view.firstDayOfWeek = firstDayOfWeek
|
||||
view.taskType = taskType
|
||||
item?.let {
|
||||
view.item = it
|
||||
view.isAddButton = false
|
||||
}
|
||||
view.valueChangedListener = {
|
||||
if (isLastChild(view)) {
|
||||
addReminderViewAt(-1)
|
||||
view.animDuration = 300
|
||||
view.isAddButton = false
|
||||
if (shouldShowNotifPermission) {
|
||||
showNotifPermission?.invoke(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
val indexToUse =
|
||||
if (index < 0) {
|
||||
childCount - index
|
||||
} else {
|
||||
index
|
||||
}
|
||||
if (childCount <= indexToUse) {
|
||||
addView(view)
|
||||
view.isAddButton = true
|
||||
} else {
|
||||
addView(view, indexToUse)
|
||||
}
|
||||
val layoutParams = view.layoutParams as? LayoutParams
|
||||
layoutParams?.updateMargins(bottom = 8.dpToPx(context))
|
||||
view.layoutParams = layoutParams
|
||||
var firstDayOfWeek: Int? = null
|
||||
set(value) {
|
||||
children
|
||||
.filterIsInstance<ReminderItemFormView>()
|
||||
.forEach { it.firstDayOfWeek = value }
|
||||
field = value
|
||||
}
|
||||
|
||||
private fun isLastChild(view: View): Boolean {
|
||||
return children.lastOrNull() == view
|
||||
}
|
||||
var showNotifPermission: ((Boolean) -> Unit)? = null
|
||||
var shouldShowNotifPermission = false
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
addReminderViewAt(0)
|
||||
}
|
||||
|
||||
private fun addReminderViewAt(
|
||||
index: Int,
|
||||
item: RemindersItem? = null,
|
||||
) {
|
||||
val view = ReminderItemFormView(context)
|
||||
view.firstDayOfWeek = firstDayOfWeek
|
||||
view.taskType = taskType
|
||||
item?.let {
|
||||
view.item = it
|
||||
view.isAddButton = false
|
||||
}
|
||||
view.valueChangedListener = {
|
||||
if (isLastChild(view)) {
|
||||
addReminderViewAt(-1)
|
||||
view.animDuration = 300
|
||||
view.isAddButton = false
|
||||
if (shouldShowNotifPermission) {
|
||||
showNotifPermission?.invoke(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
val indexToUse =
|
||||
if (index < 0) {
|
||||
childCount - index
|
||||
} else {
|
||||
index
|
||||
}
|
||||
if (childCount <= indexToUse) {
|
||||
addView(view)
|
||||
view.isAddButton = true
|
||||
} else {
|
||||
addView(view, indexToUse)
|
||||
}
|
||||
val layoutParams = view.layoutParams as? LayoutParams
|
||||
layoutParams?.updateMargins(bottom = 8.dpToPx(context))
|
||||
view.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
private fun isLastChild(view: View): Boolean {
|
||||
return children.lastOrNull() == view
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,14 +63,6 @@ Submit a new Beta Build to Google Play
|
|||
|
||||
Deploy a new version to the Google Play
|
||||
|
||||
### android upload_to_slack
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane android upload_to_slack
|
||||
```
|
||||
|
||||
Upload the latest output APK to slack
|
||||
|
||||
----
|
||||
|
||||
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
NAME=4.5.0
|
||||
CODE=9231
|
||||
CODE=9261
|
||||
Loading…
Reference in a new issue