diff --git a/Habitica/AndroidManifest.xml b/Habitica/AndroidManifest.xml index 60a1a8bc6..bc77b4c59 100644 --- a/Habitica/AndroidManifest.xml +++ b/Habitica/AndroidManifest.xml @@ -3,7 +3,6 @@ @@ -239,10 +238,12 @@ - + diff --git a/Habitica/res/layout/activity_task_form.xml b/Habitica/res/layout/activity_task_form.xml index 570474a34..554d0c01e 100644 --- a/Habitica/res/layout/activity_task_form.xml +++ b/Habitica/res/layout/activity_task_form.xml @@ -227,7 +227,6 @@ android:text="@string/push_notification_system_settings_reminders" android:textColor="@color/text_quad" android:textSize="12sp" /> - + + + + + + + + + + + diff --git a/Habitica/res/navigation/navigation.xml b/Habitica/res/navigation/navigation.xml index 75ccee437..dc5fb951e 100644 --- a/Habitica/res/navigation/navigation.xml +++ b/Habitica/res/navigation/navigation.xml @@ -108,29 +108,12 @@ - - - - - - - - - - Enable Notifications Allow Habitica notifications in the Settings app to receive push notifications Allow Habitica notifications in the Settings app to receive reminders + Habitica does not have the `Alarms & Reminders` permission in the Settings app. Reminders might not appear at the exact time. Notifications Disabled Set your push notifications settings + `Alarm & Reminders` disabled` + Allow `Alarms & Reminders` in the Settings app to ensure reminders appear at the scheduled time exactly You won a Challenge! Received a Private Message Gifted Gems @@ -1539,8 +1542,8 @@ Clear Database Clear Cache Best Deal - Unlock %d Gems per month instantly - Unlocks %d Gems per month instantly + Unlock %d Gems per month in the Market + Unlocks %d Gems per month in the Market %d Gems Earn +2 Gems every month you\'re subscribed Earns +2 Gems every month they\'re subscribed diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt index 31c7bdb86..bbf6a70f8 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt @@ -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) } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/NotificationPublisher.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/NotificationPublisher.kt index bbd89bdb6..e2b7d66eb 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/NotificationPublisher.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/NotificationPublisher.kt @@ -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( diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskAlarmBootReceiver.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskAlarmBootReceiver.kt index 3d57ae3b5..18ed98138 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskAlarmBootReceiver.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskAlarmBootReceiver.kt @@ -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()) { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskReceiver.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskReceiver.kt index 50a7470ec..dfde12b54 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskReceiver.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskReceiver.kt @@ -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) +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt index 932c8f3aa..13d95c675 100755 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt @@ -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 = diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt index 2c7a63e29..c55823dd0 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt @@ -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() { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarCustomizationFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarCustomizationFragment.kt deleted file mode 100644 index 71883c1f9..000000000 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarCustomizationFragment.kt +++ /dev/null @@ -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(), - 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>(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() - 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, - 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 - } - } -} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarEquipmentFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarEquipmentFragment.kt deleted file mode 100644 index d1b2156c6..000000000 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarEquipmentFragment.kt +++ /dev/null @@ -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(), - 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 - } - } -} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarOverviewFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarOverviewFragment.kt index 92a3527ee..925b9efea 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarOverviewFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarOverviewFragment.kt @@ -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( diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarCustomizationFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarCustomizationFragment.kt index 749adfe18..2eb19fd6b 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarCustomizationFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarCustomizationFragment.kt @@ -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( diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarEquipmentFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarEquipmentFragment.kt index d0bb1a72b..4c15da9fb 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarEquipmentFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarEquipmentFragment.kt @@ -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( diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentDetailFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentDetailFragment.kt index bf881e7e5..e55bee650 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentDetailFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentDetailFragment.kt @@ -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(), + BaseMainFragment(), 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 { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/NotificationsViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/NotificationsViewModel.kt index 8c753bf98..b5f39d006 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/NotificationsViewModel.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/NotificationsViewModel.kt @@ -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) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderContainer.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderContainer.kt index 8bbe551e7..3df8716d1 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderContainer.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderContainer.kt @@ -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 - get() { - val list = mutableListOf() - for (child in children) { - val view = child as? ReminderItemFormView ?: continue - if (view.item.time != null) { - list.add(view.item) - } + } + var reminders: List + get() { + val list = mutableListOf() + 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() - .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() + .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 + } +} diff --git a/fastlane/README.md b/fastlane/README.md index 2eff082fe..f57928b5d 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -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. diff --git a/version.properties b/version.properties index ab4db7ff1..0d6a5a78e 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ NAME=4.5.0 -CODE=9231 \ No newline at end of file +CODE=9261 \ No newline at end of file