feat: Update activity transitions, prepare for predictive-back (#650)
Previous code used a single animation type (slide) when transitioning, the transition was quite slow, and didn't behave appropriately if the device was set to a RTL writing system. In addition, handling the back affordance didn't work well with the new "Predictive Back" feature in Android 14 (https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture). Fix this. ## Transitions To update the transitions the `startActivityWithSlideInAnimation()` implementation (and associated `finishWithoutSlideOutAnimation()`) have been replaced. There are three transitions; `default`, `slide`, and `explode`, represented as an enum passed to the activity intent. The `default` transition is the activity transition from Android 14, from the Android open source project (https://cs.android.com/android/platform/superproject/+/android-14.0.0_r18:frameworks/base/core/res/res/anim/;bpv=1). This is used for most transitions. The `slide` transition is the pre-existing slide transition, with a shorter transition time so it feels more responsive, and an RTL implementation. This is used when there is a strong spatial component to the navigation, for example, when going from: - a status to its thread - a preference menu item to its subscreen - a filter in a list to the "edit filter" screen - viewing your profile to editing your profile The `explode` transition is used when the state of the app changes significantly, such as when switching accounts. Activities are now started with `startActivityWithTransition()` which sets the intent and prepares the transition. `BaseActivity` checks the intent for the transition type and makes further changes to the transition as necessary. ## Predictive back "Predictive back" needs to know what the back button would do before the user interacts with it with an `onBackPressedCallback` that is either enabled or disabled. This required refactoring some code (particularly in `ComposeActivity`) to gather data ahead of time and enable/disable the callback appropriately. ## Fixed bugs - Back button wasn't stepping back through the tabs in AccountActivity - Modifying a filter and pressing back without saving wasn't prompting the user to save the changes - Writing a content warning and then hiding it would still count the text of the content warning toward's the post's length ## Other cleanups - Use `ViewCompat.setTransitionName()` instead of setting the `transitionName` property - Delete the unused `fade_in` and `fade_out` animations. - Use androidx-activity 1.9.0 to get the latest predictive back support library code - Show validation errors when creating / editing filters
This commit is contained in:
parent
2b3cbb6465
commit
93e6b38d43
|
@ -54,7 +54,7 @@
|
|||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `networkSecurityConfig` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:networkSecurityConfig="@xml/network_security_config">"
|
||||
errorLine1=" android:networkSecurityConfig="@xml/network_security_config""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
|
@ -62,6 +62,17 @@
|
|||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `enableOnBackInvokedCallback` is only used in API level 33 and higher (current min is 23)"
|
||||
errorLine1=" android:enableOnBackInvokedCallback="true">"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="26"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="SelectedPhotoAccess"
|
||||
message="Your app is currently not handling Selected Photos Access introduced in Android 14+"
|
||||
|
@ -883,7 +894,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/account/AccountActivity.kt"
|
||||
line="493"
|
||||
line="515"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -905,7 +916,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/AccountFieldEditAdapter.kt"
|
||||
line="44"
|
||||
line="49"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -916,7 +927,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/AccountFieldEditAdapter.kt"
|
||||
line="50"
|
||||
line="55"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="false"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
|
|
|
@ -30,6 +30,7 @@ import androidx.activity.OnBackPressedCallback
|
|||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -59,6 +60,7 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
|||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -75,7 +77,9 @@ class EditProfileActivity : BaseActivity() {
|
|||
|
||||
private val binding by viewBinding(ActivityEditProfileBinding::inflate)
|
||||
|
||||
private val accountFieldEditAdapter = AccountFieldEditAdapter()
|
||||
private val accountFieldEditAdapter: AccountFieldEditAdapter = AccountFieldEditAdapter {
|
||||
viewModel.onChange(currentProfileData)
|
||||
}
|
||||
|
||||
private var maxAccountFields = InstanceInfoRepository.DEFAULT_MAX_ACCOUNT_FIELDS
|
||||
|
||||
|
@ -108,6 +112,12 @@ class EditProfileActivity : BaseActivity() {
|
|||
fields = accountFieldEditAdapter.getFieldData(),
|
||||
)
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
showUnsavedChangesDialog()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -216,19 +226,19 @@ class EditProfileActivity : BaseActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
val onBackCallback = object : OnBackPressedCallback(enabled = true) {
|
||||
override fun handleOnBackPressed() = checkForUnsavedChanges()
|
||||
lifecycleScope.launch {
|
||||
viewModel.isDirty.collectLatest { onBackPressedCallback.isEnabled = it }
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackCallback)
|
||||
}
|
||||
|
||||
fun checkForUnsavedChanges() {
|
||||
if (viewModel.hasUnsavedChanges(currentProfileData)) {
|
||||
showUnsavedChangesDialog()
|
||||
} else {
|
||||
finish()
|
||||
binding.displayNameEditText.doAfterTextChanged {
|
||||
viewModel.onChange(currentProfileData)
|
||||
}
|
||||
|
||||
binding.lockedCheckBox.setOnCheckedChangeListener { _, _ ->
|
||||
viewModel.onChange(currentProfileData)
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
|
|
@ -53,6 +53,7 @@ import androidx.core.view.GravityCompat
|
|||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.widget.MarginPageTransformer
|
||||
import app.pachli.appstore.AnnouncementReadEvent
|
||||
|
@ -70,6 +71,9 @@ import app.pachli.core.activity.AccountSelectionListener
|
|||
import app.pachli.core.activity.BottomSheetActivity
|
||||
import app.pachli.core.activity.PostLookupFallbackBehavior
|
||||
import app.pachli.core.activity.emojify
|
||||
import app.pachli.core.activity.extensions.TransitionKind
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.activity.extensions.startActivityWithTransition
|
||||
import app.pachli.core.common.di.ApplicationScope
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
|
@ -215,6 +219,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
/** Adapter for the different timeline tabs */
|
||||
private lateinit var tabAdapter: MainPagerAdapter
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
when {
|
||||
binding.mainDrawerLayout.isOpen -> binding.mainDrawerLayout.close()
|
||||
binding.viewPager.currentItem != 0 -> binding.viewPager.currentItem = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -284,7 +297,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
// otherwise show notification tab
|
||||
if (MainActivityIntent.getNotificationType(intent) == Notification.Type.FOLLOW_REQUEST) {
|
||||
val intent = AccountListActivityIntent(this, AccountListActivityIntent.Kind.FOLLOW_REQUESTS)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
} else {
|
||||
showNotificationTab = true
|
||||
}
|
||||
|
@ -379,24 +392,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
|
||||
selectedEmojiPack = sharedPreferencesRepository.getString(EMOJI_PREFERENCE, "")
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
when {
|
||||
binding.mainDrawerLayout.isOpen -> {
|
||||
binding.mainDrawerLayout.close()
|
||||
}
|
||||
binding.viewPager.currentItem != 0 -> {
|
||||
binding.viewPager.currentItem = 0
|
||||
}
|
||||
else -> {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= TIRAMISU && ActivityCompat.checkSelfPermission(this, POST_NOTIFICATIONS) != PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, arrayOf(POST_NOTIFICATIONS), 1)
|
||||
|
@ -500,7 +496,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_SEARCH -> {
|
||||
startActivityWithSlideInAnimation(SearchActivityIntent(this))
|
||||
startActivityWithDefaultTransition(SearchActivityIntent(this))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -603,6 +599,20 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
refreshMainDrawerItems(addSearchButton)
|
||||
setSavedInstance(savedInstanceState)
|
||||
}
|
||||
|
||||
binding.mainDrawerLayout.addDrawerListener(object : DrawerListener {
|
||||
override fun onDrawerSlide(drawerView: View, slideOffset: Float) { }
|
||||
|
||||
override fun onDrawerOpened(drawerView: View) {
|
||||
onBackPressedCallback.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onDrawerClosed(drawerView: View) {
|
||||
onBackPressedCallback.isEnabled = binding.tabLayout.selectedTabPosition > 0
|
||||
}
|
||||
|
||||
override fun onDrawerStateChanged(newState: Int) { }
|
||||
})
|
||||
}
|
||||
|
||||
private fun refreshMainDrawerItems(addSearchButton: Boolean) {
|
||||
|
@ -616,7 +626,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
nameText = list.title
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_list
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(
|
||||
startActivityWithDefaultTransition(
|
||||
TimelineActivityIntent.list(this@MainActivity, list.id, list.title),
|
||||
)
|
||||
}
|
||||
|
@ -635,7 +645,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
nameRes = R.string.title_notifications
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_notifications
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(
|
||||
startActivityWithDefaultTransition(
|
||||
TimelineActivityIntent.notifications(context),
|
||||
)
|
||||
}
|
||||
|
@ -644,7 +654,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
nameRes = R.string.title_public_local
|
||||
iconRes = R.drawable.ic_local_24dp
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(
|
||||
startActivityWithDefaultTransition(
|
||||
TimelineActivityIntent.publicLocal(context),
|
||||
)
|
||||
}
|
||||
|
@ -653,7 +663,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
nameRes = R.string.title_public_federated
|
||||
iconRes = R.drawable.ic_public_24dp
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(
|
||||
startActivityWithDefaultTransition(
|
||||
TimelineActivityIntent.publicFederated(context),
|
||||
)
|
||||
}
|
||||
|
@ -662,7 +672,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
nameRes = R.string.title_direct_messages
|
||||
iconRes = R.drawable.ic_reblog_direct_24dp
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(
|
||||
startActivityWithDefaultTransition(
|
||||
TimelineActivityIntent.conversations(context),
|
||||
)
|
||||
}
|
||||
|
@ -672,7 +682,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
iconicsIcon = GoogleMaterial.Icon.gmd_bookmark
|
||||
onClick = {
|
||||
val intent = TimelineActivityIntent.bookmarks(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
|
@ -681,21 +691,21 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
iconicsIcon = GoogleMaterial.Icon.gmd_star
|
||||
onClick = {
|
||||
val intent = TimelineActivityIntent.favourites(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.title_public_trending
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_trending_up
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(TrendingActivityIntent(context))
|
||||
startActivityWithDefaultTransition(TrendingActivityIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.title_followed_hashtags
|
||||
iconRes = R.drawable.ic_hashtag
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(FollowedTagsActivityIntent(context))
|
||||
startActivityWithDefaultTransition(FollowedTagsActivityIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
|
@ -703,7 +713,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
iconicsIcon = GoogleMaterial.Icon.gmd_person_add
|
||||
onClick = {
|
||||
val intent = AccountListActivityIntent(context, AccountListActivityIntent.Kind.FOLLOW_REQUESTS)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
},
|
||||
SectionDrawerItem().apply {
|
||||
|
@ -714,7 +724,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
nameRes = R.string.manage_lists
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(ListActivityIntent(context))
|
||||
startActivityWithDefaultTransition(ListActivityIntent(context))
|
||||
}
|
||||
},
|
||||
DividerDrawerItem(),
|
||||
|
@ -723,14 +733,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
iconRes = R.drawable.ic_notebook
|
||||
onClick = {
|
||||
val intent = DraftsActivityIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_access_scheduled_posts
|
||||
iconRes = R.drawable.ic_access_time
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(ScheduledStatusActivityIntent(context))
|
||||
startActivityWithDefaultTransition(ScheduledStatusActivityIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
|
@ -738,7 +748,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
nameRes = R.string.title_announcements
|
||||
iconRes = R.drawable.ic_bullhorn_24dp
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(AnnouncementsActivityIntent(context))
|
||||
startActivityWithDefaultTransition(AnnouncementsActivityIntent(context))
|
||||
}
|
||||
badgeStyle = BadgeStyle().apply {
|
||||
textColor = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, com.google.android.material.R.attr.colorOnPrimary))
|
||||
|
@ -751,7 +761,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
iconRes = R.drawable.ic_account_settings
|
||||
onClick = {
|
||||
val intent = PreferencesActivityIntent(context, PreferenceScreen.ACCOUNT)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
|
@ -759,7 +769,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
||||
onClick = {
|
||||
val intent = PreferencesActivityIntent(context, PreferenceScreen.GENERAL)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
|
@ -767,7 +777,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
iconicsIcon = GoogleMaterial.Icon.gmd_person
|
||||
onClick = {
|
||||
val intent = EditProfileActivityIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
|
@ -775,7 +785,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
iconicsIcon = GoogleMaterial.Icon.gmd_info
|
||||
onClick = {
|
||||
val intent = AboutActivityIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
|
@ -792,7 +802,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
nameRes = R.string.action_search
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_search
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(SearchActivityIntent(context))
|
||||
startActivityWithDefaultTransition(SearchActivityIntent(context))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -943,6 +953,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
|
||||
onTabSelectedListener = object : OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
onBackPressedCallback.isEnabled = tab.position > 0 || binding.mainDrawerLayout.isOpen
|
||||
|
||||
supportActionBar?.title = tabs[tab.position].title(this@MainActivity)
|
||||
|
||||
refreshComposeButtonState(tabs[tab.position])
|
||||
|
@ -952,9 +964,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
val fragment = tabAdapter.getFragment(tab.position)
|
||||
if (fragment is ReselectableFragment) {
|
||||
(fragment as ReselectableFragment).onReselect()
|
||||
}
|
||||
(fragment as? ReselectableFragment)?.onReselect()
|
||||
|
||||
refreshComposeButtonState(tabs[tab.position])
|
||||
}
|
||||
|
@ -987,12 +997,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
// open profile when active image was clicked
|
||||
if (current && activeAccount != null) {
|
||||
val intent = AccountActivityIntent(this, activeAccount.accountId)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
return
|
||||
}
|
||||
// open LoginActivity to add new account
|
||||
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
|
||||
startActivityWithSlideInAnimation(
|
||||
startActivityWithDefaultTransition(
|
||||
LoginActivityIntent(this, LoginMode.ADDITIONAL_LOGIN),
|
||||
)
|
||||
return
|
||||
|
@ -1012,12 +1022,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
intent.action = forward.action
|
||||
intent.putExtras(forward)
|
||||
}
|
||||
startActivity(intent)
|
||||
finishWithoutSlideOutAnimation()
|
||||
overridePendingTransition(
|
||||
DR.anim.explode,
|
||||
DR.anim.explode,
|
||||
)
|
||||
startActivityWithTransition(intent, TransitionKind.EXPLODE)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun logout() {
|
||||
|
@ -1040,7 +1046,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
LoginActivityIntent(this@MainActivity, LoginMode.DEFAULT)
|
||||
}
|
||||
startActivity(intent)
|
||||
finishWithoutSlideOutAnimation()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
|
|
@ -46,6 +46,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
|||
import androidx.viewpager2.widget.ViewPager2
|
||||
import app.pachli.BuildConfig.APPLICATION_ID
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
|
@ -250,7 +251,7 @@ class ViewMediaActivity : BaseActivity(), MediaActionsListener {
|
|||
|
||||
private fun onOpenStatus() {
|
||||
val attach = attachmentViewData!![binding.viewPager.currentItem]
|
||||
startActivityWithSlideInAnimation(ViewThreadActivityIntent(this, attach.statusId, attach.statusUrl))
|
||||
startActivityWithDefaultTransition(ViewThreadActivityIntent(this, attach.statusId, attach.statusUrl))
|
||||
}
|
||||
|
||||
private fun copyLink() {
|
||||
|
|
|
@ -25,7 +25,12 @@ import app.pachli.core.ui.BindingHolder
|
|||
import app.pachli.databinding.ItemEditFieldBinding
|
||||
import app.pachli.util.fixTextSelection
|
||||
|
||||
class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditFieldBinding>>() {
|
||||
/**
|
||||
* @property onChange Call this whenever data in the UI fields changes
|
||||
*/
|
||||
class AccountFieldEditAdapter(
|
||||
val onChange: () -> Unit,
|
||||
) : RecyclerView.Adapter<BindingHolder<ItemEditFieldBinding>>() {
|
||||
|
||||
private val fieldData = mutableListOf<MutableStringPair>()
|
||||
private var maxNameLength: Int? = null
|
||||
|
@ -84,10 +89,12 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
|||
|
||||
holder.binding.accountFieldNameText.doAfterTextChanged { newText ->
|
||||
fieldData.getOrNull(holder.bindingAdapterPosition)?.first = newText.toString()
|
||||
onChange()
|
||||
}
|
||||
|
||||
holder.binding.accountFieldValueText.doAfterTextChanged { newText ->
|
||||
fieldData.getOrNull(holder.bindingAdapterPosition)?.second = newText.toString()
|
||||
onChange()
|
||||
}
|
||||
|
||||
// Ensure the textview contents are selectable
|
||||
|
|
|
@ -36,6 +36,7 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
|
@ -58,6 +59,9 @@ import app.pachli.R
|
|||
import app.pachli.core.activity.AccountSelectionListener
|
||||
import app.pachli.core.activity.BottomSheetActivity
|
||||
import app.pachli.core.activity.emojify
|
||||
import app.pachli.core.activity.extensions.TransitionKind
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.activity.extensions.startActivityWithTransition
|
||||
import app.pachli.core.activity.loadAvatar
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
|
@ -180,6 +184,12 @@ class AccountActivity :
|
|||
|
||||
private var noteWatcher: TextWatcher? = null
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
binding.accountFragmentViewPager.currentItem = 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
loadResources()
|
||||
|
@ -207,6 +217,8 @@ class AccountActivity :
|
|||
} else {
|
||||
binding.saveNoteInfo.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(onBackPressedCallback)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -243,7 +255,7 @@ class AccountActivity :
|
|||
else -> throw AssertionError()
|
||||
}
|
||||
val accountListIntent = AccountListActivityIntent(this, kind, viewModel.accountId)
|
||||
startActivityWithSlideInAnimation(accountListIntent)
|
||||
startActivityWithDefaultTransition(accountListIntent)
|
||||
}
|
||||
binding.accountFollowers.setOnClickListener(accountListClickListener)
|
||||
binding.accountFollowing.setOnClickListener(accountListClickListener)
|
||||
|
@ -299,7 +311,10 @@ class AccountActivity :
|
|||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {}
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
tab?.position ?: return
|
||||
onBackPressedCallback.isEnabled = tab.position > 0
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -561,7 +576,7 @@ class AccountActivity :
|
|||
}
|
||||
|
||||
private fun viewImage(view: View, uri: String) {
|
||||
view.transitionName = uri
|
||||
ViewCompat.setTransitionName(view, uri)
|
||||
startActivity(
|
||||
ViewMediaActivityIntent(view.context, uri),
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(this, view, uri).toBundle(),
|
||||
|
@ -630,7 +645,7 @@ class AccountActivity :
|
|||
binding.accountFollowButton.setOnClickListener {
|
||||
if (viewModel.isSelf) {
|
||||
val intent = EditProfileActivityIntent(this@AccountActivity)
|
||||
startActivity(intent)
|
||||
startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
|
@ -959,12 +974,12 @@ class AccountActivity :
|
|||
|
||||
override fun onViewTag(tag: String) {
|
||||
val intent = TimelineActivityIntent.hashtag(this, tag)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
val intent = AccountActivityIntent(this, id)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
|
||||
override fun onViewUrl(url: String) {
|
||||
|
|
|
@ -32,9 +32,9 @@ import app.pachli.components.accountlist.adapter.FollowRequestsAdapter
|
|||
import app.pachli.components.accountlist.adapter.FollowRequestsHeaderAdapter
|
||||
import app.pachli.components.accountlist.adapter.MutesAdapter
|
||||
import app.pachli.core.accounts.AccountManager
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.activity.BottomSheetActivity
|
||||
import app.pachli.core.activity.PostLookupFallbackBehavior
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
|
@ -158,19 +158,15 @@ class AccountListFragment :
|
|||
}
|
||||
|
||||
override fun onViewTag(tag: String) {
|
||||
(activity as BaseActivity?)
|
||||
?.startActivityWithSlideInAnimation(TimelineActivityIntent.hashtag(requireContext(), tag))
|
||||
activity?.startActivityWithDefaultTransition(TimelineActivityIntent.hashtag(requireContext(), tag))
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
(activity as BaseActivity?)?.let {
|
||||
val intent = AccountActivityIntent(it, id)
|
||||
it.startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
activity?.startActivityWithDefaultTransition(AccountActivityIntent(requireContext(), id))
|
||||
}
|
||||
|
||||
override fun onViewUrl(url: String) {
|
||||
(activity as BottomSheetActivity?)?.viewUrl(url, PostLookupFallbackBehavior.OPEN_IN_BROWSER)
|
||||
(activity as? BottomSheetActivity)?.viewUrl(url, PostLookupFallbackBehavior.OPEN_IN_BROWSER)
|
||||
}
|
||||
|
||||
override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) {
|
||||
|
|
|
@ -29,6 +29,7 @@ import app.pachli.R
|
|||
import app.pachli.adapter.EmojiAdapter
|
||||
import app.pachli.adapter.OnEmojiSelectedListener
|
||||
import app.pachli.core.activity.BottomSheetActivity
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
|
@ -187,7 +188,7 @@ class AnnouncementsActivity :
|
|||
|
||||
override fun onViewTag(tag: String) {
|
||||
val intent = TimelineActivityIntent.hashtag(this, tag)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package app.pachli.components.compose
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
|
@ -30,7 +31,6 @@ import android.os.Bundle
|
|||
import android.provider.MediaStore
|
||||
import android.text.InputFilter
|
||||
import android.text.Spanned
|
||||
import android.text.style.URLSpan
|
||||
import android.view.KeyEvent
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
|
@ -94,7 +94,6 @@ import app.pachli.core.network.model.Status
|
|||
import app.pachli.core.preferences.AppTheme
|
||||
import app.pachli.core.preferences.PrefKeys
|
||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||
import app.pachli.core.ui.MentionSpan
|
||||
import app.pachli.databinding.ActivityComposeBinding
|
||||
import app.pachli.util.PickMediaFiles
|
||||
import app.pachli.util.getInitialLanguages
|
||||
|
@ -155,9 +154,9 @@ class ComposeActivity :
|
|||
|
||||
@VisibleForTesting
|
||||
var maximumTootCharacters = InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT
|
||||
var charactersReservedPerUrl = InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL
|
||||
|
||||
private val viewModel: ComposeViewModel by viewModels()
|
||||
@VisibleForTesting
|
||||
val viewModel: ComposeViewModel by viewModels()
|
||||
|
||||
private val binding by viewBinding(ActivityComposeBinding::inflate)
|
||||
|
||||
|
@ -206,6 +205,28 @@ class ComposeActivity :
|
|||
viewModel.cropImageItemOld = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Pressing back either (a) closes an open bottom sheet, or (b) goes
|
||||
* back, if no bottom sheets are open.
|
||||
*/
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
||||
) {
|
||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
return
|
||||
}
|
||||
|
||||
handleCloseButton()
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -268,7 +289,7 @@ class ComposeActivity :
|
|||
}
|
||||
|
||||
setupLanguageSpinner(getInitialLanguages(composeOptions?.language, activeAccount))
|
||||
setupComposeField(sharedPreferencesRepository, viewModel.startingText, composeOptions)
|
||||
setupComposeField(sharedPreferencesRepository, viewModel.initialContent, composeOptions)
|
||||
setupContentWarningField(composeOptions?.contentWarning)
|
||||
setupPollView()
|
||||
applyShareIntent(intent, savedInstanceState)
|
||||
|
@ -282,7 +303,7 @@ class ComposeActivity :
|
|||
}
|
||||
|
||||
it.getBoolean(CONTENT_WARNING_VISIBLE_KEY).apply {
|
||||
viewModel.contentWarningChanged(this)
|
||||
viewModel.showContentWarningChanged(this)
|
||||
}
|
||||
|
||||
it.getString(SCHEDULED_TIME_KEY)?.let { time ->
|
||||
|
@ -369,7 +390,9 @@ class ComposeActivity :
|
|||
if (startingContentWarning != null) {
|
||||
binding.composeContentWarningField.setText(startingContentWarning)
|
||||
}
|
||||
binding.composeContentWarningField.doOnTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() }
|
||||
binding.composeContentWarningField.doOnTextChanged { newContentWarning, _, _, _ ->
|
||||
viewModel.onContentWarningChanged(newContentWarning?.toString() ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupComposeField(
|
||||
|
@ -404,7 +427,7 @@ class ComposeActivity :
|
|||
highlightSpans(binding.composeEditField.text, mentionColour)
|
||||
binding.composeEditField.doAfterTextChanged { editable ->
|
||||
highlightSpans(editable!!, mentionColour)
|
||||
updateVisibleCharactersLeft()
|
||||
viewModel.onContentChanged(editable)
|
||||
}
|
||||
|
||||
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093
|
||||
|
@ -419,12 +442,19 @@ class ComposeActivity :
|
|||
lifecycleScope.launch {
|
||||
viewModel.instanceInfo.collect { instanceData ->
|
||||
maximumTootCharacters = instanceData.maxChars
|
||||
charactersReservedPerUrl = instanceData.charactersReservedPerUrl
|
||||
maxUploadMediaNumber = instanceData.maxMediaAttachments
|
||||
updateVisibleCharactersLeft()
|
||||
updateVisibleCharactersLeft(viewModel.statusLength.value)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.statusLength.collect { updateVisibleCharactersLeft(it) }
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.closeConfirmation.collect { updateOnBackPressedCallbackState(it, bottomSheetStates()) }
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.emoji.collect(::setEmojiList)
|
||||
}
|
||||
|
@ -493,6 +523,23 @@ class ComposeActivity :
|
|||
}
|
||||
}
|
||||
|
||||
/** @return List of states of the different bottomsheets */
|
||||
private fun bottomSheetStates() = listOf(
|
||||
composeOptionsBehavior.state,
|
||||
addMediaBehavior.state,
|
||||
emojiBehavior.state,
|
||||
scheduleBehavior.state,
|
||||
)
|
||||
|
||||
/**
|
||||
* Enables [onBackPressedCallback] if a confirmation is required, or any botttom sheet is
|
||||
* open. Otherwise disables.
|
||||
*/
|
||||
private fun updateOnBackPressedCallbackState(confirmationKind: ConfirmationKind, bottomSheetStates: List<Int>) {
|
||||
onBackPressedCallback.isEnabled = confirmationKind != ConfirmationKind.NONE ||
|
||||
bottomSheetStates.any { it != BottomSheetBehavior.STATE_HIDDEN }
|
||||
}
|
||||
|
||||
private fun setupButtons() {
|
||||
binding.composeOptionsBottomSheet.listener = this
|
||||
|
||||
|
@ -501,6 +548,17 @@ class ComposeActivity :
|
|||
scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView)
|
||||
emojiBehavior = BottomSheetBehavior.from(binding.emojiView)
|
||||
|
||||
val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
updateOnBackPressedCallbackState(viewModel.closeConfirmation.value, bottomSheetStates())
|
||||
}
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) { }
|
||||
}
|
||||
composeOptionsBehavior.addBottomSheetCallback(bottomSheetCallback)
|
||||
addMediaBehavior.addBottomSheetCallback(bottomSheetCallback)
|
||||
scheduleBehavior.addBottomSheetCallback(bottomSheetCallback)
|
||||
emojiBehavior.addBottomSheetCallback(bottomSheetCallback)
|
||||
|
||||
enableButton(binding.composeEmojiButton, clickable = false, colorActive = false)
|
||||
|
||||
// Setup the interface buttons.
|
||||
|
@ -534,26 +592,7 @@ class ComposeActivity :
|
|||
binding.actionPhotoPick.setOnClickListener { onMediaPick() }
|
||||
binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
||||
) {
|
||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
return
|
||||
}
|
||||
|
||||
handleCloseButton()
|
||||
}
|
||||
},
|
||||
)
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
private fun setupLanguageSpinner(initialLanguages: List<String>) {
|
||||
|
@ -851,7 +890,7 @@ class ComposeActivity :
|
|||
maxOptionLength = instanceParams.pollMaxLength,
|
||||
minDuration = instanceParams.pollMinDuration,
|
||||
maxDuration = instanceParams.pollMaxDuration,
|
||||
onUpdatePoll = viewModel::updatePoll,
|
||||
onUpdatePoll = viewModel::onPollChanged,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -881,30 +920,21 @@ class ComposeActivity :
|
|||
}
|
||||
|
||||
private fun removePoll() {
|
||||
viewModel.poll.value = null
|
||||
viewModel.onPollChanged(null)
|
||||
binding.pollPreview.hide()
|
||||
}
|
||||
|
||||
override fun onVisibilityChanged(visibility: Status.Visibility) {
|
||||
viewModel.onStatusVisibilityChanged(visibility)
|
||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
viewModel.statusVisibility.value = visibility
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun calculateTextLength(): Int {
|
||||
return statusLength(
|
||||
binding.composeEditField.text,
|
||||
binding.composeContentWarningField.text,
|
||||
charactersReservedPerUrl,
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
val selectedLanguage: String?
|
||||
get() = viewModel.postLanguage
|
||||
|
||||
private fun updateVisibleCharactersLeft() {
|
||||
val remainingLength = maximumTootCharacters - calculateTextLength()
|
||||
private fun updateVisibleCharactersLeft(textLength: Int) {
|
||||
val remainingLength = maximumTootCharacters - textLength
|
||||
binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength)
|
||||
|
||||
val textColor = if (remainingLength < 0) {
|
||||
|
@ -917,8 +947,7 @@ class ComposeActivity :
|
|||
|
||||
private fun onContentWarningChanged() {
|
||||
val showWarning = binding.composeContentWarningBar.isGone
|
||||
viewModel.contentWarningChanged(showWarning)
|
||||
updateVisibleCharactersLeft()
|
||||
viewModel.showContentWarningChanged(showWarning)
|
||||
}
|
||||
|
||||
private fun verifyScheduledTime(): Boolean {
|
||||
|
@ -957,11 +986,11 @@ class ComposeActivity :
|
|||
if (viewModel.showContentWarning.value) {
|
||||
spoilerText = binding.composeContentWarningField.text.toString()
|
||||
}
|
||||
val characterCount = calculateTextLength()
|
||||
if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value.isEmpty()) {
|
||||
val statusLength = viewModel.statusLength.value
|
||||
if ((statusLength <= 0 || contentText.isBlank()) && viewModel.media.value.isEmpty()) {
|
||||
binding.composeEditField.error = getString(R.string.error_empty)
|
||||
enableButtons(true, viewModel.editing)
|
||||
} else if (characterCount <= maximumTootCharacters) {
|
||||
} else if (statusLength <= maximumTootCharacters) {
|
||||
lifecycleScope.launch {
|
||||
viewModel.sendStatus(contentText, spoilerText, activeAccount.id)
|
||||
deleteDraftAndFinish()
|
||||
|
@ -1008,8 +1037,9 @@ class ComposeActivity :
|
|||
this,
|
||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||
photoFile,
|
||||
)
|
||||
takePicture.launch(photoUploadUri)
|
||||
)?.also {
|
||||
takePicture.launch(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
|
||||
|
@ -1105,6 +1135,7 @@ class ComposeActivity :
|
|||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
@SuppressLint("GestureBackNavigation")
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
if (event.isCtrlPressed) {
|
||||
|
@ -1126,10 +1157,10 @@ class ComposeActivity :
|
|||
private fun handleCloseButton() {
|
||||
val contentText = binding.composeEditField.text.toString()
|
||||
val contentWarning = binding.composeContentWarningField.text.toString()
|
||||
when (viewModel.handleCloseButton(contentText, contentWarning)) {
|
||||
when (viewModel.closeConfirmation.value) {
|
||||
ConfirmationKind.NONE -> {
|
||||
viewModel.stopUploads()
|
||||
finishWithoutSlideOutAnimation()
|
||||
finish()
|
||||
}
|
||||
ConfirmationKind.SAVE_OR_DISCARD ->
|
||||
getSaveAsDraftOrDiscardDialog(contentText, contentWarning).show()
|
||||
|
@ -1183,7 +1214,7 @@ class ComposeActivity :
|
|||
}
|
||||
.setNegativeButton(R.string.action_discard) { _, _ ->
|
||||
viewModel.stopUploads()
|
||||
finishWithoutSlideOutAnimation()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1193,13 +1224,13 @@ class ComposeActivity :
|
|||
*/
|
||||
private fun getContinueEditingOrDiscardDialog(): AlertDialog.Builder {
|
||||
return AlertDialog.Builder(this)
|
||||
.setMessage(R.string.compose_unsaved_changes)
|
||||
.setMessage(R.string.unsaved_changes)
|
||||
.setPositiveButton(R.string.action_continue_edit) { _, _ ->
|
||||
// Do nothing, dialog will dismiss, user can continue editing
|
||||
}
|
||||
.setNegativeButton(R.string.action_discard) { _, _ ->
|
||||
viewModel.stopUploads()
|
||||
finishWithoutSlideOutAnimation()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1213,7 +1244,7 @@ class ComposeActivity :
|
|||
.setPositiveButton(R.string.action_delete) { _, _ ->
|
||||
viewModel.deleteDraft()
|
||||
viewModel.stopUploads()
|
||||
finishWithoutSlideOutAnimation()
|
||||
finish()
|
||||
}
|
||||
.setNegativeButton(R.string.action_continue_edit) { _, _ ->
|
||||
// Do nothing, dialog will dismiss, user can continue editing
|
||||
|
@ -1222,7 +1253,7 @@ class ComposeActivity :
|
|||
|
||||
private fun deleteDraftAndFinish() {
|
||||
viewModel.deleteDraft()
|
||||
finishWithoutSlideOutAnimation()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun saveDraftAndFinish(contentText: String, contentWarning: String) {
|
||||
|
@ -1240,7 +1271,7 @@ class ComposeActivity :
|
|||
}
|
||||
viewModel.saveDraft(contentText, contentWarning)
|
||||
dialog?.cancel()
|
||||
finishWithoutSlideOutAnimation()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1315,54 +1346,6 @@ class ComposeActivity :
|
|||
return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain")
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the effective status length.
|
||||
*
|
||||
* Some text is counted differently:
|
||||
*
|
||||
* In the status body:
|
||||
*
|
||||
* - URLs always count for [urlLength] characters irrespective of their actual length
|
||||
* (https://docs.joinmastodon.org/user/posting/#links)
|
||||
* - Mentions ("@user@some.instance") only count the "@user" part
|
||||
* (https://docs.joinmastodon.org/user/posting/#mentions)
|
||||
* - Hashtags are always treated as their actual length, including the "#"
|
||||
* (https://docs.joinmastodon.org/user/posting/#hashtags)
|
||||
* - Emojis are treated as a single character
|
||||
*
|
||||
* Content warning text is always treated as its full length, URLs and other entities
|
||||
* are not treated differently.
|
||||
*
|
||||
* @param body status body text
|
||||
* @param contentWarning optional content warning text
|
||||
* @param urlLength the number of characters attributed to URLs
|
||||
* @return the effective status length
|
||||
*/
|
||||
fun statusLength(body: Spanned, contentWarning: Spanned?, urlLength: Int): Int {
|
||||
var length = body.toString().mastodonLength() - body.getSpans(0, body.length, URLSpan::class.java)
|
||||
.fold(0) { acc, span ->
|
||||
// Accumulate a count of characters to be *ignored* in the final length
|
||||
acc + when (span) {
|
||||
is MentionSpan -> {
|
||||
// Ignore everything from the second "@" (if present)
|
||||
span.url.length - (
|
||||
span.url.indexOf("@", 1).takeIf { it >= 0 }
|
||||
?: span.url.length
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// Expected to be negative if the URL length < maxUrlLength
|
||||
span.url.mastodonLength() - urlLength
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content warning text is treated as is, URLs or mentions there are not special
|
||||
contentWarning?.let { length += it.toString().mastodonLength() }
|
||||
|
||||
return length
|
||||
}
|
||||
|
||||
/**
|
||||
* [InputFilter] that uses the "Mastodon" length of a string, where emojis always
|
||||
* count as a single character.
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
package app.pachli.components.compose
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.Editable
|
||||
import android.text.Spanned
|
||||
import android.text.style.URLSpan
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
@ -25,6 +29,7 @@ import app.pachli.components.compose.ComposeAutoCompleteAdapter.AutocompleteResu
|
|||
import app.pachli.components.drafts.DraftHelper
|
||||
import app.pachli.components.search.SearchType
|
||||
import app.pachli.core.accounts.AccountManager
|
||||
import app.pachli.core.common.string.mastodonLength
|
||||
import app.pachli.core.common.string.randomAlphanumericString
|
||||
import app.pachli.core.data.model.InstanceInfo
|
||||
import app.pachli.core.data.repository.InstanceInfoRepository
|
||||
|
@ -35,6 +40,7 @@ import app.pachli.core.network.model.Emoji
|
|||
import app.pachli.core.network.model.NewPoll
|
||||
import app.pachli.core.network.model.Status
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import app.pachli.core.ui.MentionSpan
|
||||
import app.pachli.service.MediaToSend
|
||||
import app.pachli.service.ServiceClient
|
||||
import app.pachli.service.StatusToSend
|
||||
|
@ -49,6 +55,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
@ -66,20 +74,40 @@ class ComposeViewModel @Inject constructor(
|
|||
instanceInfoRepo: InstanceInfoRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
/** The current content */
|
||||
private var content: Editable = Editable.Factory.getInstance().newEditable("")
|
||||
|
||||
/** The current content warning */
|
||||
private var contentWarning: String = ""
|
||||
|
||||
/**
|
||||
* The effective content warning. Either the real content warning, or the empty string
|
||||
* if the content warning has been hidden
|
||||
*/
|
||||
private val effectiveContentWarning
|
||||
get() = if (showContentWarning.value) contentWarning else ""
|
||||
|
||||
private var replyingStatusAuthor: String? = null
|
||||
private var replyingStatusContent: String? = null
|
||||
internal var startingText: String? = null
|
||||
|
||||
/** The initial content for this status, before any edits */
|
||||
internal var initialContent: String = ""
|
||||
|
||||
/** The initial content warning for this status, before any edits */
|
||||
private var initialContentWarning: String = ""
|
||||
|
||||
internal var postLanguage: String? = null
|
||||
|
||||
/** If editing a draft then the ID of the draft, otherwise 0 */
|
||||
private var draftId: Int = 0
|
||||
private var scheduledTootId: String? = null
|
||||
private var startingContentWarning: String = ""
|
||||
private var inReplyToId: String? = null
|
||||
private var originalStatusId: String? = null
|
||||
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
|
||||
|
||||
private var contentWarningStateChanged: Boolean = false
|
||||
private var modifiedInitialState: Boolean = false
|
||||
private var hasScheduledTimeChanged: Boolean = false
|
||||
private var scheduledTimeChanged: Boolean = false
|
||||
|
||||
val instanceInfo: SharedFlow<InstanceInfo> = instanceInfoRepo::getInstanceInfo.asFlow()
|
||||
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
||||
|
@ -87,16 +115,27 @@ class ComposeViewModel @Inject constructor(
|
|||
val emoji: SharedFlow<List<Emoji>> = instanceInfoRepo::getEmojis.asFlow()
|
||||
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
val markMediaAsSensitive: MutableStateFlow<Boolean> =
|
||||
private val _markMediaAsSensitive: MutableStateFlow<Boolean> =
|
||||
MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
||||
val markMediaAsSensitive = _markMediaAsSensitive.asStateFlow()
|
||||
|
||||
val statusVisibility: MutableStateFlow<Status.Visibility> = MutableStateFlow(Status.Visibility.UNKNOWN)
|
||||
val showContentWarning: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
val poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null)
|
||||
val scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null)
|
||||
private val _statusVisibility: MutableStateFlow<Status.Visibility> = MutableStateFlow(Status.Visibility.UNKNOWN)
|
||||
val statusVisibility = _statusVisibility.asStateFlow()
|
||||
private val _showContentWarning: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
val showContentWarning = _showContentWarning.asStateFlow()
|
||||
private val _poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null)
|
||||
val poll = _poll.asStateFlow()
|
||||
private val _scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null)
|
||||
val scheduledAt = _scheduledAt.asStateFlow()
|
||||
|
||||
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
||||
val uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
private val _media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
||||
val media = _media.asStateFlow()
|
||||
private val _uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
val uploadError = _uploadError.asSharedFlow()
|
||||
private val _closeConfirmation = MutableStateFlow(ConfirmationKind.NONE)
|
||||
val closeConfirmation = _closeConfirmation.asStateFlow()
|
||||
private val _statusLength = MutableStateFlow(0)
|
||||
val statusLength = _statusLength.asStateFlow()
|
||||
|
||||
private lateinit var composeKind: ComposeKind
|
||||
|
||||
|
@ -133,7 +172,7 @@ class ComposeViewModel @Inject constructor(
|
|||
): QueuedMedia {
|
||||
var stashMediaItem: QueuedMedia? = null
|
||||
|
||||
media.update { mediaList ->
|
||||
_media.update { mediaList ->
|
||||
val mediaItem = QueuedMedia(
|
||||
localId = mediaUploader.getNewLocalMediaId(),
|
||||
uri = uri,
|
||||
|
@ -176,12 +215,12 @@ class ComposeViewModel @Inject constructor(
|
|||
},
|
||||
)
|
||||
is UploadEvent.ErrorEvent -> {
|
||||
media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
|
||||
uploadError.emit(event.error)
|
||||
_media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
|
||||
_uploadError.emit(event.error)
|
||||
return@collect
|
||||
}
|
||||
}
|
||||
media.update { mediaList ->
|
||||
_media.update { mediaList ->
|
||||
mediaList.map { mediaItem ->
|
||||
if (mediaItem.localId == newMediaItem.localId) {
|
||||
newMediaItem
|
||||
|
@ -192,11 +231,13 @@ class ComposeViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCloseConfirmation()
|
||||
return mediaItem
|
||||
}
|
||||
|
||||
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?, focus: Attachment.Focus?) {
|
||||
media.update { mediaList ->
|
||||
_media.update { mediaList ->
|
||||
val mediaItem = QueuedMedia(
|
||||
localId = mediaUploader.getNewLocalMediaId(),
|
||||
uri = uri,
|
||||
|
@ -214,22 +255,53 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
fun removeMediaFromQueue(item: QueuedMedia) {
|
||||
mediaUploader.cancelUploadScope(item.localId)
|
||||
media.update { mediaList -> mediaList.filter { it.localId != item.localId } }
|
||||
_media.update { mediaList -> mediaList.filter { it.localId != item.localId } }
|
||||
updateCloseConfirmation()
|
||||
}
|
||||
|
||||
fun toggleMarkSensitive() {
|
||||
this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true
|
||||
this._markMediaAsSensitive.value = this._markMediaAsSensitive.value != true
|
||||
}
|
||||
|
||||
fun handleCloseButton(contentText: String?, contentWarning: String?): ConfirmationKind {
|
||||
return if (didChange(contentText, contentWarning)) {
|
||||
/** Call this when the status' primary content changes */
|
||||
fun onContentChanged(newContent: Editable) {
|
||||
content = newContent
|
||||
updateStatusLength()
|
||||
updateCloseConfirmation()
|
||||
}
|
||||
|
||||
/** Call this when the status' content warning changes */
|
||||
fun onContentWarningChanged(newContentWarning: String) {
|
||||
contentWarning = newContentWarning
|
||||
updateStatusLength()
|
||||
updateCloseConfirmation()
|
||||
}
|
||||
|
||||
/** Call this to attach or clear the status' poll */
|
||||
fun onPollChanged(newPoll: NewPoll?) {
|
||||
_poll.value = newPoll
|
||||
updateCloseConfirmation()
|
||||
}
|
||||
|
||||
/** Call this to change the status' visibility */
|
||||
fun onStatusVisibilityChanged(newVisibility: Status.Visibility) {
|
||||
_statusVisibility.value = newVisibility
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun updateStatusLength() {
|
||||
_statusLength.value = statusLength(content, effectiveContentWarning, instanceInfo.replayCache.last().charactersReservedPerUrl)
|
||||
}
|
||||
|
||||
private fun updateCloseConfirmation() {
|
||||
_closeConfirmation.value = if (isDirty()) {
|
||||
when (composeKind) {
|
||||
ComposeKind.NEW -> if (isEmpty(contentText, contentWarning)) {
|
||||
ComposeKind.NEW -> if (isEmpty(content, effectiveContentWarning)) {
|
||||
ConfirmationKind.NONE
|
||||
} else {
|
||||
ConfirmationKind.SAVE_OR_DISCARD
|
||||
}
|
||||
ComposeKind.EDIT_DRAFT -> if (isEmpty(contentText, contentWarning)) {
|
||||
ComposeKind.EDIT_DRAFT -> if (isEmpty(content, effectiveContentWarning)) {
|
||||
ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_DRAFT
|
||||
} else {
|
||||
ConfirmationKind.UPDATE_OR_DISCARD
|
||||
|
@ -242,23 +314,30 @@ class ComposeViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun didChange(content: String?, contentWarning: String?): Boolean {
|
||||
val textChanged = content.orEmpty() != startingText.orEmpty()
|
||||
val contentWarningChanged = contentWarning.orEmpty() != startingContentWarning
|
||||
/**
|
||||
* @return True if content of this status is "dirty", meaning one or more of the
|
||||
* following have changed since the compose session started: content,
|
||||
* content warning and content warning visibility, media, polls, or the
|
||||
* scheduled time to send.
|
||||
*/
|
||||
private fun isDirty(): Boolean {
|
||||
val contentChanged = !content.contentEquals(initialContent)
|
||||
|
||||
val contentWarningChanged = effectiveContentWarning != initialContentWarning
|
||||
val mediaChanged = media.value.isNotEmpty()
|
||||
val pollChanged = poll.value != null
|
||||
val didScheduledTimeChange = hasScheduledTimeChanged
|
||||
|
||||
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange
|
||||
return modifiedInitialState || contentChanged || contentWarningChanged || mediaChanged || pollChanged || scheduledTimeChanged
|
||||
}
|
||||
|
||||
private fun isEmpty(content: String?, contentWarning: String?): Boolean {
|
||||
return !modifiedInitialState && (content.isNullOrBlank() && contentWarning.isNullOrBlank() && media.value.isEmpty() && poll.value == null)
|
||||
private fun isEmpty(content: CharSequence, contentWarning: CharSequence): Boolean {
|
||||
return !modifiedInitialState && (content.isBlank() && contentWarning.isBlank() && media.value.isEmpty() && poll.value == null)
|
||||
}
|
||||
|
||||
fun contentWarningChanged(value: Boolean) {
|
||||
showContentWarning.value = value
|
||||
fun showContentWarningChanged(value: Boolean) {
|
||||
_showContentWarning.value = value
|
||||
contentWarningStateChanged = true
|
||||
updateStatusLength()
|
||||
}
|
||||
|
||||
fun deleteDraft() {
|
||||
|
@ -296,7 +375,7 @@ class ComposeViewModel @Inject constructor(
|
|||
inReplyToId = inReplyToId,
|
||||
content = content,
|
||||
contentWarning = contentWarning,
|
||||
sensitive = markMediaAsSensitive.value,
|
||||
sensitive = _markMediaAsSensitive.value,
|
||||
visibility = statusVisibility.value,
|
||||
mediaUris = mediaUris,
|
||||
mediaDescriptions = mediaDescriptions,
|
||||
|
@ -337,7 +416,7 @@ class ComposeViewModel @Inject constructor(
|
|||
text = content,
|
||||
warningText = spoilerText,
|
||||
visibility = statusVisibility.value.serverString(),
|
||||
sensitive = attachedMedia.isNotEmpty() && (markMediaAsSensitive.value || showContentWarning.value),
|
||||
sensitive = attachedMedia.isNotEmpty() && (_markMediaAsSensitive.value || showContentWarning.value),
|
||||
media = attachedMedia,
|
||||
scheduledAt = scheduledAt.value,
|
||||
inReplyToId = inReplyToId,
|
||||
|
@ -356,7 +435,7 @@ class ComposeViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia) {
|
||||
media.update { mediaList ->
|
||||
_media.update { mediaList ->
|
||||
mediaList.map { mediaItem ->
|
||||
if (mediaItem.localId == localId) {
|
||||
mutator(mediaItem)
|
||||
|
@ -438,10 +517,10 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
val contentWarning = composeOptions?.contentWarning
|
||||
if (contentWarning != null) {
|
||||
startingContentWarning = contentWarning
|
||||
initialContentWarning = contentWarning
|
||||
}
|
||||
if (!contentWarningStateChanged) {
|
||||
showContentWarning.value = !contentWarning.isNullOrBlank()
|
||||
_showContentWarning.value = !contentWarning.isNullOrBlank()
|
||||
}
|
||||
|
||||
// recreate media list
|
||||
|
@ -468,14 +547,14 @@ class ComposeViewModel @Inject constructor(
|
|||
draftId = composeOptions?.draftId ?: 0
|
||||
scheduledTootId = composeOptions?.scheduledTootId
|
||||
originalStatusId = composeOptions?.statusId
|
||||
startingText = composeOptions?.content
|
||||
initialContent = composeOptions?.content ?: ""
|
||||
postLanguage = composeOptions?.language
|
||||
|
||||
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
||||
if (tootVisibility != Status.Visibility.UNKNOWN) {
|
||||
startingVisibility = tootVisibility
|
||||
}
|
||||
statusVisibility.value = startingVisibility
|
||||
_statusVisibility.value = startingVisibility
|
||||
val mentionedUsernames = composeOptions?.mentionedUsernames
|
||||
if (mentionedUsernames != null) {
|
||||
val builder = StringBuilder()
|
||||
|
@ -484,44 +563,101 @@ class ComposeViewModel @Inject constructor(
|
|||
builder.append(name)
|
||||
builder.append(' ')
|
||||
}
|
||||
startingText = builder.toString()
|
||||
initialContent = builder.toString()
|
||||
}
|
||||
|
||||
scheduledAt.value = composeOptions?.scheduledAt
|
||||
_scheduledAt.value = composeOptions?.scheduledAt
|
||||
|
||||
composeOptions?.sensitive?.let { markMediaAsSensitive.value = it }
|
||||
composeOptions?.sensitive?.let { _markMediaAsSensitive.value = it }
|
||||
|
||||
val poll = composeOptions?.poll
|
||||
if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) {
|
||||
this.poll.value = poll
|
||||
_poll.value = poll
|
||||
}
|
||||
replyingStatusContent = composeOptions?.replyingStatusContent
|
||||
replyingStatusAuthor = composeOptions?.replyingStatusAuthor
|
||||
|
||||
updateCloseConfirmation()
|
||||
setupComplete = true
|
||||
}
|
||||
|
||||
fun updatePoll(newPoll: NewPoll) {
|
||||
poll.value = newPoll
|
||||
}
|
||||
|
||||
fun updateScheduledAt(newScheduledAt: String?) {
|
||||
if (newScheduledAt != scheduledAt.value) {
|
||||
hasScheduledTimeChanged = true
|
||||
scheduledTimeChanged = true
|
||||
}
|
||||
|
||||
scheduledAt.value = newScheduledAt
|
||||
_scheduledAt.value = newScheduledAt
|
||||
updateCloseConfirmation()
|
||||
}
|
||||
|
||||
val editing: Boolean
|
||||
get() = !originalStatusId.isNullOrEmpty()
|
||||
|
||||
enum class ConfirmationKind {
|
||||
NONE, // just close
|
||||
/** No confirmation, finish */
|
||||
NONE,
|
||||
|
||||
/** Content has changed and it's an un-posted status, show "save or discard" */
|
||||
SAVE_OR_DISCARD,
|
||||
|
||||
/** Content has changed when editing a draft, show "update draft or discard changes" */
|
||||
UPDATE_OR_DISCARD,
|
||||
CONTINUE_EDITING_OR_DISCARD_CHANGES, // editing post
|
||||
CONTINUE_EDITING_OR_DISCARD_DRAFT, // edit draft
|
||||
|
||||
/** Content has changed when editing a posted status or scheduled status */
|
||||
CONTINUE_EDITING_OR_DISCARD_CHANGES,
|
||||
|
||||
/** Content has been cleared when editing a draft */
|
||||
CONTINUE_EDITING_OR_DISCARD_DRAFT,
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Calculate the effective status length.
|
||||
*
|
||||
* Some text is counted differently:
|
||||
*
|
||||
* In the status body:
|
||||
*
|
||||
* - URLs always count for [urlLength] characters irrespective of their actual length
|
||||
* (https://docs.joinmastodon.org/user/posting/#links)
|
||||
* - Mentions ("@user@some.instance") only count the "@user" part
|
||||
* (https://docs.joinmastodon.org/user/posting/#mentions)
|
||||
* - Hashtags are always treated as their actual length, including the "#"
|
||||
* (https://docs.joinmastodon.org/user/posting/#hashtags)
|
||||
* - Emojis are treated as a single character
|
||||
*
|
||||
* Content warning text is always treated as its full length, URLs and other entities
|
||||
* are not treated differently.
|
||||
*
|
||||
* @param body status body text
|
||||
* @param contentWarning optional content warning text
|
||||
* @param urlLength the number of characters attributed to URLs
|
||||
* @return the effective status length
|
||||
*/
|
||||
fun statusLength(body: Spanned, contentWarning: String, urlLength: Int): Int {
|
||||
var length = body.toString().mastodonLength() - body.getSpans(0, body.length, URLSpan::class.java)
|
||||
.fold(0) { acc, span ->
|
||||
// Accumulate a count of characters to be *ignored* in the final length
|
||||
acc + when (span) {
|
||||
is MentionSpan -> {
|
||||
// Ignore everything from the second "@" (if present)
|
||||
span.url.length - (
|
||||
span.url.indexOf("@", 1).takeIf { it >= 0 }
|
||||
?: span.url.length
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// Expected to be negative if the URL length < maxUrlLength
|
||||
span.url.mastodonLength() - urlLength
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content warning text is treated as is, URLs or mentions there are not special
|
||||
length += contentWarning.mastodonLength()
|
||||
|
||||
return length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
package app.pachli.components.filters
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface.BUTTON_NEGATIVE
|
||||
import android.content.DialogInterface.BUTTON_POSITIVE
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.size
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -21,6 +23,7 @@ import app.pachli.core.network.model.Filter
|
|||
import app.pachli.core.network.model.FilterContext
|
||||
import app.pachli.core.network.model.FilterKeyword
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import app.pachli.core.ui.extensions.await
|
||||
import app.pachli.databinding.ActivityEditFilterBinding
|
||||
import app.pachli.databinding.DialogFilterBinding
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
|
@ -28,8 +31,8 @@ import com.google.android.material.chip.Chip
|
|||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
|
||||
|
@ -51,11 +54,20 @@ class EditFilterActivity : BaseActivity() {
|
|||
private var originalFilter: Filter? = null
|
||||
private lateinit var filterContextSwitches: Map<SwitchMaterial, FilterContext>
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
lifecycleScope.launch {
|
||||
if (showUnsavedChangesFilterDialog() == BUTTON_NEGATIVE) finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
onBackPressedDispatcher.addCallback(onBackPressedCallback)
|
||||
|
||||
originalFilter = EditFilterActivityIntent.getFilter(intent)
|
||||
filter = originalFilter ?: Filter("", "", listOf(), null, Filter.Action.WARN, listOf())
|
||||
filter = originalFilter ?: Filter()
|
||||
binding.apply {
|
||||
filterContextSwitches = mapOf(
|
||||
filterContextHome to FilterContext.HOME,
|
||||
|
@ -69,7 +81,6 @@ class EditFilterActivity : BaseActivity() {
|
|||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
supportActionBar?.run {
|
||||
// Back button
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
@ -99,12 +110,10 @@ class EditFilterActivity : BaseActivity() {
|
|||
} else {
|
||||
viewModel.removeContext(context)
|
||||
}
|
||||
validateSaveButton()
|
||||
}
|
||||
}
|
||||
binding.filterTitle.doAfterTextChanged { editable ->
|
||||
viewModel.setTitle(editable.toString())
|
||||
validateSaveButton()
|
||||
}
|
||||
binding.filterActionWarn.setOnCheckedChangeListener { _, checked ->
|
||||
viewModel.setAction(
|
||||
|
@ -130,13 +139,8 @@ class EditFilterActivity : BaseActivity() {
|
|||
viewModel.setDuration(0)
|
||||
}
|
||||
}
|
||||
validateSaveButton()
|
||||
|
||||
if (originalFilter == null) {
|
||||
binding.filterActionWarn.isChecked = true
|
||||
} else {
|
||||
loadFilter()
|
||||
}
|
||||
loadFilter()
|
||||
observeModel()
|
||||
}
|
||||
|
||||
|
@ -170,6 +174,25 @@ class EditFilterActivity : BaseActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.isDirty.collectLatest { onBackPressedCallback.isEnabled = it }
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.validationErrors.collectLatest { errors ->
|
||||
binding.filterSaveButton.isEnabled = errors.isEmpty()
|
||||
|
||||
binding.filterTitleWrapper.error = if (errors.contains(FilterValidationError.NO_TITLE)) {
|
||||
getString(R.string.error_filter_missing_title)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
binding.keywordChipsError.isVisible = errors.contains(FilterValidationError.NO_KEYWORDS)
|
||||
binding.filterContextError.isVisible = errors.contains(FilterValidationError.NO_CONTEXT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the UI from the filter's members
|
||||
|
@ -213,7 +236,6 @@ class EditFilterActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
filter = filter.copy(keywords = newKeywords)
|
||||
validateSaveButton()
|
||||
}
|
||||
|
||||
private fun showAddKeywordDialog() {
|
||||
|
@ -256,9 +278,18 @@ class EditFilterActivity : BaseActivity() {
|
|||
.show()
|
||||
}
|
||||
|
||||
private fun validateSaveButton() {
|
||||
binding.filterSaveButton.isEnabled = viewModel.validate()
|
||||
}
|
||||
/**
|
||||
* Dialog that warns the user they have unsaved changes, and prompts
|
||||
* to continue editing or discard the changes.
|
||||
*
|
||||
* @return [BUTTON_NEGATIVE] if the user chose to discard the changes,
|
||||
* [BUTTON_POSITIVE] if the user chose to continue editing.
|
||||
*/
|
||||
suspend fun showUnsavedChangesFilterDialog() = AlertDialog.Builder(this)
|
||||
.setMessage(R.string.unsaved_changes)
|
||||
.setCancelable(true)
|
||||
.create()
|
||||
.await(R.string.action_continue_edit, R.string.action_discard)
|
||||
|
||||
private fun saveChanges() {
|
||||
// TODO use a progress bar here (see EditProfileActivity/activity_edit_profile.xml for example)?
|
||||
|
@ -297,16 +328,4 @@ class EditFilterActivity : BaseActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Mastodon *stores* the absolute date in the filter,
|
||||
// but create/edit take a number of seconds (relative to the time the operation is posted)
|
||||
fun getSecondsForDurationIndex(index: Int, context: Context?, default: Date? = null): String? {
|
||||
return when (index) {
|
||||
-1 -> default?.let { ((default.time - System.currentTimeMillis()) / 1000).toString() }
|
||||
0 -> ""
|
||||
else -> context?.resources?.getStringArray(R.array.filter_duration_values)?.get(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package app.pachli.components.filters
|
|||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.pachli.R
|
||||
import app.pachli.appstore.EventHub
|
||||
import app.pachli.appstore.FilterChangedEvent
|
||||
import app.pachli.core.network.model.Filter
|
||||
|
@ -11,20 +12,38 @@ import app.pachli.core.network.model.FilterKeyword
|
|||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import retrofit2.HttpException
|
||||
|
||||
@HiltViewModel
|
||||
class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub: EventHub) : ViewModel() {
|
||||
private var originalFilter: Filter? = null
|
||||
private lateinit var originalFilter: Filter
|
||||
val title = MutableStateFlow("")
|
||||
val keywords = MutableStateFlow(listOf<FilterKeyword>())
|
||||
val action = MutableStateFlow(Filter.Action.WARN)
|
||||
val duration = MutableStateFlow(0)
|
||||
val contexts = MutableStateFlow(listOf<FilterContext>())
|
||||
|
||||
/** Track whether the duration has been modified, for use in [onChange] */
|
||||
// TODO: Rethink how duration is shown in the UI.
|
||||
// Could show the actual end time with the date/time widget to set the duration,
|
||||
// along with dropdown for quick settings (1h, etc).
|
||||
private var durationIsDirty = false
|
||||
|
||||
private val _isDirty = MutableStateFlow(false)
|
||||
|
||||
/** True if the user has made unsaved changes to the filter */
|
||||
val isDirty = _isDirty.asStateFlow()
|
||||
|
||||
private val _validationErrors = MutableStateFlow(emptySet<FilterValidationError>())
|
||||
|
||||
/** True if the filter is valid and can be saved */
|
||||
val validationErrors = _validationErrors.asStateFlow()
|
||||
|
||||
fun load(filter: Filter) {
|
||||
originalFilter = filter
|
||||
title.value = filter.title
|
||||
|
@ -40,10 +59,12 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||
|
||||
fun addKeyword(keyword: FilterKeyword) {
|
||||
keywords.value += keyword
|
||||
onChange()
|
||||
}
|
||||
|
||||
fun deleteKeyword(keyword: FilterKeyword) {
|
||||
keywords.value = keywords.value.filterNot { it == keyword }
|
||||
onChange()
|
||||
}
|
||||
|
||||
fun modifyKeyword(original: FilterKeyword, updated: FilterKeyword) {
|
||||
|
@ -52,35 +73,66 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||
keywords.value = keywords.value.toMutableList().apply {
|
||||
set(index, updated)
|
||||
}
|
||||
onChange()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTitle(title: String) {
|
||||
this.title.value = title
|
||||
onChange()
|
||||
}
|
||||
|
||||
fun setDuration(index: Int) {
|
||||
if (!durationIsDirty && duration.value != index) durationIsDirty = true
|
||||
|
||||
duration.value = index
|
||||
onChange()
|
||||
}
|
||||
|
||||
fun setAction(action: Filter.Action) {
|
||||
this.action.value = action
|
||||
onChange()
|
||||
}
|
||||
|
||||
fun addContext(filterContext: FilterContext) {
|
||||
if (!contexts.value.contains(filterContext)) {
|
||||
contexts.value += filterContext
|
||||
onChange()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeContext(filterContext: FilterContext) {
|
||||
contexts.value = contexts.value.filter { it != filterContext }
|
||||
onChange()
|
||||
}
|
||||
|
||||
fun validate(): Boolean {
|
||||
return title.value.isNotBlank() &&
|
||||
keywords.value.isNotEmpty() &&
|
||||
contexts.value.isNotEmpty()
|
||||
private fun validate() {
|
||||
_validationErrors.value = buildSet {
|
||||
if (title.value.isBlank()) add(FilterValidationError.NO_TITLE)
|
||||
if (keywords.value.isEmpty()) add(FilterValidationError.NO_KEYWORDS)
|
||||
if (contexts.value.isEmpty()) add(FilterValidationError.NO_CONTEXT)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when the contents of the filter change; recalculates validity
|
||||
* and dirty state.
|
||||
*/
|
||||
private fun onChange() {
|
||||
validate()
|
||||
|
||||
if (durationIsDirty) {
|
||||
_isDirty.value = true
|
||||
return
|
||||
}
|
||||
|
||||
_isDirty.value = when {
|
||||
originalFilter.title != title.value -> true
|
||||
originalFilter.contexts != contexts.value -> true
|
||||
originalFilter.action != action.value -> true
|
||||
originalFilter.keywords.toSet() != keywords.value.toSet() -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveChanges(context: Context): Boolean {
|
||||
|
@ -90,15 +142,17 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||
val action = action.value
|
||||
|
||||
return withContext(viewModelScope.coroutineContext) {
|
||||
val success = originalFilter?.let { filter ->
|
||||
updateFilter(filter, title, contexts, action, durationIndex, context)
|
||||
} ?: createFilter(title, contexts, action, durationIndex, context)
|
||||
val success = if (originalFilter.id == "") {
|
||||
createFilter(title, contexts, action, durationIndex, context)
|
||||
} else {
|
||||
updateFilter(originalFilter, title, contexts, action, durationIndex, context)
|
||||
}
|
||||
|
||||
// Send FilterChangedEvent for old and new contexts, to ensure that
|
||||
// e.g., removing a filter from "home" still notifies anything showing
|
||||
// the home timeline, so the timeline can be refreshed.
|
||||
if (success) {
|
||||
val originalContexts = originalFilter?.contexts ?: emptyList()
|
||||
val originalContexts = originalFilter.contexts
|
||||
val newFilterContexts = contexts
|
||||
(originalContexts + newFilterContexts).distinct().forEach {
|
||||
eventHub.dispatch(FilterChangedEvent(it))
|
||||
|
@ -109,7 +163,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||
}
|
||||
|
||||
private suspend fun createFilter(title: String, contexts: List<FilterContext>, action: Filter.Action, durationIndex: Int, context: Context): Boolean {
|
||||
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
|
||||
val expiresInSeconds = getSecondsForDurationIndex(durationIndex, context)
|
||||
api.createFilter(
|
||||
title = title,
|
||||
context = contexts,
|
||||
|
@ -133,7 +187,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||
}
|
||||
|
||||
private suspend fun updateFilter(originalFilter: Filter, title: String, contexts: List<FilterContext>, action: Filter.Action, durationIndex: Int, context: Context): Boolean {
|
||||
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
|
||||
val expiresInSeconds = getSecondsForDurationIndex(durationIndex, context)
|
||||
api.updateFilter(
|
||||
id = originalFilter.id,
|
||||
title = title,
|
||||
|
@ -176,7 +230,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||
|
||||
private suspend fun updateFilterV1(contexts: List<FilterContext>, expiresInSeconds: String?): Boolean {
|
||||
val results = keywords.value.map { keyword ->
|
||||
if (originalFilter == null) {
|
||||
if (originalFilter.id == "") {
|
||||
api.createFilterV1(
|
||||
phrase = keyword.keyword,
|
||||
context = contexts,
|
||||
|
@ -186,7 +240,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||
)
|
||||
} else {
|
||||
api.updateFilterV1(
|
||||
id = originalFilter!!.id,
|
||||
id = originalFilter.id,
|
||||
phrase = keyword.keyword,
|
||||
context = contexts,
|
||||
irreversible = false,
|
||||
|
@ -199,4 +253,18 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||
|
||||
return results.none { it.isFailure }
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Mastodon *stores* the absolute date in the filter,
|
||||
* but create/edit take a number of seconds (relative to the time the operation is posted)
|
||||
*/
|
||||
fun getSecondsForDurationIndex(index: Int, context: Context?, default: Date? = null): String? {
|
||||
return when (index) {
|
||||
-1 -> default?.let { ((default.time - System.currentTimeMillis()) / 1000).toString() }
|
||||
0 -> ""
|
||||
else -> context?.resources?.getStringArray(R.array.filter_duration_values)?.get(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,10 @@
|
|||
package app.pachli.components.filters
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import app.pachli.R
|
||||
import app.pachli.core.network.model.Filter
|
||||
import app.pachli.core.ui.extensions.await
|
||||
|
||||
internal suspend fun Activity.showDeleteFilterDialog(filterTitle: String) = AlertDialog.Builder(this)
|
||||
|
@ -27,3 +29,36 @@ internal suspend fun Activity.showDeleteFilterDialog(filterTitle: String) = Aler
|
|||
.setCancelable(true)
|
||||
.create()
|
||||
.await(R.string.dialog_delete_filter_positive_action, android.R.string.cancel)
|
||||
|
||||
/** Reasons why a filter might be invalid */
|
||||
enum class FilterValidationError {
|
||||
/** Filter title is empty or blank */
|
||||
NO_TITLE,
|
||||
|
||||
/** Filter has no keywords */
|
||||
NO_KEYWORDS,
|
||||
|
||||
/** Filter has no contexts */
|
||||
NO_CONTEXT,
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Set of validation errors for this filter, empty set if there
|
||||
* are no errors.
|
||||
*/
|
||||
fun Filter.validate() = buildSet {
|
||||
if (title.isBlank()) add(FilterValidationError.NO_TITLE)
|
||||
if (keywords.isEmpty()) add(FilterValidationError.NO_KEYWORDS)
|
||||
if (contexts.isEmpty()) add(FilterValidationError.NO_CONTEXT)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return String resource containing an error message for this
|
||||
* validation error.
|
||||
*/
|
||||
@StringRes
|
||||
fun FilterValidationError.stringResource() = when (this) {
|
||||
FilterValidationError.NO_TITLE -> R.string.error_filter_missing_title
|
||||
FilterValidationError.NO_KEYWORDS -> R.string.error_filter_missing_keyword
|
||||
FilterValidationError.NO_CONTEXT -> R.string.error_filter_missing_context
|
||||
}
|
||||
|
|
|
@ -6,11 +6,12 @@ import androidx.activity.viewModels
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import app.pachli.R
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.activity.extensions.TransitionKind
|
||||
import app.pachli.core.activity.extensions.startActivityWithTransition
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
import app.pachli.core.common.extensions.visible
|
||||
import app.pachli.core.designsystem.R as DR
|
||||
import app.pachli.core.navigation.EditFilterActivityIntent
|
||||
import app.pachli.core.network.model.Filter
|
||||
import app.pachli.core.ui.BackgroundMessage
|
||||
|
@ -94,8 +95,7 @@ class FiltersActivity : BaseActivity(), FiltersListener {
|
|||
|
||||
private fun launchEditFilterActivity(filter: Filter? = null) {
|
||||
val intent = EditFilterActivityIntent(this, filter)
|
||||
startActivity(intent)
|
||||
overridePendingTransition(DR.anim.slide_from_right, DR.anim.slide_to_left)
|
||||
startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||
}
|
||||
|
||||
override fun deleteFilter(filter: Filter) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import app.pachli.core.network.model.Filter
|
|||
import app.pachli.core.ui.BindingHolder
|
||||
import app.pachli.databinding.ItemRemovableBinding
|
||||
import app.pachli.util.getRelativeTimeSpanString
|
||||
import com.google.android.material.color.MaterialColors
|
||||
|
||||
class FiltersAdapter(val listener: FiltersListener, val filters: List<Filter>) :
|
||||
RecyclerView.Adapter<BindingHolder<ItemRemovableBinding>>() {
|
||||
|
@ -34,11 +35,25 @@ class FiltersAdapter(val listener: FiltersListener, val filters: List<Filter>) :
|
|||
)
|
||||
} ?: filter.title
|
||||
|
||||
binding.textSecondary.text = context.getString(
|
||||
R.string.filter_description_format,
|
||||
actions.getOrNull(filter.action.ordinal - 1),
|
||||
filter.contexts.map { filterContextNames.getOrNull(it.ordinal) }.joinToString("/"),
|
||||
)
|
||||
// Secondary row shows filter actions and contexts, or errors if the filter is invalid
|
||||
val errors = filter.validate()
|
||||
val secondaryText: String
|
||||
val secondaryTextColor: Int
|
||||
|
||||
if (errors.isEmpty()) {
|
||||
secondaryText = context.getString(
|
||||
R.string.filter_description_format,
|
||||
actions.getOrNull(filter.action.ordinal - 1),
|
||||
filter.contexts.map { filterContextNames.getOrNull(it.ordinal) }.joinToString("/"),
|
||||
)
|
||||
secondaryTextColor = android.R.attr.textColorTertiary
|
||||
} else {
|
||||
secondaryText = context.getString(errors.first().stringResource())
|
||||
secondaryTextColor = androidx.appcompat.R.attr.colorError
|
||||
}
|
||||
|
||||
binding.textSecondary.text = secondaryText
|
||||
binding.textSecondary.setTextColor(MaterialColors.getColor(binding.textSecondary, secondaryTextColor))
|
||||
|
||||
binding.delete.setOnClickListener {
|
||||
listener.deleteFilter(filter)
|
||||
|
|
|
@ -16,6 +16,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
|||
import app.pachli.R
|
||||
import app.pachli.components.compose.ComposeAutoCompleteAdapter
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
|
@ -172,7 +173,7 @@ class FollowedTagsActivity :
|
|||
}
|
||||
|
||||
override fun onViewTag(tag: String) {
|
||||
startActivityWithSlideInAnimation(TimelineActivityIntent.hashtag(this, tag))
|
||||
startActivityWithDefaultTransition(TimelineActivityIntent.hashtag(this, tag))
|
||||
}
|
||||
|
||||
override suspend fun search(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
||||
|
|
|
@ -26,7 +26,8 @@ import app.pachli.R
|
|||
import app.pachli.appstore.EventHub
|
||||
import app.pachli.components.notifications.currentAccountNeedsMigration
|
||||
import app.pachli.core.accounts.AccountManager
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.activity.extensions.TransitionKind
|
||||
import app.pachli.core.activity.extensions.startActivityWithTransition
|
||||
import app.pachli.core.common.util.unsafeLazy
|
||||
import app.pachli.core.designsystem.R as DR
|
||||
import app.pachli.core.navigation.AccountListActivityIntent
|
||||
|
@ -104,11 +105,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
|||
setIcon(R.drawable.ic_add_to_tab_24)
|
||||
setOnPreferenceClickListener {
|
||||
val intent = TabPreferenceActivityIntent(context)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(
|
||||
DR.anim.slide_from_right,
|
||||
DR.anim.slide_to_left,
|
||||
)
|
||||
activity?.startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -118,11 +115,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
|||
setIcon(R.drawable.ic_hashtag)
|
||||
setOnPreferenceClickListener {
|
||||
val intent = FollowedTagsActivityIntent(context)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(
|
||||
DR.anim.slide_from_right,
|
||||
DR.anim.slide_to_left,
|
||||
)
|
||||
activity?.startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -132,11 +125,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
|||
setIcon(R.drawable.ic_mute_24dp)
|
||||
setOnPreferenceClickListener {
|
||||
val intent = AccountListActivityIntent(context, AccountListActivityIntent.Kind.MUTES)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(
|
||||
DR.anim.slide_from_right,
|
||||
DR.anim.slide_to_left,
|
||||
)
|
||||
activity?.startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -146,11 +135,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
|||
icon = makeIcon(GoogleMaterial.Icon.gmd_block)
|
||||
setOnPreferenceClickListener {
|
||||
val intent = AccountListActivityIntent(context, AccountListActivityIntent.Kind.BLOCKS)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(
|
||||
DR.anim.slide_from_right,
|
||||
DR.anim.slide_to_left,
|
||||
)
|
||||
activity?.startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -160,11 +145,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
|||
setIcon(R.drawable.ic_mute_24dp)
|
||||
setOnPreferenceClickListener {
|
||||
val intent = InstanceListActivityIntent(context)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(
|
||||
DR.anim.slide_from_right,
|
||||
DR.anim.slide_to_left,
|
||||
)
|
||||
activity?.startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -175,7 +156,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
|||
setIcon(R.drawable.ic_logout)
|
||||
setOnPreferenceClickListener {
|
||||
val intent = LoginActivityIntent(context, LoginMode.MIGRATION)
|
||||
(activity as BaseActivity).startActivityWithSlideInAnimation(intent)
|
||||
activity?.startActivityWithTransition(intent, TransitionKind.EXPLODE)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +166,8 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
|||
setTitle(R.string.pref_title_timeline_filters)
|
||||
setIcon(R.drawable.ic_filter_24dp)
|
||||
setOnPreferenceClickListener {
|
||||
launchFilterActivity()
|
||||
val intent = FiltersActivityIntent(requireContext())
|
||||
activity?.startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||
true
|
||||
}
|
||||
val server = serverRepository.flow.value.getOrElse { null }
|
||||
|
@ -298,14 +280,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
|||
val intent = Intent()
|
||||
intent.action = "android.settings.APP_NOTIFICATION_SETTINGS"
|
||||
intent.putExtra("android.provider.extra.APP_PACKAGE", BuildConfig.APPLICATION_ID)
|
||||
startActivity(intent)
|
||||
requireActivity().startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||
} else {
|
||||
activity?.let {
|
||||
val intent =
|
||||
PreferencesActivityIntent(it, PreferenceScreen.NOTIFICATION)
|
||||
it.startActivity(intent)
|
||||
it.overridePendingTransition(DR.anim.slide_from_right, DR.anim.slide_to_left)
|
||||
}
|
||||
val intent = PreferencesActivityIntent(requireContext(), PreferenceScreen.NOTIFICATION)
|
||||
requireActivity().startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -356,12 +334,6 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun launchFilterActivity() {
|
||||
val intent = FiltersActivityIntent(requireContext())
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(DR.anim.slide_from_right, DR.anim.slide_to_left)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = AccountPreferencesFragment()
|
||||
}
|
||||
|
|
|
@ -27,7 +27,8 @@ import androidx.preference.PreferenceFragmentCompat
|
|||
import app.pachli.R
|
||||
import app.pachli.appstore.EventHub
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.designsystem.R as DR
|
||||
import app.pachli.core.activity.extensions.TransitionKind
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.navigation.MainActivityIntent
|
||||
import app.pachli.core.navigation.PreferencesActivityIntent
|
||||
import app.pachli.core.navigation.PreferencesActivityIntent.PreferenceScreen
|
||||
|
@ -61,7 +62,7 @@ class PreferencesActivity :
|
|||
* back stack. */
|
||||
val intent = MainActivityIntent(this@PreferencesActivity)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,11 +108,11 @@ class PreferencesActivity :
|
|||
setAppNightMode(theme)
|
||||
|
||||
restartActivitiesOnBackPressedCallback.isEnabled = true
|
||||
this@PreferencesActivity.restartCurrentActivity()
|
||||
this@PreferencesActivity.recreate()
|
||||
}
|
||||
PrefKeys.FONT_FAMILY, PrefKeys.UI_TEXT_SCALE_RATIO -> {
|
||||
restartActivitiesOnBackPressedCallback.isEnabled = true
|
||||
this@PreferencesActivity.restartCurrentActivity()
|
||||
this@PreferencesActivity.recreate()
|
||||
}
|
||||
PrefKeys.STATUS_TEXT_SIZE, PrefKeys.ABSOLUTE_TIME_VIEW, PrefKeys.SHOW_BOT_OVERLAY, PrefKeys.ANIMATE_GIF_AVATARS, PrefKeys.USE_BLURHASH,
|
||||
PrefKeys.SHOW_SELF_USERNAME, PrefKeys.SHOW_CARDS_IN_TIMELINES, PrefKeys.CONFIRM_REBLOGS, PrefKeys.CONFIRM_FAVOURITES,
|
||||
|
@ -136,11 +137,13 @@ class PreferencesActivity :
|
|||
fragment.arguments = args
|
||||
fragment.setTargetFragment(caller, 0)
|
||||
supportFragmentManager.commit {
|
||||
// Slide transition, as sub preference screens are "attached" to the
|
||||
// parent screen.
|
||||
setCustomAnimations(
|
||||
DR.anim.slide_from_right,
|
||||
DR.anim.slide_to_left,
|
||||
DR.anim.slide_from_left,
|
||||
DR.anim.slide_to_right,
|
||||
TransitionKind.SLIDE_FROM_END.openEnter,
|
||||
TransitionKind.SLIDE_FROM_END.openExit,
|
||||
TransitionKind.SLIDE_FROM_END.closeEnter,
|
||||
TransitionKind.SLIDE_FROM_END.closeExit,
|
||||
)
|
||||
replace(R.id.fragment_container, fragment)
|
||||
addToBackStack(null)
|
||||
|
@ -148,25 +151,11 @@ class PreferencesActivity :
|
|||
return true
|
||||
}
|
||||
|
||||
private fun saveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(EXTRA_RESTART_ON_BACK, restartActivitiesOnBackPressedCallback.isEnabled)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(EXTRA_RESTART_ON_BACK, restartActivitiesOnBackPressedCallback.isEnabled)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun restartCurrentActivity() {
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
val savedInstanceState = Bundle()
|
||||
saveInstanceState(savedInstanceState)
|
||||
intent.putExtras(savedInstanceState)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
finish()
|
||||
overridePendingTransition(DR.anim.fade_in, DR.anim.fade_out)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_RESTART_ON_BACK = "restart"
|
||||
}
|
||||
|
|
|
@ -88,10 +88,6 @@ class SearchActivity : BottomSheetActivity(), MenuProvider, SearchView.OnQueryTe
|
|||
return false
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finishWithoutSlideOutAnimation()
|
||||
}
|
||||
|
||||
private fun getPageTitle(position: Int): CharSequence {
|
||||
return when (position) {
|
||||
0 -> getString(R.string.title_posts)
|
||||
|
|
|
@ -19,6 +19,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|||
import app.pachli.R
|
||||
import app.pachli.components.search.SearchViewModel
|
||||
import app.pachli.core.activity.BottomSheetActivity
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
import app.pachli.core.common.extensions.visible
|
||||
import app.pachli.core.navigation.AccountActivityIntent
|
||||
|
@ -140,11 +141,11 @@ abstract class SearchFragment<T : Any> :
|
|||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivityIntent(requireContext(), id))
|
||||
bottomSheetActivity?.startActivityWithDefaultTransition(AccountActivityIntent(requireContext(), id))
|
||||
}
|
||||
|
||||
override fun onViewTag(tag: String) {
|
||||
bottomSheetActivity?.startActivityWithSlideInAnimation(TimelineActivityIntent.hashtag(requireContext(), tag))
|
||||
bottomSheetActivity?.startActivityWithDefaultTransition(TimelineActivityIntent.hashtag(requireContext(), tag))
|
||||
}
|
||||
|
||||
override fun onViewUrl(url: String) {
|
||||
|
|
|
@ -40,6 +40,7 @@ import app.pachli.R
|
|||
import app.pachli.components.search.adapter.SearchStatusesAdapter
|
||||
import app.pachli.core.activity.AccountSelectionListener
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.activity.openLink
|
||||
import app.pachli.core.database.model.AccountEntity
|
||||
import app.pachli.core.navigation.AttachmentViewData
|
||||
|
@ -187,7 +188,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData>(), StatusActionLis
|
|||
kind = ComposeOptions.ComposeKind.NEW,
|
||||
),
|
||||
)
|
||||
bottomSheetActivity?.startActivityWithSlideInAnimation(intent)
|
||||
bottomSheetActivity?.startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
|
||||
private fun more(statusViewData: StatusViewData, view: View) {
|
||||
|
|
|
@ -49,8 +49,8 @@ import app.pachli.components.timeline.viewmodel.StatusAction
|
|||
import app.pachli.components.timeline.viewmodel.StatusActionSuccess
|
||||
import app.pachli.components.timeline.viewmodel.TimelineViewModel
|
||||
import app.pachli.components.timeline.viewmodel.UiSuccess
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.activity.RefreshableFragment
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
|
@ -637,12 +637,12 @@ class TimelineFragment :
|
|||
|
||||
override fun onShowReblogs(statusId: String) {
|
||||
val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.REBLOGGED, statusId)
|
||||
(activity as BaseActivity).startActivityWithSlideInAnimation(intent)
|
||||
activity?.startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
|
||||
override fun onShowFavs(statusId: String) {
|
||||
val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.FAVOURITED, statusId)
|
||||
(activity as BaseActivity).startActivityWithSlideInAnimation(intent)
|
||||
activity?.startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
|
||||
override fun onContentCollapsedChange(viewData: StatusViewData, isCollapsed: Boolean) {
|
||||
|
|
|
@ -35,8 +35,11 @@ import app.pachli.core.model.Timeline
|
|||
import app.pachli.core.ui.extensions.reduceSwipeSensitivity
|
||||
import app.pachli.databinding.ActivityTrendingBinding
|
||||
import app.pachli.interfaces.AppBarLayoutHost
|
||||
import app.pachli.interfaces.ReselectableFragment
|
||||
import app.pachli.pager.MainPagerAdapter
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
@ -55,6 +58,12 @@ class TrendingActivity : BottomSheetActivity(), AppBarLayoutHost, MenuProvider {
|
|||
|
||||
private lateinit var adapter: MainPagerAdapter
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
binding.pager.currentItem = 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(binding.root)
|
||||
|
@ -90,14 +99,19 @@ class TrendingActivity : BottomSheetActivity(), AppBarLayoutHost, MenuProvider {
|
|||
}
|
||||
}.attach()
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (binding.pager.currentItem != 0) binding.pager.currentItem = 0 else finish()
|
||||
}
|
||||
},
|
||||
)
|
||||
binding.tabLayout.addOnTabSelectedListener(object : OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
onBackPressedCallback.isEnabled = tab.position > 0
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
(adapter.getFragment(tab.position) as? ReselectableFragment)?.onReselect()
|
||||
}
|
||||
})
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
|
|
|
@ -37,8 +37,8 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
|||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||
import app.pachli.R
|
||||
import app.pachli.components.trending.viewmodel.TrendingTagsViewModel
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.activity.RefreshableFragment
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
|
@ -173,7 +173,7 @@ class TrendingTagsFragment :
|
|||
}
|
||||
|
||||
fun onViewTag(tag: String) {
|
||||
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(
|
||||
activity?.startActivityWithDefaultTransition(
|
||||
TimelineActivityIntent.hashtag(
|
||||
requireContext(),
|
||||
tag,
|
||||
|
|
|
@ -33,7 +33,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
|||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||
import app.pachli.R
|
||||
import app.pachli.components.viewthread.edits.ViewEditsFragment
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.activity.openLink
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
|
@ -337,12 +337,12 @@ class ViewThreadFragment :
|
|||
|
||||
override fun onShowReblogs(statusId: String) {
|
||||
val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.REBLOGGED, statusId)
|
||||
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent)
|
||||
activity?.startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
|
||||
override fun onShowFavs(statusId: String) {
|
||||
val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.FAVOURITED, statusId)
|
||||
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent)
|
||||
activity?.startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
|
||||
override fun onContentCollapsedChange(viewData: StatusViewData, isCollapsed: Boolean) {
|
||||
|
@ -374,7 +374,12 @@ class ViewThreadFragment :
|
|||
val viewEditsFragment = ViewEditsFragment.newInstance(statusId)
|
||||
|
||||
parentFragmentManager.commit {
|
||||
setCustomAnimations(DR.anim.slide_from_right, DR.anim.slide_to_left, DR.anim.slide_from_left, DR.anim.slide_to_right)
|
||||
setCustomAnimations(
|
||||
DR.anim.activity_open_enter,
|
||||
DR.anim.activity_open_exit,
|
||||
DR.anim.activity_close_enter,
|
||||
DR.anim.activity_close_exit,
|
||||
)
|
||||
replace(R.id.fragment_container, viewEditsFragment, "ViewEditsFragment_$id")
|
||||
addToBackStack(null)
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
|||
import app.pachli.R
|
||||
import app.pachli.core.activity.BottomSheetActivity
|
||||
import app.pachli.core.activity.emojify
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.activity.loadAvatar
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
|
@ -183,11 +184,11 @@ class ViewEditsFragment :
|
|||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivityIntent(requireContext(), id))
|
||||
bottomSheetActivity?.startActivityWithDefaultTransition(AccountActivityIntent(requireContext(), id))
|
||||
}
|
||||
|
||||
override fun onViewTag(tag: String) {
|
||||
bottomSheetActivity?.startActivityWithSlideInAnimation(TimelineActivityIntent.hashtag(requireContext(), tag))
|
||||
bottomSheetActivity?.startActivityWithDefaultTransition(TimelineActivityIntent.hashtag(requireContext(), tag))
|
||||
}
|
||||
|
||||
override fun onViewUrl(url: String) {
|
||||
|
|
|
@ -34,6 +34,7 @@ import androidx.annotation.CallSuper
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -44,10 +45,10 @@ import app.pachli.core.activity.AccountSelectionListener
|
|||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.activity.BottomSheetActivity
|
||||
import app.pachli.core.activity.PostLookupFallbackBehavior
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.activity.openLink
|
||||
import app.pachli.core.database.model.AccountEntity
|
||||
import app.pachli.core.database.model.TranslationState
|
||||
import app.pachli.core.designsystem.R as DR
|
||||
import app.pachli.core.navigation.AttachmentViewData
|
||||
import app.pachli.core.navigation.ComposeActivityIntent
|
||||
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
|
||||
|
@ -94,8 +95,7 @@ abstract class SFragment<T : IStatusViewData> : Fragment(), StatusActionListener
|
|||
private var serverCanTranslate = false
|
||||
|
||||
override fun startActivity(intent: Intent) {
|
||||
super.startActivity(intent)
|
||||
requireActivity().overridePendingTransition(DR.anim.slide_from_right, DR.anim.slide_to_left)
|
||||
requireActivity().startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
|
@ -400,7 +400,7 @@ abstract class SFragment<T : IStatusViewData> : Fragment(), StatusActionListener
|
|||
val intent = ViewMediaActivityIntent(requireContext(), attachments, urlIndex)
|
||||
if (view != null) {
|
||||
val url = attachment.url
|
||||
view.transitionName = url
|
||||
ViewCompat.setTransitionName(view, url)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
requireActivity(),
|
||||
view,
|
||||
|
|
|
@ -30,6 +30,7 @@ import android.view.ViewGroup
|
|||
import android.widget.ImageView
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
|
@ -62,7 +63,7 @@ class ViewImageFragment : ViewMediaFragment() {
|
|||
private var scheduleToolbarHide = false
|
||||
|
||||
override fun setupMediaView(showingDescription: Boolean) {
|
||||
binding.photoView.transitionName = attachment.url
|
||||
ViewCompat.setTransitionName(binding.photoView, attachment.url)
|
||||
binding.mediaDescription.text = attachment.description
|
||||
binding.captionSheet.visible(showingDescription)
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import android.widget.FrameLayout
|
|||
import android.widget.LinearLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
|
@ -359,7 +360,7 @@ class ViewVideoFragment : ViewMediaFragment() {
|
|||
// Ensure the description is visible over the video
|
||||
binding.mediaDescription.elevation = binding.videoView.elevation + 1
|
||||
|
||||
binding.videoView.transitionName = attachment.url
|
||||
ViewCompat.setTransitionName(binding.videoView, attachment.url)
|
||||
|
||||
if (!startedTransition && shouldCallMediaReady) {
|
||||
startedTransition = true
|
||||
|
|
|
@ -16,14 +16,10 @@
|
|||
|
||||
package app.pachli.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import app.pachli.components.notifications.pendingIntentFlags
|
||||
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
|
||||
import app.pachli.core.navigation.MainActivityIntent
|
||||
|
||||
|
@ -33,21 +29,14 @@ import app.pachli.core.navigation.MainActivityIntent
|
|||
*/
|
||||
@TargetApi(24)
|
||||
class PachliTileService : TileService() {
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
override fun onClick() {
|
||||
val intent = MainActivityIntent.openCompose(this, ComposeOptions())
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startActivityAndCollapse(getActivityPendingIntent(this, 0, intent))
|
||||
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
startActivityAndCollapse(pendingIntent)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getActivityPendingIntent(context: Context, requestCode: Int, intent: Intent): PendingIntent {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.getActivity(context, requestCode, intent, pendingIntentFlags(false))
|
||||
} else {
|
||||
PendingIntent.getActivity(context, requestCode, intent, pendingIntentFlags(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,8 +40,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
|
@ -77,6 +79,11 @@ class EditProfileViewModel @Inject constructor(
|
|||
|
||||
private var apiProfileAccount: Account? = null
|
||||
|
||||
private val _isDirty = MutableStateFlow(false)
|
||||
|
||||
/** True if the user has made unsaved changes to the profile */
|
||||
val isDirty = _isDirty.asStateFlow()
|
||||
|
||||
fun obtainProfile() = viewModelScope.launch {
|
||||
if (profileData.value == null || profileData.value is Error) {
|
||||
profileData.postValue(Loading())
|
||||
|
@ -170,10 +177,8 @@ class EditProfileViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
internal fun hasUnsavedChanges(newProfileData: ProfileDataInUi): Boolean {
|
||||
val diff = getProfileDiff(apiProfileAccount, newProfileData)
|
||||
|
||||
return diff.hasChanges()
|
||||
internal fun onChange(newProfileData: ProfileDataInUi) {
|
||||
_isDirty.value = getProfileDiff(apiProfileAccount, newProfileData).hasChanges()
|
||||
}
|
||||
|
||||
private fun getProfileDiff(oldProfileAccount: Account?, newProfileData: ProfileDataInUi): DiffProfileData {
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:errorEnabled="true"
|
||||
android:hint="@string/label_filter_title">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/filterTitle"
|
||||
|
@ -43,6 +44,15 @@
|
|||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?attr/colorAccent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/keywordChipsError"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/error_filter_missing_keyword"
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="?attr/colorError" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/keywordChips"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -100,8 +110,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:entries="@array/filter_duration_names"
|
||||
/>
|
||||
android:entries="@array/filter_duration_names" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
|
@ -146,6 +155,15 @@
|
|||
android:minHeight="48dp"
|
||||
android:text="@string/pref_title_account_filter_keywords" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/filterContextError"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/error_filter_missing_context"
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="?attr/colorError" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -503,7 +503,7 @@
|
|||
<string name="notification_report_description">ارسال إشعار عن شكاوى المدراء</string>
|
||||
<string name="set_focus_description">اضغط على الدائرة أو اسحبها لاختيار النقطة المحورية التي ستكون مرئية دائمًا في الصور المصغرة.</string>
|
||||
<string name="compose_save_draft_loses_media">حفظ المسودة؟ (سيتم رفع المرفقات مرة أخرى عند استعادة المسودة.)</string>
|
||||
<string name="compose_unsaved_changes">لديك تعديلات لم تحفظ.</string>
|
||||
<string name="unsaved_changes">لديك تعديلات لم تحفظ.</string>
|
||||
<string name="post_edited">عدَّلَ %s</string>
|
||||
<string name="notification_report_format">شكوى جديدة عن %s</string>
|
||||
<string name="status_edit_info">%1$s :عدّله</string>
|
||||
|
|
|
@ -374,7 +374,7 @@
|
|||
<item quantity="many"><b>%s</b> пашыраных</item>
|
||||
<item quantity="other"><b>%s</b> пашыраных</item>
|
||||
</plurals>
|
||||
<string name="compose_unsaved_changes">У Вас засталіся незахаваныя змены.</string>
|
||||
<string name="unsaved_changes">У Вас засталіся незахаваныя змены.</string>
|
||||
<string name="expand_collapse_all_posts">Разгарнуць/згарнуць допісы</string>
|
||||
<string name="action_open_post">Адкрыць допіс</string>
|
||||
<string name="restart_required">Патрэбна перазапусціць праграму</string>
|
||||
|
|
|
@ -508,7 +508,7 @@
|
|||
<string name="pachli_compose_post_quicksetting_label">Redacta la publicació</string>
|
||||
<string name="pref_title_confirm_favourites">Mostra el diàleg de confirmació abans de marcar com a preferit</string>
|
||||
<string name="action_unfollow_hashtag_format">Deixar de seguir #%s\?</string>
|
||||
<string name="compose_unsaved_changes">Tens canvis no desats.</string>
|
||||
<string name="unsaved_changes">Tens canvis no desats.</string>
|
||||
<string name="set_focus_description">Toqueu o arrossegueu el cercle per triar el punt focal que sempre serà visible a les miniatures.</string>
|
||||
<string name="url_domain_notifier">\u0020(🔗 %s)</string>
|
||||
<string name="pref_title_show_self_username">Mostra el nom d\'usuari a les barres d\'eines</string>
|
||||
|
|
|
@ -563,7 +563,7 @@
|
|||
<string name="title_edits">Golygiadau</string>
|
||||
<string name="post_media_alt">AMGEN</string>
|
||||
<string name="action_discard">Hepgor newidiadau</string>
|
||||
<string name="compose_unsaved_changes">Mae gennych newidiadau heb eu cadw.</string>
|
||||
<string name="unsaved_changes">Mae gennych newidiadau heb eu cadw.</string>
|
||||
<string name="mute_notifications_switch">Tewi hysbysiadau</string>
|
||||
<string name="a11y_label_loading_thread">Llwytho trywydd</string>
|
||||
<string name="action_share_account_link">Rhannu ddolen i gyfrif</string>
|
||||
|
|
|
@ -515,7 +515,7 @@
|
|||
<string name="pref_summary_http_proxy_disabled">Deaktiviert</string>
|
||||
<string name="pref_summary_http_proxy_missing"><nicht gesetzt></string>
|
||||
<string name="pref_summary_http_proxy_invalid"><ungültig></string>
|
||||
<string name="compose_unsaved_changes">Du hast nicht gespeicherte Änderungen.</string>
|
||||
<string name="unsaved_changes">Du hast nicht gespeicherte Änderungen.</string>
|
||||
<string name="pref_title_http_proxy_port_message">Port sollte zwischen %d und %d liegen</string>
|
||||
<string name="error_muting_hashtag_format">Fehler beim Stummschalten von #%s</string>
|
||||
<string name="action_post_failed">Hochladen fehlgeschlagen</string>
|
||||
|
|
|
@ -526,7 +526,7 @@
|
|||
<string name="pref_title_notification_filter_reports">Hay una nueva denuncia</string>
|
||||
<string name="action_discard">Descartar cambios</string>
|
||||
<string name="action_continue_edit">Continuar editando</string>
|
||||
<string name="compose_unsaved_changes">Tienes cambios sin guardar.</string>
|
||||
<string name="unsaved_changes">Tienes cambios sin guardar.</string>
|
||||
<string name="title_edits">Ediciones</string>
|
||||
<string name="action_post_failed">La subida falló</string>
|
||||
<string name="action_post_failed_do_nothing">Descartar</string>
|
||||
|
|
|
@ -534,7 +534,7 @@
|
|||
<string name="description_browser_login">ممکن است از روشهای تأیید خویت اضافی پشتیبانی کند؛ ولی نیازمند مرورگری پشتیبانی شده است.</string>
|
||||
<string name="action_discard">دور انداختن تغییرات</string>
|
||||
<string name="action_continue_edit">ادامهٔ ویرایش</string>
|
||||
<string name="compose_unsaved_changes">تغییراتی ذخیره نشده دارید.</string>
|
||||
<string name="unsaved_changes">تغییراتی ذخیره نشده دارید.</string>
|
||||
<string name="action_share_account_link">همرسانی پیوند به حساب</string>
|
||||
<string name="action_share_account_username">همرسانی نام کاربری حساب</string>
|
||||
<string name="send_account_link_to">همرسانی نشانی حساب به…</string>
|
||||
|
|
|
@ -376,7 +376,7 @@
|
|||
<string name="pref_title_notification_filter_poll">päättyneet äänestykset</string>
|
||||
<string name="abbreviated_in_seconds">%ds</string>
|
||||
<string name="failed_to_unpin">Irrottaminen epäonnistui</string>
|
||||
<string name="compose_unsaved_changes">Sinulla on tallentamattomia muutoksia.</string>
|
||||
<string name="unsaved_changes">Sinulla on tallentamattomia muutoksia.</string>
|
||||
<string name="state_follow_requested">Seuraamista pyydetty</string>
|
||||
<string name="compose_delete_draft">Poista luonnos?</string>
|
||||
<string name="notification_notification_worker">Ilmoituksia haetaan…</string>
|
||||
|
|
|
@ -568,7 +568,7 @@
|
|||
<string name="description_login">Fonctionne dans la plupart des cas. Aucune autre application n\'aura accès à vos données.</string>
|
||||
<string name="description_browser_login">Peut permettre des méthodes d\'authentification supplémentaires, mais un navigateur compatible est nécessaire.</string>
|
||||
<string name="mute_notifications_switch">Masquer les notifications</string>
|
||||
<string name="compose_unsaved_changes">Il y a des modifications non enregistrées.</string>
|
||||
<string name="unsaved_changes">Il y a des modifications non enregistrées.</string>
|
||||
<string name="description_post_edited">Modifié</string>
|
||||
<string name="select_list_empty">Vous n\'avez pas encore de liste</string>
|
||||
<string name="select_list_manage">Gérer les listes</string>
|
||||
|
|
|
@ -548,7 +548,7 @@
|
|||
<string name="notification_summary_report_format">%s · Tha postaichean ris, %d dhiubh</string>
|
||||
<string name="action_share_account_link">Co-roinn ceangal dhan chunntas</string>
|
||||
<string name="account_username_copied">Chaidh lethbhreac a dhèanamh dhen ainm-chleachdaiche</string>
|
||||
<string name="compose_unsaved_changes">Tha atharraichean gun sàbhaladh agad.</string>
|
||||
<string name="unsaved_changes">Tha atharraichean gun sàbhaladh agad.</string>
|
||||
<string name="error_status_source_load">Dh’fhàillig luchdadh bun-tùs a’ phuist on fhrithealaiche.</string>
|
||||
<string name="action_post_failed_detail">Dh’fhàillig luchdadh suas a’ phuist agad is chaidh a shàbhaladh ’na dhreachd.
|
||||
\n
|
||||
|
|
|
@ -514,7 +514,7 @@
|
|||
<string name="status_created_info">Creado por %1$s</string>
|
||||
<string name="action_discard">Desbotar cambios</string>
|
||||
<string name="action_continue_edit">Continuar a edición</string>
|
||||
<string name="compose_unsaved_changes">Hai cambios non gardados.</string>
|
||||
<string name="unsaved_changes">Hai cambios non gardados.</string>
|
||||
<string name="action_share_account_link">Comparte ligazón da conta</string>
|
||||
<string name="action_share_account_username">Comparte identificador da conta</string>
|
||||
<string name="send_account_link_to">Compartir URL da conta en…</string>
|
||||
|
|
|
@ -524,7 +524,7 @@
|
|||
<string name="post_media_alt">ALT</string>
|
||||
<string name="action_discard">Változtatások elvetése</string>
|
||||
<string name="action_continue_edit">Szerkesztés folytatása</string>
|
||||
<string name="compose_unsaved_changes">Elmentetlen változtatásaid vannak.</string>
|
||||
<string name="unsaved_changes">Elmentetlen változtatásaid vannak.</string>
|
||||
<string name="action_share_account_link">Fiókra történő hivatkozás megosztása</string>
|
||||
<string name="action_share_account_username">Fiók felhasználói nevének megosztása</string>
|
||||
<string name="send_account_link_to">Fiók URL megosztása vele…</string>
|
||||
|
|
|
@ -507,7 +507,7 @@
|
|||
<string name="pref_title_http_proxy_port_message">Gáttin ætti að vera á milli %d og %d</string>
|
||||
<string name="error_status_source_load">Mistókst að hlaða inn uppruna stöðufærslu af netþjóninum.</string>
|
||||
<string name="a11y_label_loading_thread">Hleð inn þræði</string>
|
||||
<string name="compose_unsaved_changes">Þú ert með óvistaðar breytingar.</string>
|
||||
<string name="unsaved_changes">Þú ert með óvistaðar breytingar.</string>
|
||||
<string name="pref_summary_http_proxy_disabled">Óvirkt</string>
|
||||
<string name="pref_summary_http_proxy_missing"><ekki stillt></string>
|
||||
<string name="pref_summary_http_proxy_invalid"><ógilt></string>
|
||||
|
|
|
@ -542,7 +542,7 @@
|
|||
<string name="notification_header_report_format">%s ha segnalato %s</string>
|
||||
<string name="notification_summary_report_format">%s · %d post allegati</string>
|
||||
<string name="action_add_reaction">aggiungi reazione</string>
|
||||
<string name="compose_unsaved_changes">Hai delle modifiche non salvate.</string>
|
||||
<string name="unsaved_changes">Hai delle modifiche non salvate.</string>
|
||||
<string name="confirmation_hashtag_unfollowed">Non segui più #%s</string>
|
||||
<string name="error_status_source_load">Caricamento dello status della sorgente dal server fallito.</string>
|
||||
<string name="post_edited">Modificato %s</string>
|
||||
|
|
|
@ -512,7 +512,7 @@
|
|||
<string name="status_created_info">%1$s の投稿</string>
|
||||
<string name="follow_requests_info">アカウントがロックされていなかったとしても、%1$s のスタッフは以下のアカウントのフォローリクエストを確認した方がいいと判断しました。</string>
|
||||
<string name="action_set_focus">中心点の設定</string>
|
||||
<string name="compose_unsaved_changes">保存していない変更があります。</string>
|
||||
<string name="unsaved_changes">保存していない変更があります。</string>
|
||||
<string name="error_status_source_load">サーバーからステータスの元情報を取得できませんでした。</string>
|
||||
<string name="pref_summary_http_proxy_disabled">無効</string>
|
||||
<string name="pref_summary_http_proxy_missing"><設定なし></string>
|
||||
|
|
|
@ -331,7 +331,7 @@
|
|||
<string name="notification_summary_large">%1$s, %2$s, %3$s un %4$d citi</string>
|
||||
<string name="title_media">Multivide</string>
|
||||
<string name="pref_title_alway_show_sensitive_media">Vienmēr rādīt sensitīvu saturu</string>
|
||||
<string name="compose_unsaved_changes">Tev ir nesaglabātas izmaiņas.</string>
|
||||
<string name="unsaved_changes">Tev ir nesaglabātas izmaiņas.</string>
|
||||
<string name="description_post_media">Multivide: %s</string>
|
||||
<string name="description_poll">Aptauja ar izvēlēm: %1$s, %2$s, %3$s, %4$s; %5$s</string>
|
||||
<string name="poll_info_format"><!-- 15 balsis • atlikusi 1 stunda -->%1$s • %2$s</string>
|
||||
|
|
|
@ -554,7 +554,7 @@
|
|||
<string name="ui_success_accepted_follow_request">Følgeforespørsel akseptert</string>
|
||||
<string name="action_discard">Forkast endringer</string>
|
||||
<string name="action_continue_edit">Fortsett endring</string>
|
||||
<string name="compose_unsaved_changes">Du har ulagrede endringer.</string>
|
||||
<string name="unsaved_changes">Du har ulagrede endringer.</string>
|
||||
<string name="select_list_empty">Du har ingen lister, enda</string>
|
||||
<string name="error_list_load">Feil under lading av lister</string>
|
||||
<string name="select_list_manage">Forvalte lister</string>
|
||||
|
|
|
@ -488,7 +488,7 @@
|
|||
<string name="delete_scheduled_post_warning">Dit ingeplande bericht verwijderen\?</string>
|
||||
<string name="failed_to_pin">Kan niet vastmaken</string>
|
||||
<string name="pref_summary_http_proxy_disabled">Uitgeschakeld</string>
|
||||
<string name="compose_unsaved_changes">Er zijn niet opgeslagen wijzigingen.</string>
|
||||
<string name="unsaved_changes">Er zijn niet opgeslagen wijzigingen.</string>
|
||||
<string name="mute_notifications_switch">Meldingen negeren</string>
|
||||
<string name="title_edits">Bewerkingen</string>
|
||||
<string name="pref_default_post_language">Standaardtaal van berichten</string>
|
||||
|
|
|
@ -520,7 +520,7 @@
|
|||
<string name="post_media_alt">ALT</string>
|
||||
<string name="action_discard">Ignorar las modificacions</string>
|
||||
<string name="action_continue_edit">Téner de modificar</string>
|
||||
<string name="compose_unsaved_changes">Avètz de modificacions pas salvadas.</string>
|
||||
<string name="unsaved_changes">Avètz de modificacions pas salvadas.</string>
|
||||
<string name="a11y_label_loading_thread">Cargament del fil</string>
|
||||
<string name="pref_summary_http_proxy_disabled">Desactivat</string>
|
||||
<string name="pref_summary_http_proxy_missing"><pas definit></string>
|
||||
|
|
|
@ -526,7 +526,7 @@
|
|||
<string name="status_edit_info">%1$s edytował</string>
|
||||
<string name="status_created_info">%1$s stworzył</string>
|
||||
<string name="title_edits">Edycje</string>
|
||||
<string name="compose_unsaved_changes">Masz niezapisane zmiany.</string>
|
||||
<string name="unsaved_changes">Masz niezapisane zmiany.</string>
|
||||
<string name="action_post_failed">Błąd wysyłania</string>
|
||||
<string name="action_post_failed_show_drafts">Pokaż szkice</string>
|
||||
<string name="action_post_failed_do_nothing">Odrzuć</string>
|
||||
|
|
|
@ -510,7 +510,7 @@
|
|||
<string name="description_browser_login">Pode oferecer suporte a métodos de autenticação adicionais, mas requer um navegador compatível.</string>
|
||||
<string name="status_edit_info">%1$s editou</string>
|
||||
<string name="action_continue_edit">Continuar editando</string>
|
||||
<string name="compose_unsaved_changes">Você tem alterações não salvas.</string>
|
||||
<string name="unsaved_changes">Você tem alterações não salvas.</string>
|
||||
<string name="description_post_edited">Editado</string>
|
||||
<string name="delete_scheduled_post_warning">Excluir este Toot agendado?</string>
|
||||
<string name="action_unfollow_hashtag_format">Deixar de seguir #%s\?</string>
|
||||
|
|
|
@ -481,7 +481,7 @@
|
|||
<string name="dialog_follow_hashtag_title">व्यक्तित्वविवरणलेखा अनुसरतु</string>
|
||||
<string name="limit_notifications">कालानुक्रमपङ्क्त्याः सूचनाः परिमिताः कुरुताम्</string>
|
||||
<string name="notification_report_description">परिमितावेदनानि प्रति ज्ञापनसूचनाः</string>
|
||||
<string name="compose_unsaved_changes">भवतः अरक्षितानि परिवर्तनानि सन्ति।</string>
|
||||
<string name="unsaved_changes">भवतः अरक्षितानि परिवर्तनानि सन्ति।</string>
|
||||
<string name="pref_title_notification_filter_reports">नूतनम् आवेदनमस्ति</string>
|
||||
<string name="pref_title_wellbeing_mode">सुस्थितिः</string>
|
||||
<string name="delete_scheduled_post_warning">इदं कालबद्धदौत्यं विनश्येत् किम् \?</string>
|
||||
|
|
|
@ -532,7 +532,7 @@
|
|||
<string name="status_created_info">%1$s skapade</string>
|
||||
<string name="action_discard">Förkasta ändringar</string>
|
||||
<string name="action_continue_edit">Fortsätt redigera</string>
|
||||
<string name="compose_unsaved_changes">Du har ändringar som inte sparats.</string>
|
||||
<string name="unsaved_changes">Du har ändringar som inte sparats.</string>
|
||||
<string name="action_post_failed">Uppladdning misslyckades</string>
|
||||
<string name="action_post_failed_detail">Ett fel inträffade när inlägget skulle laddas upp och har sparats till utkast.
|
||||
\n
|
||||
|
|
|
@ -524,7 +524,7 @@
|
|||
<string name="status_created_info">%1$s oluşturdu</string>
|
||||
<string name="dialog_follow_hashtag_title">Etiketi takip et</string>
|
||||
<string name="dialog_follow_hashtag_hint">#etiket</string>
|
||||
<string name="compose_unsaved_changes">Kaydedilmemiş değişikliklerin var.</string>
|
||||
<string name="unsaved_changes">Kaydedilmemiş değişikliklerin var.</string>
|
||||
<string name="status_edit_info">%1$s düzenledi</string>
|
||||
<string name="title_edits">Düzenlemeler</string>
|
||||
<string name="hint_description">Açıklama</string>
|
||||
|
|
|
@ -530,7 +530,7 @@
|
|||
<string name="error_status_source_load">Не вдалося завантажити джерело стану з сервера.</string>
|
||||
<string name="post_media_alt">ALT</string>
|
||||
<string name="action_discard">Відкинути зміни</string>
|
||||
<string name="compose_unsaved_changes">У вас є незбережені зміни.</string>
|
||||
<string name="unsaved_changes">У вас є незбережені зміни.</string>
|
||||
<string name="action_continue_edit">Продовжити редагування</string>
|
||||
<string name="mute_notifications_switch">Беззвучні сповіщення</string>
|
||||
<string name="title_edits">Редагування</string>
|
||||
|
|
|
@ -499,7 +499,7 @@
|
|||
<string name="post_media_alt">✦</string>
|
||||
<string name="action_discard">Hủy bỏ thay đổi</string>
|
||||
<string name="action_continue_edit">Tiếp tục sửa</string>
|
||||
<string name="compose_unsaved_changes">Thay đổi chưa được lưu.</string>
|
||||
<string name="unsaved_changes">Thay đổi chưa được lưu.</string>
|
||||
<string name="mute_notifications_switch">Ẩn thông báo</string>
|
||||
<string name="status_edit_info">Sửa %1$s</string>
|
||||
<string name="status_created_info">Đăng %1$s</string>
|
||||
|
|
|
@ -513,7 +513,7 @@
|
|||
<string name="post_media_alt">ALT</string>
|
||||
<string name="action_discard">放弃更改</string>
|
||||
<string name="action_continue_edit">继续编辑</string>
|
||||
<string name="compose_unsaved_changes">你有未保存的更改。</string>
|
||||
<string name="unsaved_changes">你有未保存的更改。</string>
|
||||
<string name="status_created_info">%1$s 创建了</string>
|
||||
<string name="mute_notifications_switch">将通知静音</string>
|
||||
<string name="title_edits">编辑</string>
|
||||
|
|
|
@ -412,7 +412,7 @@
|
|||
<string name="compose_delete_draft">Delete draft?</string>
|
||||
<string name="compose_save_draft">Save draft?</string>
|
||||
<string name="compose_save_draft_loses_media">Save draft? (Attachments will be uploaded again when you restore the draft.)</string>
|
||||
<string name="compose_unsaved_changes">You have unsaved changes.</string>
|
||||
<string name="unsaved_changes">You have unsaved changes.</string>
|
||||
<string name="send_post_notification_title">Sending post…</string>
|
||||
<string name="send_post_notification_error_title">Error sending post</string>
|
||||
<string name="send_post_notification_channel_name">Sending Posts</string>
|
||||
|
@ -703,5 +703,8 @@
|
|||
<string name="action_add_to_tab_success">Added \'%1$s\' to tabs</string>
|
||||
<string name="action_remove_tab">Remove tab</string>
|
||||
<string name="action_manage_tabs">Manage tabs</string>
|
||||
<string name="error_filter_missing_keyword">At least one keyword or phrase is required</string>
|
||||
<string name="error_filter_missing_context">At least one filter context is required</string>
|
||||
<string name="error_filter_missing_title">Title is required</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
package app.pachli
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import app.pachli.components.filters.EditFilterActivity
|
||||
import app.pachli.components.filters.EditFilterViewModel.Companion.getSecondsForDurationIndex
|
||||
import app.pachli.core.network.model.Attachment
|
||||
import app.pachli.core.network.model.Filter
|
||||
import app.pachli.core.network.model.FilterContext
|
||||
|
@ -259,7 +259,7 @@ class FilterV1Test {
|
|||
fun unchangedExpiration_shouldBeNegative_whenFilterIsExpired() {
|
||||
val expiredBySeconds = 3600
|
||||
val expiredDate = Date.from(Instant.now().minusSeconds(expiredBySeconds.toLong()))
|
||||
val updatedDuration = EditFilterActivity.getSecondsForDurationIndex(-1, null, expiredDate)
|
||||
val updatedDuration = getSecondsForDurationIndex(-1, null, expiredDate)
|
||||
assert(updatedDuration != null && updatedDuration.toInt() <= -expiredBySeconds)
|
||||
}
|
||||
|
||||
|
@ -267,7 +267,7 @@ class FilterV1Test {
|
|||
fun unchangedExpiration_shouldBePositive_whenFilterIsUnexpired() {
|
||||
val expiresInSeconds = 3600
|
||||
val expiredDate = Date.from(Instant.now().plusSeconds(expiresInSeconds.toLong()))
|
||||
val updatedDuration = EditFilterActivity.getSecondsForDurationIndex(-1, null, expiredDate)
|
||||
val updatedDuration = getSecondsForDurationIndex(-1, null, expiredDate)
|
||||
assert(updatedDuration != null && updatedDuration.toInt() > (expiresInSeconds - 60))
|
||||
}
|
||||
|
||||
|
|
|
@ -239,7 +239,7 @@ class ComposeActivityTest {
|
|||
rule.launch()
|
||||
rule.getScenario().onActivity {
|
||||
insertSomeTextInContent(it, content)
|
||||
assertEquals(content.length, it.calculateTextLength())
|
||||
assertEquals(content.length, it.viewModel.statusLength.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,7 +249,7 @@ class ComposeActivityTest {
|
|||
rule.launch()
|
||||
rule.getScenario().onActivity {
|
||||
insertSomeTextInContent(it, content)
|
||||
assertEquals(6, it.calculateTextLength())
|
||||
assertEquals(6, it.viewModel.statusLength.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -259,7 +259,7 @@ class ComposeActivityTest {
|
|||
rule.launch()
|
||||
rule.getScenario().onActivity {
|
||||
insertSomeTextInContent(it, content)
|
||||
assertEquals(7, it.calculateTextLength())
|
||||
assertEquals(7, it.viewModel.statusLength.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,7 +271,7 @@ class ComposeActivityTest {
|
|||
insertSomeTextInContent(it, content)
|
||||
assertEquals(
|
||||
InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL,
|
||||
it.calculateTextLength(),
|
||||
it.viewModel.statusLength.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -282,7 +282,7 @@ class ComposeActivityTest {
|
|||
rule.launch()
|
||||
rule.getScenario().onActivity {
|
||||
insertSomeTextInContent(it, content)
|
||||
assertEquals(21, it.calculateTextLength())
|
||||
assertEquals(21, it.viewModel.statusLength.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -295,7 +295,7 @@ class ComposeActivityTest {
|
|||
insertSomeTextInContent(it, additionalContent + url)
|
||||
assertEquals(
|
||||
additionalContent.length + InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL,
|
||||
it.calculateTextLength(),
|
||||
it.viewModel.statusLength.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -310,7 +310,7 @@ class ComposeActivityTest {
|
|||
insertSomeTextInContent(it, shortUrl + additionalContent + url)
|
||||
assertEquals(
|
||||
additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2),
|
||||
it.calculateTextLength(),
|
||||
it.viewModel.statusLength.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -324,7 +324,7 @@ class ComposeActivityTest {
|
|||
insertSomeTextInContent(it, url + additionalContent + url)
|
||||
assertEquals(
|
||||
additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2),
|
||||
it.calculateTextLength(),
|
||||
it.viewModel.statusLength.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -340,7 +340,7 @@ class ComposeActivityTest {
|
|||
insertSomeTextInContent(it, additionalContent + url)
|
||||
assertEquals(
|
||||
additionalContent.length + customUrlLength,
|
||||
it.calculateTextLength(),
|
||||
it.viewModel.statusLength.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -357,7 +357,7 @@ class ComposeActivityTest {
|
|||
insertSomeTextInContent(it, shortUrl + additionalContent + url)
|
||||
assertEquals(
|
||||
additionalContent.length + (customUrlLength * 2),
|
||||
it.calculateTextLength(),
|
||||
it.viewModel.statusLength.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -373,7 +373,7 @@ class ComposeActivityTest {
|
|||
insertSomeTextInContent(it, url + additionalContent + url)
|
||||
assertEquals(
|
||||
additionalContent.length + (customUrlLength * 2),
|
||||
it.calculateTextLength(),
|
||||
it.viewModel.statusLength.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,15 +22,15 @@ import app.pachli.util.highlightSpans
|
|||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
@RunWith(ParameterizedRobolectricTestRunner::class)
|
||||
class StatusLengthTest(
|
||||
private val text: String,
|
||||
private val expectedLength: Int,
|
||||
) {
|
||||
companion object {
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
fun data(): Iterable<Any> {
|
||||
return listOf(
|
||||
|
@ -61,7 +61,7 @@ class StatusLengthTest(
|
|||
|
||||
assertEquals(
|
||||
expectedLength,
|
||||
ComposeActivity.statusLength(spannedText, null, 23),
|
||||
ComposeViewModel.statusLength(spannedText, "", 23),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ class StatusLengthTest(
|
|||
)
|
||||
assertEquals(
|
||||
expectedLength + cwText.length,
|
||||
ComposeActivity.statusLength(spannedText, cwText, 23),
|
||||
ComposeViewModel.statusLength(spannedText, cwText.toString(), 23),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,9 @@ import androidx.core.app.ActivityCompat
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.MenuProvider
|
||||
import app.pachli.core.accounts.AccountManager
|
||||
import app.pachli.core.activity.extensions.canOverrideActivityTransitions
|
||||
import app.pachli.core.activity.extensions.getTransitionKind
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.database.model.AccountEntity
|
||||
import app.pachli.core.designsystem.EmbeddedFontFamily
|
||||
import app.pachli.core.designsystem.R as DR
|
||||
|
@ -82,6 +85,13 @@ abstract class BaseActivity : AppCompatActivity(), MenuProvider {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (canOverrideActivityTransitions()) {
|
||||
intent.getTransitionKind()?.let {
|
||||
overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, it.openEnter, it.openExit)
|
||||
overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, it.closeEnter, it.closeExit)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the theme from preferences
|
||||
val theme = AppTheme.from(sharedPreferencesRepository)
|
||||
Timber.d("activeTheme: %s", theme)
|
||||
|
@ -156,11 +166,6 @@ abstract class BaseActivity : AppCompatActivity(), MenuProvider {
|
|||
return true
|
||||
}
|
||||
|
||||
fun startActivityWithSlideInAnimation(intent: Intent) {
|
||||
super.startActivity(intent)
|
||||
overridePendingTransition(DR.anim.slide_from_right, DR.anim.slide_to_left)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
|
@ -171,11 +176,13 @@ abstract class BaseActivity : AppCompatActivity(), MenuProvider {
|
|||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
overridePendingTransition(DR.anim.slide_from_left, DR.anim.slide_to_right)
|
||||
}
|
||||
|
||||
fun finishWithoutSlideOutAnimation() {
|
||||
super.finish()
|
||||
if (!canOverrideActivityTransitions()) {
|
||||
intent.getTransitionKind()?.let {
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(it.closeEnter, it.closeExit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun redirectIfNotLoggedIn() {
|
||||
|
@ -183,7 +190,7 @@ abstract class BaseActivity : AppCompatActivity(), MenuProvider {
|
|||
if (account == null) {
|
||||
val intent = LoginActivityIntent(this)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
@ -259,7 +266,7 @@ abstract class BaseActivity : AppCompatActivity(), MenuProvider {
|
|||
accountManager.setActiveAccount(account.id)
|
||||
val intent = MainActivityIntent.redirect(this, account.id, url)
|
||||
startActivity(intent)
|
||||
finishWithoutSlideOutAnimation()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
|
|
|
@ -24,6 +24,9 @@ import android.widget.LinearLayout
|
|||
import android.widget.Toast
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import app.pachli.core.activity.extensions.TransitionKind
|
||||
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||
import app.pachli.core.activity.extensions.startActivityWithTransition
|
||||
import app.pachli.core.navigation.AccountActivityIntent
|
||||
import app.pachli.core.navigation.ViewThreadActivityIntent
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
|
@ -105,13 +108,13 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
open fun viewThread(statusId: String, url: String?) {
|
||||
if (!isSearching()) {
|
||||
val intent = ViewThreadActivityIntent(this, statusId, url)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||
}
|
||||
}
|
||||
|
||||
open fun viewAccount(id: String) {
|
||||
val intent = AccountActivityIntent(this, id)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
startActivityWithDefaultTransition(intent)
|
||||
}
|
||||
|
||||
protected open fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) {
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright 2024 Pachli Association
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package app.pachli.core.activity.extensions
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Activity.OVERRIDE_TRANSITION_CLOSE
|
||||
import android.app.Activity.OVERRIDE_TRANSITION_OPEN
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.activity.BuildConfig
|
||||
|
||||
/**
|
||||
* Starts the activity in [intent] (which must be a subclass of [BaseActivity])
|
||||
* using [transitionKind] as the open/close transition.
|
||||
*/
|
||||
fun Activity.startActivityWithTransition(intent: Intent, transitionKind: TransitionKind) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
if (this !is BaseActivity) {
|
||||
throw IllegalStateException("startActivityWithTransition must be used with BaseActivity subclass")
|
||||
}
|
||||
}
|
||||
|
||||
intent.putExtra(EXTRA_TRANSITION_KIND, transitionKind)
|
||||
startActivity(intent)
|
||||
|
||||
if (canOverrideActivityTransitions()) {
|
||||
overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, transitionKind.openEnter, transitionKind.openExit)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(transitionKind.openEnter, transitionKind.openExit)
|
||||
}
|
||||
}
|
||||
|
||||
/** See [Activity.startActivityWithTransition] */
|
||||
fun Activity.startActivityWithDefaultTransition(intent: Intent) = startActivityWithTransition(intent, TransitionKind.DEFAULT)
|
||||
|
||||
/**
|
||||
* Overrides any "close" transition already set for this activity and
|
||||
* replaces them with [transitionKind].
|
||||
*
|
||||
* Call this after calling [Activity.finish]
|
||||
*/
|
||||
fun Activity.setCloseTransition(transitionKind: TransitionKind) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
if (this !is BaseActivity) {
|
||||
throw IllegalStateException("startActivityWithTransition must be used with BaseActivity subclass")
|
||||
}
|
||||
}
|
||||
|
||||
if (canOverrideActivityTransitions()) {
|
||||
overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, transitionKind.closeEnter, transitionKind.closeExit)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(transitionKind.closeEnter, transitionKind.closeExit)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the Android version supports [Activity.overrideActivityTransition],
|
||||
* false if [Activity.overridePendingTransition] must be used.
|
||||
*/
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
fun canOverrideActivityTransitions() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright 2024 Pachli Association
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package app.pachli.core.activity.extensions
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.AnimRes
|
||||
import app.pachli.core.designsystem.R as DR
|
||||
|
||||
const val EXTRA_TRANSITION_KIND = "transition_kind"
|
||||
|
||||
/**
|
||||
* The type of transition and animation resources to use when opening and closing
|
||||
* an activity.
|
||||
*
|
||||
* @property openEnter When opening an activity, a resource ID of the animation resource to
|
||||
* use for the incoming activity. Use 0 for no animation.
|
||||
* @property openExit When opening an activity, a resource ID of the animation resource to
|
||||
* use for the outgoing activity. Use 0 for no animation
|
||||
* @property closeEnter When closing an activity, a resource ID of the animation resource to
|
||||
* use for the incoming activity. Use 0 for no animation.
|
||||
* @property closeExit When closing an activity, a resource ID of the animation resource to
|
||||
* use for the outgoing activity. Use 0 for no animation
|
||||
*/
|
||||
enum class TransitionKind(
|
||||
@AnimRes val openEnter: Int,
|
||||
@AnimRes val openExit: Int,
|
||||
@AnimRes val closeEnter: Int,
|
||||
@AnimRes val closeExit: Int,
|
||||
) {
|
||||
/** Default transition */
|
||||
DEFAULT(DR.anim.activity_open_enter, DR.anim.activity_open_exit, DR.anim.activity_close_enter, DR.anim.activity_close_exit),
|
||||
|
||||
/**
|
||||
* Slide from the user's "end" perspective (right side for LTR text, left side for RTL text).
|
||||
* Use when a spatial relationship makes sense, such as transitioning from a single status
|
||||
* to the thread that contains that status.
|
||||
*/
|
||||
SLIDE_FROM_END(DR.anim.slide_from_end, DR.anim.slide_to_start, DR.anim.slide_from_start, DR.anim.slide_to_end),
|
||||
|
||||
/**
|
||||
* Explode out from the centre of the screen. Use to indicate a significant change in
|
||||
* application state (e.g., changing accounts).
|
||||
*/
|
||||
EXPLODE(DR.anim.explode, DR.anim.activity_open_exit, 0, 0),
|
||||
}
|
||||
|
||||
/** @return The [TransitionKind] included in this intent, or null */
|
||||
fun Intent.getTransitionKind(): TransitionKind? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getSerializableExtra(EXTRA_TRANSITION_KIND, TransitionKind::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getSerializableExtra(EXTRA_TRANSITION_KIND) as? TransitionKind
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2024 Pachli Association
|
||||
~
|
||||
~ This file is a part of Pachli.
|
||||
~
|
||||
~ This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
~ License, or (at your option) any later version.
|
||||
~
|
||||
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
~ Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||
~ see <http://www.gnu.org/licenses>.
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="1.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/linear_interpolator"
|
||||
android:startOffset="0"
|
||||
android:duration="450" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="96dp"
|
||||
android:toXDelta="0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
|
||||
<extend
|
||||
android:fromExtendLeft="96dp"
|
||||
android:fromExtendTop="0"
|
||||
android:fromExtendRight="0"
|
||||
android:fromExtendBottom="0"
|
||||
android:toExtendLeft="96dp"
|
||||
android:toExtendTop="0"
|
||||
android:toExtendRight="0"
|
||||
android:toExtendBottom="0"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2024 Pachli Association
|
||||
~
|
||||
~ This file is a part of Pachli.
|
||||
~
|
||||
~ This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
~ License, or (at your option) any later version.
|
||||
~
|
||||
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
~ Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||
~ see <http://www.gnu.org/licenses>.
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/linear_interpolator"
|
||||
android:startOffset="35"
|
||||
android:duration="83" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="-96dp"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
|
||||
<extend
|
||||
android:fromExtendLeft="0"
|
||||
android:fromExtendTop="0"
|
||||
android:fromExtendRight="96dp"
|
||||
android:fromExtendBottom="0"
|
||||
android:toExtendLeft="0"
|
||||
android:toExtendTop="0"
|
||||
android:toExtendRight="96dp"
|
||||
android:toExtendBottom="0"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
/*
|
||||
** Copyright 2022, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="0"
|
||||
android:toAlpha="1.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/linear_interpolator"
|
||||
android:startOffset="50"
|
||||
android:duration="83" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="-96dp"
|
||||
android:toXDelta="0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
|
||||
<extend
|
||||
android:fromExtendLeft="0"
|
||||
android:fromExtendTop="0"
|
||||
android:fromExtendRight="96dp"
|
||||
android:fromExtendBottom="0"
|
||||
android:toExtendLeft="0"
|
||||
android:toExtendTop="0"
|
||||
android:toExtendRight="96dp"
|
||||
android:toExtendBottom="0"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
/*
|
||||
** Copyright 2022, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="1.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/standard_accelerate"
|
||||
android:startOffset="0"
|
||||
android:duration="450" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="96dp"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
|
||||
<extend
|
||||
android:fromExtendLeft="9dp"
|
||||
android:fromExtendTop="0"
|
||||
android:fromExtendRight="0"
|
||||
android:fromExtendBottom="0"
|
||||
android:toExtendLeft="96dp"
|
||||
android:toExtendTop="0"
|
||||
android:toExtendRight="0"
|
||||
android:toExtendBottom="0"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
** Copyright 2022, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="1.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/linear_interpolator"
|
||||
android:startOffset="0"
|
||||
android:duration="450" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="96dp"
|
||||
android:toXDelta="0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
** Copyright 2022, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/linear_interpolator"
|
||||
android:startOffset="35"
|
||||
android:duration="83" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="-96dp"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
/*
|
||||
** Copyright 2022, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="0"
|
||||
android:toAlpha="1.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/linear_interpolator"
|
||||
android:startOffset="50"
|
||||
android:duration="83" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="-96dp"
|
||||
android:toXDelta="0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
/*
|
||||
** Copyright 2022, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="1.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/standard_accelerate"
|
||||
android:startOffset="0"
|
||||
android:duration="450" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="96dp"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -2,5 +2,5 @@
|
|||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate android:fromXDelta="0" android:toXDelta="100%p"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:duration="300"/>
|
||||
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||
</set>
|
|
@ -2,5 +2,5 @@
|
|||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate android:fromXDelta="0" android:toXDelta="-100%p"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:duration="300"/>
|
||||
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||
</set>
|
|
@ -2,5 +2,5 @@
|
|||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate android:fromXDelta="100%p" android:toXDelta="0"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:duration="300"/>
|
||||
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||
</set>
|
|
@ -2,5 +2,5 @@
|
|||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate android:fromXDelta="-100%p" android:toXDelta="0"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:duration="300"/>
|
||||
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||
</set>
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
** Copyright 2009, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="1.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/linear_interpolator"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="-96dp"
|
||||
android:toXDelta="0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
|
||||
<extend
|
||||
android:fromExtendLeft="0"
|
||||
android:fromExtendTop="0"
|
||||
android:fromExtendRight="96dp"
|
||||
android:fromExtendBottom="0"
|
||||
android:toExtendLeft="0"
|
||||
android:toExtendTop="0"
|
||||
android:toExtendRight="96dp"
|
||||
android:toExtendBottom="0"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
** Copyright 2009, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/linear_interpolator"
|
||||
android:startOffset="35"
|
||||
android:duration="83" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="96dp"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
|
||||
<extend
|
||||
android:fromExtendLeft="96dp"
|
||||
android:fromExtendTop="0"
|
||||
android:fromExtendRight="0"
|
||||
android:fromExtendBottom="0"
|
||||
android:toExtendLeft="96dp"
|
||||
android:toExtendTop="0"
|
||||
android:toExtendRight="0"
|
||||
android:toExtendBottom="0"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
/*
|
||||
** Copyright 2009, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="0"
|
||||
android:toAlpha="1.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/linear_interpolator"
|
||||
android:startOffset="50"
|
||||
android:duration="83" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="96dp"
|
||||
android:toXDelta="0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
|
||||
<extend
|
||||
android:fromExtendLeft="96dp"
|
||||
android:fromExtendTop="0"
|
||||
android:fromExtendRight="0"
|
||||
android:fromExtendBottom="0"
|
||||
android:toExtendLeft="96dp"
|
||||
android:toExtendTop="0"
|
||||
android:toExtendRight="0"
|
||||
android:toExtendBottom="0"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
/*
|
||||
** Copyright 2009, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="1.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/standard_accelerate"
|
||||
android:startOffset="0"
|
||||
android:duration="450" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="-96dp"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
|
||||
<extend
|
||||
android:fromExtendLeft="0"
|
||||
android:fromExtendTop="0"
|
||||
android:fromExtendRight="96dp"
|
||||
android:fromExtendBottom="0"
|
||||
android:toExtendLeft="0"
|
||||
android:toExtendTop="0"
|
||||
android:toExtendRight="96dp"
|
||||
android:toExtendBottom="0"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
** Copyright 2009, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="1.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/linear_interpolator"
|
||||
android:startOffset="0"
|
||||
android:duration="450" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="-96dp"
|
||||
android:toXDelta="0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
** Copyright 2009, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false"
|
||||
android:zAdjustment="top">
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/linear_interpolator"
|
||||
android:startOffset="35"
|
||||
android:duration="83" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="96dp"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
** Copyright 2009, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
<alpha
|
||||
android:fromAlpha="0"
|
||||
android:toAlpha="1.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/linear_interpolator"
|
||||
android:startOffset="50"
|
||||
android:duration="83" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="96dp"
|
||||
android:toXDelta="0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
** Copyright 2009, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="1.0"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/standard_accelerate"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
|
||||
<translate
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="-96dp"
|
||||
android:fillEnabled="true"
|
||||
android:fillBefore="true"
|
||||
android:fillAfter="true"
|
||||
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||
android:startOffset="0"
|
||||
android:duration="@integer/activity_transition_duration_ms" />
|
||||
</set>
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:interpolator="@android:anim/linear_interpolator"
|
||||
android:fromAlpha="0"
|
||||
android:toAlpha="1"
|
||||
android:duration="300" />
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:interpolator="@android:anim/linear_interpolator"
|
||||
android:fromAlpha="1"
|
||||
android:toAlpha="0"
|
||||
android:duration="300" />
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2017 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License
|
||||
-->
|
||||
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1"/>
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2024 Pachli Association
|
||||
~
|
||||
~ This file is a part of Pachli.
|
||||
~
|
||||
~ This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
~ License, or (at your option) any later version.
|
||||
~
|
||||
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
~ Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||
~ see <http://www.gnu.org/licenses>.
|
||||
-->
|
||||
|
||||
<linearInterpolator />
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate android:fromXDelta="100%p" android:toXDelta="0"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||
</set>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate android:fromXDelta="-100%p" android:toXDelta="0"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||
</set>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate android:fromXDelta="0" android:toXDelta="100%p"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||
</set>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate android:fromXDelta="0" android:toXDelta="-100%p"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||
</set>
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2021 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:controlX1="0.3"
|
||||
android:controlY1="0"
|
||||
android:controlX2="1"
|
||||
android:controlY2="1"/>
|
|
@ -3,4 +3,10 @@
|
|||
<integer name="profile_media_column_count">3</integer>
|
||||
|
||||
<integer name="trending_column_count">1</integer>
|
||||
|
||||
<!-- Duration for default activity transition, in ms -->
|
||||
<integer name="activity_transition_duration_ms">450</integer>
|
||||
|
||||
<!-- Duration for slide transition, in ms -->
|
||||
<integer name="activity_slide_transition_duration_ms">200</integer>
|
||||
</resources>
|
||||
|
|
|
@ -11,11 +11,11 @@ import kotlinx.parcelize.Parcelize
|
|||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Filter(
|
||||
val id: String,
|
||||
val title: String,
|
||||
@Json(name = "context") val contexts: List<FilterContext>,
|
||||
@Json(name = "expires_at") val expiresAt: Date?,
|
||||
@Json(name = "filter_action") val action: Action,
|
||||
val id: String = "",
|
||||
val title: String = "",
|
||||
@Json(name = "context") val contexts: List<FilterContext> = emptyList(),
|
||||
@Json(name = "expires_at") val expiresAt: Date? = null,
|
||||
@Json(name = "filter_action") val action: Action = Action.WARN,
|
||||
// This should not normally be empty. However, Mastodon does not include
|
||||
// this in a status' `filtered.filter` property (it's not null or empty,
|
||||
// it's missing) which breaks deserialisation. Patch this by ensuring it's
|
||||
|
|
|
@ -25,20 +25,30 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import app.pachli.core.activity.BottomSheetActivity
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
import app.pachli.core.designsystem.R as DR
|
||||
import app.pachli.core.ui.extensions.reduceSwipeSensitivity
|
||||
import app.pachli.feature.about.databinding.ActivityAboutBinding
|
||||
import com.bumptech.glide.request.target.FixedSizeDrawable
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AboutActivity : BottomSheetActivity(), MenuProvider {
|
||||
|
||||
private val binding: ActivityAboutBinding by viewBinding(ActivityAboutBinding::inflate)
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
binding.pager.currentItem = 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val binding = ActivityAboutBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
@ -65,14 +75,17 @@ class AboutActivity : BottomSheetActivity(), MenuProvider {
|
|||
tab.text = adapter.title(position)
|
||||
}.attach()
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (binding.pager.currentItem != 0) binding.pager.currentItem = 0 else finish()
|
||||
}
|
||||
},
|
||||
)
|
||||
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
onBackPressedCallback.isEnabled = tab.position > 0
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||
})
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue